Frontend created and rewritten a few times, with some backend fixes #1
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,22 @@
|
|||
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 {
|
||||
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 Router from '@/router/index'
|
||||
|
||||
interface Path {
|
||||
path: string;
|
||||
pathList: string[];
|
||||
path: string
|
||||
pathList: string[]
|
||||
}
|
||||
const documentStore = useDocumentStore()
|
||||
const path: ComputedRef<Path> = computed(() => {
|
||||
|
@ -31,11 +36,17 @@
|
|||
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)
|
||||
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 }
|
||||
|
|
|
@ -29,7 +29,10 @@ body {
|
|||
color: var(--font-color);
|
||||
margin: 0;
|
||||
}
|
||||
a:link, a:visited, a:active, a:hover {
|
||||
a:link,
|
||||
a:visited,
|
||||
a:active,
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
nav,
|
||||
span {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
span:hover, .last{
|
||||
color: var(--blue-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,22 +3,63 @@
|
|||
<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>
|
||||
<button @click="() => (editing = doc)">🖊️</button>
|
||||
</template>
|
||||
</td>
|
||||
<td class="right">{{ doc.modified }}</td>
|
||||
|
@ -35,16 +76,16 @@
|
|||
<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()
|
||||
|
@ -53,41 +94,44 @@ const linkBasePath = computed(()=>{
|
|||
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
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
:value="doc.name"
|
||||
@keyup.esc="exit"
|
||||
@keyup.enter="apply"
|
||||
>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
|
|
@ -2,38 +2,36 @@
|
|||
<object
|
||||
v-if="props.type === 'pdf'"
|
||||
:data="dataURL"
|
||||
type="application/pdf" width="100%"
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
height="100%"
|
||||
>
|
||||
</object>
|
||||
></object>
|
||||
<a-image
|
||||
v-else-if="props.type === 'image'"
|
||||
width="50%"
|
||||
:src="dataURL"
|
||||
@click="() => setVisible(true)"
|
||||
:previewMask=false
|
||||
:previewMask="false"
|
||||
:preview="{
|
||||
visibleImg,
|
||||
onVisibleChange: setVisible,
|
||||
onVisibleChange: setVisible
|
||||
}"
|
||||
/>
|
||||
<!-- Unknown case -->
|
||||
<h1 v-else>
|
||||
Unsupported file type
|
||||
</h1>
|
||||
<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(() => {
|
||||
dataURL.value = new URL(
|
||||
url_document_get + Router.currentRoute.value.path,
|
||||
location.origin
|
||||
).toString();
|
||||
).toString()
|
||||
})
|
||||
const emit = defineEmits({
|
||||
visibleImg(value: boolean) {
|
||||
|
@ -45,7 +43,6 @@ function setVisible(value: boolean) {
|
|||
emit('visibleImg', value)
|
||||
}
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
type?: string
|
||||
visibleImg: boolean
|
||||
|
|
|
@ -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,7 +105,6 @@ function download(){
|
|||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
|
||||
.actions-container,
|
||||
.actions-list {
|
||||
gap: 6px;
|
||||
|
@ -147,14 +115,14 @@ function download(){
|
|||
}
|
||||
|
||||
.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,4 +1,33 @@
|
|||
<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">
|
||||
|
@ -11,53 +40,41 @@
|
|||
</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();
|
||||
await logoutUser()
|
||||
} finally {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
error: '',
|
||||
});
|
||||
error: ''
|
||||
})
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
loginForm.value.error = '';
|
||||
confirmLoading.value = true;
|
||||
const user = await loginUser(loginForm.value.username, loginForm.value.password);
|
||||
loginForm.value.error = ''
|
||||
confirmLoading.value = true
|
||||
const user = await loginUser(loginForm.value.username, loginForm.value.password)
|
||||
if (user) {
|
||||
location.reload();
|
||||
location.reload()
|
||||
}
|
||||
} catch (error) {
|
||||
const httpError = error as ISimpleError
|
||||
|
@ -65,17 +82,17 @@ const login = async () => {
|
|||
loginForm.value.error = httpError.message
|
||||
}
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
confirmLoading.value = false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 30vh;
|
||||
}
|
||||
.button-login {
|
||||
background-color: var(--secondary-color);
|
||||
|
@ -83,9 +100,8 @@ const login = async () => {
|
|||
}
|
||||
.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>
|
|
@ -1,12 +1,12 @@
|
|||
<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) {
|
||||
/*
|
||||
|
@ -17,47 +17,48 @@ 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;
|
||||
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')
|
||||
|
@ -65,14 +66,15 @@ async function uploadFileChangeHandler(event: Event) {
|
|||
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('progress: ' + (100 * (i + 1)) / numChunks)
|
||||
console.log('Num Chunks: ' + numChunks)
|
||||
documentStore.updateUploadingDocuments( document.key, ((100 * (i + 1) ) / numChunks))
|
||||
documentStore.updateUploadingDocuments(document.key, (100 * (i + 1)) / numChunks)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
(buttons here)
|
||||
<!--
|
||||
|
||||
<a-tooltip title="Upload files from disk">
|
||||
|
|
|
@ -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,14 +4,16 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -58,33 +58,33 @@ export const url_document_get ='/files'
|
|||
|
||||
export class DocumentHandler {
|
||||
constructor(private store: DocumentStore = useDocumentStore()) {
|
||||
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this);
|
||||
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]
|
||||
|
@ -102,8 +102,8 @@ export class DocumentHandler {
|
|||
}
|
||||
private handleError(msg: errorEvent) {
|
||||
if (msg.error.code === 401) {
|
||||
this.store.user.isOpenLoginModal = true;
|
||||
this.store.user.isLoggedIn = false;
|
||||
this.store.user.isOpenLoginModal = true
|
||||
this.store.user.isLoggedIn = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -111,15 +111,15 @@ export class DocumentHandler {
|
|||
|
||||
export class DocumentUploadHandler {
|
||||
constructor(private store: DocumentStore = useDocumentStore()) {
|
||||
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this);
|
||||
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:
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
username,
|
||||
password
|
||||
})
|
||||
return user;
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
return user
|
||||
}
|
||||
export async function logoutUser() {
|
||||
try {
|
||||
const data = await Client.post(url_logout)
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
return data
|
||||
}
|
|
@ -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
|
|
@ -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({
|
||||
|
@ -52,27 +58,34 @@ 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
|
||||
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"})) {
|
||||
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) {
|
||||
|
@ -83,14 +96,19 @@ 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
|
||||
|
@ -103,7 +121,7 @@ export const useDocumentStore = defineStore({
|
|||
}
|
||||
},
|
||||
pushUploadingDocuments(name: string) {
|
||||
this.uploadCount++;
|
||||
this.uploadCount++
|
||||
const document = {
|
||||
key: this.uploadCount,
|
||||
name: name,
|
||||
|
@ -113,20 +131,20 @@ export const useDocumentStore = defineStore({
|
|||
return document
|
||||
},
|
||||
deleteUploadingDocument(key: number) {
|
||||
this.uploadingDocuments = this.uploadingDocuments.filter((e)=> e.key !== key)
|
||||
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 {
|
||||
return this.user.isLoggedIn
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
export function determineFileType(inputString: string): "file" | "folder" {
|
||||
export function determineFileType(inputString: string): 'file' | 'folder' {
|
||||
if (inputString.includes('.') && !inputString.endsWith('.')) {
|
||||
return 'file';
|
||||
return 'file'
|
||||
} else {
|
||||
return 'folder';
|
||||
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'
|
||||
}
|
||||
|
@ -40,29 +42,34 @@ export function formatUnixDate(t: number) {
|
|||
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"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user