Frontend created and rewritten a few times, with some backend fixes #1

Merged
leo merged 110 commits from plaintable into main 2023-11-08 20:38:40 +00:00
35 changed files with 727 additions and 6286 deletions
Showing only changes of commit 119aba2b3c - Show all commits

View File

@ -7,6 +7,9 @@ yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
# No locking
package-lock.json
node_modules node_modules
.DS_Store .DS_Store
dist dist

View File

@ -8,11 +8,13 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AppNavigation: typeof import('./src/components/AppNavigation.vue')['default'] AppNavigation: typeof import('./src/components/AppNavigation.vue')['default']
BreadCrumb: typeof import('./src/components/BreadCrumb.vue')['default']
FileExplorer: typeof import('./src/components/FileExplorer.vue')['default'] FileExplorer: typeof import('./src/components/FileExplorer.vue')['default']
FileRenameInput: typeof import('./src/components/FileRenameInput.vue')['default'] FileRenameInput: typeof import('./src/components/FileRenameInput.vue')['default']
FileViewer: typeof import('./src/components/FileViewer.vue')['default'] FileViewer: typeof import('./src/components/FileViewer.vue')['default']
HeaderMain: typeof import('./src/components/HeaderMain.vue')['default'] HeaderMain: typeof import('./src/components/HeaderMain.vue')['default']
LoginModal: typeof import('./src/components/LoginModal.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'] NotificationLoading: typeof import('./src/components/NotificationLoading.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']

File diff suppressed because it is too large Load Diff

View File

@ -36,8 +36,9 @@
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"@vue/test-utils": "^2.4.1", "@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0", "@vue/tsconfig": "^0.4.0",
"eslint": "^8.49.0", "babel-eslint": "^10.1.0",
"eslint-plugin-vue": "^9.17.0", "eslint": "^8.52.0",
"eslint-plugin-vue": "^9.18.1",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"npm-run-all2": "^6.0.6", "npm-run-all2": "^6.0.6",
"prettier": "^3.0.3", "prettier": "^3.0.3",
@ -45,5 +46,13 @@
"vite": "^4.4.9", "vite": "^4.4.9",
"vitest": "^0.34.4", "vitest": "^0.34.4",
"vue-tsc": "^1.8.11" "vue-tsc": "^1.8.11"
},
"prettier": {
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"arrowParens": "avoid",
"endOfLine": "lf",
"printWidth": 88
} }
} }

View File

