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