Frontend created and rewritten a few times, with some backend fixes #1

Merged
leo merged 110 commits from plaintable into main 2023-11-08 20:38:40 +00:00
11 changed files with 84 additions and 751 deletions
Showing only changes of commit 831b2716f7 - Show all commits

View File

@ -22,7 +22,6 @@ declare module 'vue' {
AProgress: typeof import('ant-design-vue/es')['Progress'] AProgress: typeof import('ant-design-vue/es')['Progress']
ARow: typeof import('ant-design-vue/es')['Row'] ARow: typeof import('ant-design-vue/es')['Row']
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
FileCarousel: typeof import('./src/components/FileCarousel.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']

View File

@ -1,137 +0,0 @@
<template>
<div class="carousel" ref="fileCarousel">
<a-page-header :style="{ visibility: idle ? 'hidden' : 'visible' }">
<template #extra>
<a-button type="text" class="action-button" :onclick="toggle" :icon="h(isFullscreen? FullscreenExitOutlined: FullscreenOutlined)" />
<a-button type="text" class="action-button" :onclick="redirectBack" :icon="h(CloseOutlined)" />
</template>
</a-page-header>
<a-row class="slider">
<a-col :span="2" class="centered-vertically">
<div class="custom-slick-arrow slick-arrow slick-prev centered" @click="next(-1)" style="left: 10px; z-index: 1">
<LeftOutlined v-show="!idle" />
</div>
</a-col>
<a-col :span="20" class="centered">
<FileViewer v-if="!documentStore.loading" :visibleImg="visible" @visibleImg="setVisible" :type="fileType"></FileViewer>
</a-col>
<a-col :span="2" class="centered-vertically right">
<div class="custom-slick-arrow slick-arrow slick-prev centered" @click="next(1)" style="right: 10px">
<RightOutlined v-show="!idle" />
</div>
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import { h, ref, watchEffect } from 'vue'
import { useDocumentStore } from '@/stores/documents'
import { useIdle, useFullscreen } from '@vueuse/core'
import { LeftOutlined, RightOutlined, FullscreenOutlined, FullscreenExitOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { getFileType } from '@/utils'
import Router from '@/router/index';
import FileViewer from './FileViewer.vue';
const fileCarousel = ref<HTMLElement | null>(null)
const { isFullscreen, toggle } = useFullscreen(fileCarousel)
const visible = ref<boolean>(false);
const documentStore = useDocumentStore()
const fileType = ref<string| undefined>(undefined);
watchEffect(() => {
if(documentStore.mainDocument[0] && documentStore.mainDocument[0].type === 'file'){
const fileExt = documentStore.mainDocument[0].ext
fileType.value = getFileType(fileExt)
}
})
function setVisible(value: boolean) {
visible.value = value;
}
const { idle } = useIdle(2000)
function redirectBack() {
const currentPath = Router.currentRoute.value.path;
const pathParts = currentPath.split('/').filter(part => part); // Remove empty parts
// Ensure there's always at least one part in the path
if (pathParts.length <= 1) {
// If it's empty, set it to the base URL
pathParts[0] = '/';
} else {
pathParts.pop();
}
const newPath = pathParts.join('/');
Router.push(newPath);
}
function next(direction: number){
const path = decodeURIComponent(new String(Router.currentRoute.value.path) as string)
const name = documentStore.getNextDocumentInRoute(direction, path)
let nextFileLocation : string[] | string = path.split('/')
nextFileLocation.pop()
nextFileLocation.push(name)
nextFileLocation = nextFileLocation.join('/')
Router.push(nextFileLocation)
}
</script>
<style scoped>
:deep(.slick-arrow.custom-slick-arrow) {
width: 60px;
height: 60px;
font-size: 60px;
color: var(--primary-color);
transition: ease-in all 0.3s;
opacity: 0.3;
z-index: 1;
}
:deep(.slick-arrow.custom-slick-arrow:before) {
display: none;
}
:deep(.slick-arrow.custom-slick-arrow:hover) {
color: var(--primary-color);
opacity: 1;
}
.slider {
height: 80vh;
background-color: inherit;
}
.centered {
display: flex;
justify-content: center;
align-content: center;
}
.centered-vertically {
display: flex;
align-items: center;
}
.right {
flex-direction: row-reverse;
}
.action-button {
padding: 0;
font-size: 1.5em;
opacity: 0.5;
color: var(--secondary-color);
&:hover {
color: var(--blue-color);
}
}
.ant-page-header {
padding: 0;
}
.carousel{
margin: 0;
height: inherit;
background-color: var(--table-background);
}
</style>

View File

@ -1,12 +1,6 @@
<template> <template>
<main> <main>
<context-holder /> <table v-if="documentStore.mainDocument.length">
<!-- <h2 v-if="!documentStore.loading && documentStore.error"> {{ documentStore.error }} </h2> -->
<div class="carousel-container" v-if="!documentStore.loading && documentStore.mainDocument[0] && documentStore.mainDocument[0].type === 'file'">
<FileCarousel></FileCarousel>
</div>
<table v-else-if="!documentStore.loading && documentStore.mainDocument">
<thead> <thead>
<tr> <tr>
<th class="selection"><input type="checkbox" v-model="allSelected" :indeterminate="selectionIndeterminate"></th> <th class="selection"><input type="checkbox" v-model="allSelected" :indeterminate="selectionIndeterminate"></th>
@ -17,7 +11,9 @@
</thead> </thead>
<tbody> <tbody>
<tr v-for="doc of sorted(documentStore.mainDocument as FolderDocument[])" :key="doc.key" :class="doc.type === 'folder' ? 'folder' : 'file'"> <tr v-for="doc of sorted(documentStore.mainDocument as FolderDocument[])" :key="doc.key" :class="doc.type === 'folder' ? 'folder' : 'file'">
<td class="selection"><input type="checkbox" v-model="doc.selected"></td> <td class="selection">
<input type="checkbox" :checked="doc.key in documentStore.selected" @change="documentStore.selected.add(doc.key)">
</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>
@ -39,7 +35,6 @@ import { ref, computed } from 'vue'
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
import Router from '@/router/index'; import Router from '@/router/index';
import type { Document, FolderDocument } from '@/repositories/Document'; import type { Document, FolderDocument } from '@/repositories/Document';
import FileCarousel from './FileCarousel.vue';
import FileRenameInput from './FileRenameInput.vue' import FileRenameInput from './FileRenameInput.vue'
import createWebSocket from '@/repositories/WS'; import createWebSocket from '@/repositories/WS';
@ -93,17 +88,21 @@ const sorted = (documents: FolderDocument[]) => {
} }
const selectionIndeterminate = computed({ const selectionIndeterminate = computed({
get: () => { get: () => {
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.some((doc: Document) => doc.selected) && !allSelected.value return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.some((doc: Document) => doc.key in documentStore.selected) && !allSelected.value
}, },
set: (value: boolean) => {} set: (value: boolean) => {}
}) })
const allSelected = computed({ const allSelected = computed({
get: () => { get: () => {
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.every((doc: Document) => doc.selected) return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.every((doc: Document) => doc.key in documentStore.selected)
}, },
set: (value: boolean) => { set: (value: boolean) => {
if (documentStore.mainDocument) { for (const doc of documentStore.mainDocument) {
documentStore.mainDocument.forEach((doc: Document) => doc.selected = value) if (value) {
documentStore.selected.add(doc.key)
} else {
documentStore.selected.delete(doc.key)
}
} }
} }
}) })

View File

@ -48,11 +48,7 @@ function about() {
console.log("About ...") console.log("About ...")
} }
function deleteHandler(){ function deleteHandler(){
if(documentStore.selectedDocuments){ console.log("Delete ...")
documentStore.selectedDocuments.forEach(document => {
documentStore.deleteDocument(document)
})
}
} }
function share(){ function share(){
console.log("Share ...") console.log("Share ...")
@ -80,7 +76,7 @@ function download(){
<a-button @click="createFolderHandler" type="text" class="action-button" :icon="h(FolderFilled)" /> <a-button @click="createFolderHandler" type="text" class="action-button" :icon="h(FolderFilled)" />
</a-tooltip> </a-tooltip>
<!-- TODO ADD CONDITIONAL RENDER --> <!-- TODO ADD CONDITIONAL RENDER -->
<template v-if="documentStore.selectedDocuments && documentStore.selectedDocuments.length > 0"> <template v-if="documentStore.selected.size > 0">
<a-tooltip title="Share"> <a-tooltip title="Share">
<a-button type="text" @click="share" class="action-button" :icon="h(LinkOutlined)" /> <a-button type="text" @click="share" class="action-button" :icon="h(LinkOutlined)" />
</a-tooltip> </a-tooltip>

View File

@ -1,28 +1,22 @@
import type { FileStructure, DocumentStore } from '@/stores/documents' import type { DocumentStore } from '@/stores/documents'
import { useDocumentStore } from '@/stores/documents' import { useDocumentStore } from '@/stores/documents'
import { getFileExtension } from '@/utils'
import Client from '@/repositories/Client'
export type FUID = string;
type BaseDocument = { type BaseDocument = {
name: string name: string
key?: number | string key: FUID
selected?: boolean
}; };
export type FolderDocument = BaseDocument & { export type FolderDocument = BaseDocument & {
type: 'folder' | 'folder-file'; type: 'folder' | 'file'
size: number; size: number
sizedisp: string; sizedisp: string
mtime: number; mtime: number
modified: string; modified: string
}; };
export type FileDocument = BaseDocument & { export type Document = FolderDocument
type: 'file';
ext: string;
data: string;
};
export type errorEvent = { export type errorEvent = {
error: { error: {
@ -32,8 +26,31 @@ export type errorEvent = {
} }
}; };
export type Document = FolderDocument | FileDocument; // Raw types the backend /api/watch sends us
export type FileEntry = {
id: FUID
size: number
mtime: number
}
export type DirEntry = {
id: FUID
size: number
mtime: number
dir: DirList
}
export type DirList = Record<string, FileEntry | DirEntry>
export type UpdateEntry = {
name: string
deleted?: boolean
id?: FUID
size?: number
mtime?: number
dir?: DirList
}
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'
@ -60,18 +77,27 @@ export class DocumentHandler {
} }
} }
private handleRootMessage({ root }: { root: FileStructure }) { 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: FileStructure[] }) { let node: DirEntry = this.store.root;
const root = updateData.update[0] for (const elem of updateData.update) {
if(root) { if (elem.deleted) {
this.store.user.isLoggedIn = true; delete node.dir[elem.name]
this.store.root = root break // Deleted elements can't have further children
}
if (elem.name !== undefined) {
// @ts-ignore
node = node.dir[elem.name] ||= {}
}
if (elem.id !== undefined) node.id = elem.id
if (elem.size !== undefined) node.size = elem.size
if (elem.mtime !== undefined) node.mtime = elem.mtime
if (elem.dir !== undefined) node.dir = elem.dir
} }
} }
private handleError(msg: errorEvent){ private handleError(msg: errorEvent){
@ -103,13 +129,3 @@ export class DocumentUploadHandler {
console.log('Written message', msg.written) console.log('Written message', msg.written)
} }
} }
export async function fetchFile(path: string): Promise<FileDocument>{
const file = await Client.get(url_document_get + path)
const name = path.substring(1 , path.length)
return {
name,
data: file.data,
type: 'file',
ext: getFileExtension(name)
}
}