@ -1,44 +1,55 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import { watchEffect } from 'vue' import { watchEffect } from 'vue'
import createWebSocket from '@/repositories/WS' import createWebSocket from '@/repositories/WS'
import { url_document_watch_ws, url_document_upload_ws, DocumentHandler, DocumentUploadHandler } from '@/repositories/Document' import {
import { useDocumentStore } from '@/stores/documents' url_document_watch_ws,
url_document_upload_ws,
DocumentHandler,
DocumentUploadHandler
} from '@/repositories/Document'
import { useDocumentStore } from '@/stores/documents'
import { computed } from 'vue' import { computed } from 'vue'
import HeaderMain from '@/components/HeaderMain.vue' import HeaderMain from '@/components/HeaderMain.vue'
import AppNavigation from '@/components/AppNavigation.vue' import AppNavigation from '@/components/AppNavigation.vue'
import Router from '@/router/index'; import Router from '@/router/index'
interface Path { interface Path {
path: string; path: string
pathList: 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( () => { // Update human-readable x seconds ago messages from mtimes
const pathList = Router.currentRoute.value.path setInterval(documentStore.updateModified, 1000)
.split('/') watchEffect(() => {
.filter( value => value !== '') 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 { documentStore.wsWatch = wsWatch
path: Router.currentRoute.value.path, documentStore.wsUpload = wsUpload
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; export type { Path }
documentStore.wsUpload = wsUpload;
})
export type { Path }
</script> </script>
<template> <template>
@ -51,14 +62,14 @@
</template> </template>
<style scoped> <style scoped>
.wrapper{ .wrapper {
background-color: var(--header-background); background-color: var(--header-background);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.page-container{ .page-container {
flex-grow: 2; flex-grow: 2;
padding: 0; padding: 0;
} }
</style> </style>

View File

@ -1,45 +1,48 @@
@charset "UTF-8"; @charset "UTF-8";
:root { :root {
--primary-background: #181818; --primary-background: #181818;
--secondary-background: #ffffff; --secondary-background: #ffffff;
--font-color: #333; --font-color: #333;
--header-background: #000; --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; --table-background: #535353;
--primary-color: #ffffff; --primary-color: #ffffff;
--secondary-color: #ccc; --secondary-color: #ccc;
--blue-color: #66ffeb; --blue-color: #66ffeb;
--red-color: #ff4d4f; --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 { body {
background-color: var(--primary-background); background-color: var(--primary-background);
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
color: var(--font-color); color: var(--font-color);
margin: 0; margin: 0;
} }
a:link, a:visited, a:active, a:hover { a:link,
color: var(--primary-color); a:visited,
text-decoration: none; a:active,
a:hover {
color: var(--primary-color);
text-decoration: none;
} }
table { table {
border-collapse: collapse; border-collapse: collapse;
border: 0; border: 0;
gap: 0; gap: 0;
} }
#app{ #app {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }

View File

@ -1,17 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink } from 'vue-router'
import Breadcrumb from '@/components/Breadcrumb.vue'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
path: Array<string> path: Array<string>
}>(), }>(),
{}, {}
) )
function generateUrl(pathIndex: number) {
return "/" + props.path.slice(0, pathIndex + 1).join('/')
}
</script> </script>
<template> <template>
@ -63,16 +56,17 @@ function generateUrl(pathIndex: number) {
{{{svg "triangle"}}} {{{svg "triangle"}}}
</div> </div>
--> -->
<Breadcrumb :path="props.path"/> <BreadCrumb :path="props.path" />
</nav> </nav>
</template> </template>
<style scoped> <style scoped>
nav, span{ nav,
color: var(--primary-color); span {
} color: var(--primary-color);
span:hover, .last{ }
color: var(--blue-color) span:hover,
.last {
} color: var(--blue-color);
}
</style> </style>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -3,26 +3,67 @@
<table v-if="props.documents.length"> <table v-if="props.documents.length">
<thead> <thead>
<tr> <tr>
<th class="selection"><input type="checkbox" v-model="allSelected" :indeterminate="selectionIndeterminate"></th> <th class="selection">
<th class="sortcolumn" :class="{sortactive: sort === 'name'}" @click="toggleSort('name')">Name</th> <input
<th class="sortcolumn modified right" :class="{sortactive: sort === 'modified'}" @click="toggleSort('modified')">Modified</th> type="checkbox"
<th class="sortcolumn size right" :class="{sortactive: sort === 'size'}" @click="toggleSort('size')">Size</th> 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> </tr>
</thead> </thead>
<tbody> <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"> <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>
<td class="name"> <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> <template v-else>
<a :href="url_for(doc)">{{doc.name}}</a> <a :href="url_for(doc)">{{ doc.name }}</a>
<button @click="() => editing = doc">🖊</button> <button @click="() => (editing = doc)">🖊</button>
</template> </template>
</td> </td>
<td class="right">{{doc.modified}}</td> <td class="right">{{ doc.modified }}</td>
<td class="right">{{doc.sizedisp}}</td> <td class="right">{{ doc.sizedisp }}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -35,59 +76,62 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useDocumentStore } from '@/stores/documents' 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 FileRenameInput from './FileRenameInput.vue'
import createWebSocket from '@/repositories/WS'; import createWebSocket from '@/repositories/WS'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
path: string, path: string
documents: Document[], documents: Document[]
}>(), }>(),
{}, {}
) )
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
const linkBasePath = computed(()=>{ const linkBasePath = computed(() => {
const path = props.path const path = props.path
return path === '/' ? '' : path return path === '/' ? '' : path
}) })
const filesBasePath = computed(() => `/files${linkBasePath.value}`) const filesBasePath = computed(() => `/files${linkBasePath.value}`)
const url_for = (doc: FolderDocument) => ( const url_for = (doc: FolderDocument) =>
doc.type === "folder" ? doc.type === 'folder'
`#${linkBasePath.value}/${doc.name}` : ? `#${linkBasePath.value}/${doc.name}`
`${filesBasePath.value}/${doc.name}` : `${filesBasePath.value}/${doc.name}`
)
// File rename // File rename
const editing = ref<FolderDocument | null>(null) const editing = ref<FolderDocument | null>(null)
const rename = (doc: FolderDocument, newName: string) => { const rename = (doc: FolderDocument, newName: string) => {
const oldName = doc.name const oldName = doc.name
const control = createWebSocket("/api/control", (ev: MessageEvent) => { const control = createWebSocket('/api/control', (ev: MessageEvent) => {
const msg = JSON.parse(ev.data) const msg = JSON.parse(ev.data)
if ("error" in msg) { if ('error' in msg) {
console.error("Rename failed", msg.error.message, msg.error) console.error('Rename failed', msg.error.message, msg.error)
doc.name = oldName doc.name = oldName
} else { } else {
console.log("Rename succeeded", msg) console.log('Rename succeeded', msg)
} }
}) })
control.onopen = () => { control.onopen = () => {
control.send(JSON.stringify({ control.send(
"op": "rename", JSON.stringify({
"path": `${linkBasePath.value}/${oldName}`, op: 'rename',
"to": newName 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 // Column sort
const toggleSort = (name: string) => { sort.value = sort.value === name ? "" : name } const toggleSort = (name: string) => {
const sort = ref<string>("") sort.value = sort.value === name ? '' : name
}
const sort = ref<string>('')
const sortCompare = { const sortCompare = {
"name": (a: Document, b: Document) => a.name.localeCompare(b.name), name: (a: Document, b: Document) => a.name.localeCompare(b.name),
"modified": (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime, modified: (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
"size": (a: FolderDocument, b: FolderDocument) => b.size - a.size size: (a: FolderDocument, b: FolderDocument) => b.size - a.size
} }
const sorted = (documents: FolderDocument[]) => { const sorted = (documents: FolderDocument[]) => {
const cmp = sortCompare[sort.value as keyof typeof sortCompare] const cmp = sortCompare[sort.value as keyof typeof sortCompare]
@ -97,13 +141,21 @@ const sorted = (documents: FolderDocument[]) => {
} }
const selectionIndeterminate = computed({ const selectionIndeterminate = computed({
get: () => { 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) => {} set: (value: boolean) => {}
}) })
const allSelected = computed({ const allSelected = computed({
get: () => { 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) => { set: (value: boolean) => {
for (const doc of props.documents) { for (const doc of props.documents) {
@ -122,14 +174,19 @@ table {
width: 100%; width: 100%;
table-layout: fixed; table-layout: fixed;
} }
table input[type=checkbox] { table input[type='checkbox'] {
width: 1em; width: 1em;
height: 1em; height: 1em;
} }
table .modified { width: 10em; } table .modified {
table .size { width: 6em; } width: 10em;
table th, table td { }
padding: .5em; table .size {
width: 6em;
}
table th,
table td {
padding: 0.5em;
font-weight: normal; font-weight: normal;
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
@ -177,9 +234,9 @@ tbody tr:hover {
padding-right: 1.7em; padding-right: 1.7em;
} }
.sortcolumn::after { .sortcolumn::after {
content: "▸"; content: '▸';
color: #888; color: #888;
margin: 0 1em 0 .5em; margin: 0 1em 0 0.5em;
position: absolute; position: absolute;
transition: all 0.2s linear; transition: all 0.2s linear;
} }
@ -191,19 +248,19 @@ main {
padding: 5px; padding: 5px;
height: 100%; height: 100%;
} }
.more-action{ .more-action {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: start; justify-content: start;
} }
.action-container{ .action-container {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.edit-action{ .edit-action {
min-width: 5%; min-width: 5%;
} }
.carousel-container{ .carousel-container {
height: inherit; height: inherit;
} }
.name a { .name a {

View File

@ -1,12 +1,12 @@
<template> <template>
<input <input
ref="input" ref="input"
id="FileRenameInput" id="FileRenameInput"
type="text" type="text"
:value="doc.name" :value="doc.name"
@keyup.esc="exit" @keyup.esc="exit"
@keyup.enter="apply" @keyup.enter="apply"
> />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -21,11 +21,11 @@ onMounted(() => {
input.value!.setSelectionRange(0, ext > 0 ? ext : input.value!.value.length) input.value!.setSelectionRange(0, ext > 0 ? ext : input.value!.value.length)
}) })
const props = defineProps < { const props = defineProps<{
doc: FolderDocument doc: FolderDocument
rename: (doc: FolderDocument, newName: string) => void rename: (doc: FolderDocument, newName: string) => void
exit: () => void exit: () => void
} > () }>()
const apply = () => { const apply = () => {
const name = input.value!.value const name = input.value!.value

View File

@ -1,55 +1,52 @@
<template> <template>
<object <object
v-if="props.type === 'pdf'" v-if="props.type === 'pdf'"
:data= "dataURL" :data="dataURL"
type="application/pdf" width="100%" type="application/pdf"
height="100%" width="100%"
> height="100%"
</object> ></object>
<a-image <a-image
v-else-if="props.type === 'image'" v-else-if="props.type === 'image'"
width="50%" width="50%"
:src="dataURL" :src="dataURL"
@click="() => setVisible(true)" @click="() => setVisible(true)"
:previewMask=false :previewMask="false"
:preview="{ :preview="{
visibleImg, visibleImg,
onVisibleChange: setVisible, onVisibleChange: setVisible
}" }"
/> />
<!-- Unknown case --> <!-- Unknown case -->
<h1 v-else> <h1 v-else>Unsupported file type</h1>
Unsupported file type
</h1>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watchEffect, ref } from 'vue' import { watchEffect, ref } from 'vue'
import Router from '@/router/index'; import Router from '@/router/index'
import { url_document_get } from '@/repositories/Document'; import { url_document_get } from '@/repositories/Document'
const dataURL = ref('') const dataURL = ref('')
watchEffect(()=>{ watchEffect(() => {
dataURL.value = new URL( dataURL.value = new URL(
url_document_get + Router.currentRoute.value.path, url_document_get + Router.currentRoute.value.path,
location.origin location.origin
).toString(); ).toString()
}) })
const emit = defineEmits({ const emit = defineEmits({
visibleImg(value: boolean){ visibleImg(value: boolean) {
return value return value
} }
}) })
function setVisible(value: boolean) { function setVisible(value: boolean) {
emit('visibleImg', value) emit('visibleImg', value)
} }
const props = defineProps<{
const props = defineProps < { type?: string
type?: string visibleImg: boolean
visibleImg: boolean }>()
} > ()
</script> </script>
<style></style> <style></style>

View File

@ -2,14 +2,14 @@
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
import LoginModal from '@/components/LoginModal.vue' import LoginModal from '@/components/LoginModal.vue'
import UploadButton from '@/components/UploadButton.vue' import UploadButton from '@/components/UploadButton.vue'
import { ref } from 'vue'; import { ref } from 'vue'
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
const searchQuery = ref<string>('') const searchQuery = ref<string>('')
const showSearchInput = ref<boolean>(false) const showSearchInput = ref<boolean>(false)
const toggleSearchInput = () => { const toggleSearchInput = () => {
showSearchInput.value = !showSearchInput.value; showSearchInput.value = !showSearchInput.value
if (!showSearchInput.value) { if (!showSearchInput.value) {
searchQuery.value = '' searchQuery.value = ''
} }
@ -18,40 +18,10 @@ const toggleSearchInput = () => {
const executeSearch = (ev: InputEvent) => { const executeSearch = (ev: InputEvent) => {
// FIXME: Make reactive instead of this update handler // FIXME: Make reactive instead of this update handler
const query = (ev.target as HTMLInputElement).value const query = (ev.target as HTMLInputElement).value
console.log("Searching", query) console.log('Searching', query)
documentStore.setFilter(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> </script>
<template> <template>
@ -88,7 +58,7 @@ function download(){
<div class="actions-list"> <div class="actions-list">
<LoginModal></LoginModal> <LoginModal></LoginModal>
<template v-if="showSearchInput"> <template v-if="showSearchInput">
<input type="search" v-model="searchQuery" class="margin-input"> <input type="search" v-model="searchQuery" class="margin-input" />
</template> </template>
<!-- <!--
@ -108,7 +78,6 @@ function download(){
<a-button @click="about" type="text" class="action-button" :icon="h(InfoCircleOutlined)" /> <a-button @click="about" type="text" class="action-button" :icon="h(InfoCircleOutlined)" />
</a-tooltip> </a-tooltip>
--> -->
</div> </div>
</div> </div>
</template> </template>
@ -136,25 +105,24 @@ function download(){
} }
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
.actions-container, .actions-container,
.actions-list { .actions-list {
gap: 6px; gap: 6px;
} }
} }
.margin-input{ .margin-input {
margin-top: 5px; margin-top: 5px;
} }
.path { .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; overflow: hidden;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
z-index: 1; z-index: 1;
flex: 0 0 1.5rem; flex: 0 0 1.5rem;
order: 1; order: 1;
font-size: .9rem; font-size: 0.9rem;
position: relative; position: relative;
} }
</style> </style>

View File

@ -1,5 +1,34 @@
<template> <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"> <a-tooltip title="Login">
<template v-if="DocumentStore.isUserLogged"> <template v-if="DocumentStore.isUserLogged">
@ -11,81 +40,68 @@
</a-tooltip> </a-tooltip>
<a-modal v-model:open="DocumentStore.user.isOpenLoginModal" :confirm-loading="confirmLoading" okText="Login" @ok="login"> <a-modal v-model:open="DocumentStore.user.isOpenLoginModal" :confirm-loading="confirmLoading" okText="Login" @ok="login">
<div class="login-container"> <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> </div>
</a-modal> </a-modal>
--> -->
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, h } from 'vue'; import { ref } from 'vue'
import { useDocumentStore } from '@/stores/documents'; import { loginUser, logoutUser } from '@/repositories/User'
import { loginUser, logoutUser } from '@/repositories/User'; import type { ISimpleError } from '@/repositories/Client'
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 () => { const logout = async () => {
try { try {
await logoutUser(); await logoutUser()
} catch (error) {} finally { } finally {
location.reload(); location.reload()
} }
} }
const loginForm = ref({ const loginForm = ref({
username: '', username: '',
password: '', password: '',
error: '', error: ''
}); })
const login = async () => { const login = async () => {
try { try {
loginForm.value.error = ''; loginForm.value.error = ''
confirmLoading.value = true; confirmLoading.value = true
const user = await loginUser(loginForm.value.username, loginForm.value.password); const user = await loginUser(loginForm.value.username, loginForm.value.password)
if(user){ if (user) {
location.reload(); location.reload()
}
} catch (error) {
const httpError = error as ISimpleError
if(httpError.name){
loginForm.value.error = httpError.message
}
}finally{
confirmLoading.value = false;
} }
}; } catch (error) {
const httpError = error as ISimpleError
if (httpError.name) {
loginForm.value.error = httpError.message
}
} finally {
confirmLoading.value = false
}
}
</script> </script>
<style scoped> <style scoped>
.login-container { .login-container {
display: flex; display: grid;
justify-content: center; grid-template-columns: 1fr 2fr;
align-items: center; justify-content: center;
min-height: 30vh; align-items: center;
} }
.button-login { .button-login {
background-color: var(--secondary-color); background-color: var(--secondary-color);
color: var(--secondary-background); color: var(--secondary-background);
} }
.ant-btn-primary:not(:disabled):hover{ .ant-btn-primary:not(:disabled):hover {
background-color: var(--blue-color); background-color: var(--blue-color);
} }
.error-text { .error-text {
color :var(--red-color) color: var(--red-color);
} }
</style> </style>

View 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>

View File

@ -11,17 +11,17 @@
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
function dismissUpload(key: number){ function dismissUpload(key: number) {
documentStore.deleteUploadingDocument(key) documentStore.deleteUploadingDocument(key)
} }
</script> </script>
<style scoped> <style scoped>
.progress-container{ .progress-container {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.close-button:hover{ .close-button:hover {
color: #b81414; color: #b81414;
} }
</style> </style>

View File

@ -1,14 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
import { h, ref } from 'vue'; import { h, ref } from 'vue'
const fileUploadButton = ref() const fileUploadButton = ref()
const documentStore = useDocumentStore(); const documentStore = useDocumentStore()
const open = (placement: any) => openNotification(placement); const open = (placement: any) => openNotification(placement)
const isNotificationOpen = ref(false); const isNotificationOpen = ref(false)
const openNotification = (placement: any) => { const openNotification = (placement: any) => {
if(!isNotificationOpen.value){ if (!isNotificationOpen.value) {
/* /*
api.open({ api.open({
message: `Uploading documents`, message: `Uploading documents`,
@ -17,62 +17,64 @@ const openNotification = (placement: any) => {
duration: 0, duration: 0,
onClose: () => { isNotificationOpen.value = false } onClose: () => { isNotificationOpen.value = false }
});*/ });*/
isNotificationOpen.value = true; isNotificationOpen.value = true
} }
}; }
function uploadFileHandler() { function uploadFileHandler() {
fileUploadButton.value.click() fileUploadButton.value.click()
} }
async function load(file: File, start: number, end: number): Promise<ArrayBuffer> { async function load(file: File, start: number, end: number): Promise<ArrayBuffer> {
const reader = new FileReader(); const reader = new FileReader()
const load = new Promise<Event>((resolve) => (reader.onload = resolve)); const load = new Promise<Event>(resolve => (reader.onload = resolve))
reader.readAsArrayBuffer(file.slice(start, end)); reader.readAsArrayBuffer(file.slice(start, end))
const event = await load; const event = await load
if (event.target && event.target instanceof FileReader) { if (event.target && event.target instanceof FileReader) {
return event.target.result as ArrayBuffer; return event.target.result as ArrayBuffer
} else { } else {
throw new Error('Error loading file' ); throw new Error('Error loading file')
} }
} }
async function sendChunk(file :File, start: number, end: number) { async function sendChunk(file: File, start: number, end: number) {
const ws = documentStore.wsUpload; const ws = documentStore.wsUpload
if(ws){ if (ws) {
const chunk = await load(file, start, end) const chunk = await load(file, start, end)
ws.send(JSON.stringify({ ws.send(
JSON.stringify({
name: file.name, name: file.name,
size: file.size, size: file.size,
start: start, start: start,
end: end end: end
})) })
)
ws.send(chunk) ws.send(chunk)
} }
} }
async function uploadFileChangeHandler(event: Event) { async function uploadFileChangeHandler(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement
const chunkSize = 1 << 20 const chunkSize = 1 << 20
if (target && target.files && target.files.length > 0) { 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 numChunks = Math.ceil(file.size / chunkSize)
const document = documentStore.pushUploadingDocuments(file.name) const document = documentStore.pushUploadingDocuments(file.name)
open('bottomRight') open('bottomRight')
for (let i = 0; i < numChunks; i++) { for (let i = 0; i < numChunks; i++) {
const start = i * chunkSize const start = i * chunkSize
const end = Math.min(file.size, start + chunkSize) const end = Math.min(file.size, start + chunkSize)
const res = await sendChunk(file, start, end) const res = await sendChunk(file, start, end)
console.log( 'progress: '+ ( ( 100 * (i + 1) ) / numChunks) ) console.log('progress: ' + (100 * (i + 1)) / numChunks)
console.log( 'Num Chunks: '+ numChunks ) console.log('Num Chunks: ' + numChunks)
documentStore.updateUploadingDocuments( document.key, ((100 * (i + 1) ) / numChunks)) documentStore.updateUploadingDocuments(document.key, (100 * (i + 1)) / numChunks)
} }
} }
} }
</script> </script>
<template> <template>
(buttons here)
<!-- <!--
<a-tooltip title="Upload files from disk"> <a-tooltip title="Upload files from disk">
@ -84,7 +86,7 @@ async function uploadFileChangeHandler(event: Event) {
</template> </template>
<style scoped> <style scoped>
/* Extends styles from HeaderMain.vue too */ /* Extends styles from HeaderMain.vue too */
.upload-input{ .upload-input {
display: none; display: none;
} }
</style> </style>

View File

@ -7,7 +7,7 @@ import App from './App.vue'
import router from './router' import router from './router'
const app = createApp(App) const app = createApp(App)
app.config.errorHandler = (err) => { app.config.errorHandler = err => {
/* handle error */ /* handle error */
console.log(err) console.log(err)
} }

View File

@ -4,25 +4,27 @@ export const baseURL = import.meta.env.VITE_URL_DOCUMENT
class ClientClass { class ClientClass {
async post(url: string, data?: Record<string, any>): Promise<any> { async post(url: string, data?: Record<string, any>): Promise<any> {
const res = await fetch(`${baseURL}/`, { const res = await fetch(`${baseURL}/`, {
method: "POST", method: 'POST',
headers: { headers: {
accept: 'application/json', 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 const Client = new ClientClass()
export interface ISimpleError extends Error { export interface ISimpleError extends Error {
code : number code: number
} }
class SimpleError extends Error implements ISimpleError { class SimpleError extends Error implements ISimpleError {
code : number code: number
constructor(code: number, message:string) { constructor(code: number, message: string) {
super(message) super(message)
this.code = code this.code = code
} }

View File

@ -1,12 +1,12 @@
import type { DocumentStore } from '@/stores/documents' import type { DocumentStore } from '@/stores/documents'
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
export type FUID = string; export type FUID = string
type BaseDocument = { type BaseDocument = {
name: string name: string
key: FUID key: FUID
}; }
export type FolderDocument = BaseDocument & { export type FolderDocument = BaseDocument & {
type: 'folder' | 'file' type: 'folder' | 'file'
@ -14,17 +14,17 @@ export type FolderDocument = BaseDocument & {
sizedisp: string sizedisp: string
mtime: number mtime: number
modified: string modified: string
}; }
export type Document = FolderDocument export type Document = FolderDocument
export type errorEvent = { export type errorEvent = {
error: { error: {
code : number; code: number
message: string; message: string
redirect: string; redirect: string
} }
}; }
// Raw types the backend /api/watch sends us // 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_watch_ws = '/api/watch'
export const url_document_upload_ws = '/api/upload' export const url_document_upload_ws = '/api/upload'
export const url_document_get ='/files' export const url_document_get = '/files'
export class DocumentHandler { export class DocumentHandler {
constructor( private store: DocumentStore = useDocumentStore() ) { constructor(private store: DocumentStore = useDocumentStore()) {
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this); this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this)
} }
handleWebSocketMessage(event: MessageEvent) { handleWebSocketMessage(event: MessageEvent) {
const msg = JSON.parse(event.data); const msg = JSON.parse(event.data)
switch (true) { switch (true) {
case !!msg.root: case !!msg.root:
this.handleRootMessage(msg); this.handleRootMessage(msg)
break; break
case !!msg.update: case !!msg.update:
this.handleUpdateMessage(msg); this.handleUpdateMessage(msg)
break; break
case !!msg.error: case !!msg.error:
this.handleError(msg); this.handleError(msg)
break; break
default: default:
} }
} }
private handleRootMessage({ root }: { root: DirEntry }) { private handleRootMessage({ root }: { root: DirEntry }) {
if (this.store && this.store.root) { if (this.store && this.store.root) {
this.store.user.isLoggedIn = true; this.store.user.isLoggedIn = true
this.store.root = root; this.store.root = root
} }
} }
private handleUpdateMessage(updateData: { update: UpdateEntry[] }) { private handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
let node: DirEntry = this.store.root; let node: DirEntry = this.store.root
for (const elem of updateData.update) { for (const elem of updateData.update) {
if (elem.deleted) { if (elem.deleted) {
delete node.dir[elem.name] 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) { if (elem.name !== undefined) {
// @ts-ignore // @ts-ignore
@ -100,31 +100,31 @@ export class DocumentHandler {
if (elem.dir !== undefined) node.dir = elem.dir if (elem.dir !== undefined) node.dir = elem.dir
} }
} }
private handleError(msg: errorEvent){ private handleError(msg: errorEvent) {
if(msg.error.code === 401){ if (msg.error.code === 401) {
this.store.user.isOpenLoginModal = true; this.store.user.isOpenLoginModal = true
this.store.user.isLoggedIn = false; this.store.user.isLoggedIn = false
return return
} }
} }
} }
export class DocumentUploadHandler { export class DocumentUploadHandler {
constructor( private store: DocumentStore = useDocumentStore() ) { constructor(private store: DocumentStore = useDocumentStore()) {
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this); this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this)
} }
handleWebSocketMessage(event: MessageEvent) { handleWebSocketMessage(event: MessageEvent) {
const msg = JSON.parse(event.data); const msg = JSON.parse(event.data)
switch (true) { switch (true) {
case !!msg.written: case !!msg.written:
this.handleWrittenMessage(msg); this.handleWrittenMessage(msg)
break; break
default: default:
} }
} }
private handleWrittenMessage(msg : { written : number}) { private handleWrittenMessage(msg: { written: number }) {
// if (this.store && this.store.root) this.store.root = root; // if (this.store && this.store.root) this.store.root = root;
console.log('Written message', msg.written) console.log('Written message', msg.written)
} }

View File

@ -1,23 +1,15 @@
import Client from '@/repositories/Client' import Client from '@/repositories/Client'
export const url_login = '/login' export const url_login = '/login'
export const url_logout = '/logout ' export const url_logout = '/logout '
export async function loginUser(username : string, password: string){ export async function loginUser(username: string, password: string) {
try { const user = await Client.post(url_login, {
const user = await Client.post(url_login, { username,
username, password password
}) })
return user; return user
} catch (error) { }
throw error 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
}
}

View File

@ -1,8 +1,8 @@
function createWebSocket(url: string, eventHandler: (event: MessageEvent) => void) { function createWebSocket(url: string, eventHandler: (event: MessageEvent) => void) {
const urlObject = new URL(url, location.origin.replace( /^http/, 'ws')); const urlObject = new URL(url, location.origin.replace(/^http/, 'ws'))
const webSocket = new WebSocket(urlObject); const webSocket = new WebSocket(urlObject)
webSocket.onmessage = eventHandler; webSocket.onmessage = eventHandler
return webSocket; return webSocket
} }
export default createWebSocket export default createWebSocket

View File

@ -7,8 +7,8 @@ const router = createRouter({
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'explorer', name: 'explorer',
component: ExplorerView, component: ExplorerView
}, }
] ]
}) })

View File

@ -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 { formatSize, formatUnixDate } from '@/utils'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
// @ts-ignore // @ts-ignore
import { localeIncludes } from 'locale-includes' 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 = { type DirectoryData = {
[filename: string]: FileData; [filename: string]: FileData
}; }
type User = { type User = {
isOpenLoginModal: boolean, isOpenLoginModal: boolean
isLoggedIn : boolean, isLoggedIn: boolean
} }
export type DocumentStore = { export type DocumentStore = {
root: DirEntry, root: DirEntry
document: Document[], document: Document[]
selected: Set<FUID>, selected: Set<FUID>
uploadingDocuments: Array<{key: number, name: string, progress: number}>, uploadingDocuments: Array<{ key: number; name: string; progress: number }>
uploadCount: number, uploadCount: number
wsWatch: WebSocket | undefined, wsWatch: WebSocket | undefined
wsUpload: WebSocket | undefined, wsUpload: WebSocket | undefined
user: User, user: User
error: string, error: string
} }
export const useDocumentStore = defineStore({ export const useDocumentStore = defineStore({
@ -42,9 +48,9 @@ export const useDocumentStore = defineStore({
actions: { actions: {
updateTable(matched: DirList) { updateTable(matched: DirList) {
// Transform data // Transform data
const dataMapped = [] const dataMapped = []
for (const [name, attr] of Object.entries(matched)) { for (const [name, attr] of Object.entries(matched)) {
const {id, size, mtime} = attr const { id, size, mtime } = attr
const element: Document = { const element: Document = {
name, name,
key: id, key: id,
@ -52,30 +58,37 @@ export const useDocumentStore = defineStore({
sizedisp: formatSize(size), sizedisp: formatSize(size),
mtime, mtime,
modified: formatUnixDate(mtime), modified: formatUnixDate(mtime),
type: "dir" in attr ? 'folder' : 'file', type: 'dir' in attr ? 'folder' : 'file'
} }
dataMapped.push(element) dataMapped.push(element)
} }
// Pre sort directory entries folders first then files, names in natural ordering // 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 this.document = dataMapped
}, },
setFilter(filter: string){ setFilter(filter: string) {
function traverseDir(data: DirEntry | FileEntry, path: string){ function traverseDir(data: DirEntry | FileEntry, path: string) {
if (!("dir" in data)) return if (!('dir' in data)) return
for (const [name, attr] of Object.entries(data.dir)) { for (const [name, attr] of Object.entries(data.dir)) {
const fullname = `${path}/${name}` const fullname = `${path}/${name}`
if (localeIncludes(name, filter, {usage: "search", sensitivity: "base"})) { if (
matched[fullname.slice(1)] = attr // No initial slash on name localeIncludes(name, filter, {
usage: 'search',
sensitivity: 'base'
})
) {
matched[fullname.slice(1)] = attr // No initial slash on name
} }
traverseDir(attr, fullname) traverseDir(attr, fullname)
} }
} }
const matched: any = {} const matched: any = {}
traverseDir(this.root, "") traverseDir(this.root, '')
this.updateTable(matched) this.updateTable(matched)
}, },
setActualDocument(location: string){ setActualDocument(location: string) {
location = decodeURIComponent(location) location = decodeURIComponent(location)
let data: FileEntry | DirEntry = this.root let data: FileEntry | DirEntry = this.root
const actualDirArr = [] const actualDirArr = []
@ -83,27 +96,32 @@ export const useDocumentStore = defineStore({
// Navigate to target folder // Navigate to target folder
for (const dirname of location.split('/').slice(1)) { for (const dirname of location.split('/').slice(1)) {
if (!dirname) continue 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) actualDirArr.push(dirname)
data = data.dir[dirname] data = data.dir[dirname]
} }
} catch (error) { } 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 // Target folder not available
this.document = [] this.document = []
return return
} }
this.updateTable(data.dir) this.updateTable(data.dir)
}, },
updateUploadingDocuments(key: number, progress: number){ updateUploadingDocuments(key: number, progress: number) {
for (const d of this.uploadingDocuments) { for (const d of this.uploadingDocuments) {
if(d.key === key) d.progress = progress if (d.key === key) d.progress = progress
} }
}, },
pushUploadingDocuments(name: string){ pushUploadingDocuments(name: string) {
this.uploadCount++; this.uploadCount++
const document = { const document = {
key: this.uploadCount, key: this.uploadCount,
name: name, name: name,
@ -112,21 +130,21 @@ export const useDocumentStore = defineStore({
this.uploadingDocuments.push(document) this.uploadingDocuments.push(document)
return document return document
}, },
deleteUploadingDocument(key: number){ deleteUploadingDocument(key: number) {
this.uploadingDocuments = this.uploadingDocuments.filter((e)=> e.key !== key) this.uploadingDocuments = this.uploadingDocuments.filter(e => e.key !== key)
}, },
updateModified() { updateModified() {
for (const d of this.document) { 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: { getters: {
mainDocument(): Document[] { mainDocument(): Document[] {
return this.document; return this.document
}, },
isUserLogged(): boolean{ isUserLogged(): boolean {
return this.user.isLoggedIn return this.user.isLoggedIn
} }
}, }
}); })

View File

@ -1,68 +1,75 @@
export function determineFileType(inputString: string): "file" | "folder" { export function determineFileType(inputString: string): 'file' | 'folder' {
if (inputString.includes('.') && !inputString.endsWith('.')) { if (inputString.includes('.') && !inputString.endsWith('.')) {
return 'file'; return 'file'
} else { } else {
return 'folder'; return 'folder'
} }
} }
export function formatSize(size: number) { export function formatSize(size: number) {
if (size === 0) return 'empty' if (size === 0) return 'empty'
for (const unit of [null, 'kB', 'MB', 'GB', 'TB', 'PB', 'EB']) { 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) size = Math.round(size / 1000)
} }
return "huge" return 'huge'
} }
export function formatUnixDate(t: number) { export function formatUnixDate(t: number) {
const date = new Date(t * 1000) const date = new Date(t * 1000)
const now = new Date() const now = new Date()
const diff = date.getTime() - now.getTime() const diff = date.getTime() - now.getTime()
const formatter = new Intl.RelativeTimeFormat('en', { numeric: const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
'auto' })
if (Math.abs(diff) <= 5000) { if (Math.abs(diff) <= 5000) {
return 'now' return 'now'
} }
if (Math.abs(diff) <= 60000) { 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) { 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) { 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) { 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) { export function getFileExtension(filename: string) {
const parts = filename.split("."); const parts = filename.split('.')
if (parts.length > 1) { if (parts.length > 1) {
return parts[parts.length - 1]; return parts[parts.length - 1]
} else { } else {
return ""; // No hay extensión return '' // No hay extensión
} }
} }
export function getFileType(extension: string): string { export function getFileType(extension: string): string {
const videoExtensions = ["mp4", "avi", "mkv", "mov"]; const videoExtensions = ['mp4', 'avi', 'mkv', 'mov']
const imageExtensions = ["jpg", "jpeg", "png", "gif"]; const imageExtensions = ['jpg', 'jpeg', 'png', 'gif']
const pdfExtensions = ["pdf"]; const pdfExtensions = ['pdf']
if (videoExtensions.includes(extension)) { if (videoExtensions.includes(extension)) {
return "video"; return 'video'
} else if (imageExtensions.includes(extension)) { } else if (imageExtensions.includes(extension)) {
return "image"; return 'image'
} else if (pdfExtensions.includes(extension)) { } else if (pdfExtensions.includes(extension)) {
return "pdf"; return 'pdf'
} else { } else {
return "unknown"; return 'unknown'
} }
} }

View File

@ -16,8 +16,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { watchEffect } from 'vue' import { watchEffect } from 'vue'
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
import Router from '@/router/index'; import Router from '@/router/index'
import FileExplorer from '@/components/FileExplorer.vue'; import FileExplorer from '@/components/FileExplorer.vue'
const documentStore = useDocumentStore() const documentStore = useDocumentStore()
@ -26,21 +26,23 @@ watchEffect(async () => {
documentStore.setActualDocument(path.toString()) documentStore.setActualDocument(path.toString())
}) })
function beforeEnter(el) { function beforeEnter(el: Element) {
el.style.transform = 'translateX(100%)' 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(() => { setTimeout(() => {
el.style.transform = 'translateX(0)' elem.style.transform = 'translateX(0)'
done() done()
}, 0) }, 0)
} }
function leave(el, done) { function leave(el: Element, done: () => void) {
el.style.transform = 'translateX(-100%)' const elem = el as HTMLElement
elem.style.transform = 'translateX(-100%)'
setTimeout(done, 300) // Assuming 300ms is your transition duration setTimeout(done, 300) // Assuming 300ms is your transition duration
} }
</script> </script>
<style scoped> <style scoped>

View File

@ -6,6 +6,7 @@ import vue from '@vitejs/plugin-vue'
// @ts-ignore // @ts-ignore
import pluginRewriteAll from 'vite-plugin-rewrite-all' import pluginRewriteAll from 'vite-plugin-rewrite-all'
import svgLoader from 'vite-svg-loader' import svgLoader from 'vite-svg-loader'
import Components from 'unplugin-vue-components/vite'
// Development mode: // Development mode:
// npm run dev # Run frontend that proxies to dev_backend // npm run dev # Run frontend that proxies to dev_backend
@ -21,7 +22,8 @@ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
pluginRewriteAll(), pluginRewriteAll(),
svgLoader(), svgLoader(), // import svg files
Components(), // auto import components
], ],
css: { css: {
preprocessorOptions: { preprocessorOptions: {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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}

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang=en> <html lang=en>
<script type="module" crossorigin src="/assets/index-68773a87.js"></script> <script type="module" crossorigin src="/assets/index-2034a7a8.js"></script>
<link rel="stylesheet" href="/assets/index-d4bfeeb6.css"> <link rel="stylesheet" href="/assets/index-c3cea0a2.css">
<meta charset=UTF-8> <meta charset=UTF-8>
<title>Cista</title> <title>Cista</title>
@ -10,5 +10,6 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <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"> <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
View File

@ -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=="
}
}
}

View File

@ -1,5 +0,0 @@
{
"dependencies": {
"locale-includes": "^1.0.5"
}
}