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*
lerna-debug.log*
# No locking
package-lock.json
node_modules
.DS_Store
dist

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,34 @@
<template>
<!--
<button v-if="store.isUserLogged" @click="logout" class="action-button">
Logout
</button>
<ModalDialog v-else title="Login">
<form @submit="login">
<label for="username">Username:</label
><input
id="username"
name="username"
autocomplete="username"
required
v-model="loginForm.username"
/>
<label for="password">Password:</label
><input
id="password"
name="password"
type="password"
autocomplete="current-password"
required
v-model="loginForm.password"
/>
<h3 v-if="loginForm.error.length > 0" class="error-text">
{{ loginForm.error }}
</h3>
<input type="submit" />
</form>
</ModalDialog>
<!--
<a-tooltip title="Login">
<template v-if="DocumentStore.isUserLogged">
@ -11,81 +40,68 @@
</a-tooltip>
<a-modal v-model:open="DocumentStore.user.isOpenLoginModal" :confirm-loading="confirmLoading" okText="Login" @ok="login">
<div class="login-container">
<a-form :model="loginForm">
<a-form-item label="Username" prop="username" :rules="[{ required: true, message: 'Please input your username!' }]">
<a-input v-model:value="loginForm.username" />
</a-form-item>
<a-form-item label="Password" prop="password" :rules="[{ required: true, message: 'Please input your password!' }]">
<a-input type="password" v-model:value="loginForm.password" />
</a-form-item>
<h3 v-if="loginForm.error.length > 0" class="error-text">{{loginForm.error}}</h3>
</a-form>
</div>
</a-modal>
-->
</template>
<script lang="ts" setup>
import { ref, h } from 'vue';
import { useDocumentStore } from '@/stores/documents';
import { loginUser, logoutUser } from '@/repositories/User';
import type { ISimpleError } from '@/repositories/Client';
import { ref } from 'vue'
import { loginUser, logoutUser } from '@/repositories/User'
import type { ISimpleError } from '@/repositories/Client'
import { useDocumentStore } from '@/stores/documents'
const DocumentStore = useDocumentStore();
const confirmLoading = ref<boolean>(false);
const confirmLoading = ref<boolean>(false)
const store = useDocumentStore()
const showModal = () => {
DocumentStore.user.isOpenLoginModal = true;
};
const logout = async () => {
try {
await logoutUser();
} catch (error) {} finally {
location.reload();
}
try {
await logoutUser()
} finally {
location.reload()
}
}
const loginForm = ref({
username: '',
password: '',
error: '',
});
username: '',
password: '',
error: ''
})
const login = async () => {
try {
loginForm.value.error = '';
confirmLoading.value = true;
const user = await loginUser(loginForm.value.username, loginForm.value.password);
if(user){
location.reload();
}
} catch (error) {
const httpError = error as ISimpleError
if(httpError.name){
loginForm.value.error = httpError.message
}
}finally{
confirmLoading.value = false;
try {
loginForm.value.error = ''
confirmLoading.value = true
const user = await loginUser(loginForm.value.username, loginForm.value.password)
if (user) {
location.reload()
}
};
} catch (error) {
const httpError = error as ISimpleError
if (httpError.name) {
loginForm.value.error = httpError.message
}
} finally {
confirmLoading.value = false
}
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 30vh;
display: grid;
grid-template-columns: 1fr 2fr;
justify-content: center;
align-items: center;
}
.button-login {
background-color: var(--secondary-color);
color: var(--secondary-background);
background-color: var(--secondary-color);
color: var(--secondary-background);
}
.ant-btn-primary:not(:disabled):hover{
background-color: var(--blue-color);
.ant-btn-primary:not(:disabled):hover {
background-color: var(--blue-color);
}
.error-text {
color :var(--red-color)
color: var(--red-color);
}
</style>

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'
const documentStore = useDocumentStore()
function dismissUpload(key: number){
function dismissUpload(key: number) {
documentStore.deleteUploadingDocument(key)
}
</script>
<style scoped>
.progress-container{
.progress-container {
display: flex;
align-items: center;
}
.close-button:hover{
.close-button:hover {
color: #b81414;
}
</style>

View File

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

View File

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

View File

@ -4,25 +4,27 @@ export const baseURL = import.meta.env.VITE_URL_DOCUMENT
class ClientClass {
async post(url: string, data?: Record<string, any>): Promise<any> {
const res = await fetch(`${baseURL}/`, {
method: "POST",
method: 'POST',
headers: {
accept: 'application/json',
"content-type": 'application/json',
'content-type': 'application/json'
},
body: data !== undefined ? JSON.stringify(data) : undefined,
body: data !== undefined ? JSON.stringify(data) : undefined
})
return await res.json()
const msg = await res.json()
if ('error' in msg) throw new SimpleError(msg.error.code, msg.error.message)
return msg
}
}
export const Client = new ClientClass()
export interface ISimpleError extends Error {
code : number
code: number
}
class SimpleError extends Error implements ISimpleError {
code : number
constructor(code: number, message:string) {
code: number
constructor(code: number, message: string) {
super(message)
this.code = code
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,68 +1,75 @@
export function determineFileType(inputString: string): "file" | "folder" {
if (inputString.includes('.') && !inputString.endsWith('.')) {
return 'file';
} else {
return 'folder';
}
export function determineFileType(inputString: string): 'file' | 'folder' {
if (inputString.includes('.') && !inputString.endsWith('.')) {
return 'file'
} else {
return 'folder'
}
}
export function formatSize(size: number) {
if (size === 0) return 'empty'
for (const unit of [null, 'kB', 'MB', 'GB', 'TB', 'PB', 'EB']) {
if (size < 1e4) return size.toLocaleString().replace(',', '\u202F') + (unit ? `\u202F${unit}` : '')
if (size < 1e4)
return (
size.toLocaleString().replace(',', '\u202F') + (unit ? `\u202F${unit}` : '')
)
size = Math.round(size / 1000)
}
return "huge"
return 'huge'
}
export function formatUnixDate(t: number) {
const date = new Date(t * 1000)
const now = new Date()
const diff = date.getTime() - now.getTime()
const formatter = new Intl.RelativeTimeFormat('en', { numeric:
'auto' })
const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
if (Math.abs(diff) <= 5000) {
return 'now'
return 'now'
}
if (Math.abs(diff) <= 60000) {
return formatter.format(Math.round(diff / 1000), 'second')
return formatter.format(Math.round(diff / 1000), 'second')
}
if (Math.abs(diff) <= 3600000) {
return formatter.format(Math.round(diff / 60000), 'minute')
return formatter.format(Math.round(diff / 60000), 'minute')
}
if (Math.abs(diff) <= 86400000) {
return formatter.format(Math.round(diff / 3600000), 'hour')
return formatter.format(Math.round(diff / 3600000), 'hour')
}
if (Math.abs(diff) <= 604800000) {
return formatter.format(Math.round(diff / 86400000), 'day')
return formatter.format(Math.round(diff / 86400000), 'day')
}
return date.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })
return date.toLocaleDateString(undefined, {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
export function getFileExtension(filename: string) {
const parts = filename.split(".");
const parts = filename.split('.')
if (parts.length > 1) {
return parts[parts.length - 1];
return parts[parts.length - 1]
} else {
return ""; // No hay extensión
return '' // No hay extensión
}
}
export function getFileType(extension: string): string {
const videoExtensions = ["mp4", "avi", "mkv", "mov"];
const imageExtensions = ["jpg", "jpeg", "png", "gif"];
const pdfExtensions = ["pdf"];
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov']
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif']
const pdfExtensions = ['pdf']
if (videoExtensions.includes(extension)) {
return "video";
return 'video'
} else if (imageExtensions.includes(extension)) {
return "image";
return 'image'
} else if (pdfExtensions.includes(extension)) {
return "pdf";
return 'pdf'
} else {
return "unknown";
return 'unknown'
}
}

View File

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

View File

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

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