Cleanup, lint, format etc.
This commit is contained in:
		
							
								
								
									
										3
									
								
								cista-front/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								cista-front/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -7,6 +7,9 @@ yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| # No locking | ||||
| package-lock.json | ||||
|  | ||||
| node_modules | ||||
| .DS_Store | ||||
| dist | ||||
|   | ||||
							
								
								
									
										2
									
								
								cista-front/components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								cista-front/components.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -8,11 +8,13 @@ export {} | ||||
| declare module 'vue' { | ||||
|   export interface GlobalComponents { | ||||
|     AppNavigation: typeof import('./src/components/AppNavigation.vue')['default'] | ||||
|     BreadCrumb: typeof import('./src/components/BreadCrumb.vue')['default'] | ||||
|     FileExplorer: typeof import('./src/components/FileExplorer.vue')['default'] | ||||
|     FileRenameInput: typeof import('./src/components/FileRenameInput.vue')['default'] | ||||
|     FileViewer: typeof import('./src/components/FileViewer.vue')['default'] | ||||
|     HeaderMain: typeof import('./src/components/HeaderMain.vue')['default'] | ||||
|     LoginModal: typeof import('./src/components/LoginModal.vue')['default'] | ||||
|     ModalDialog: typeof import('./src/components/ModalDialog.vue')['default'] | ||||
|     NotificationLoading: typeof import('./src/components/NotificationLoading.vue')['default'] | ||||
|     RouterLink: typeof import('vue-router')['RouterLink'] | ||||
|     RouterView: typeof import('vue-router')['RouterView'] | ||||
|   | ||||
							
								
								
									
										5654
									
								
								cista-front/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5654
									
								
								cista-front/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -36,8 +36,9 @@ | ||||
|     "@vue/eslint-config-typescript": "^12.0.0", | ||||
|     "@vue/test-utils": "^2.4.1", | ||||
|     "@vue/tsconfig": "^0.4.0", | ||||
|     "eslint": "^8.49.0", | ||||
|     "eslint-plugin-vue": "^9.17.0", | ||||
|     "babel-eslint": "^10.1.0", | ||||
|     "eslint": "^8.52.0", | ||||
|     "eslint-plugin-vue": "^9.18.1", | ||||
|     "jsdom": "^22.1.0", | ||||
|     "npm-run-all2": "^6.0.6", | ||||
|     "prettier": "^3.0.3", | ||||
| @@ -45,5 +46,13 @@ | ||||
|     "vite": "^4.4.9", | ||||
|     "vitest": "^0.34.4", | ||||
|     "vue-tsc": "^1.8.11" | ||||
|   }, | ||||
|   "prettier": { | ||||
|     "semi": false, | ||||
|     "singleQuote": true, | ||||
|     "trailingComma": "none", | ||||
|     "arrowParens": "avoid", | ||||
|     "endOfLine": "lf", | ||||
|     "printWidth": 88 | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,44 +1,55 @@ | ||||
| <script setup lang="ts"> | ||||
|   import { RouterView } from 'vue-router' | ||||
|   import type { ComputedRef } from 'vue' | ||||
|   import { watchEffect } from 'vue' | ||||
|   import createWebSocket from '@/repositories/WS' | ||||
|   import { url_document_watch_ws, url_document_upload_ws, DocumentHandler, DocumentUploadHandler } from '@/repositories/Document' | ||||
|   import { useDocumentStore } from '@/stores/documents' | ||||
| import { RouterView } from 'vue-router' | ||||
| import type { ComputedRef } from 'vue' | ||||
| import { watchEffect } from 'vue' | ||||
| import createWebSocket from '@/repositories/WS' | ||||
| import { | ||||
|   url_document_watch_ws, | ||||
|   url_document_upload_ws, | ||||
|   DocumentHandler, | ||||
|   DocumentUploadHandler | ||||
| } from '@/repositories/Document' | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
|  | ||||
|   import { computed } from 'vue' | ||||
|   import HeaderMain from '@/components/HeaderMain.vue' | ||||
|   import AppNavigation from '@/components/AppNavigation.vue' | ||||
|   import Router from '@/router/index'; | ||||
| import { computed } from 'vue' | ||||
| import HeaderMain from '@/components/HeaderMain.vue' | ||||
| import AppNavigation from '@/components/AppNavigation.vue' | ||||
| import Router from '@/router/index' | ||||
|  | ||||
|   interface Path { | ||||
|     path: string; | ||||
|     pathList: string[]; | ||||
| interface Path { | ||||
|   path: string | ||||
|   pathList: string[] | ||||
| } | ||||
| const documentStore = useDocumentStore() | ||||
| const path: ComputedRef<Path> = computed(() => { | ||||
|   const pathList = Router.currentRoute.value.path | ||||
|     .split('/') | ||||
|     .filter(value => value !== '') | ||||
|  | ||||
|   return { | ||||
|     path: Router.currentRoute.value.path, | ||||
|     pathList | ||||
|   } | ||||
|   const documentStore = useDocumentStore() | ||||
|   const path: ComputedRef<Path> = computed( () => { | ||||
|     const pathList = Router.currentRoute.value.path | ||||
|       .split('/') | ||||
|       .filter( value => value !== '') | ||||
| }) | ||||
| // Update human-readable x seconds ago messages from mtimes | ||||
| setInterval(documentStore.updateModified, 1000) | ||||
| watchEffect(() => { | ||||
|   const documentHandler = new DocumentHandler() | ||||
|   const documentUploadHandler = new DocumentUploadHandler() | ||||
|   const wsWatch = createWebSocket( | ||||
|     url_document_watch_ws, | ||||
|     documentHandler.handleWebSocketMessage | ||||
|   ) | ||||
|   const wsUpload = createWebSocket( | ||||
|     url_document_upload_ws, | ||||
|     documentUploadHandler.handleWebSocketMessage | ||||
|   ) | ||||
|  | ||||
|     return { | ||||
|       path: Router.currentRoute.value.path, | ||||
|       pathList | ||||
|     } | ||||
|   }) | ||||
|   // Update human-readable x seconds ago messages from mtimes | ||||
|   setInterval(documentStore.updateModified, 1000) | ||||
|   watchEffect(() => { | ||||
|     const documentHandler = new DocumentHandler() | ||||
|     const documentUploadHandler = new DocumentUploadHandler() | ||||
|     const wsWatch = createWebSocket(url_document_watch_ws, documentHandler.handleWebSocketMessage) | ||||
|     const wsUpload = createWebSocket(url_document_upload_ws, documentUploadHandler.handleWebSocketMessage) | ||||
|   documentStore.wsWatch = wsWatch | ||||
|   documentStore.wsUpload = wsUpload | ||||
| }) | ||||
|  | ||||
|     documentStore.wsWatch = wsWatch; | ||||
|     documentStore.wsUpload = wsUpload; | ||||
|   }) | ||||
|  | ||||
|   export type { Path } | ||||
| export type { Path } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
| @@ -51,14 +62,14 @@ | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
|   .wrapper{ | ||||
|     background-color: var(--header-background); | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 10px; | ||||
|   } | ||||
|   .page-container{ | ||||
|     flex-grow: 2; | ||||
|     padding: 0; | ||||
|   } | ||||
| .wrapper { | ||||
|   background-color: var(--header-background); | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 10px; | ||||
| } | ||||
| .page-container { | ||||
|   flex-grow: 2; | ||||
|   padding: 0; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,45 +1,48 @@ | ||||
| @charset "UTF-8"; | ||||
|  | ||||
| :root { | ||||
|     --primary-background: #181818; | ||||
|     --secondary-background: #ffffff; | ||||
|     --font-color: #333; | ||||
|     --header-background: #000; | ||||
|   --primary-background: #181818; | ||||
|   --secondary-background: #ffffff; | ||||
|   --font-color: #333; | ||||
|   --header-background: #000; | ||||
|   --table-background: #535353; | ||||
|   --primary-color: #ffffff; | ||||
|   --secondary-color: #ccc; | ||||
|   --blue-color: #66ffeb; | ||||
|   --red-color: #ff4d4f; | ||||
| } | ||||
| @media (prefers-color-scheme: dark) { | ||||
|   :root { | ||||
|     --primary-background: #333; | ||||
|     --secondary-background: #666; | ||||
|     --font-color: #ddd; | ||||
|     --table-background: #535353; | ||||
|     --primary-color: #ffffff; | ||||
|     --secondary-color: #ccc; | ||||
|     --blue-color: #66ffeb; | ||||
|     --red-color: #ff4d4f; | ||||
| } | ||||
| @media (prefers-color-scheme: dark) { | ||||
|     :root { | ||||
|         --primary-background: #333; | ||||
|         --secondary-background: #666; | ||||
|         --font-color: #ddd; | ||||
|         --table-background: #535353; | ||||
|         --primary-color: #ffffff; | ||||
|         --secondary-color: #ccc; | ||||
|         --blue-color: #66ffeb; | ||||
|         --red-color: #ff4d4f; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| body { | ||||
|     background-color: var(--primary-background); | ||||
|     font-family: 'Roboto', sans-serif; | ||||
|     color: var(--font-color); | ||||
|     margin: 0; | ||||
|   background-color: var(--primary-background); | ||||
|   font-family: 'Roboto', sans-serif; | ||||
|   color: var(--font-color); | ||||
|   margin: 0; | ||||
| } | ||||
| a:link, a:visited, a:active, a:hover { | ||||
|     color: var(--primary-color); | ||||
|     text-decoration: none; | ||||
| a:link, | ||||
| a:visited, | ||||
| a:active, | ||||
| a:hover { | ||||
|   color: var(--primary-color); | ||||
|   text-decoration: none; | ||||
| } | ||||
| table { | ||||
|     border-collapse: collapse; | ||||
|     border: 0; | ||||
|     gap: 0; | ||||
|   border-collapse: collapse; | ||||
|   border: 0; | ||||
|   gap: 0; | ||||
| } | ||||
| #app{ | ||||
|     height: 100%; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
| #app { | ||||
|   height: 100%; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,10 @@ | ||||
| <script setup lang="ts"> | ||||
| import { RouterLink } from 'vue-router' | ||||
| import Breadcrumb from '@/components/Breadcrumb.vue' | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     path: Array<string> | ||||
|   }>(), | ||||
|   {}, | ||||
|   {} | ||||
| ) | ||||
|  | ||||
| function generateUrl(pathIndex: number) { | ||||
|   return "/" + props.path.slice(0, pathIndex + 1).join('/') | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
| @@ -63,16 +56,17 @@ function generateUrl(pathIndex: number) { | ||||
|       {{{svg "triangle"}}} | ||||
|     </div> | ||||
|     --> | ||||
|     <Breadcrumb :path="props.path"/> | ||||
|     <BreadCrumb :path="props.path" /> | ||||
|   </nav> | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
|     nav, span{ | ||||
|       color: var(--primary-color); | ||||
|     } | ||||
|     span:hover, .last{ | ||||
|     color: var(--blue-color) | ||||
|  | ||||
|   } | ||||
| nav, | ||||
| span { | ||||
|   color: var(--primary-color); | ||||
| } | ||||
| span:hover, | ||||
| .last { | ||||
|   color: var(--blue-color); | ||||
| } | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										105
									
								
								cista-front/src/components/BreadCrumb.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								cista-front/src/components/BreadCrumb.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| <template> | ||||
|   <div class="breadcrumb"> | ||||
|     <a href="#/"><component :is="home" /></a> | ||||
|     <template v-for="(location, index) in props.path" :key="index"> | ||||
|       <a :href="`/#/${props.path.slice(0, index + 1).join('/')}/`">{{ | ||||
|         decodeURIComponent(location) | ||||
|       }}</a> | ||||
|     </template> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import home from '@/assets/svg/home.svg' | ||||
| import { withDefaults, defineProps } from 'vue' | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     path: Array<string> | ||||
|   }>(), | ||||
|   {} | ||||
| ) | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| :root { | ||||
|   --breadcrumb-background-odd: #2d2d2d; | ||||
|   --breadcrumb-background-even: #404040; | ||||
|   --breadcrumb-color: #ddd; | ||||
|   --breadcrumb-hover-color: #fff; | ||||
|   --breadcrumb-hover-background-odd: #25a; | ||||
|   --breadcrumb-hover-background-even: #812; | ||||
|   --breadcrumb-transtime: 0.3s; | ||||
| } | ||||
| .breadcrumb { | ||||
|   display: flex; | ||||
|   list-style: none; | ||||
|   margin: 0; | ||||
|   padding: 0 1em 0 0; | ||||
| } | ||||
| .breadcrumb > a { | ||||
|   margin: 0 -0.7rem 0 -0.7rem; | ||||
|   padding: 0; | ||||
|   max-width: 8em; | ||||
|   font-size: 1.3em; | ||||
|   white-space: nowrap; | ||||
|   text-overflow: ellipsis; | ||||
|   overflow: hidden; | ||||
|   height: 1em; | ||||
|   color: var(--breadcrumb-color); | ||||
|   padding: 0.3em 1.5em; | ||||
|   clip-path: polygon(0 0, 1em 50%, 0 100%, 100% 100%, 100% 0, 0 0); | ||||
|   transition: all var(--breadcrumb-transtime); | ||||
| } | ||||
| .breadcrumb a:first-child { | ||||
|   margin-left: 0; | ||||
|   padding-left: 0; | ||||
|   clip-path: none; | ||||
| } | ||||
| .breadcrumb a:last-child { | ||||
|   max-width: none; | ||||
|   clip-path: polygon( | ||||
|     0 0, | ||||
|     calc(100% - 1em) 0, | ||||
|     100% 50%, | ||||
|     calc(100% - 1em) 100%, | ||||
|     0 100%, | ||||
|     1em 50%, | ||||
|     0 0 | ||||
|   ); | ||||
| } | ||||
| .breadcrumb a:only-child { | ||||
|   clip-path: polygon( | ||||
|     0 0, | ||||
|     calc(100% - 1em) 0, | ||||
|     100% 50%, | ||||
|     calc(100% - 1em) 100%, | ||||
|     0 100%, | ||||
|     0 0 | ||||
|   ); | ||||
| } | ||||
| .breadcrumb svg { | ||||
|   /* FIXME: Custom positioning to align it well; needs proper solution */ | ||||
|   transform: translate(0.3rem, -0.3rem) scale(80%); | ||||
|   fill: var(--breadcrumb-color); | ||||
|   transition: fill var(--breadcrumb-transtime); | ||||
| } | ||||
| .breadcrumb a:nth-child(odd) { | ||||
|   background: var(--breadcrumb-background-odd); | ||||
| } | ||||
| .breadcrumb a:nth-child(even) { | ||||
|   background: var(--breadcrumb-background-even); | ||||
| } | ||||
| .breadcrumb a:nth-child(odd):hover { | ||||
|   background: var(--breadcrumb-hover-background-odd); | ||||
| } | ||||
| .breadcrumb a:nth-child(even):hover { | ||||
|   background: var(--breadcrumb-hover-background-even); | ||||
| } | ||||
| .breadcrumb a:hover { | ||||
|   color: var(--breadcrumb-hover-color); | ||||
| } | ||||
| .breadcrumb a:hover svg { | ||||
|   fill: var(--breadcrumb-hover-color); | ||||
| } | ||||
| </style> | ||||
| @@ -1,76 +0,0 @@ | ||||
| <template> | ||||
|   <div class="breadcrumb"> | ||||
|     <a href="#/"><component :is="home"/></a> | ||||
|     <template v-for="(location, index) in props.path"> | ||||
|       <a :href="`/#/${props.path.slice(0, index + 1).join('/')}/`">{{ decodeURIComponent(location) }}</a> | ||||
|     </template> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import home from '@/assets/svg/home.svg' | ||||
| import { withDefaults, defineProps } from 'vue' | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     path: Array<string> | ||||
|   }>(), | ||||
|   {}, | ||||
| ) | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   :root { | ||||
|     --breadcrumb-background-odd: #2d2d2d; | ||||
|     --breadcrumb-background-even: #404040; | ||||
|     --breadcrumb-color: #ddd; | ||||
|     --breadcrumb-hover-color: #fff; | ||||
|     --breadcrumb-hover-background-odd: #25a; | ||||
|     --breadcrumb-hover-background-even: #812; | ||||
|     --breadcrumb-transtime: 0.3s; | ||||
|   } | ||||
|   .breadcrumb { | ||||
|     display: flex; | ||||
|     list-style: none; | ||||
|     margin: 0; | ||||
|     padding: 0 1em 0 0; | ||||
|   } | ||||
|   .breadcrumb > a { | ||||
|     margin: 0 -0.7rem 0 -.7rem; | ||||
|     padding: 0; | ||||
|     max-width: 8em; | ||||
|     font-size: 1.3em; | ||||
|     white-space: nowrap; | ||||
|     text-overflow: ellipsis; | ||||
|     overflow: hidden; | ||||
|     height: 1em; | ||||
|     color: var(--breadcrumb-color); | ||||
|     padding: .3em 1.5em; | ||||
|     clip-path: polygon(0 0, 1em 50%, 0 100%, 100% 100%, 100% 0, 0 0); | ||||
|     transition: all var(--breadcrumb-transtime); | ||||
|   } | ||||
|   .breadcrumb a:first-child { | ||||
|     margin-left: 0; | ||||
|     padding-left: 0; | ||||
|     clip-path: none; | ||||
|   } | ||||
|   .breadcrumb a:last-child { | ||||
|     max-width: none; | ||||
|     clip-path: polygon(0 0, calc(100% - 1em) 0, 100% 50%, calc(100% - 1em) 100%, 0 100%, 1em 50%, 0 0); | ||||
|   } | ||||
|   .breadcrumb a:only-child { | ||||
|     clip-path: polygon(0 0, calc(100% - 1em) 0, 100% 50%, calc(100% - 1em) 100%, 0 100%, 0 0); | ||||
|   } | ||||
|   .breadcrumb svg { | ||||
|     /* FIXME: Custom positioning to align it well; needs proper solution */ | ||||
|     transform: translate(.3rem, -.3rem) scale(80%); | ||||
|     fill: var(--breadcrumb-color); | ||||
|     transition: fill var(--breadcrumb-transtime); | ||||
|   } | ||||
|   .breadcrumb a:nth-child(odd) { background: var(--breadcrumb-background-odd); } | ||||
|   .breadcrumb a:nth-child(even) { background: var(--breadcrumb-background-even) } | ||||
|   .breadcrumb a:nth-child(odd):hover { background: var(--breadcrumb-hover-background-odd); } | ||||
|   .breadcrumb a:nth-child(even):hover { background: var(--breadcrumb-hover-background-even); } | ||||
|   .breadcrumb a:hover { color: var(--breadcrumb-hover-color); } | ||||
|   .breadcrumb a:hover svg { fill: var(--breadcrumb-hover-color); } | ||||
| </style> | ||||
| @@ -1,69 +0,0 @@ | ||||
| <template> | ||||
|   <dialog ref="dialog"> | ||||
|     <h1 v-if="title">{{ title }}</h1> | ||||
|     <div> | ||||
|       <slot>Dialog with no content</slot> | ||||
|     </div> | ||||
|     <button onclick="dialog.close()">OK</button> | ||||
|   </dialog> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref, onMounted } from 'vue' | ||||
|  | ||||
| const dialog = ref<HTMLDialogElement | null>(null) | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     title: string | ||||
|   }>(), | ||||
|   { | ||||
|     title: '', | ||||
|   }, | ||||
| ) | ||||
|  | ||||
| onMounted(() => { | ||||
|   dialog.value!.showModal() | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
|   /* Style for the background */ | ||||
|   body:has(dialog[open])::before { | ||||
|     content: ''; | ||||
|     display: block; | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     background: #0008; | ||||
|     backdrop-filter: blur(.2em); | ||||
|     z-index: 1000; | ||||
|   } | ||||
|  | ||||
|   /* Hide the dialog by default */ | ||||
|   dialog[open] { | ||||
|     display: block; | ||||
|     border: none; | ||||
|     border-radius: .5rem; | ||||
|     box-shadow: .2rem .2rem 1rem #000; | ||||
|     padding: 1rem; | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     z-index: 1001; | ||||
|   } | ||||
|  | ||||
|   dialog[open] > h1 { | ||||
|     background: #00f; | ||||
|     color: #fff; | ||||
|     font-size: 1rem; | ||||
|     margin: -1rem -1rem 0 -1rem; | ||||
|     padding: .5rem 1rem .5rem 1rem; | ||||
|   } | ||||
|  | ||||
|   dialog[open] > div { | ||||
|     padding: 1em 0; | ||||
|   } | ||||
| </style> | ||||
| @@ -3,26 +3,67 @@ | ||||
|     <table v-if="props.documents.length"> | ||||
|       <thead> | ||||
|         <tr> | ||||
|           <th class="selection"><input type="checkbox" v-model="allSelected" :indeterminate="selectionIndeterminate"></th> | ||||
|           <th class="sortcolumn" :class="{sortactive: sort === 'name'}" @click="toggleSort('name')">Name</th> | ||||
|           <th class="sortcolumn modified right" :class="{sortactive: sort === 'modified'}" @click="toggleSort('modified')">Modified</th> | ||||
|           <th class="sortcolumn size right" :class="{sortactive: sort === 'size'}" @click="toggleSort('size')">Size</th> | ||||
|           <th class="selection"> | ||||
|             <input | ||||
|               type="checkbox" | ||||
|               v-model="allSelected" | ||||
|               :indeterminate="selectionIndeterminate" | ||||
|             /> | ||||
|           </th> | ||||
|           <th | ||||
|             class="sortcolumn" | ||||
|             :class="{ sortactive: sort === 'name' }" | ||||
|             @click="toggleSort('name')" | ||||
|           > | ||||
|             Name | ||||
|           </th> | ||||
|           <th | ||||
|             class="sortcolumn modified right" | ||||
|             :class="{ sortactive: sort === 'modified' }" | ||||
|             @click="toggleSort('modified')" | ||||
|           > | ||||
|             Modified | ||||
|           </th> | ||||
|           <th | ||||
|             class="sortcolumn size right" | ||||
|             :class="{ sortactive: sort === 'size' }" | ||||
|             @click="toggleSort('size')" | ||||
|           > | ||||
|             Size | ||||
|           </th> | ||||
|         </tr> | ||||
|       </thead> | ||||
|       <tbody> | ||||
|         <tr v-for="doc of sorted(props.documents as FolderDocument[])" :key="doc.key" :class="doc.type === 'folder' ? 'folder' : 'file'"> | ||||
|         <tr | ||||
|           v-for="doc of sorted(props.documents as FolderDocument[])" | ||||
|           :key="doc.key" | ||||
|           :class="doc.type === 'folder' ? 'folder' : 'file'" | ||||
|         > | ||||
|           <td class="selection"> | ||||
|             <input type="checkbox" :checked="doc.key in documentStore.selected" @change="documentStore.selected.add(doc.key)"> | ||||
|             <input | ||||
|               type="checkbox" | ||||
|               :checked="doc.key in documentStore.selected" | ||||
|               @change="documentStore.selected.add(doc.key)" | ||||
|             /> | ||||
|           </td> | ||||
|           <td class="name"> | ||||
|             <template v-if="editing === doc"><FileRenameInput :doc="doc" :rename="rename" :exit="() => { editing = null}"/></template> | ||||
|             <template v-if="editing === doc" | ||||
|               ><FileRenameInput | ||||
|                 :doc="doc" | ||||
|                 :rename="rename" | ||||
|                 :exit=" | ||||
|                   () => { | ||||
|                     editing = null | ||||
|                   } | ||||
|                 " | ||||
|             /></template> | ||||
|             <template v-else> | ||||
|               <a :href="url_for(doc)">{{doc.name}}</a> | ||||
|               <button @click="() => editing = doc">🖊️</button> | ||||
|               <a :href="url_for(doc)">{{ doc.name }}</a> | ||||
|               <button @click="() => (editing = doc)">🖊️</button> | ||||
|             </template> | ||||
|           </td> | ||||
|           <td class="right">{{doc.modified}}</td> | ||||
|           <td class="right">{{doc.sizedisp}}</td> | ||||
|           <td class="right">{{ doc.modified }}</td> | ||||
|           <td class="right">{{ doc.sizedisp }}</td> | ||||
|         </tr> | ||||
|       </tbody> | ||||
|     </table> | ||||
| @@ -35,59 +76,62 @@ | ||||
| <script setup lang="ts"> | ||||
| import { ref, computed } from 'vue' | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
| import type { Document, FolderDocument } from '@/repositories/Document'; | ||||
| import type { Document, FolderDocument } from '@/repositories/Document' | ||||
| import FileRenameInput from './FileRenameInput.vue' | ||||
| import createWebSocket from '@/repositories/WS'; | ||||
| import createWebSocket from '@/repositories/WS' | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     path: string, | ||||
|     documents: Document[], | ||||
|     path: string | ||||
|     documents: Document[] | ||||
|   }>(), | ||||
|   {}, | ||||
|   {} | ||||
| ) | ||||
|  | ||||
| const documentStore = useDocumentStore() | ||||
| const linkBasePath = computed(()=>{ | ||||
| const linkBasePath = computed(() => { | ||||
|   const path = props.path | ||||
|   return path === '/' ? '' : path | ||||
| }) | ||||
| const filesBasePath = computed(() => `/files${linkBasePath.value}`) | ||||
| const url_for = (doc: FolderDocument) => ( | ||||
|   doc.type === "folder" ? | ||||
|   `#${linkBasePath.value}/${doc.name}` : | ||||
|   `${filesBasePath.value}/${doc.name}` | ||||
| ) | ||||
| const url_for = (doc: FolderDocument) => | ||||
|   doc.type === 'folder' | ||||
|     ? `#${linkBasePath.value}/${doc.name}` | ||||
|     : `${filesBasePath.value}/${doc.name}` | ||||
| // File rename | ||||
| const editing = ref<FolderDocument | null>(null) | ||||
| const rename = (doc: FolderDocument, newName: string) => { | ||||
|   const oldName = doc.name | ||||
|   const control = createWebSocket("/api/control", (ev: MessageEvent) => { | ||||
|   const control = createWebSocket('/api/control', (ev: MessageEvent) => { | ||||
|     const msg = JSON.parse(ev.data) | ||||
|     if ("error" in msg) { | ||||
|       console.error("Rename failed", msg.error.message, msg.error) | ||||
|     if ('error' in msg) { | ||||
|       console.error('Rename failed', msg.error.message, msg.error) | ||||
|       doc.name = oldName | ||||
|     } else { | ||||
|       console.log("Rename succeeded", msg) | ||||
|       console.log('Rename succeeded', msg) | ||||
|     } | ||||
|   }) | ||||
|   control.onopen = () => { | ||||
|     control.send(JSON.stringify({ | ||||
|       "op": "rename", | ||||
|       "path": `${linkBasePath.value}/${oldName}`, | ||||
|       "to": newName | ||||
|     })) | ||||
|     control.send( | ||||
|       JSON.stringify({ | ||||
|         op: 'rename', | ||||
|         path: `${linkBasePath.value}/${oldName}`, | ||||
|         to: newName | ||||
|       }) | ||||
|     ) | ||||
|   } | ||||
|   doc.name = newName  // We should get an update from watch but this is quicker | ||||
|   doc.name = newName // We should get an update from watch but this is quicker | ||||
| } | ||||
|  | ||||
| // Column sort | ||||
| const toggleSort = (name: string) => { sort.value = sort.value === name ? "" : name } | ||||
| const sort = ref<string>("") | ||||
| const toggleSort = (name: string) => { | ||||
|   sort.value = sort.value === name ? '' : name | ||||
| } | ||||
| const sort = ref<string>('') | ||||
| const sortCompare = { | ||||
|   "name": (a: Document, b: Document) => a.name.localeCompare(b.name), | ||||
|   "modified": (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime, | ||||
|   "size": (a: FolderDocument, b: FolderDocument) => b.size - a.size | ||||
|   name: (a: Document, b: Document) => a.name.localeCompare(b.name), | ||||
|   modified: (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime, | ||||
|   size: (a: FolderDocument, b: FolderDocument) => b.size - a.size | ||||
| } | ||||
| const sorted = (documents: FolderDocument[]) => { | ||||
|   const cmp = sortCompare[sort.value as keyof typeof sortCompare] | ||||
| @@ -97,13 +141,21 @@ const sorted = (documents: FolderDocument[]) => { | ||||
| } | ||||
| const selectionIndeterminate = computed({ | ||||
|   get: () => { | ||||
|     return props.documents.length > 0 && props.documents.some((doc: Document) => doc.key in documentStore.selected) && !allSelected.value | ||||
|     return ( | ||||
|       props.documents.length > 0 && | ||||
|       props.documents.some((doc: Document) => doc.key in documentStore.selected) && | ||||
|       !allSelected.value | ||||
|     ) | ||||
|   }, | ||||
|   // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   set: (value: boolean) => {} | ||||
| }) | ||||
| const allSelected = computed({ | ||||
|   get: () => { | ||||
|     return props.documents.length > 0 && props.documents.every((doc: Document) => doc.key in documentStore.selected) | ||||
|     return ( | ||||
|       props.documents.length > 0 && | ||||
|       props.documents.every((doc: Document) => doc.key in documentStore.selected) | ||||
|     ) | ||||
|   }, | ||||
|   set: (value: boolean) => { | ||||
|     for (const doc of props.documents) { | ||||
| @@ -122,14 +174,19 @@ table { | ||||
|   width: 100%; | ||||
|   table-layout: fixed; | ||||
| } | ||||
| table input[type=checkbox] { | ||||
| table input[type='checkbox'] { | ||||
|   width: 1em; | ||||
|   height: 1em; | ||||
| } | ||||
| table .modified { width: 10em; } | ||||
| table .size { width: 6em; } | ||||
| table th, table td { | ||||
|   padding: .5em; | ||||
| table .modified { | ||||
|   width: 10em; | ||||
| } | ||||
| table .size { | ||||
|   width: 6em; | ||||
| } | ||||
| table th, | ||||
| table td { | ||||
|   padding: 0.5em; | ||||
|   font-weight: normal; | ||||
|   text-align: left; | ||||
|   white-space: nowrap; | ||||
| @@ -177,9 +234,9 @@ tbody tr:hover { | ||||
|   padding-right: 1.7em; | ||||
| } | ||||
| .sortcolumn::after { | ||||
|   content: "▸"; | ||||
|   content: '▸'; | ||||
|   color: #888; | ||||
|   margin: 0 1em 0 .5em; | ||||
|   margin: 0 1em 0 0.5em; | ||||
|   position: absolute; | ||||
|   transition: all 0.2s linear; | ||||
| } | ||||
| @@ -191,19 +248,19 @@ main { | ||||
|   padding: 5px; | ||||
|   height: 100%; | ||||
| } | ||||
| .more-action{ | ||||
| .more-action { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   justify-content: start; | ||||
| } | ||||
| .action-container{ | ||||
| .action-container { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
| .edit-action{ | ||||
| .edit-action { | ||||
|   min-width: 5%; | ||||
| } | ||||
| .carousel-container{ | ||||
| .carousel-container { | ||||
|   height: inherit; | ||||
| } | ||||
| .name a { | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| <template> | ||||
|   <input | ||||
|   ref="input" | ||||
|   id="FileRenameInput" | ||||
|   type="text" | ||||
|   :value="doc.name" | ||||
|   @keyup.esc="exit" | ||||
|   @keyup.enter="apply" | ||||
|   > | ||||
|     ref="input" | ||||
|     id="FileRenameInput" | ||||
|     type="text" | ||||
|     :value="doc.name" | ||||
|     @keyup.esc="exit" | ||||
|     @keyup.enter="apply" | ||||
|   /> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| @@ -21,11 +21,11 @@ onMounted(() => { | ||||
|   input.value!.setSelectionRange(0, ext > 0 ? ext : input.value!.value.length) | ||||
| }) | ||||
|  | ||||
| const props = defineProps < { | ||||
|     doc: FolderDocument | ||||
|     rename: (doc: FolderDocument, newName: string) => void | ||||
|     exit: () => void | ||||
| } > () | ||||
| const props = defineProps<{ | ||||
|   doc: FolderDocument | ||||
|   rename: (doc: FolderDocument, newName: string) => void | ||||
|   exit: () => void | ||||
| }>() | ||||
|  | ||||
| const apply = () => { | ||||
|   const name = input.value!.value | ||||
|   | ||||
| @@ -1,55 +1,52 @@ | ||||
| <template> | ||||
|     <object | ||||
|         v-if="props.type === 'pdf'" | ||||
|         :data= "dataURL" | ||||
|         type="application/pdf" width="100%" | ||||
|         height="100%" | ||||
|       > | ||||
|       </object> | ||||
|     <a-image | ||||
|         v-else-if="props.type === 'image'" | ||||
|         width="50%" | ||||
|         :src="dataURL" | ||||
|         @click="() => setVisible(true)" | ||||
|         :previewMask=false | ||||
|         :preview="{ | ||||
|           visibleImg, | ||||
|           onVisibleChange: setVisible, | ||||
|         }" | ||||
|       /> | ||||
|       <!-- Unknown case --> | ||||
|     <h1 v-else> | ||||
|       Unsupported file type | ||||
|       </h1> | ||||
|   <object | ||||
|     v-if="props.type === 'pdf'" | ||||
|     :data="dataURL" | ||||
|     type="application/pdf" | ||||
|     width="100%" | ||||
|     height="100%" | ||||
|   ></object> | ||||
|   <a-image | ||||
|     v-else-if="props.type === 'image'" | ||||
|     width="50%" | ||||
|     :src="dataURL" | ||||
|     @click="() => setVisible(true)" | ||||
|     :previewMask="false" | ||||
|     :preview="{ | ||||
|       visibleImg, | ||||
|       onVisibleChange: setVisible | ||||
|     }" | ||||
|   /> | ||||
|   <!-- Unknown case --> | ||||
|   <h1 v-else>Unsupported file type</h1> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { watchEffect, ref } from 'vue' | ||||
| import Router from '@/router/index'; | ||||
| import { url_document_get } from '@/repositories/Document'; | ||||
| import Router from '@/router/index' | ||||
| import { url_document_get } from '@/repositories/Document' | ||||
|  | ||||
| const dataURL = ref('') | ||||
| watchEffect(()=>{ | ||||
| watchEffect(() => { | ||||
|   dataURL.value = new URL( | ||||
|     url_document_get + Router.currentRoute.value.path, | ||||
|     location.origin | ||||
|   ).toString(); | ||||
|   ).toString() | ||||
| }) | ||||
| const emit = defineEmits({ | ||||
|     visibleImg(value: boolean){ | ||||
|         return value | ||||
|     } | ||||
|   visibleImg(value: boolean) { | ||||
|     return value | ||||
|   } | ||||
| }) | ||||
|  | ||||
| function setVisible(value: boolean) { | ||||
|   emit('visibleImg', value) | ||||
| } | ||||
|  | ||||
|  | ||||
| const props = defineProps < { | ||||
|     type?: string | ||||
|     visibleImg: boolean | ||||
| } > () | ||||
| const props = defineProps<{ | ||||
|   type?: string | ||||
|   visibleImg: boolean | ||||
| }>() | ||||
| </script> | ||||
|  | ||||
| <style></style> | ||||
|   | ||||
| @@ -2,14 +2,14 @@ | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
| import LoginModal from '@/components/LoginModal.vue' | ||||
| import UploadButton from '@/components/UploadButton.vue' | ||||
| import { ref  } from 'vue'; | ||||
| import { ref } from 'vue' | ||||
|  | ||||
| const documentStore = useDocumentStore() | ||||
| const searchQuery = ref<string>('') | ||||
| const showSearchInput = ref<boolean>(false) | ||||
|  | ||||
| const toggleSearchInput = () => { | ||||
|   showSearchInput.value = !showSearchInput.value; | ||||
|   showSearchInput.value = !showSearchInput.value | ||||
|   if (!showSearchInput.value) { | ||||
|     searchQuery.value = '' | ||||
|   } | ||||
| @@ -18,40 +18,10 @@ const toggleSearchInput = () => { | ||||
| const executeSearch = (ev: InputEvent) => { | ||||
|   // FIXME: Make reactive instead of this update handler | ||||
|   const query = (ev.target as HTMLInputElement).value | ||||
|   console.log("Searching", query) | ||||
|   console.log('Searching', query) | ||||
|   documentStore.setFilter(query) | ||||
|   console.log("Filtered") | ||||
|   console.log('Filtered') | ||||
| } | ||||
|  | ||||
| function createFileHandler() { | ||||
|   console.log("Creating file") | ||||
| } | ||||
|  | ||||
| function uploadFolderHandler() { | ||||
|   console.log("Uploading Folder") | ||||
| } | ||||
| function createFolderHandler() { | ||||
|   console.log("Uploading Folder") | ||||
| } | ||||
| function newViewHandler() { | ||||
|   console.log("Creating new view ...") | ||||
| } | ||||
| function preferencesHandler() { | ||||
|   console.log("Preferences ...") | ||||
| } | ||||
| function about() { | ||||
|   console.log("About ...") | ||||
| } | ||||
| function deleteHandler(){ | ||||
|   console.log("Delete ...") | ||||
| } | ||||
| function share(){ | ||||
|   console.log("Share ...") | ||||
| } | ||||
| function download(){ | ||||
|   console.log("Download ...") | ||||
| } | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
| @@ -88,7 +58,7 @@ function download(){ | ||||
|     <div class="actions-list"> | ||||
|       <LoginModal></LoginModal> | ||||
|       <template v-if="showSearchInput"> | ||||
|         <input type="search" v-model="searchQuery" class="margin-input"> | ||||
|         <input type="search" v-model="searchQuery" class="margin-input" /> | ||||
|       </template> | ||||
|       <!-- | ||||
|  | ||||
| @@ -108,7 +78,6 @@ function download(){ | ||||
|         <a-button @click="about" type="text" class="action-button" :icon="h(InfoCircleOutlined)" /> | ||||
|       </a-tooltip> | ||||
|       --> | ||||
|  | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -136,25 +105,24 @@ function download(){ | ||||
| } | ||||
|  | ||||
| @media only screen and (max-width: 600px) { | ||||
|  | ||||
|   .actions-container, | ||||
|   .actions-list { | ||||
|     gap: 6px; | ||||
|   } | ||||
| } | ||||
| .margin-input{ | ||||
| .margin-input { | ||||
|   margin-top: 5px; | ||||
| } | ||||
|  | ||||
| .path { | ||||
|   box-shadow: 0 0 0.5em rgba(0,0,0,.15); | ||||
|   box-shadow: 0 0 0.5em rgba(0, 0, 0, 0.15); | ||||
|   overflow: hidden; | ||||
|   white-space: nowrap; | ||||
|   width: 100%; | ||||
|   z-index: 1; | ||||
|   flex: 0 0 1.5rem; | ||||
|   order: 1; | ||||
|   font-size: .9rem; | ||||
|   font-size: 0.9rem; | ||||
|   position: relative; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,5 +1,34 @@ | ||||
| <template> | ||||
|     <!-- | ||||
|   <button v-if="store.isUserLogged" @click="logout" class="action-button"> | ||||
|     Logout | ||||
|   </button> | ||||
|   <ModalDialog v-else title="Login"> | ||||
|     <form @submit="login"> | ||||
|       <label for="username">Username:</label | ||||
|       ><input | ||||
|         id="username" | ||||
|         name="username" | ||||
|         autocomplete="username" | ||||
|         required | ||||
|         v-model="loginForm.username" | ||||
|       /> | ||||
|       <label for="password">Password:</label | ||||
|       ><input | ||||
|         id="password" | ||||
|         name="password" | ||||
|         type="password" | ||||
|         autocomplete="current-password" | ||||
|         required | ||||
|         v-model="loginForm.password" | ||||
|       /> | ||||
|       <h3 v-if="loginForm.error.length > 0" class="error-text"> | ||||
|         {{ loginForm.error }} | ||||
|       </h3> | ||||
|       <input type="submit" /> | ||||
|     </form> | ||||
|   </ModalDialog> | ||||
|  | ||||
|   <!-- | ||||
|  | ||||
|     <a-tooltip title="Login"> | ||||
|         <template v-if="DocumentStore.isUserLogged"> | ||||
| @@ -11,81 +40,68 @@ | ||||
|     </a-tooltip> | ||||
|     <a-modal v-model:open="DocumentStore.user.isOpenLoginModal" :confirm-loading="confirmLoading" okText="Login" @ok="login"> | ||||
|         <div class="login-container"> | ||||
|             <a-form :model="loginForm"> | ||||
|                 <a-form-item label="Username" prop="username" :rules="[{ required: true, message: 'Please input your username!' }]"> | ||||
|                     <a-input v-model:value="loginForm.username" /> | ||||
|                 </a-form-item> | ||||
|                 <a-form-item label="Password" prop="password" :rules="[{ required: true, message: 'Please input your password!' }]"> | ||||
|                     <a-input type="password" v-model:value="loginForm.password" /> | ||||
|                 </a-form-item> | ||||
|                 <h3 v-if="loginForm.error.length > 0" class="error-text">{{loginForm.error}}</h3> | ||||
|             </a-form> | ||||
|         </div> | ||||
|     </a-modal> | ||||
|     --> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { ref, h } from 'vue'; | ||||
| import { useDocumentStore } from '@/stores/documents'; | ||||
| import { loginUser, logoutUser } from '@/repositories/User'; | ||||
| import type { ISimpleError } from '@/repositories/Client'; | ||||
| import { ref } from 'vue' | ||||
| import { loginUser, logoutUser } from '@/repositories/User' | ||||
| import type { ISimpleError } from '@/repositories/Client' | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
|  | ||||
| const DocumentStore = useDocumentStore(); | ||||
| const confirmLoading = ref<boolean>(false); | ||||
| const confirmLoading = ref<boolean>(false) | ||||
| const store = useDocumentStore() | ||||
|  | ||||
| const showModal = () => { | ||||
|     DocumentStore.user.isOpenLoginModal = true; | ||||
| }; | ||||
| const logout = async () => { | ||||
|     try { | ||||
|         await logoutUser(); | ||||
|     } catch (error) {} finally { | ||||
|         location.reload(); | ||||
|     } | ||||
|   try { | ||||
|     await logoutUser() | ||||
|   } finally { | ||||
|     location.reload() | ||||
|   } | ||||
| } | ||||
|  | ||||
| const loginForm = ref({ | ||||
|     username: '', | ||||
|     password: '', | ||||
|     error: '', | ||||
| }); | ||||
|   username: '', | ||||
|   password: '', | ||||
|   error: '' | ||||
| }) | ||||
|  | ||||
| const login = async () => { | ||||
|     try { | ||||
|         loginForm.value.error = ''; | ||||
|         confirmLoading.value = true; | ||||
|         const user = await loginUser(loginForm.value.username, loginForm.value.password); | ||||
|         if(user){ | ||||
|             location.reload(); | ||||
|         } | ||||
|     } catch (error) { | ||||
|         const httpError = error as ISimpleError | ||||
|         if(httpError.name){ | ||||
|             loginForm.value.error = httpError.message | ||||
|         } | ||||
|     }finally{ | ||||
|         confirmLoading.value = false; | ||||
|   try { | ||||
|     loginForm.value.error = '' | ||||
|     confirmLoading.value = true | ||||
|     const user = await loginUser(loginForm.value.username, loginForm.value.password) | ||||
|     if (user) { | ||||
|       location.reload() | ||||
|     } | ||||
| }; | ||||
|   } catch (error) { | ||||
|     const httpError = error as ISimpleError | ||||
|     if (httpError.name) { | ||||
|       loginForm.value.error = httpError.message | ||||
|     } | ||||
|   } finally { | ||||
|     confirmLoading.value = false | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .login-container { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     min-height: 30vh; | ||||
|   display: grid; | ||||
|   grid-template-columns: 1fr 2fr; | ||||
|   justify-content: center; | ||||
|   align-items: center; | ||||
| } | ||||
| .button-login { | ||||
|     background-color: var(--secondary-color); | ||||
|     color: var(--secondary-background); | ||||
|   background-color: var(--secondary-color); | ||||
|   color: var(--secondary-background); | ||||
| } | ||||
| .ant-btn-primary:not(:disabled):hover{ | ||||
|     background-color: var(--blue-color); | ||||
|  | ||||
| .ant-btn-primary:not(:disabled):hover { | ||||
|   background-color: var(--blue-color); | ||||
| } | ||||
| .error-text { | ||||
|     color :var(--red-color) | ||||
|   color: var(--red-color); | ||||
| } | ||||
| </style> | ||||
|   | ||||
							
								
								
									
										71
									
								
								cista-front/src/components/ModalDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								cista-front/src/components/ModalDialog.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| <template> | ||||
|   <dialog ref="dialog"> | ||||
|     <h1 v-if="props.title">{{ props.title }}</h1> | ||||
|     <div> | ||||
|       <slot> | ||||
|         Dialog with no content | ||||
|         <button onclick="dialog.close()">OK</button> | ||||
|       </slot> | ||||
|     </div> | ||||
|   </dialog> | ||||
| </template> | ||||
|  | ||||
| <script setup lang="ts"> | ||||
| import { ref, onMounted } from 'vue' | ||||
|  | ||||
| const dialog = ref<HTMLDialogElement | null>(null) | ||||
|  | ||||
| const props = withDefaults( | ||||
|   defineProps<{ | ||||
|     title: string | ||||
|   }>(), | ||||
|   { | ||||
|     title: '' | ||||
|   } | ||||
| ) | ||||
|  | ||||
| onMounted(() => { | ||||
|   dialog.value!.showModal() | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style> | ||||
| /* Style for the background */ | ||||
| body:has(dialog[open])::before { | ||||
|   content: ''; | ||||
|   display: block; | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   background: #0008; | ||||
|   backdrop-filter: blur(0.2em); | ||||
|   z-index: 1000; | ||||
| } | ||||
|  | ||||
| /* Hide the dialog by default */ | ||||
| dialog[open] { | ||||
|   display: block; | ||||
|   border: none; | ||||
|   border-radius: 0.5rem; | ||||
|   box-shadow: 0.2rem 0.2rem 1rem #000; | ||||
|   padding: 1rem; | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   z-index: 1001; | ||||
| } | ||||
|  | ||||
| dialog[open] > h1 { | ||||
|   background: #00f; | ||||
|   color: #fff; | ||||
|   font-size: 1rem; | ||||
|   margin: -1rem -1rem 0 -1rem; | ||||
|   padding: 0.5rem 1rem 0.5rem 1rem; | ||||
| } | ||||
|  | ||||
| dialog[open] > div { | ||||
|   padding: 1em 0; | ||||
| } | ||||
| </style> | ||||
| @@ -11,17 +11,17 @@ | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
| const documentStore = useDocumentStore() | ||||
|  | ||||
| function dismissUpload(key: number){ | ||||
| function dismissUpload(key: number) { | ||||
|   documentStore.deleteUploadingDocument(key) | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .progress-container{ | ||||
| .progress-container { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
| .close-button:hover{ | ||||
| .close-button:hover { | ||||
|   color: #b81414; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
| import { h, ref } from 'vue'; | ||||
| import { h, ref } from 'vue' | ||||
|  | ||||
| const fileUploadButton = ref() | ||||
| const documentStore = useDocumentStore(); | ||||
| const open = (placement: any) => openNotification(placement); | ||||
| const documentStore = useDocumentStore() | ||||
| const open = (placement: any) => openNotification(placement) | ||||
|  | ||||
| const isNotificationOpen = ref(false); | ||||
| const isNotificationOpen = ref(false) | ||||
| const openNotification = (placement: any) => { | ||||
|   if(!isNotificationOpen.value){ | ||||
|   if (!isNotificationOpen.value) { | ||||
|     /* | ||||
|     api.open({ | ||||
|       message: `Uploading documents`, | ||||
| @@ -17,62 +17,64 @@ const openNotification = (placement: any) => { | ||||
|       duration: 0, | ||||
|       onClose: () => { isNotificationOpen.value = false } | ||||
|     });*/ | ||||
|     isNotificationOpen.value = true; | ||||
|     isNotificationOpen.value = true | ||||
|   } | ||||
| }; | ||||
|  | ||||
| } | ||||
|  | ||||
| function uploadFileHandler() { | ||||
|   fileUploadButton.value.click() | ||||
| } | ||||
|  | ||||
| async function load(file: File, start: number, end: number): Promise<ArrayBuffer> { | ||||
|   const reader = new FileReader(); | ||||
|   const load = new Promise<Event>((resolve) => (reader.onload = resolve)); | ||||
|   reader.readAsArrayBuffer(file.slice(start, end)); | ||||
|   const event = await load; | ||||
|   const reader = new FileReader() | ||||
|   const load = new Promise<Event>(resolve => (reader.onload = resolve)) | ||||
|   reader.readAsArrayBuffer(file.slice(start, end)) | ||||
|   const event = await load | ||||
|   if (event.target && event.target instanceof FileReader) { | ||||
|     return event.target.result as ArrayBuffer; | ||||
|     return event.target.result as ArrayBuffer | ||||
|   } else { | ||||
|     throw new Error('Error loading file' ); | ||||
|     throw new Error('Error loading file') | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function sendChunk(file :File, start: number, end: number) { | ||||
|   const ws = documentStore.wsUpload; | ||||
|   if(ws){ | ||||
| async function sendChunk(file: File, start: number, end: number) { | ||||
|   const ws = documentStore.wsUpload | ||||
|   if (ws) { | ||||
|     const chunk = await load(file, start, end) | ||||
|  | ||||
|     ws.send(JSON.stringify({ | ||||
|     ws.send( | ||||
|       JSON.stringify({ | ||||
|         name: file.name, | ||||
|         size: file.size, | ||||
|         start: start, | ||||
|         end: end | ||||
|       })) | ||||
|       }) | ||||
|     ) | ||||
|     ws.send(chunk) | ||||
|   } | ||||
| } | ||||
|  | ||||
| async function uploadFileChangeHandler(event: Event) { | ||||
|   const target = event.target as HTMLInputElement; | ||||
|   const target = event.target as HTMLInputElement | ||||
|   const chunkSize = 1 << 20 | ||||
|   if (target && target.files && target.files.length > 0) { | ||||
|     const file = target.files[0]; | ||||
|     const file = target.files[0] | ||||
|     const numChunks = Math.ceil(file.size / chunkSize) | ||||
|     const document = documentStore.pushUploadingDocuments(file.name) | ||||
|     open('bottomRight') | ||||
|     for (let i = 0; i < numChunks; i++) { | ||||
|       const start = i * chunkSize | ||||
|       const end = Math.min(file.size, start + chunkSize) | ||||
|       const res  = await sendChunk(file, start, end) | ||||
|       console.log( 'progress: '+ ( ( 100 * (i + 1) ) / numChunks) ) | ||||
|       console.log( 'Num Chunks: '+ numChunks ) | ||||
|       documentStore.updateUploadingDocuments(  document.key, ((100 * (i + 1) ) / numChunks)) | ||||
|       const res = await sendChunk(file, start, end) | ||||
|       console.log('progress: ' + (100 * (i + 1)) / numChunks) | ||||
|       console.log('Num Chunks: ' + numChunks) | ||||
|       documentStore.updateUploadingDocuments(document.key, (100 * (i + 1)) / numChunks) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| <template> | ||||
|   (buttons here) | ||||
|   <!-- | ||||
|  | ||||
|   <a-tooltip title="Upload files from disk"> | ||||
| @@ -84,7 +86,7 @@ async function uploadFileChangeHandler(event: Event) { | ||||
| </template> | ||||
| <style scoped> | ||||
| /* Extends styles from HeaderMain.vue too  */ | ||||
| .upload-input{ | ||||
| .upload-input { | ||||
|   display: none; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import App from './App.vue' | ||||
| import router from './router' | ||||
|  | ||||
| const app = createApp(App) | ||||
| app.config.errorHandler = (err) => { | ||||
| app.config.errorHandler = err => { | ||||
|   /* handle error */ | ||||
|   console.log(err) | ||||
| } | ||||
|   | ||||
| @@ -4,25 +4,27 @@ export const baseURL = import.meta.env.VITE_URL_DOCUMENT | ||||
| class ClientClass { | ||||
|   async post(url: string, data?: Record<string, any>): Promise<any> { | ||||
|     const res = await fetch(`${baseURL}/`, { | ||||
|       method: "POST", | ||||
|       method: 'POST', | ||||
|       headers: { | ||||
|         accept: 'application/json', | ||||
|         "content-type": 'application/json', | ||||
|         'content-type': 'application/json' | ||||
|       }, | ||||
|       body: data !== undefined ? JSON.stringify(data) : undefined, | ||||
|       body: data !== undefined ? JSON.stringify(data) : undefined | ||||
|     }) | ||||
|     return await res.json() | ||||
|     const msg = await res.json() | ||||
|     if ('error' in msg) throw new SimpleError(msg.error.code, msg.error.message) | ||||
|     return msg | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const Client = new ClientClass() | ||||
| export interface ISimpleError extends Error { | ||||
|   code : number | ||||
|   code: number | ||||
| } | ||||
|  | ||||
| class SimpleError extends Error implements ISimpleError { | ||||
|   code : number | ||||
|   constructor(code: number, message:string) { | ||||
|   code: number | ||||
|   constructor(code: number, message: string) { | ||||
|     super(message) | ||||
|     this.code = code | ||||
|   } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import type { DocumentStore } from '@/stores/documents' | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
|  | ||||
| export type FUID = string; | ||||
| export type FUID = string | ||||
|  | ||||
| type BaseDocument = { | ||||
|   name: string | ||||
|   key: FUID | ||||
| }; | ||||
| } | ||||
|  | ||||
| export type FolderDocument = BaseDocument & { | ||||
|   type: 'folder' | 'file' | ||||
| @@ -14,17 +14,17 @@ export type FolderDocument = BaseDocument & { | ||||
|   sizedisp: string | ||||
|   mtime: number | ||||
|   modified: string | ||||
| }; | ||||
| } | ||||
|  | ||||
| export type Document = FolderDocument | ||||
|  | ||||
| export type errorEvent = { | ||||
|   error: { | ||||
|     code : number; | ||||
|     message: string; | ||||
|     redirect: string; | ||||
|     code: number | ||||
|     message: string | ||||
|     redirect: string | ||||
|   } | ||||
| }; | ||||
| } | ||||
|  | ||||
| // Raw types the backend /api/watch sends us | ||||
|  | ||||
| @@ -54,41 +54,41 @@ export type UpdateEntry = { | ||||
|  | ||||
| export const url_document_watch_ws = '/api/watch' | ||||
| export const url_document_upload_ws = '/api/upload' | ||||
| export const url_document_get ='/files' | ||||
| export const url_document_get = '/files' | ||||
|  | ||||
| export class DocumentHandler { | ||||
|   constructor( private store: DocumentStore = useDocumentStore() ) { | ||||
|     this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this); | ||||
|   constructor(private store: DocumentStore = useDocumentStore()) { | ||||
|     this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this) | ||||
|   } | ||||
|  | ||||
|   handleWebSocketMessage(event: MessageEvent) { | ||||
|     const msg = JSON.parse(event.data); | ||||
|     const msg = JSON.parse(event.data) | ||||
|     switch (true) { | ||||
|       case !!msg.root: | ||||
|         this.handleRootMessage(msg); | ||||
|         break; | ||||
|         this.handleRootMessage(msg) | ||||
|         break | ||||
|       case !!msg.update: | ||||
|         this.handleUpdateMessage(msg); | ||||
|         break; | ||||
|         this.handleUpdateMessage(msg) | ||||
|         break | ||||
|       case !!msg.error: | ||||
|         this.handleError(msg); | ||||
|         break; | ||||
|         this.handleError(msg) | ||||
|         break | ||||
|       default: | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private handleRootMessage({ root }: { root: DirEntry }) { | ||||
|     if (this.store && this.store.root) { | ||||
|       this.store.user.isLoggedIn = true; | ||||
|       this.store.root = root; | ||||
|       this.store.user.isLoggedIn = true | ||||
|       this.store.root = root | ||||
|     } | ||||
|   } | ||||
|   private handleUpdateMessage(updateData: { update: UpdateEntry[] }) { | ||||
|     let node: DirEntry = this.store.root; | ||||
|     let node: DirEntry = this.store.root | ||||
|     for (const elem of updateData.update) { | ||||
|       if (elem.deleted) { | ||||
|         delete node.dir[elem.name] | ||||
|         break  // Deleted elements can't have further children | ||||
|         break // Deleted elements can't have further children | ||||
|       } | ||||
|       if (elem.name !== undefined) { | ||||
|         // @ts-ignore | ||||
| @@ -100,31 +100,31 @@ export class DocumentHandler { | ||||
|       if (elem.dir !== undefined) node.dir = elem.dir | ||||
|     } | ||||
|   } | ||||
|   private handleError(msg: errorEvent){ | ||||
|     if(msg.error.code === 401){ | ||||
|       this.store.user.isOpenLoginModal = true; | ||||
|       this.store.user.isLoggedIn = false; | ||||
|   private handleError(msg: errorEvent) { | ||||
|     if (msg.error.code === 401) { | ||||
|       this.store.user.isOpenLoginModal = true | ||||
|       this.store.user.isLoggedIn = false | ||||
|       return | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export class DocumentUploadHandler { | ||||
|   constructor( private store: DocumentStore = useDocumentStore() ) { | ||||
|     this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this); | ||||
|   constructor(private store: DocumentStore = useDocumentStore()) { | ||||
|     this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this) | ||||
|   } | ||||
|  | ||||
|   handleWebSocketMessage(event: MessageEvent) { | ||||
|     const msg = JSON.parse(event.data); | ||||
|     const msg = JSON.parse(event.data) | ||||
|     switch (true) { | ||||
|       case !!msg.written: | ||||
|         this.handleWrittenMessage(msg); | ||||
|         break; | ||||
|         this.handleWrittenMessage(msg) | ||||
|         break | ||||
|       default: | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private handleWrittenMessage(msg : { written : number}) { | ||||
|   private handleWrittenMessage(msg: { written: number }) { | ||||
|     // if (this.store && this.store.root) this.store.root = root; | ||||
|     console.log('Written message', msg.written) | ||||
|   } | ||||
|   | ||||
| @@ -1,23 +1,15 @@ | ||||
|  | ||||
| import Client from '@/repositories/Client' | ||||
| export const url_login = '/login' | ||||
| export const url_logout = '/logout ' | ||||
|  | ||||
| export async function loginUser(username : string, password: string){ | ||||
|     try { | ||||
|         const user = await Client.post(url_login, { | ||||
|             username, password | ||||
|         }) | ||||
|         return user; | ||||
|     } catch (error) { | ||||
|         throw error  | ||||
|     } | ||||
| export async function loginUser(username: string, password: string) { | ||||
|   const user = await Client.post(url_login, { | ||||
|     username, | ||||
|     password | ||||
|   }) | ||||
|   return user | ||||
| } | ||||
| export async function logoutUser() { | ||||
|   const data = await Client.post(url_logout) | ||||
|   return data | ||||
| } | ||||
| export async function logoutUser(){ | ||||
|     try { | ||||
|         const data = await Client.post(url_logout) | ||||
|         return data; | ||||
|     } catch (error) { | ||||
|         throw error  | ||||
|     } | ||||
| } | ||||
| @@ -1,8 +1,8 @@ | ||||
| function createWebSocket(url: string, eventHandler: (event: MessageEvent) => void) { | ||||
|     const urlObject = new URL(url, location.origin.replace( /^http/, 'ws')); | ||||
|     const webSocket = new WebSocket(urlObject); | ||||
|     webSocket.onmessage = eventHandler; | ||||
|     return webSocket; | ||||
|   const urlObject = new URL(url, location.origin.replace(/^http/, 'ws')) | ||||
|   const webSocket = new WebSocket(urlObject) | ||||
|   webSocket.onmessage = eventHandler | ||||
|   return webSocket | ||||
| } | ||||
|  | ||||
| export default createWebSocket | ||||
| export default createWebSocket | ||||
|   | ||||
| @@ -7,8 +7,8 @@ const router = createRouter({ | ||||
|     { | ||||
|       path: '/:pathMatch(.*)*', | ||||
|       name: 'explorer', | ||||
|       component: ExplorerView, | ||||
|     }, | ||||
|       component: ExplorerView | ||||
|     } | ||||
|   ] | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,28 +1,34 @@ | ||||
| import type { Document, DirEntry, FileEntry, FUID, DirList } from '@/repositories/Document' | ||||
| import type { | ||||
|   Document, | ||||
|   DirEntry, | ||||
|   FileEntry, | ||||
|   FUID, | ||||
|   DirList | ||||
| } from '@/repositories/Document' | ||||
| import { formatSize, formatUnixDate } from '@/utils' | ||||
| import { defineStore } from 'pinia' | ||||
| // @ts-ignore | ||||
| import { localeIncludes } from 'locale-includes' | ||||
|  | ||||
| type FileData = { id: string, mtime: number, size: number, dir: DirectoryData}; | ||||
| type FileData = { id: string; mtime: number; size: number; dir: DirectoryData } | ||||
| type DirectoryData = { | ||||
|   [filename: string]: FileData; | ||||
| }; | ||||
|   [filename: string]: FileData | ||||
| } | ||||
| type User = { | ||||
|   isOpenLoginModal: boolean, | ||||
|   isLoggedIn : boolean, | ||||
|   isOpenLoginModal: boolean | ||||
|   isLoggedIn: boolean | ||||
| } | ||||
|  | ||||
| export type DocumentStore = { | ||||
|   root: DirEntry, | ||||
|   document: Document[], | ||||
|   selected: Set<FUID>, | ||||
|   uploadingDocuments: Array<{key: number, name: string, progress: number}>, | ||||
|   uploadCount: number, | ||||
|   wsWatch: WebSocket | undefined, | ||||
|   wsUpload: WebSocket | undefined, | ||||
|   user: User, | ||||
|   error: string, | ||||
|   root: DirEntry | ||||
|   document: Document[] | ||||
|   selected: Set<FUID> | ||||
|   uploadingDocuments: Array<{ key: number; name: string; progress: number }> | ||||
|   uploadCount: number | ||||
|   wsWatch: WebSocket | undefined | ||||
|   wsUpload: WebSocket | undefined | ||||
|   user: User | ||||
|   error: string | ||||
| } | ||||
|  | ||||
| export const useDocumentStore = defineStore({ | ||||
| @@ -42,9 +48,9 @@ export const useDocumentStore = defineStore({ | ||||
|   actions: { | ||||
|     updateTable(matched: DirList) { | ||||
|       // Transform data | ||||
|       const dataMapped  = [] | ||||
|       const dataMapped = [] | ||||
|       for (const [name, attr] of Object.entries(matched)) { | ||||
|         const {id, size, mtime} = attr | ||||
|         const { id, size, mtime } = attr | ||||
|         const element: Document = { | ||||
|           name, | ||||
|           key: id, | ||||
| @@ -52,30 +58,37 @@ export const useDocumentStore = defineStore({ | ||||
|           sizedisp: formatSize(size), | ||||
|           mtime, | ||||
|           modified: formatUnixDate(mtime), | ||||
|           type: "dir" in attr ? 'folder' : 'file', | ||||
|           type: 'dir' in attr ? 'folder' : 'file' | ||||
|         } | ||||
|         dataMapped.push(element) | ||||
|       } | ||||
|       // Pre sort directory entries folders first then files, names in natural ordering | ||||
|       dataMapped.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "folder" ? -1 : 1) | ||||
|       dataMapped.sort((a, b) => | ||||
|         a.type === b.type ? a.name.localeCompare(b.name) : a.type === 'folder' ? -1 : 1 | ||||
|       ) | ||||
|       this.document = dataMapped | ||||
|     }, | ||||
|     setFilter(filter: string){ | ||||
|       function traverseDir(data: DirEntry | FileEntry, path: string){ | ||||
|         if (!("dir" in data)) return | ||||
|     setFilter(filter: string) { | ||||
|       function traverseDir(data: DirEntry | FileEntry, path: string) { | ||||
|         if (!('dir' in data)) return | ||||
|         for (const [name, attr] of Object.entries(data.dir)) { | ||||
|           const fullname = `${path}/${name}` | ||||
|           if (localeIncludes(name, filter, {usage: "search", sensitivity: "base"})) { | ||||
|             matched[fullname.slice(1)] = attr  // No initial slash on name | ||||
|           if ( | ||||
|             localeIncludes(name, filter, { | ||||
|               usage: 'search', | ||||
|               sensitivity: 'base' | ||||
|             }) | ||||
|           ) { | ||||
|             matched[fullname.slice(1)] = attr // No initial slash on name | ||||
|           } | ||||
|           traverseDir(attr, fullname) | ||||
|         } | ||||
|       } | ||||
|       const matched: any = {} | ||||
|       traverseDir(this.root, "") | ||||
|       traverseDir(this.root, '') | ||||
|       this.updateTable(matched) | ||||
|     }, | ||||
|     setActualDocument(location: string){ | ||||
|     setActualDocument(location: string) { | ||||
|       location = decodeURIComponent(location) | ||||
|       let data: FileEntry | DirEntry = this.root | ||||
|       const actualDirArr = [] | ||||
| @@ -83,27 +96,32 @@ export const useDocumentStore = defineStore({ | ||||
|         // Navigate to target folder | ||||
|         for (const dirname of location.split('/').slice(1)) { | ||||
|           if (!dirname) continue | ||||
|           if (!("dir" in data)) throw Error("Target folder not available") | ||||
|           if (!('dir' in data)) throw Error('Target folder not available') | ||||
|           actualDirArr.push(dirname) | ||||
|           data = data.dir[dirname] | ||||
|         } | ||||
|       } catch (error) { | ||||
|         console.error("Cannot show requested folder", location, actualDirArr.join('/'), error) | ||||
|         console.error( | ||||
|           'Cannot show requested folder', | ||||
|           location, | ||||
|           actualDirArr.join('/'), | ||||
|           error | ||||
|         ) | ||||
|       } | ||||
|       if (!("dir" in data)) { | ||||
|       if (!('dir' in data)) { | ||||
|         // Target folder not available | ||||
|         this.document = [] | ||||
|         return | ||||
|       } | ||||
|       this.updateTable(data.dir) | ||||
|     }, | ||||
|     updateUploadingDocuments(key: number, progress: number){ | ||||
|     updateUploadingDocuments(key: number, progress: number) { | ||||
|       for (const d of this.uploadingDocuments) { | ||||
|         if(d.key === key) d.progress = progress | ||||
|         if (d.key === key) d.progress = progress | ||||
|       } | ||||
|     }, | ||||
|     pushUploadingDocuments(name: string){ | ||||
|       this.uploadCount++; | ||||
|     pushUploadingDocuments(name: string) { | ||||
|       this.uploadCount++ | ||||
|       const document = { | ||||
|         key: this.uploadCount, | ||||
|         name: name, | ||||
| @@ -112,21 +130,21 @@ export const useDocumentStore = defineStore({ | ||||
|       this.uploadingDocuments.push(document) | ||||
|       return document | ||||
|     }, | ||||
|     deleteUploadingDocument(key: number){ | ||||
|       this.uploadingDocuments = this.uploadingDocuments.filter((e)=> e.key !== key) | ||||
|     deleteUploadingDocument(key: number) { | ||||
|       this.uploadingDocuments = this.uploadingDocuments.filter(e => e.key !== key) | ||||
|     }, | ||||
|     updateModified() { | ||||
|       for (const d of this.document) { | ||||
|         if ("mtime" in d) d.modified = formatUnixDate(d.mtime) | ||||
|         if ('mtime' in d) d.modified = formatUnixDate(d.mtime) | ||||
|       } | ||||
|     }, | ||||
|     } | ||||
|   }, | ||||
|   getters: { | ||||
|     mainDocument(): Document[] { | ||||
|       return this.document; | ||||
|       return this.document | ||||
|     }, | ||||
|     isUserLogged(): boolean{ | ||||
|     isUserLogged(): boolean { | ||||
|       return this.user.isLoggedIn | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
|   } | ||||
| }) | ||||
|   | ||||
| @@ -1,68 +1,75 @@ | ||||
| export function determineFileType(inputString: string): "file" | "folder" { | ||||
|     if (inputString.includes('.') && !inputString.endsWith('.')) { | ||||
|       return 'file'; | ||||
|     } else { | ||||
|       return 'folder'; | ||||
|     } | ||||
| export function determineFileType(inputString: string): 'file' | 'folder' { | ||||
|   if (inputString.includes('.') && !inputString.endsWith('.')) { | ||||
|     return 'file' | ||||
|   } else { | ||||
|     return 'folder' | ||||
|   } | ||||
| } | ||||
|  | ||||
| export function formatSize(size: number) { | ||||
|   if (size === 0) return 'empty' | ||||
|   for (const unit of [null, 'kB', 'MB', 'GB', 'TB', 'PB', 'EB']) { | ||||
|     if (size < 1e4) return size.toLocaleString().replace(',', '\u202F') + (unit ? `\u202F${unit}` : '') | ||||
|     if (size < 1e4) | ||||
|       return ( | ||||
|         size.toLocaleString().replace(',', '\u202F') + (unit ? `\u202F${unit}` : '') | ||||
|       ) | ||||
|     size = Math.round(size / 1000) | ||||
|   } | ||||
|   return "huge" | ||||
|   return 'huge' | ||||
| } | ||||
|  | ||||
| export function formatUnixDate(t: number) { | ||||
|   const date = new Date(t * 1000) | ||||
|   const now = new Date() | ||||
|   const diff = date.getTime() - now.getTime() | ||||
|   const formatter = new Intl.RelativeTimeFormat('en', { numeric: | ||||
| 'auto' }) | ||||
|   const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) | ||||
|   if (Math.abs(diff) <= 5000) { | ||||
|       return 'now' | ||||
|     return 'now' | ||||
|   } | ||||
|   if (Math.abs(diff) <= 60000) { | ||||
|       return formatter.format(Math.round(diff / 1000), 'second') | ||||
|     return formatter.format(Math.round(diff / 1000), 'second') | ||||
|   } | ||||
|  | ||||
|   if (Math.abs(diff) <= 3600000) { | ||||
|       return formatter.format(Math.round(diff / 60000), 'minute') | ||||
|     return formatter.format(Math.round(diff / 60000), 'minute') | ||||
|   } | ||||
|  | ||||
|   if (Math.abs(diff) <= 86400000) { | ||||
|       return formatter.format(Math.round(diff / 3600000), 'hour') | ||||
|     return formatter.format(Math.round(diff / 3600000), 'hour') | ||||
|   } | ||||
|  | ||||
|   if (Math.abs(diff) <= 604800000) { | ||||
|       return formatter.format(Math.round(diff / 86400000), 'day') | ||||
|     return formatter.format(Math.round(diff / 86400000), 'day') | ||||
|   } | ||||
|  | ||||
|   return date.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' }) | ||||
|   return date.toLocaleDateString(undefined, { | ||||
|     weekday: 'short', | ||||
|     year: 'numeric', | ||||
|     month: 'short', | ||||
|     day: 'numeric' | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export function getFileExtension(filename: string) { | ||||
|   const parts = filename.split("."); | ||||
|   const parts = filename.split('.') | ||||
|   if (parts.length > 1) { | ||||
|     return parts[parts.length - 1]; | ||||
|     return parts[parts.length - 1] | ||||
|   } else { | ||||
|     return ""; // No hay extensión | ||||
|     return '' // No hay extensión | ||||
|   } | ||||
| } | ||||
| export function getFileType(extension: string): string { | ||||
|   const videoExtensions = ["mp4", "avi", "mkv", "mov"]; | ||||
|   const imageExtensions = ["jpg", "jpeg", "png", "gif"]; | ||||
|   const pdfExtensions = ["pdf"]; | ||||
|   const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'] | ||||
|   const imageExtensions = ['jpg', 'jpeg', 'png', 'gif'] | ||||
|   const pdfExtensions = ['pdf'] | ||||
|  | ||||
|   if (videoExtensions.includes(extension)) { | ||||
|     return "video"; | ||||
|     return 'video' | ||||
|   } else if (imageExtensions.includes(extension)) { | ||||
|     return "image"; | ||||
|     return 'image' | ||||
|   } else if (pdfExtensions.includes(extension)) { | ||||
|     return "pdf"; | ||||
|     return 'pdf' | ||||
|   } else { | ||||
|     return "unknown"; | ||||
|     return 'unknown' | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -16,8 +16,8 @@ | ||||
| <script setup lang="ts"> | ||||
| import { watchEffect } from 'vue' | ||||
| import { useDocumentStore } from '@/stores/documents' | ||||
| import Router from '@/router/index'; | ||||
| import FileExplorer from '@/components/FileExplorer.vue'; | ||||
| import Router from '@/router/index' | ||||
| import FileExplorer from '@/components/FileExplorer.vue' | ||||
|  | ||||
| const documentStore = useDocumentStore() | ||||
|  | ||||
| @@ -26,21 +26,23 @@ watchEffect(async () => { | ||||
|   documentStore.setActualDocument(path.toString()) | ||||
| }) | ||||
|  | ||||
| function beforeEnter(el) { | ||||
|   el.style.transform = 'translateX(100%)' | ||||
| function beforeEnter(el: Element) { | ||||
|   const elem = el as HTMLElement | ||||
|   elem.style.transform = 'translateX(100%)' | ||||
| } | ||||
|  | ||||
| function enter(el, done) { | ||||
| function enter(el: Element, done: () => void) { | ||||
|   const elem = el as HTMLElement | ||||
|   setTimeout(() => { | ||||
|     el.style.transform = 'translateX(0)' | ||||
|     elem.style.transform = 'translateX(0)' | ||||
|     done() | ||||
|   }, 0) | ||||
| } | ||||
| function leave(el, done) { | ||||
|   el.style.transform = 'translateX(-100%)' | ||||
| function leave(el: Element, done: () => void) { | ||||
|   const elem = el as HTMLElement | ||||
|   elem.style.transform = 'translateX(-100%)' | ||||
|   setTimeout(done, 300) // Assuming 300ms is your transition duration | ||||
| } | ||||
|  | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import vue from '@vitejs/plugin-vue' | ||||
| // @ts-ignore | ||||
| import pluginRewriteAll from 'vite-plugin-rewrite-all' | ||||
| import svgLoader from 'vite-svg-loader' | ||||
| import Components from 'unplugin-vue-components/vite' | ||||
|  | ||||
| // Development mode: | ||||
| // npm run dev           # Run frontend that proxies to dev_backend | ||||
| @@ -21,7 +22,8 @@ export default defineConfig({ | ||||
|   plugins: [ | ||||
|     vue(), | ||||
|     pluginRewriteAll(), | ||||
|     svgLoader(), | ||||
|     svgLoader(),          // import svg files | ||||
|     Components(),         // auto import components | ||||
|   ], | ||||
|   css: { | ||||
|     preprocessorOptions: { | ||||
|   | ||||
							
								
								
									
										9
									
								
								cista/wwwroot/assets/index-2034a7a8.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								cista/wwwroot/assets/index-2034a7a8.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								cista/wwwroot/assets/index-c3cea0a2.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								cista/wwwroot/assets/index-c3cea0a2.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1 +0,0 @@ | ||||
| @charset "UTF-8";:root{--primary-background: #181818;--secondary-background: #ffffff;--font-color: #333333;--table-background: #535353;--primary-color: #ffffff;--secondary-color: #ccc;--blue-color: #66ffeb;--red-color: #ff4d4f}@media (prefers-color-scheme: dark){:root{--primary-background: #333;--secondary-background: #666;--font-color: #ffffff;--table-background: #535353;--primary-color: #ffffff;--secondary-color: #ccc;--blue-color: #66ffeb;--red-color: #ff4d4f}}body{background-color:var(--primary-background);font-family:Roboto,sans-serif;margin:0}a:link,a:visited,a:active,a:hover{color:var(--primary-color);text-decoration:none}table{border-collapse:collapse;border:0;gap:0}#app{height:100%;display:flex;flex-direction:column;background-color:var(--secondary-background)}.login-container[data-v-b14929a5]{display:flex;justify-content:center;align-items:center;min-height:30vh}.button-login[data-v-b14929a5]{background-color:var(--secondary-color);color:var(--secondary-background)}.ant-btn-primary[data-v-b14929a5]:not(:disabled):hover{background-color:var(--blue-color)}.error-text[data-v-b14929a5]{color:var(--red-color)}.upload-input[data-v-8cbd2944]{display:none}.actions-container,.actions-list{display:flex;flex-wrap:nowrap;gap:15px}.actions-container{justify-content:space-between}.action-button{padding:0;font-size:1.5em;color:var(--secondary-color)}.action-button:hover{color:var(--blue-color)!important}@media only screen and (max-width: 600px){.actions-container,.actions-list{gap:6px}}.margin-input{margin-top:5px}.path{box-shadow:0 0 .5em #00000026;overflow:hidden;white-space:nowrap;width:100%;z-index:1;flex:0 0 1.5rem;order:1;font-size:.9rem;position:relative}nav[data-v-953811b0],span[data-v-953811b0]{color:var(--primary-color)}span[data-v-953811b0]:hover,.last[data-v-953811b0]{color:var(--blue-color)}input#FileRenameInput{color:#8f8;border:0;padding:0;width:90%;outline:none;background:transparent}table{width:100%;table-layout:fixed}table input[type=checkbox]{width:1em;height:1em}table .modified{width:10em}table .size{width:6em}table th,table td{padding:.5em;font-weight:400;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.name{white-space:nowrap;text-overflow:initial;overflow:initial}.name button{visibility:hidden;padding-left:1em}.name:hover button{visibility:visible}.name button{cursor:pointer;border:0;background:transparent}thead tr{border:1px solid #ddd;background:#ddd}tbody tr{background:#444;color:#ddd}tbody tr:hover{background:#00f8}.right{text-align:right}.selection{width:2em}.sortcolumn:hover{cursor:pointer}.sortcolumn:hover:after{color:#f80}.sortcolumn{padding-right:1.7em}.sortcolumn:after{content:"▸";color:#888;margin:0 1em 0 .5em;position:absolute;transition:transform .2s linear}.sortactive:after{transform:rotate(90deg);color:#000}main{padding:5px;height:100%}.more-action{display:flex;flex-direction:column;justify-content:start}.action-container{display:flex;align-items:center}.edit-action{min-width:5%}.carousel-container{height:inherit}.name a{text-decoration:none}.file .name:before{content:"📄 ";font-size:1.5em}.folder .name:before{content:"📁 ";font-size:1.5em}.wrapper[data-v-aa2747c4]{background-color:var(--primary-background);padding:.2em .5em;display:flex;flex-direction:column;gap:10px}.page-container[data-v-aa2747c4]{flex-grow:2;padding:0} | ||||
| @@ -1,7 +1,7 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang=en> | ||||
| <script type="module" crossorigin src="/assets/index-68773a87.js"></script> | ||||
| <link rel="stylesheet" href="/assets/index-d4bfeeb6.css"> | ||||
| <script type="module" crossorigin src="/assets/index-2034a7a8.js"></script> | ||||
| <link rel="stylesheet" href="/assets/index-c3cea0a2.css"> | ||||
|  | ||||
| <meta charset=UTF-8> | ||||
| <title>Cista</title> | ||||
| @@ -10,5 +10,6 @@ | ||||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||||
| <link href="https://fonts.googleapis.com/css2?family=Noto+Emoji&family=Roboto:wght@400;700&display=swap" rel="stylesheet"> | ||||
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
| <div id="app"></div> | ||||
|  | ||||
|  | ||||
| <div id="app"></div> | ||||
|   | ||||
							
								
								
									
										17
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,17 +0,0 @@ | ||||
| { | ||||
|   "name": "cista-storage", | ||||
|   "lockfileVersion": 3, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "dependencies": { | ||||
|         "locale-includes": "^1.0.5" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/locale-includes": { | ||||
|       "version": "1.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/locale-includes/-/locale-includes-1.0.5.tgz", | ||||
|       "integrity": "sha512-8pcOkyBbMZvHGskk3gbi+o6dYSOmkLJ+hh1lle+LaULxB2YtwNrCMEhgpAJb3WruTUC2cSEu71bOe6im6DuCuA==" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| { | ||||
|   "dependencies": { | ||||
|     "locale-includes": "^1.0.5" | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Leo Vasanko
					Leo Vasanko