Cleanup, lint, format etc.

This commit is contained in:
Leo Vasanko
2023-11-03 20:07:05 +00:00
parent f52d58d645
commit 119aba2b3c
35 changed files with 727 additions and 6286 deletions

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() {
const data = await Client.post(url_logout)
return data
}
export async function logoutUser(){
try {
const data = await Client.post(url_logout)
return data;
} catch (error) {
throw error
}
}

View File

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

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