View File

@ -1,7 +1,6 @@
import type { Document } from '@/repositories/Document'; import type { Document, DirEntry, FileEntry, FUID, DirList } from '@/repositories/Document'
import { fetchFile } 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'
@ -9,21 +8,20 @@ type FileData = { id: string, mtime: number, size: number, dir: DirectoryData};
type DirectoryData = { type DirectoryData = {
[filename: string]: FileData; [filename: string]: FileData;
}; };
export type FileStructure = {id: string, mtime: number, size: number, dir: DirectoryData};
type User = { type User = {
isOpenLoginModal: boolean, isOpenLoginModal: boolean,
isLoggedIn : boolean, isLoggedIn : boolean,
} }
export type DocumentStore = { export type DocumentStore = {
root: FileStructure, root: DirEntry,
document: Document[], document: Document[],
selected: Set<FUID>,
loading: boolean, loading: boolean,
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,
selectedDocuments: Document[],
user: User, user: User,
error: string, error: string,
} }
@ -31,24 +29,24 @@ export type DocumentStore = {
export const useDocumentStore = defineStore({ export const useDocumentStore = defineStore({
id: 'documents', id: 'documents',
state: (): DocumentStore => ({ state: (): DocumentStore => ({
root: {} as FileStructure, root: {} as DirEntry,
document: [] as Document[], document: [] as Document[],
selected: new Set<FUID>(),
loading: true as boolean, loading: true as boolean,
uploadingDocuments: [], uploadingDocuments: [],
uploadCount: 0 as number, uploadCount: 0 as number,
wsWatch: undefined, wsWatch: undefined,
wsUpload: undefined, wsUpload: undefined,
selectedDocuments: [] as Document[],
error: '' as string, error: '' as string,
user: { isLoggedIn: false, isOpenLoginModal: false } as User user: { isLoggedIn: false, isOpenLoginModal: false } as User
}), }),
actions: { actions: {
updateTable(matched: DirectoryData) { 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, dir} = attr const {id, size, mtime} = attr
const element: Document = { const element: Document = {
name, name,
key: id, key: id,
@ -56,7 +54,7 @@ export const useDocumentStore = defineStore({
sizedisp: formatSize(size), sizedisp: formatSize(size),
mtime, mtime,
modified: formatUnixDate(mtime), modified: formatUnixDate(mtime),
type: dir === undefined ? 'folder-file' : 'folder', type: "dir" in attr ? 'folder' : 'file',
} }
dataMapped.push(element) dataMapped.push(element)
} }
@ -66,8 +64,8 @@ export const useDocumentStore = defineStore({
this.loading = false; this.loading = false;
}, },
setFilter(filter: string){ setFilter(filter: string){
function traverseDir(data: FileStructure, path: string){ function traverseDir(data: DirEntry | FileEntry, path: string){
if (data.dir === undefined) 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 (localeIncludes(name, filter, {usage: "search", sensitivity: "base"})) {
@ -84,39 +82,26 @@ export const useDocumentStore = defineStore({
setActualDocument(location: string){ setActualDocument(location: string){
location = decodeURIComponent(location) location = decodeURIComponent(location)
this.loading = true this.loading = true
let data = this.root let data: FileEntry | DirEntry = this.root
const actualDirArr = [] const actualDirArr = []
try { try {
// 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")
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 (data.dir === undefined) { if (!("dir" in data)) {
// Target folder not available // Target folder not available
this.document = [] this.document = []
this.loading = false // ???
return return
} }
this.updateTable(data.dir) this.updateTable(data.dir)
}, },
async setActualDocumentFile(path: string){
this.loading = true;
const file = await fetchFile(path)
this.document = [file];
this.loading = false;
},
setSelectedDocuments(document: Document[]){
this.selectedDocuments = document
},
deleteDocument(document: Document){
this.document = this.document.filter(e => document.key !== e.key)
this.selectedDocuments = this.selectedDocuments.filter(e => document.key !== e.key)
},
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
@ -135,39 +120,6 @@ export const useDocumentStore = defineStore({
deleteUploadingDocument(key: number){ deleteUploadingDocument(key: number){
this.uploadingDocuments = this.uploadingDocuments.filter((e)=> e.key !== key) this.uploadingDocuments = this.uploadingDocuments.filter((e)=> e.key !== key)
}, },
getNextDocumentInRoute(direction: number, path: string){
const locations = path.split('/').slice(1)
locations.pop()
let data = this.root
const actualDirArr = []
// Get data target location
locations.forEach(location => {
// location = decodeURIComponent(location)
if(data && data.dir){
for (const key in data.dir) {
if(key === location) data = data.dir[key]
}
}
})
//Store in a temporary array
for (const key in data.dir) {
actualDirArr.push({
name: key,
content: data.dir[key]
})
}
const actualFileName = decodeURIComponent(this.mainDocument[0].name).split('/').pop()
let index = actualDirArr.findIndex(e => e.name === actualFileName)
if(index < 1 && direction === -1 ){
index = actualDirArr.length -1
}else if(index >= actualDirArr.length - 1 && direction === 1){
index = 0
}else {
index = index + direction
}
return actualDirArr[index].name
},
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)
@ -178,12 +130,6 @@ export const useDocumentStore = defineStore({
mainDocument(): Document[] { mainDocument(): Document[] {
return this.document; return this.document;
}, },
rootSize(): number | undefined {
if(this.root) return this.root.size
},
rootMain(): DirectoryData | undefined {
if(this.root) return this.root.dir
},
isUserLogged(): boolean{ isUserLogged(): boolean{
return this.user.isLoggedIn return this.user.isLoggedIn
} }

View File

@ -7,8 +7,9 @@ export function determineFileType(inputString: string): "file" | "folder" {
} }
export function formatSize(size: number) { export function formatSize(size: number) {
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 < 1e5) 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"

View File

@ -24,7 +24,7 @@ watchEffect(async () => {
if(!file){ if(!file){
documentStore.setActualDocument(path.toString()) documentStore.setActualDocument(path.toString())
}else { }else {
documentStore.setActualDocumentFile(path) //documentStore.setActualDocumentFile(path)
} }
setTimeout( () => { setTimeout( () => {
documentStore.loading = false documentStore.loading = false
@ -33,4 +33,4 @@ watchEffect(async () => {
</script> </script>
<style scoped></style> <style scoped></style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<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">
<title>Vite Vasanko</title> <title>Vite Vasanko</title>
<script type="module" crossorigin src="/assets/index-0332a49a.js"></script> <script type="module" crossorigin src="/assets/index-b757d1a1.js"></script>
<link rel="stylesheet" href="/assets/index-2503e4fd.css"> <link rel="stylesheet" href="/assets/index-1cbf2643.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>