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