Compare commits
	
		
			3 Commits
		
	
	
		
			19a5c4ad8a
			...
			ffafbc87d0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | ffafbc87d0 | ||
|   | f0fc4a7d30 | ||
|   | e4a62e1197 | 
| @@ -17,7 +17,7 @@ import type { ComputedRef } from 'vue' | |||||||
| import type HeaderMain from '@/components/HeaderMain.vue' | import type HeaderMain from '@/components/HeaderMain.vue' | ||||||
| import { onMounted, onUnmounted, ref, watchEffect } from 'vue' | import { onMounted, onUnmounted, ref, watchEffect } from 'vue' | ||||||
| import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS' | import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS' | ||||||
| import { useDocumentStore } from '@/stores/documents' | import { useMainStore } from '@/stores/main' | ||||||
|  |  | ||||||
| import { computed } from 'vue' | import { computed } from 'vue' | ||||||
| import Router from '@/router/index' | import Router from '@/router/index' | ||||||
| @@ -27,7 +27,7 @@ interface Path { | |||||||
|   pathList: string[] |   pathList: string[] | ||||||
|   query: string |   query: string | ||||||
| } | } | ||||||
| const documentStore = useDocumentStore() | const store = useMainStore() | ||||||
| const path: ComputedRef<Path> = computed(() => { | const path: ComputedRef<Path> = computed(() => { | ||||||
|   const p = decodeURIComponent(Router.currentRoute.value.path).split('//') |   const p = decodeURIComponent(Router.currentRoute.value.path).split('//') | ||||||
|   const pathList = p[0].split('/').filter(value => value !== '') |   const pathList = p[0].split('/').filter(value => value !== '') | ||||||
| @@ -39,7 +39,7 @@ const path: ComputedRef<Path> = computed(() => { | |||||||
|   } |   } | ||||||
| }) | }) | ||||||
| watchEffect(() => { | watchEffect(() => { | ||||||
|   document.title = path.value.path.replace(/\/$/, '').split('/').pop() || documentStore.server.name || 'Cista Storage' |   document.title = path.value.path.replace(/\/$/, '').split('/').pop() || store.server.name || 'Cista Storage' | ||||||
| }) | }) | ||||||
| onMounted(loadSession) | onMounted(loadSession) | ||||||
| onMounted(watchConnect) | onMounted(watchConnect) | ||||||
| @@ -48,7 +48,7 @@ const headerMain = ref<typeof HeaderMain | null>(null) | |||||||
| let vert = 0 | let vert = 0 | ||||||
| let timer: any = null | let timer: any = null | ||||||
| const globalShortcutHandler = (event: KeyboardEvent) => { | const globalShortcutHandler = (event: KeyboardEvent) => { | ||||||
|   const fileExplorer = documentStore.fileExplorer as any |   const fileExplorer = store.fileExplorer as any | ||||||
|   if (!fileExplorer) return |   if (!fileExplorer) return | ||||||
|   const c = fileExplorer.isCursor() |   const c = fileExplorer.isCursor() | ||||||
|   const keyup = event.type === 'keyup' |   const keyup = event.type === 'keyup' | ||||||
| @@ -124,3 +124,4 @@ onUnmounted(() => { | |||||||
| }) | }) | ||||||
| export type { Path } | export type { Path } | ||||||
| </script> | </script> | ||||||
|  | @/stores/main | ||||||
|   | |||||||
| @@ -48,9 +48,11 @@ const navigate = (index: number) => { | |||||||
|   if (!link) throw Error(`No link at index ${index} (path: ${props.path})`) |   if (!link) throw Error(`No link at index ${index} (path: ${props.path})`) | ||||||
|   const url = `/${longest.value.slice(0, index).join('/')}/` |   const url = `/${longest.value.slice(0, index).join('/')}/` | ||||||
|   const here = `/${longest.value.join('/')}/` |   const here = `/${longest.value.join('/')}/` | ||||||
|  |   const current = decodeURIComponent(location.hash.slice(1).split('//')[0]) | ||||||
|  |   const u = url.replaceAll('?', '%3F').replaceAll('#', '%23') | ||||||
|  |   if (here.startsWith(current)) router.replace(u) | ||||||
|  |   else router.push(u) | ||||||
|   link.focus() |   link.focus() | ||||||
|   if (here.startsWith(location.hash.slice(1))) router.replace(url) |  | ||||||
|   else router.push(url) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| const move = (dir: number) => { | const move = (dir: number) => { | ||||||
|   | |||||||
| @@ -36,11 +36,11 @@ | |||||||
|             <input |             <input | ||||||
|               type="checkbox" |               type="checkbox" | ||||||
|               tabindex="-1" |               tabindex="-1" | ||||||
|               :checked="documentStore.selected.has(doc.key)" |               :checked="store.selected.has(doc.key)" | ||||||
|               @change=" |               @change=" | ||||||
|                 ($event.target as HTMLInputElement).checked |                 ($event.target as HTMLInputElement).checked | ||||||
|                   ? documentStore.selected.add(doc.key) |                   ? store.selected.add(doc.key) | ||||||
|                   : documentStore.selected.delete(doc.key) |                   : store.selected.delete(doc.key) | ||||||
|               " |               " | ||||||
|             /> |             /> | ||||||
|           </td> |           </td> | ||||||
| @@ -50,7 +50,7 @@ | |||||||
|             </template> |             </template> | ||||||
|             <template v-else> |             <template v-else> | ||||||
|               <a |               <a | ||||||
|                 :href="url_for(doc)" |                 :href="doc.url" | ||||||
|                 tabindex="-1" |                 tabindex="-1" | ||||||
|                 @contextmenu.prevent |                 @contextmenu.prevent | ||||||
|                 @focus.stop="cursor = doc" |                 @focus.stop="cursor = doc" | ||||||
| @@ -80,7 +80,7 @@ | |||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref, computed, watchEffect, shallowRef, onMounted, onUnmounted } from 'vue' | import { ref, computed, watchEffect, shallowRef, onMounted, onUnmounted } from 'vue' | ||||||
| import { useDocumentStore } from '@/stores/documents' | import { useMainStore } from '@/stores/main' | ||||||
| import { Doc } from '@/repositories/Document' | import { Doc } from '@/repositories/Document' | ||||||
| import FileRenameInput from './FileRenameInput.vue' | import FileRenameInput from './FileRenameInput.vue' | ||||||
| import { connect, controlUrl } from '@/repositories/WS' | import { connect, controlUrl } from '@/repositories/WS' | ||||||
| @@ -91,12 +91,8 @@ const props = defineProps<{ | |||||||
|   path: Array<string> |   path: Array<string> | ||||||
|   documents: Doc[] |   documents: Doc[] | ||||||
| }>() | }>() | ||||||
| const documentStore = useDocumentStore() | const store = useMainStore() | ||||||
| const router = useRouter() | const router = useRouter() | ||||||
| const url_for = (doc: Doc) => { |  | ||||||
|   const p = doc.loc ? `${doc.loc}/${doc.name}` : doc.name |  | ||||||
|   return doc.dir ? `#/${p}/` : `/files/${p}` |  | ||||||
| } |  | ||||||
| const cursor = shallowRef<Doc | null>(null) | const cursor = shallowRef<Doc | null>(null) | ||||||
| // File rename | // File rename | ||||||
| const editing = shallowRef<Doc | null>(null) | const editing = shallowRef<Doc | null>(null) | ||||||
| @@ -159,10 +155,10 @@ defineExpose({ | |||||||
|   cursorSelect() { |   cursorSelect() { | ||||||
|     const doc = cursor.value |     const doc = cursor.value | ||||||
|     if (!doc) return |     if (!doc) return | ||||||
|     if (documentStore.selected.has(doc.key)) { |     if (store.selected.has(doc.key)) { | ||||||
|       documentStore.selected.delete(doc.key) |       store.selected.delete(doc.key) | ||||||
|     } else { |     } else { | ||||||
|       documentStore.selected.add(doc.key) |       store.selected.add(doc.key) | ||||||
|     } |     } | ||||||
|     this.cursorMove(1) |     this.cursorMove(1) | ||||||
|   }, |   }, | ||||||
| @@ -187,8 +183,8 @@ defineExpose({ | |||||||
|       for (let p = begin; p !== end; p = increment(p, 1)) { |       for (let p = begin; p !== end; p = increment(p, 1)) { | ||||||
|         if (p === N) continue |         if (p === N) continue | ||||||
|         const key = documents[p].key |         const key = documents[p].key | ||||||
|         if (documentStore.selected.has(key)) documentStore.selected.delete(key) |         if (store.selected.has(key)) store.selected.delete(key) | ||||||
|         else documentStore.selected.add(key) |         else store.selected.add(key) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     // @ts-ignore |     // @ts-ignore | ||||||
| @@ -249,7 +245,7 @@ const mkdir = (doc: Doc, name: string) => { | |||||||
|         editing.value = null |         editing.value = null | ||||||
|       } else { |       } else { | ||||||
|         console.log('mkdir', msg) |         console.log('mkdir', msg) | ||||||
|         router.push(doc.loc ? `/${doc.loc}/${name}/` : `/${name}/`) |         router.push(doc.urlrouter) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| @@ -278,7 +274,7 @@ const selectionIndeterminate = computed({ | |||||||
|   get: () => { |   get: () => { | ||||||
|     return ( |     return ( | ||||||
|       props.documents.length > 0 && |       props.documents.length > 0 && | ||||||
|       props.documents.some((doc: Doc) => documentStore.selected.has(doc.key)) && |       props.documents.some((doc: Doc) => store.selected.has(doc.key)) && | ||||||
|       !allSelected.value |       !allSelected.value | ||||||
|     ) |     ) | ||||||
|   }, |   }, | ||||||
| @@ -289,16 +285,16 @@ const allSelected = computed({ | |||||||
|   get: () => { |   get: () => { | ||||||
|     return ( |     return ( | ||||||
|       props.documents.length > 0 && |       props.documents.length > 0 && | ||||||
|       props.documents.every((doc: Doc) => documentStore.selected.has(doc.key)) |       props.documents.every((doc: Doc) => store.selected.has(doc.key)) | ||||||
|     ) |     ) | ||||||
|   }, |   }, | ||||||
|   set: (value: boolean) => { |   set: (value: boolean) => { | ||||||
|     console.log('Setting allSelected', value) |     console.log('Setting allSelected', value) | ||||||
|     for (const doc of props.documents) { |     for (const doc of props.documents) { | ||||||
|       if (value) { |       if (value) { | ||||||
|         documentStore.selected.add(doc.key) |         store.selected.add(doc.key) | ||||||
|       } else { |       } else { | ||||||
|         documentStore.selected.delete(doc.key) |         store.selected.delete(doc.key) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -456,3 +452,4 @@ tbody .selection input { | |||||||
|   color: #888; |   color: #888; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | @/stores/main | ||||||
|   | |||||||
| @@ -1,15 +1,15 @@ | |||||||
| <template> | <template> | ||||||
|   <nav class="headermain"> |   <nav class="headermain"> | ||||||
|     <div class="buttons"> |     <div class="buttons"> | ||||||
|       <template v-if="documentStore.error"> |       <template v-if="store.error"> | ||||||
|         <div class="error-message" @click="documentStore.error = ''">{{ documentStore.error }}</div> |         <div class="error-message" @click="store.error = ''">{{ store.error }}</div> | ||||||
|         <div class="smallgap"></div> |         <div class="smallgap"></div> | ||||||
|       </template> |       </template> | ||||||
|       <UploadButton :path="props.path" /> |       <UploadButton :path="props.path" /> | ||||||
|       <SvgButton |       <SvgButton | ||||||
|         name="create-folder" |         name="create-folder" | ||||||
|         data-tooltip="New folder" |         data-tooltip="New folder" | ||||||
|         @click="() => documentStore.fileExplorer!.newFolder()" |         @click="() => store.fileExplorer!.newFolder()" | ||||||
|       /> |       /> | ||||||
|       <slot></slot> |       <slot></slot> | ||||||
|       <div class="spacer smallgap"></div> |       <div class="spacer smallgap"></div> | ||||||
| @@ -32,15 +32,19 @@ | |||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useDocumentStore } from '@/stores/documents' | import { useMainStore } from '@/stores/main' | ||||||
| import { ref, nextTick, watchEffect } from 'vue' | import { ref, nextTick, watchEffect } from 'vue' | ||||||
| import ContextMenu from '@imengyu/vue3-context-menu' | import ContextMenu from '@imengyu/vue3-context-menu' | ||||||
| import router from '@/router'; | import router from '@/router'; | ||||||
|  |  | ||||||
| const documentStore = useDocumentStore() | const store = useMainStore() | ||||||
| const showSearchInput = ref<boolean>(false) | const showSearchInput = ref<boolean>(false) | ||||||
| const search = ref<HTMLInputElement | null>() | const search = ref<HTMLInputElement | null>() | ||||||
| const searchButton = ref<HTMLButtonElement | null>() | const searchButton = ref<HTMLButtonElement | null>() | ||||||
|  |   const props = defineProps<{ | ||||||
|  |   path: Array<string> | ||||||
|  |   query: string | ||||||
|  | }>() | ||||||
|  |  | ||||||
| const closeSearch = (ev: Event) => { | const closeSearch = (ev: Event) => { | ||||||
|   if (!showSearchInput.value) return  // Already closing |   if (!showSearchInput.value) return  // Already closing | ||||||
| @@ -54,9 +58,9 @@ const updateSearch = (ev: Event) => { | |||||||
|   let p = props.path.join('/') |   let p = props.path.join('/') | ||||||
|   p = p ? `/${p}` : '' |   p = p ? `/${p}` : '' | ||||||
|   const url = q ? `${p}//${q}` : (p || '/') |   const url = q ? `${p}//${q}` : (p || '/') | ||||||
|   console.log("Update search", url) |   const u = url.replaceAll('?', '%3F').replaceAll('#', '%23') | ||||||
|   if (!props.query && q) router.push(url) |   if (!props.query && q) router.push(u) | ||||||
|   else router.replace(url) |   else router.replace(u) | ||||||
| } | } | ||||||
| const toggleSearchInput = (ev: Event) => { | const toggleSearchInput = (ev: Event) => { | ||||||
|   showSearchInput.value = !showSearchInput.value |   showSearchInput.value = !showSearchInput.value | ||||||
| @@ -72,10 +76,10 @@ watchEffect(() => { | |||||||
| const settingsMenu = (e: Event) => { | const settingsMenu = (e: Event) => { | ||||||
|   // show the context menu |   // show the context menu | ||||||
|   const items = [] |   const items = [] | ||||||
|   if (documentStore.user.isLoggedIn) { |   if (store.user.isLoggedIn) { | ||||||
|     items.push({ label: `Logout ${documentStore.user.username ?? ''}`, onClick: () => documentStore.logout() }) |     items.push({ label: `Logout ${store.user.username ?? ''}`, onClick: () => store.logout() }) | ||||||
|   } else { |   } else { | ||||||
|     items.push({ label: 'Login', onClick: () => documentStore.loginDialog() }) |     items.push({ label: 'Login', onClick: () => store.loginDialog() }) | ||||||
|   } |   } | ||||||
|   ContextMenu.showContextMenu({ |   ContextMenu.showContextMenu({ | ||||||
|     // @ts-ignore |     // @ts-ignore | ||||||
| @@ -83,11 +87,6 @@ const settingsMenu = (e: Event) => { | |||||||
|     items, |     items, | ||||||
|   }) |   }) | ||||||
| } | } | ||||||
| const props = defineProps<{ |  | ||||||
|   path: Array<string> |  | ||||||
|   query: string |  | ||||||
| }>() |  | ||||||
|  |  | ||||||
| defineExpose({ | defineExpose({ | ||||||
|   toggleSearchInput, |   toggleSearchInput, | ||||||
|   closeSearch, |   closeSearch, | ||||||
| @@ -116,3 +115,4 @@ input[type='search'] { | |||||||
|   max-width: 30vw; |   max-width: 30vw; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | @/stores/main | ||||||
|   | |||||||
| @@ -1,29 +1,29 @@ | |||||||
| <template> | <template> | ||||||
|   <template v-if="documentStore.selected.size"> |   <template v-if="store.selected.size"> | ||||||
|     <div class="smallgap"></div> |     <div class="smallgap"></div> | ||||||
|     <p class="select-text">{{ documentStore.selected.size }} selected ➤</p> |     <p class="select-text">{{ store.selected.size }} selected ➤</p> | ||||||
|     <SvgButton name="download" data-tooltip="Download" @click="download" /> |     <SvgButton name="download" data-tooltip="Download" @click="download" /> | ||||||
|     <SvgButton name="copy" data-tooltip="Copy here" @click="op('cp', dst)" /> |     <SvgButton name="copy" data-tooltip="Copy here" @click="op('cp', dst)" /> | ||||||
|     <SvgButton name="paste" data-tooltip="Move here" @click="op('mv', dst)" /> |     <SvgButton name="paste" data-tooltip="Move here" @click="op('mv', dst)" /> | ||||||
|     <SvgButton name="trash" data-tooltip="Delete ⚠️" @click="op('rm')" /> |     <SvgButton name="trash" data-tooltip="Delete ⚠️" @click="op('rm')" /> | ||||||
|     <button class="action-button unselect" data-tooltip="Unselect all" @click="documentStore.selected.clear()">❌</button> |     <button class="action-button unselect" data-tooltip="Unselect all" @click="store.selected.clear()">❌</button> | ||||||
|   </template> |   </template> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import {connect, controlUrl} from '@/repositories/WS' | import {connect, controlUrl} from '@/repositories/WS' | ||||||
| import { useDocumentStore } from '@/stores/documents' | import { useMainStore } from '@/stores/main' | ||||||
| import { computed } from 'vue' | import { computed } from 'vue' | ||||||
| import type { SelectedItems } from '@/repositories/Document' | import type { SelectedItems } from '@/repositories/Document' | ||||||
|  |  | ||||||
| const documentStore = useDocumentStore() | const store = useMainStore() | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   path: Array<string> |   path: Array<string> | ||||||
| }) | }) | ||||||
|  |  | ||||||
| const dst = computed(() => props.path!.join('/')) | const dst = computed(() => props.path!.join('/')) | ||||||
| const op = (op: string, dst?: string) => { | const op = (op: string, dst?: string) => { | ||||||
|   const sel = documentStore.selectedFiles |   const sel = store.selectedFiles | ||||||
|   const msg = { |   const msg = { | ||||||
|     op, |     op, | ||||||
|     sel: sel.keys.map(key => { |     sel: sel.keys.map(key => { | ||||||
| @@ -38,12 +38,12 @@ const op = (op: string, dst?: string) => { | |||||||
|       const res = JSON.parse(ev.data) |       const res = JSON.parse(ev.data) | ||||||
|       if ('error' in res) { |       if ('error' in res) { | ||||||
|         console.error('Control socket error', msg, res.error) |         console.error('Control socket error', msg, res.error) | ||||||
|         documentStore.error = res.error.message |         store.error = res.error.message | ||||||
|         return |         return | ||||||
|       } else if (res.status === 'ack') { |       } else if (res.status === 'ack') { | ||||||
|         console.log('Control ack OK', res) |         console.log('Control ack OK', res) | ||||||
|         control.close() |         control.close() | ||||||
|         documentStore.selected.clear() |         store.selected.clear() | ||||||
|         return |         return | ||||||
|       } else console.log('Unknown control response', msg, res) |       } else console.log('Unknown control response', msg, res) | ||||||
|     } |     } | ||||||
| @@ -108,17 +108,17 @@ const filesystemdl = async (sel: SelectedItems, handle: FileSystemDirectoryHandl | |||||||
| } | } | ||||||
|  |  | ||||||
| const download = async () => { | const download = async () => { | ||||||
|   const sel = documentStore.selectedFiles |   const sel = store.selectedFiles | ||||||
|   console.log('Download', sel) |   console.log('Download', sel) | ||||||
|   if (sel.keys.length === 0) { |   if (sel.keys.length === 0) { | ||||||
|     console.warn('Attempted download but no files found. Missing selected keys:', sel.missing) |     console.warn('Attempted download but no files found. Missing selected keys:', sel.missing) | ||||||
|     documentStore.selected.clear() |     store.selected.clear() | ||||||
|     return |     return | ||||||
|   } |   } | ||||||
|   // Plain old a href download if only one file (ignoring any folders) |   // Plain old a href download if only one file (ignoring any folders) | ||||||
|   const files = sel.recursive.filter(([rel, full, doc]) => !doc.dir) |   const files = sel.recursive.filter(([rel, full, doc]) => !doc.dir) | ||||||
|   if (files.length === 1) { |   if (files.length === 1) { | ||||||
|     documentStore.selected.clear() |     store.selected.clear() | ||||||
|     return linkdl(`/files/${files[0][1]}`) |     return linkdl(`/files/${files[0][1]}`) | ||||||
|   } |   } | ||||||
|   // Use FileSystem API if multiple files and the browser supports it |   // Use FileSystem API if multiple files and the browser supports it | ||||||
| @@ -130,7 +130,7 @@ const download = async () => { | |||||||
|         mode: 'readwrite' |         mode: 'readwrite' | ||||||
|       }) |       }) | ||||||
|       filesystemdl(sel, handle).then(() => { |       filesystemdl(sel, handle).then(() => { | ||||||
|         documentStore.selected.clear() |         store.selected.clear() | ||||||
|       }) |       }) | ||||||
|       return |       return | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
| @@ -140,7 +140,7 @@ const download = async () => { | |||||||
|   // Otherwise, zip and download |   // Otherwise, zip and download | ||||||
|   const name = sel.keys.length === 1 ? sel.docs[sel.keys[0]].name : 'download' |   const name = sel.keys.length === 1 ? sel.docs[sel.keys[0]].name : 'download' | ||||||
|   linkdl(`/zip/${Array.from(sel.keys).join('+')}/${name}.zip`) |   linkdl(`/zip/${Array.from(sel.keys).join('+')}/${name}.zip`) | ||||||
|   documentStore.selected.clear() |   store.selected.clear() | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| @@ -152,3 +152,4 @@ const download = async () => { | |||||||
|   text-overflow: ellipsis; |   text-overflow: ellipsis; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | @/stores/main | ||||||
|   | |||||||
| @@ -39,10 +39,10 @@ | |||||||
| import { reactive, ref } from 'vue' | import { reactive, ref } from 'vue' | ||||||
| import { loginUser } from '@/repositories/User' | import { loginUser } from '@/repositories/User' | ||||||
| import type { ISimpleError } from '@/repositories/Client' | import type { ISimpleError } from '@/repositories/Client' | ||||||
| import { useDocumentStore } from '@/stores/documents' | import { useMainStore } from '@/stores/main' | ||||||
|  |  | ||||||
| const confirmLoading = ref<boolean>(false) | const confirmLoading = ref<boolean>(false) | ||||||
| const store = useDocumentStore() | const store = useMainStore() | ||||||
|  |  | ||||||
| const loginForm = reactive({ | const loginForm = reactive({ | ||||||
|   username: '', |   username: '', | ||||||
| @@ -99,3 +99,4 @@ const login = async () => { | |||||||
|   height: 1em; |   height: 1em; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|  | @/stores/main | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { connect, uploadUrl } from '@/repositories/WS'; | import { connect, uploadUrl } from '@/repositories/WS'; | ||||||
| import { useDocumentStore } from '@/stores/documents' | import { useMainStore } from '@/stores/main' | ||||||
| import { collator } from '@/utils'; | import { collator } from '@/utils'; | ||||||
| import { computed, onMounted, onUnmounted, reactive, ref } from 'vue' | import { computed, onMounted, onUnmounted, reactive, ref } from 'vue' | ||||||
|  |  | ||||||
| const fileInput = ref() | const fileInput = ref() | ||||||
| const folderInput = ref() | const folderInput = ref() | ||||||
| const documentStore = useDocumentStore() | const store = useMainStore() | ||||||
| const props = defineProps({ | const props = defineProps({ | ||||||
|   path: Array<string> |   path: Array<string> | ||||||
| }) | }) | ||||||
| @@ -75,7 +75,7 @@ const uploadFiles = (infiles: File[]) => { | |||||||
| const uploadCloudFiles = (files: CloudFile[]) => { | const uploadCloudFiles = (files: CloudFile[]) => { | ||||||
|   const dotfiles = files.filter(f => f.cloudName.includes('/.')) |   const dotfiles = files.filter(f => f.cloudName.includes('/.')) | ||||||
|   if (dotfiles.length) { |   if (dotfiles.length) { | ||||||
|     documentStore.error = "Won't upload dotfiles" |     store.error = "Won't upload dotfiles" | ||||||
|     console.log("Dotfiles omitted", dotfiles) |     console.log("Dotfiles omitted", dotfiles) | ||||||
|     files = files.filter(f => !f.cloudName.includes('/.')) |     files = files.filter(f => !f.cloudName.includes('/.')) | ||||||
|   } |   } | ||||||
| @@ -171,13 +171,13 @@ const WSCreate = async () => await new Promise<WebSocket>(resolve => { | |||||||
|     open(ev: Event) { resolve(ws) }, |     open(ev: Event) { resolve(ws) }, | ||||||
|     error(ev: Event) { |     error(ev: Event) { | ||||||
|       console.error('Upload socket error', ev) |       console.error('Upload socket error', ev) | ||||||
|       documentStore.error = 'Upload socket error' |       store.error = 'Upload socket error' | ||||||
|     }, |     }, | ||||||
|     message(ev: MessageEvent) { |     message(ev: MessageEvent) { | ||||||
|       const res = JSON.parse(ev!.data) |       const res = JSON.parse(ev!.data) | ||||||
|       if ('error' in res) { |       if ('error' in res) { | ||||||
|         console.error('Upload socket error', res.error) |         console.error('Upload socket error', res.error) | ||||||
|         documentStore.error = res.error.message |         store.error = res.error.message | ||||||
|         return |         return | ||||||
|       } |       } | ||||||
|       if (res.status === 'ack') { |       if (res.status === 'ack') { | ||||||
| @@ -302,3 +302,4 @@ span { | |||||||
| .position { min-width: 4em } | .position { min-width: 4em } | ||||||
| .speed { min-width: 4em } | .speed { min-width: 4em } | ||||||
| </style> | </style> | ||||||
|  | @/stores/main | ||||||
|   | |||||||
| @@ -23,11 +23,19 @@ export class Doc { | |||||||
|   constructor(props: Partial<DocProps> = {}) { Object.assign(this, props) } |   constructor(props: Partial<DocProps> = {}) { Object.assign(this, props) } | ||||||
|   get name() { return this._name } |   get name() { return this._name } | ||||||
|   set name(name: string) { |   set name(name: string) { | ||||||
|  |     if (name.includes('/') || name.startsWith('.')) throw Error(`Invalid name: ${name}`) | ||||||
|     this._name = name |     this._name = name | ||||||
|     this.haystack = haystackFormat(name) |     this.haystack = haystackFormat(name) | ||||||
|   } |   } | ||||||
|   get sizedisp(): string { return formatSize(this.size) } |   get sizedisp(): string { return formatSize(this.size) } | ||||||
|   get modified(): string { return formatUnixDate(this.mtime) } |   get modified(): string { return formatUnixDate(this.mtime) } | ||||||
|  |   get url(): string { | ||||||
|  |     const p = this.loc ? `${this.loc}/${this.name}` : this.name | ||||||
|  |     return this.dir ? '/#/' + `${p}/`.replaceAll('#', '%23') : `/files/${p}`.replaceAll('?', '%3F').replaceAll('#', '%23') | ||||||
|  |   } | ||||||
|  |   get urlrouter(): string { | ||||||
|  |     return this.url.replace(/^\/#/, '') | ||||||
|  |   } | ||||||
| } | } | ||||||
| export type errorEvent = { | export type errorEvent = { | ||||||
|   error: { |   error: { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { useDocumentStore } from "@/stores/documents" | import { useMainStore } from "@/stores/main" | ||||||
| import type { FileEntry, UpdateEntry, errorEvent } from "./Document" | import type { FileEntry, UpdateEntry, errorEvent } from "./Document" | ||||||
|  |  | ||||||
| export const controlUrl = '/api/control' | export const controlUrl = '/api/control' | ||||||
| @@ -12,7 +12,7 @@ let wsWatch = null as WebSocket | null | |||||||
| export const loadSession = () => { | export const loadSession = () => { | ||||||
|   const s = localStorage['cista-files'] |   const s = localStorage['cista-files'] | ||||||
|   if (!s) return false |   if (!s) return false | ||||||
|   const store = useDocumentStore() |   const store = useMainStore() | ||||||
|   try { |   try { | ||||||
|     tree = JSON.parse(s) |     tree = JSON.parse(s) | ||||||
|     store.updateRoot(tree) |     store.updateRoot(tree) | ||||||
| @@ -39,7 +39,7 @@ export const watchConnect = () => { | |||||||
|     clearTimeout(watchTimeout) |     clearTimeout(watchTimeout) | ||||||
|     watchTimeout = null |     watchTimeout = null | ||||||
|   } |   } | ||||||
|   const store = useDocumentStore() |   const store = useMainStore() | ||||||
|   if (store.error !== 'Reconnecting...') store.error = 'Connecting...' |   if (store.error !== 'Reconnecting...') store.error = 'Connecting...' | ||||||
|   console.log(store.error) |   console.log(store.error) | ||||||
|  |  | ||||||
| @@ -81,7 +81,7 @@ export const watchDisconnect = () => { | |||||||
| let watchTimeout: any = null | let watchTimeout: any = null | ||||||
|  |  | ||||||
| const watchReconnect = (event: MessageEvent) => { | const watchReconnect = (event: MessageEvent) => { | ||||||
|   const store = useDocumentStore() |   const store = useMainStore() | ||||||
|   if (store.connected) { |   if (store.connected) { | ||||||
|     console.warn("Disconnected from server", event) |     console.warn("Disconnected from server", event) | ||||||
|     store.connected = false |     store.connected = false | ||||||
| @@ -114,7 +114,7 @@ const handleWatchMessage = (event: MessageEvent) => { | |||||||
| } | } | ||||||
|  |  | ||||||
| function handleRootMessage({ root }: { root: FileEntry[] }) { | function handleRootMessage({ root }: { root: FileEntry[] }) { | ||||||
|   const store = useDocumentStore() |   const store = useMainStore() | ||||||
|   console.log('Watch root', root) |   console.log('Watch root', root) | ||||||
|   store.updateRoot(root) |   store.updateRoot(root) | ||||||
|   tree = root |   tree = root | ||||||
| @@ -122,7 +122,7 @@ function handleRootMessage({ root }: { root: FileEntry[] }) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function handleUpdateMessage(updateData: { update: UpdateEntry[] }) { | function handleUpdateMessage(updateData: { update: UpdateEntry[] }) { | ||||||
|   const store = useDocumentStore() |   const store = useMainStore() | ||||||
|   const update = updateData.update |   const update = updateData.update | ||||||
|   console.log('Watch update', update) |   console.log('Watch update', update) | ||||||
|   if (!tree) return console.error('Watch update before root') |   if (!tree) return console.error('Watch update before root') | ||||||
| @@ -146,7 +146,7 @@ function handleUpdateMessage(updateData: { update: UpdateEntry[] }) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function handleError(msg: errorEvent) { | function handleError(msg: errorEvent) { | ||||||
|   const store = useDocumentStore() |   const store = useMainStore() | ||||||
|   if (msg.error.code === 401) { |   if (msg.error.code === 401) { | ||||||
|     store.user.isOpenLoginModal = true |     store.user.isOpenLoginModal = true | ||||||
|     store.user.isLoggedIn = false |     store.user.isLoggedIn = false | ||||||
|   | |||||||
| @@ -1,126 +0,0 @@ | |||||||
| 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 User = { |  | ||||||
|   username: string |  | ||||||
|   privileged: boolean |  | ||||||
|   isOpenLoginModal: boolean |  | ||||||
|   isLoggedIn: boolean |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const useDocumentStore = defineStore({ |  | ||||||
|   id: 'documents', |  | ||||||
|   state: () => ({ |  | ||||||
|     document: shallowRef<Doc[]>([]), |  | ||||||
|     selected: new Set<FUID>(), |  | ||||||
|     fileExplorer: null as any, |  | ||||||
|     error: '' as string, |  | ||||||
|     connected: false, |  | ||||||
|     server: {} as Record<string, any>, |  | ||||||
|     user: { |  | ||||||
|       username: '', |  | ||||||
|       privileged: false, |  | ||||||
|       isLoggedIn: false, |  | ||||||
|       isOpenLoginModal: false |  | ||||||
|     } as User |  | ||||||
|   }), |  | ||||||
|   actions: { |  | ||||||
|     updateRoot(root: FileEntry[]) { |  | ||||||
|       const docs = [] |  | ||||||
|       let loc = [] as string[] |  | ||||||
|       for (const [level, name, key, mtime, size, isfile] of root) { |  | ||||||
|         loc = loc.slice(0, level - 1) |  | ||||||
|         docs.push(new Doc({ |  | ||||||
|           name, |  | ||||||
|           loc: level ? loc.join('/') : '/', |  | ||||||
|           key, |  | ||||||
|           size, |  | ||||||
|           mtime, |  | ||||||
|           dir: !isfile, |  | ||||||
|         })) |  | ||||||
|         loc.push(name) |  | ||||||
|       } |  | ||||||
|       this.document = docs |  | ||||||
|     }, |  | ||||||
|     login(username: string, privileged: boolean) { |  | ||||||
|       this.user.username = username |  | ||||||
|       this.user.privileged = privileged |  | ||||||
|       this.user.isLoggedIn = true |  | ||||||
|       this.user.isOpenLoginModal = false |  | ||||||
|       if (!this.connected) watchConnect() |  | ||||||
|     }, |  | ||||||
|     loginDialog() { |  | ||||||
|       this.user.isOpenLoginModal = true |  | ||||||
|     }, |  | ||||||
|     async logout() { |  | ||||||
|       console.log("Logout") |  | ||||||
|       await logoutUser() |  | ||||||
|       this.$reset() |  | ||||||
|       localStorage.clear() |  | ||||||
|       history.go() // Reload page |  | ||||||
|     } |  | ||||||
|   }, |  | ||||||
|   getters: { |  | ||||||
|     isUserLogged(): boolean { |  | ||||||
|       return this.user.isLoggedIn |  | ||||||
|     }, |  | ||||||
|     recentDocuments(): Doc[] { |  | ||||||
|       const ret = [...this.document] |  | ||||||
|       ret.sort((a, b) => b.mtime - a.mtime) |  | ||||||
|       return ret |  | ||||||
|     }, |  | ||||||
|     largeDocuments(): Doc[] { |  | ||||||
|       const ret = [...this.document] |  | ||||||
|       ret.sort((a, b) => b.size - a.size) |  | ||||||
|       return ret |  | ||||||
|     }, |  | ||||||
|     selectedFiles(): SelectedItems { |  | ||||||
|       const selected = this.selected |  | ||||||
|       const found = new Set<FUID>() |  | ||||||
|       const ret: SelectedItems = { |  | ||||||
|         missing: new Set(), |  | ||||||
|         docs: {}, |  | ||||||
|         keys: [], |  | ||||||
|         recursive: [], |  | ||||||
|       } |  | ||||||
|       for (const doc of this.document) { |  | ||||||
|         if (selected.has(doc.key)) { |  | ||||||
|           found.add(doc.key) |  | ||||||
|           ret.keys.push(doc.key) |  | ||||||
|           ret.docs[doc.key] = doc |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       // What did we not select? |  | ||||||
|       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: Doc) { |  | ||||||
|         if (!doc.dir && relnames.has(rel)) throw Error(`Multiple selections conflict for: ${rel}`) |  | ||||||
|         relnames.add(rel) |  | ||||||
|         ret.recursive.push([rel, full, doc]) |  | ||||||
|       } |  | ||||||
|       for (const key of ret.keys) { |  | ||||||
|         const base = ret.docs[key] |  | ||||||
|         const basepath = base.loc ? `${base.loc}/${base.name}` : base.name |  | ||||||
|         const nremove = base.loc.length |  | ||||||
|         add(base.name, basepath, base) |  | ||||||
|         for (const doc of this.document) { |  | ||||||
|           if (doc.loc === basepath || doc.loc.startsWith(basepath) && doc.loc[basepath.length] === '/') { |  | ||||||
|             const full = doc.loc ? `${doc.loc}/${doc.name}` : doc.name |  | ||||||
|             const rel = full.slice(nremove) |  | ||||||
|             add(rel, full, doc) |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       // Sort by rel (name stored as on download) |  | ||||||
|       ret.recursive.sort((a, b) => collator.compare(a[0], b[0])) |  | ||||||
|  |  | ||||||
|       return ret |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| }) |  | ||||||
| @@ -10,11 +10,11 @@ | |||||||
|  |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { watchEffect, ref, computed } from 'vue' | import { watchEffect, ref, computed } from 'vue' | ||||||
| import { useDocumentStore } from '@/stores/documents' | import { useMainStore } from '@/stores/main' | ||||||
| import Router from '@/router/index' | import Router from '@/router/index' | ||||||
| import { needleFormat, localeIncludes, collator } from '@/utils'; | import { needleFormat, localeIncludes, collator } from '@/utils'; | ||||||
|  |  | ||||||
| const documentStore = useDocumentStore() | const store = useMainStore() | ||||||
| const fileExplorer = ref() | const fileExplorer = ref() | ||||||
| const props = defineProps<{ | const props = defineProps<{ | ||||||
|   path: Array<string> |   path: Array<string> | ||||||
| @@ -24,12 +24,12 @@ const documents = computed(() => { | |||||||
|   const loc = props.path.join('/') |   const loc = props.path.join('/') | ||||||
|   const query = props.query |   const query = props.query | ||||||
|   // List the current location |   // List the current location | ||||||
|   if (!query) return documentStore.document.filter(doc => doc.loc === loc) |   if (!query) return store.document.filter(doc => doc.loc === loc) | ||||||
|   // Find up to 100 newest documents that match the search |   // Find up to 100 newest documents that match the search | ||||||
|   const needle = needleFormat(query) |   const needle = needleFormat(query) | ||||||
|   let limit = 100 |   let limit = 100 | ||||||
|   let docs = [] |   let docs = [] | ||||||
|   for (const doc of documentStore.recentDocuments) { |   for (const doc of store.recentDocuments) { | ||||||
|     if (localeIncludes(doc.haystack, needle)) { |     if (localeIncludes(doc.haystack, needle)) { | ||||||
|       docs.push(doc) |       docs.push(doc) | ||||||
|       if (--limit === 0) break |       if (--limit === 0) break | ||||||
| @@ -53,6 +53,6 @@ const documents = computed(() => { | |||||||
| }) | }) | ||||||
|  |  | ||||||
| watchEffect(() => { | watchEffect(() => { | ||||||
|   documentStore.fileExplorer = fileExplorer.value |   store.fileExplorer = fileExplorer.value | ||||||
| }) | }) | ||||||
| </script> | </script> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user