Compare commits
	
		
			13 Commits
		
	
	
		
			v0.2.0
			...
			19a5c4ad8a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 19a5c4ad8a | ||
|   | 3d3b078e60 | ||
|   | d4e91ea9a6 | ||
|   | dc4bb494f3 | ||
|   | 9b58b887b4 | ||
|   | 07848907f3 | ||
|   | 7a08f7cbe2 | ||
|   | dd37238510 | ||
|   | c8d5f335b1 | ||
|   | bb80b3ee54 | ||
|   | 06d860c601 | ||
|   | c321de13fd | ||
|   | 278e8303c4 | 
							
								
								
									
										13
									
								
								cista/api.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								cista/api.py
									
									
									
									
									
								
							| @@ -37,16 +37,23 @@ async def upload(req, ws): | ||||
|             ) | ||||
|         req = msgspec.json.decode(text, type=FileRange) | ||||
|         pos = req.start | ||||
|         data = None | ||||
|         while pos < req.end and (data := await ws.recv()) and isinstance(data, bytes): | ||||
|         while True: | ||||
|             data = await ws.recv() | ||||
|             if not isinstance(data, bytes): | ||||
|                 break | ||||
|             if len(data) > req.end - pos: | ||||
|                 raise ValueError( | ||||
|                     f"Expected up to {req.end - pos} bytes, got {len(data)} bytes" | ||||
|                 ) | ||||
|             sentsize = await alink(("upload", req.name, pos, data, req.size)) | ||||
|             pos += typing.cast(int, sentsize) | ||||
|             if pos >= req.end: | ||||
|                 break | ||||
|         if pos != req.end: | ||||
|             d = f"{len(data)} bytes" if isinstance(data, bytes) else data | ||||
|             raise ValueError(f"Expected {req.end - pos} more bytes, got {d}") | ||||
|         # Report success | ||||
|         res = StatusMsg(status="ack", req=req) | ||||
|         print("ack", res) | ||||
|         await asend(ws, res) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -34,9 +34,11 @@ class File: | ||||
|             self.open_rw() | ||||
|         assert self.fd is not None | ||||
|         if file_size is not None: | ||||
|             assert pos + len(buffer) <= file_size | ||||
|             os.ftruncate(self.fd, file_size) | ||||
|         os.lseek(self.fd, pos, os.SEEK_SET) | ||||
|         os.write(self.fd, buffer) | ||||
|         if buffer: | ||||
|             os.lseek(self.fd, pos, os.SEEK_SET) | ||||
|             os.write(self.fd, buffer) | ||||
|  | ||||
|     def __getitem__(self, slice): | ||||
|         if self.fd is None: | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import os | ||||
| import re | ||||
| from pathlib import Path, PurePath | ||||
| from pathlib import Path | ||||
|  | ||||
| from sanic import Sanic | ||||
|  | ||||
| @@ -15,7 +15,6 @@ def run(*, dev=False): | ||||
|     # Silence Sanic's warning about running in production rather than debug | ||||
|     os.environ["SANIC_IGNORE_PRODUCTION_WARNING"] = "1" | ||||
|     confdir = config.conffile.parent | ||||
|     wwwroot = PurePath(__file__).parent / "wwwroot" | ||||
|     if opts.get("ssl"): | ||||
|         # Run plain HTTP redirect/acme server on port 80 | ||||
|         server80.app.prepare(port=80, motd=False) | ||||
| @@ -27,7 +26,7 @@ def run(*, dev=False): | ||||
|         motd=False, | ||||
|         dev=dev, | ||||
|         auto_reload=dev, | ||||
|         reload_dir={confdir, wwwroot}, | ||||
|         reload_dir={confdir}, | ||||
|         access_log=True, | ||||
|     )  # type: ignore | ||||
|     if dev: | ||||
|   | ||||
| @@ -44,8 +44,6 @@ watchEffect(() => { | ||||
| onMounted(loadSession) | ||||
| onMounted(watchConnect) | ||||
| onUnmounted(watchDisconnect) | ||||
| // Update human-readable x seconds ago messages from mtimes | ||||
| setInterval(documentStore.updateModified, 1000) | ||||
| const headerMain = ref<typeof HeaderMain | null>(null) | ||||
| let vert = 0 | ||||
| let timer: any = null | ||||
|   | ||||
| @@ -17,7 +17,7 @@ | ||||
|         <td class="name"> | ||||
|           <FileRenameInput :doc="editing" :rename="mkdir" :exit="() => {editing = null}" /> | ||||
|         </td> | ||||
|         <FileModified :doc=editing /> | ||||
|         <FileModified :doc=editing :key=nowkey /> | ||||
|         <FileSize :doc=editing /> | ||||
|         <td class="menu"></td> | ||||
|       </tr> | ||||
| @@ -61,7 +61,7 @@ | ||||
|               <button v-if="cursor == doc" class="rename-button" @click="() => (editing = doc)">🖊️</button> | ||||
|             </template> | ||||
|           </td> | ||||
|           <FileModified :doc=doc /> | ||||
|           <FileModified :doc=doc :key=nowkey /> | ||||
|           <FileSize :doc=doc /> | ||||
|           <td class="menu"> | ||||
|             <button tabindex="-1" @click.stop="contextMenu($event, doc)">⋮</button> | ||||
| @@ -79,28 +79,28 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref, computed, watchEffect } from 'vue' | ||||
| import { ref, computed, watchEffect, shallowRef, onMounted, onUnmounted } from 'vue' | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
| import type { Document } from '@/repositories/Document' | ||||
| import { Doc } from '@/repositories/Document' | ||||
| import FileRenameInput from './FileRenameInput.vue' | ||||
| import { connect, controlUrl } from '@/repositories/WS' | ||||
| import { collator, formatSize, formatUnixDate } from '@/utils' | ||||
| import { collator, formatSize } from '@/utils' | ||||
| import { useRouter } from 'vue-router' | ||||
|  | ||||
| const props = defineProps<{ | ||||
|   path: Array<string> | ||||
|   documents: Document[] | ||||
|   documents: Doc[] | ||||
| }>() | ||||
| const documentStore = useDocumentStore() | ||||
| const router = useRouter() | ||||
| const url_for = (doc: Document) => { | ||||
| const url_for = (doc: Doc) => { | ||||
|   const p = doc.loc ? `${doc.loc}/${doc.name}` : doc.name | ||||
|   return doc.dir ? `#/${p}/` : `/files/${p}` | ||||
| } | ||||
| const cursor = ref<Document | null>(null) | ||||
| const cursor = shallowRef<Doc | null>(null) | ||||
| // File rename | ||||
| const editing = ref<Document | null>(null) | ||||
| const rename = (doc: Document, newName: string) => { | ||||
| const editing = shallowRef<Doc | null>(null) | ||||
| const rename = (doc: Doc, newName: string) => { | ||||
|   const oldName = doc.name | ||||
|   const control = connect(controlUrl, { | ||||
|     message(ev: MessageEvent) { | ||||
| @@ -124,7 +124,7 @@ const rename = (doc: Document, newName: string) => { | ||||
|   } | ||||
|   doc.name = newName // We should get an update from watch but this is quicker | ||||
| } | ||||
| const sortedDocuments = computed(() => sorted(props.documents as Document[])) | ||||
| const sortedDocuments = computed(() => sorted(props.documents)) | ||||
| const showFolderBreadcrumb = (i: number) => { | ||||
|   const docs = sortedDocuments.value | ||||
|   const docloc = docs[i].loc | ||||
| @@ -132,19 +132,15 @@ const showFolderBreadcrumb = (i: number) => { | ||||
| } | ||||
| defineExpose({ | ||||
|   newFolder() { | ||||
|     const now = Date.now() / 1000 | ||||
|     editing.value = { | ||||
|     const now = Math.floor(Date.now() / 1000) | ||||
|     editing.value = new Doc({ | ||||
|       loc: loc.value, | ||||
|       key: 'new', | ||||
|       name: 'New Folder', | ||||
|       dir: true, | ||||
|       mtime: now, | ||||
|       size: 0, | ||||
|       sizedisp: formatSize(0), | ||||
|       modified: formatUnixDate(now), | ||||
|       haystack: '', | ||||
|     } | ||||
|     console.log("New") | ||||
|     }) | ||||
|   }, | ||||
|   toggleSelectAll() { | ||||
|     console.log('Select') | ||||
| @@ -229,7 +225,14 @@ watchEffect(() => { | ||||
|     focusBreadcrumb() | ||||
|   } | ||||
| }) | ||||
| const mkdir = (doc: Document, name: string) => { | ||||
| let nowkey = ref(0) | ||||
| let modifiedTimer: any = null | ||||
| const updateModified = () => { | ||||
|   nowkey.value = Math.floor(Date.now() / 1000) | ||||
| } | ||||
| onMounted(() => { updateModified(); modifiedTimer = setInterval(updateModified, 1000) }) | ||||
| onUnmounted(() => { clearInterval(modifiedTimer) }) | ||||
| const mkdir = (doc: Doc, name: string) => { | ||||
|   const control = connect(controlUrl, { | ||||
|     open() { | ||||
|       control.send( | ||||
| @@ -250,7 +253,9 @@ const mkdir = (doc: Document, name: string) => { | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|   doc.name = name // We should get an update from watch but this is quicker | ||||
|   // We should get an update from watch but this is quicker | ||||
|   doc.name = name | ||||
|   doc.key = crypto.randomUUID() | ||||
| } | ||||
|  | ||||
| // Column sort | ||||
| @@ -259,11 +264,11 @@ const toggleSort = (name: string) => { | ||||
| } | ||||
| const sort = ref<string>('') | ||||
| const sortCompare = { | ||||
|   name: (a: Document, b: Document) => collator.compare(a.name, b.name), | ||||
|   modified: (a: Document, b: Document) => b.mtime - a.mtime, | ||||
|   size: (a: Document, b: Document) => b.size - a.size | ||||
|   name: (a: Doc, b: Doc) => collator.compare(a.name, b.name), | ||||
|   modified: (a: Doc, b: Doc) => b.mtime - a.mtime, | ||||
|   size: (a: Doc, b: Doc) => b.size - a.size | ||||
| } | ||||
| const sorted = (documents: Document[]) => { | ||||
| const sorted = (documents: Doc[]) => { | ||||
|   const cmp = sortCompare[sort.value as keyof typeof sortCompare] | ||||
|   const sorted = [...documents] | ||||
|   if (cmp) sorted.sort(cmp) | ||||
| @@ -273,7 +278,7 @@ const selectionIndeterminate = computed({ | ||||
|   get: () => { | ||||
|     return ( | ||||
|       props.documents.length > 0 && | ||||
|       props.documents.some((doc: Document) => documentStore.selected.has(doc.key)) && | ||||
|       props.documents.some((doc: Doc) => documentStore.selected.has(doc.key)) && | ||||
|       !allSelected.value | ||||
|     ) | ||||
|   }, | ||||
| @@ -284,7 +289,7 @@ const allSelected = computed({ | ||||
|   get: () => { | ||||
|     return ( | ||||
|       props.documents.length > 0 && | ||||
|       props.documents.every((doc: Document) => documentStore.selected.has(doc.key)) | ||||
|       props.documents.every((doc: Doc) => documentStore.selected.has(doc.key)) | ||||
|     ) | ||||
|   }, | ||||
|   set: (value: boolean) => { | ||||
| @@ -301,7 +306,7 @@ const allSelected = computed({ | ||||
|  | ||||
| const loc = computed(() => props.path.join('/')) | ||||
|  | ||||
| const contextMenu = (ev: Event, doc: Document) => { | ||||
| const contextMenu = (ev: Event, doc: Doc) => { | ||||
|   cursor.value = doc | ||||
|   console.log('Context menu', ev, doc) | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { Document } from '@/repositories/Document' | ||||
| import { Doc } from '@/repositories/Document' | ||||
| import { computed } from 'vue' | ||||
|  | ||||
| const datetime = computed(() => | ||||
| @@ -17,6 +17,6 @@ const tooltip = computed(() => | ||||
| ) | ||||
|  | ||||
| const props = defineProps<{ | ||||
|     doc: Document | ||||
|     doc: Doc | ||||
| }>() | ||||
| </script> | ||||
|   | ||||
| @@ -12,7 +12,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { Document } from '@/repositories/Document' | ||||
| import { Doc } from '@/repositories/Document' | ||||
| import { ref, onMounted, nextTick } from 'vue' | ||||
|  | ||||
| const input = ref<HTMLInputElement | null>(null) | ||||
| @@ -28,8 +28,8 @@ onMounted(() => { | ||||
| }) | ||||
|  | ||||
| const props = defineProps<{ | ||||
|   doc: Document | ||||
|   rename: (doc: Document, newName: string) => void | ||||
|   doc: Doc | ||||
|   rename: (doc: Doc, newName: string) => void | ||||
|   exit: () => void | ||||
| }>() | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import type { Document } from '@/repositories/Document' | ||||
| import { Doc } from '@/repositories/Document' | ||||
| import { computed } from 'vue' | ||||
|  | ||||
| const sizeClass = computed(() => { | ||||
| @@ -12,7 +12,7 @@ const sizeClass = computed(() => { | ||||
| }) | ||||
|  | ||||
| const props = defineProps<{ | ||||
|     doc: Document | ||||
|     doc: Doc | ||||
| }>() | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -16,13 +16,51 @@ type CloudFile = { | ||||
|   cloudName: string | ||||
|   cloudPos: number | ||||
| } | ||||
|  | ||||
| function pasteHandler(event: ClipboardEvent) { | ||||
|   const items = Array.from(event.clipboardData?.items ?? []) | ||||
|   const infiles = [] as File[] | ||||
|   const dirs = [] as FileSystemDirectoryEntry[] | ||||
|   for (const item of items) { | ||||
|     if (item.kind !== 'file') continue | ||||
|     const entry = item.webkitGetAsEntry() | ||||
|     if (entry?.isFile) { | ||||
|       const file = item.getAsFile() | ||||
|       if (file) infiles.push(file) | ||||
|     } else if (entry?.isDirectory) { | ||||
|       dirs.push(entry as FileSystemDirectoryEntry) | ||||
|     } | ||||
|   } | ||||
|   if (infiles.length || dirs.length) { | ||||
|     event.preventDefault() | ||||
|     uploadFiles(infiles) | ||||
|     for (const entry of dirs) pasteDirectory(entry, `${props.path!.join('/')}/${entry.name}`) | ||||
|   } | ||||
| } | ||||
| const pasteDirectory = async (entry: FileSystemDirectoryEntry, loc: string) => { | ||||
|   const reader = entry.createReader() | ||||
|   const entries = await new Promise<any[]>(resolve => reader.readEntries(resolve)) | ||||
|   const cloudfiles = [] as CloudFile[] | ||||
|   for (const entry of entries) { | ||||
|     const cloudName = `${loc}/${entry.name}` | ||||
|     if (entry.isFile) { | ||||
|       const file = await new Promise(resolve => entry.file(resolve)) as File | ||||
|       cloudfiles.push({file, cloudName, cloudPos: 0}) | ||||
|     } else if (entry.isDirectory) { | ||||
|       await pasteDirectory(entry, cloudName) | ||||
|     } | ||||
|   } | ||||
|   if (cloudfiles.length) uploadCloudFiles(cloudfiles) | ||||
| } | ||||
| function uploadHandler(event: Event) { | ||||
|   event.preventDefault() | ||||
|   event.stopPropagation() | ||||
|   // @ts-ignore | ||||
|   const infiles = Array.from(event.dataTransfer?.files || event.target.files) as File[] | ||||
|   if (!infiles.length) return | ||||
|   const input = event.target as HTMLInputElement | null | ||||
|   const infiles = Array.from((input ?? (event as DragEvent).dataTransfer)?.files ?? []) as File[] | ||||
|   if (input) input.value = '' | ||||
|   if (infiles.length) uploadFiles(infiles) | ||||
| } | ||||
|  | ||||
| const uploadFiles = (infiles: File[]) => { | ||||
|   const loc = props.path!.join('/') | ||||
|   let files = [] | ||||
|   for (const file of infiles) { | ||||
| @@ -32,6 +70,9 @@ function uploadHandler(event: Event) { | ||||
|       cloudPos: 0, | ||||
|     }) | ||||
|   } | ||||
|   uploadCloudFiles(files) | ||||
| } | ||||
| const uploadCloudFiles = (files: CloudFile[]) => { | ||||
|   const dotfiles = files.filter(f => f.cloudName.includes('/.')) | ||||
|   if (dotfiles.length) { | ||||
|     documentStore.error = "Won't upload dotfiles" | ||||
| @@ -76,7 +117,7 @@ const speed = computed(() => { | ||||
|   if (tsince > 1 / s) return 1 / tsince  // Next block is late or not coming, decay | ||||
|   return s  // "Current speed" | ||||
| }) | ||||
| const speeddisp = computed(() => speed.value ? speed.value.toFixed(speed.value < 100 ? 1 : 0) + '\u202FMB/s': 'stalled') | ||||
| const speeddisp = computed(() => speed.value ? speed.value.toFixed(speed.value < 10 ? 1 : 0) + '\u202FMB/s': 'stalled') | ||||
| setInterval(() => { | ||||
|   if (Date.now() - uprogress.tlast > 3000) { | ||||
|     // Reset | ||||
| @@ -165,10 +206,6 @@ const worker = async () => { | ||||
|   const ws = await WSCreate() | ||||
|   while (upqueue.length) { | ||||
|     const f = upqueue[0] | ||||
|     if (f.cloudPos === f.file.size) { | ||||
|       upqueue.shift() | ||||
|       continue | ||||
|     } | ||||
|     const start = f.cloudPos | ||||
|     const end = Math.min(f.file.size, start + (1<<20)) | ||||
|     const control = { name: f.cloudName, size: f.file.size, start, end } | ||||
| @@ -179,6 +216,7 @@ const worker = async () => { | ||||
|     ws.sendMsg(control) | ||||
|     // @ts-ignore | ||||
|     await ws.sendData(data) | ||||
|     if (f.cloudPos === f.file.size) upqueue.shift() | ||||
|   } | ||||
|   if (upqueue.length) startWorker() | ||||
|   uprogress.status = "idle" | ||||
| @@ -196,8 +234,10 @@ onMounted(() => { | ||||
|   // Need to prevent both to prevent browser from opening the file | ||||
|   addEventListener('dragover', uploadHandler) | ||||
|   addEventListener('drop', uploadHandler) | ||||
|   addEventListener('paste', pasteHandler) | ||||
| }) | ||||
| onUnmounted(() => { | ||||
|   removeEventListener('paste', pasteHandler) | ||||
|   removeEventListener('dragover', uploadHandler) | ||||
|   removeEventListener('drop', uploadHandler) | ||||
| }) | ||||
| @@ -219,7 +259,7 @@ onUnmounted(() => { | ||||
|           {{ (uprogress.filepos / uprogress.filesize * 100).toFixed(0) + '\u202F%' }} | ||||
|         </span> | ||||
|       </span> | ||||
|       <span class="position" v-if="uprogress.filesize > 1e7"> | ||||
|       <span class="position" v-if="uprogress.total > 1e7"> | ||||
|         {{ (uprogress.uploaded / 1e6).toFixed(0) + '\u202F/\u202F' + (uprogress.total / 1e6).toFixed(0) + '\u202FMB' }} | ||||
|       </span> | ||||
|       <span class="speed">{{ speeddisp }}</span> | ||||
|   | ||||
| @@ -1,17 +1,34 @@ | ||||
| import { formatSize, formatUnixDate, haystackFormat } from "@/utils" | ||||
|  | ||||
| export type FUID = string | ||||
|  | ||||
| export type Document = { | ||||
| export type DocProps = { | ||||
|   loc: string | ||||
|   name: string | ||||
|   key: FUID | ||||
|   size: number | ||||
|   sizedisp: string | ||||
|   mtime: number | ||||
|   modified: string | ||||
|   haystack: string | ||||
|   dir: boolean | ||||
| } | ||||
|  | ||||
| export class Doc { | ||||
|   private _name: string = "" | ||||
|   public loc: string = "" | ||||
|   public key: FUID = "" | ||||
|   public size: number = 0 | ||||
|   public mtime: number = 0 | ||||
|   public haystack: string = "" | ||||
|   public dir: boolean = false | ||||
|  | ||||
|   constructor(props: Partial<DocProps> = {}) { Object.assign(this, props) } | ||||
|   get name() { return this._name } | ||||
|   set name(name: string) { | ||||
|     this._name = name | ||||
|     this.haystack = haystackFormat(name) | ||||
|   } | ||||
|   get sizedisp(): string { return formatSize(this.size) } | ||||
|   get modified(): string { return formatUnixDate(this.mtime) } | ||||
| } | ||||
| export type errorEvent = { | ||||
|   error: { | ||||
|     code: number | ||||
| @@ -36,7 +53,7 @@ export type UpdateEntry = ['k', number] | ['d', number] | ['i', Array<FileEntry> | ||||
| // Helper structure for selections | ||||
| export interface SelectedItems { | ||||
|   keys: FUID[] | ||||
|   docs: Record<FUID, Document> | ||||
|   recursive: Array<[string, string, Document]> | ||||
|   docs: Record<FUID, Doc> | ||||
|   recursive: Array<[string, string, Doc]> | ||||
|   missing: Set<FUID> | ||||
| } | ||||
|   | ||||
| @@ -6,22 +6,26 @@ export const uploadUrl = '/api/upload' | ||||
| export const watchUrl = '/api/watch' | ||||
|  | ||||
| let tree = [] as FileEntry[] | ||||
| let reconnectDuration = 500 | ||||
| let reconnDelay = 500 | ||||
| let wsWatch = null as WebSocket | null | ||||
|  | ||||
| export const loadSession = () => { | ||||
|   const s = localStorage['cista-files'] | ||||
|   if (!s) return false | ||||
|   const store = useDocumentStore() | ||||
|   try { | ||||
|     tree = JSON.parse(sessionStorage["cista-files"]) | ||||
|     tree = JSON.parse(s) | ||||
|     store.updateRoot(tree) | ||||
|     console.log(`Loaded session with ${tree.length} items cached`) | ||||
|     return true | ||||
|   } catch (error) { | ||||
|     console.log("Loading session failed", error) | ||||
|     return false | ||||
|   } | ||||
| } | ||||
|  | ||||
| const saveSession = () => { | ||||
|   sessionStorage["cista-files"] = JSON.stringify(tree) | ||||
|   localStorage["cista-files"] = JSON.stringify(tree) | ||||
| } | ||||
|  | ||||
| export const connect = (path: string, handlers: Partial<Record<keyof WebSocketEventMap, any>>) => { | ||||
| @@ -59,7 +63,7 @@ export const watchConnect = () => { | ||||
|       console.log('Connected to backend', msg) | ||||
|       store.server = msg.server | ||||
|       store.connected = true | ||||
|       reconnectDuration = 500 | ||||
|       reconnDelay = 500 | ||||
|       store.error = '' | ||||
|       if (msg.user) store.login(msg.user.username, msg.user.privileged) | ||||
|       else if (store.isUserLogged) store.logout() | ||||
| @@ -83,10 +87,10 @@ const watchReconnect = (event: MessageEvent) => { | ||||
|     store.connected = false | ||||
|     store.error = 'Reconnecting...' | ||||
|   } | ||||
|   reconnectDuration = Math.min(5000, reconnectDuration + 500) | ||||
|   reconnDelay = Math.min(5000, reconnDelay + 500) | ||||
|   // The server closes the websocket after errors, so we need to reopen it | ||||
|   if (watchTimeout !== null) clearTimeout(watchTimeout) | ||||
|   watchTimeout = setTimeout(watchConnect, reconnectDuration) | ||||
|   watchTimeout = setTimeout(watchConnect, reconnDelay) | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,14 +1,11 @@ | ||||
| import type { Document, FileEntry, FUID, SelectedItems } from '@/repositories/Document' | ||||
| import { formatSize, formatUnixDate, haystackFormat } from '@/utils' | ||||
| import type { FileEntry, FUID, SelectedItems } from '@/repositories/Document' | ||||
| import { Doc } from '@/repositories/Document' | ||||
| import { defineStore } from 'pinia' | ||||
| import { collator } from '@/utils' | ||||
| import { logoutUser } from '@/repositories/User' | ||||
| import { watchConnect } from '@/repositories/WS' | ||||
| import { shallowRef } from 'vue' | ||||
|  | ||||
| type FileData = { id: string; mtime: number; size: number; dir: DirectoryData } | ||||
| type DirectoryData = { | ||||
|   [filename: string]: FileData | ||||
| } | ||||
| type User = { | ||||
|   username: string | ||||
|   privileged: boolean | ||||
| @@ -19,7 +16,7 @@ type User = { | ||||
| export const useDocumentStore = defineStore({ | ||||
|   id: 'documents', | ||||
|   state: () => ({ | ||||
|     document: [] as Document[], | ||||
|     document: shallowRef<Doc[]>([]), | ||||
|     selected: new Set<FUID>(), | ||||
|     fileExplorer: null as any, | ||||
|     error: '' as string, | ||||
| @@ -38,23 +35,17 @@ export const useDocumentStore = defineStore({ | ||||
|       let loc = [] as string[] | ||||
|       for (const [level, name, key, mtime, size, isfile] of root) { | ||||
|         loc = loc.slice(0, level - 1) | ||||
|         docs.push({ | ||||
|         docs.push(new Doc({ | ||||
|           name, | ||||
|           loc: level ? loc.join('/') : '/', | ||||
|           key, | ||||
|           size, | ||||
|           sizedisp: formatSize(size), | ||||
|           mtime, | ||||
|           modified: formatUnixDate(mtime), | ||||
|           haystack: haystackFormat(name), | ||||
|           dir: !isfile, | ||||
|         }) | ||||
|         })) | ||||
|         loc.push(name) | ||||
|       } | ||||
|       this.document = docs as Document[] | ||||
|     }, | ||||
|     updateModified() { | ||||
|       for (const doc of this.document) doc.modified = formatUnixDate(doc.mtime) | ||||
|       this.document = docs | ||||
|     }, | ||||
|     login(username: string, privileged: boolean) { | ||||
|       this.user.username = username | ||||
| @@ -70,6 +61,7 @@ export const useDocumentStore = defineStore({ | ||||
|       console.log("Logout") | ||||
|       await logoutUser() | ||||
|       this.$reset() | ||||
|       localStorage.clear() | ||||
|       history.go() // Reload page | ||||
|     } | ||||
|   }, | ||||
| @@ -77,12 +69,12 @@ export const useDocumentStore = defineStore({ | ||||
|     isUserLogged(): boolean { | ||||
|       return this.user.isLoggedIn | ||||
|     }, | ||||
|     recentDocuments(): Document[] { | ||||
|     recentDocuments(): Doc[] { | ||||
|       const ret = [...this.document] | ||||
|       ret.sort((a, b) => b.mtime - a.mtime) | ||||
|       return ret | ||||
|     }, | ||||
|     largeDocuments(): Document[] { | ||||
|     largeDocuments(): Doc[] { | ||||
|       const ret = [...this.document] | ||||
|       ret.sort((a, b) => b.size - a.size) | ||||
|       return ret | ||||
| @@ -107,7 +99,7 @@ export const useDocumentStore = defineStore({ | ||||
|       for (const key of selected) if (!found.has(key)) ret.missing.add(key) | ||||
|       // Build a flat list including contents recursively | ||||
|       const relnames = new Set<string>() | ||||
|       function add(rel: string, full: string, doc: Document) { | ||||
|       function add(rel: string, full: string, doc: Doc) { | ||||
|         if (!doc.dir && relnames.has(rel)) throw Error(`Multiple selections conflict for: ${rel}`) | ||||
|         relnames.add(rel) | ||||
|         ret.recursive.push([rel, full, doc]) | ||||
|   | ||||
| @@ -86,7 +86,7 @@ export function haystackFormat(str: string) { | ||||
| // Preformat search string for faster search | ||||
| export function needleFormat(query: string) { | ||||
|   const based = query.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase() | ||||
|   return {based, words: based.split(/\W+/)} | ||||
|   return {based, words: based.split(/\s+/)} | ||||
| } | ||||
|  | ||||
| // Test if haystack includes needle | ||||
|   | ||||
		Reference in New Issue
	
	Block a user