Prettier file listing, using browser instead of viewer for file display (for now), sorting improved, modified timestamps improved.

This commit is contained in:
Leo Vasanko 2023-10-27 07:53:30 +03:00
parent 63bbe84859
commit 0d186726b5
8 changed files with 198 additions and 185 deletions

View File

@ -9,7 +9,7 @@
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;
@ -26,15 +26,16 @@
pathList pathList
} }
}) })
// Update human-readable x seconds ago messages from mtimes
setInterval(documentStore.updateModified, 1000)
watchEffect(() => { watchEffect(() => {
const documentHandler = new DocumentHandler() const documentHandler = new DocumentHandler()
const documentUploadHandler = new DocumentUploadHandler() const documentUploadHandler = new DocumentUploadHandler()
const wsWatch = createWebSocket(url_document_watch_ws, documentHandler.handleWebSocketMessage) const wsWatch = createWebSocket(url_document_watch_ws, documentHandler.handleWebSocketMessage)
const wsUpload = createWebSocket(url_document_upload_ws, documentUploadHandler.handleWebSocketMessage) const wsUpload = createWebSocket(url_document_upload_ws, documentUploadHandler.handleWebSocketMessage)
documentStore.wsWatch = wsWatch; documentStore.wsWatch = wsWatch;
documentStore.wsUpload = wsUpload; documentStore.wsUpload = wsUpload;
}) })
export type { Path } export type { Path }
@ -45,14 +46,14 @@
<HeaderMain WS="WS"></HeaderMain> <HeaderMain WS="WS"></HeaderMain>
<AppNavigation :path="path.pathList"></AppNavigation> <AppNavigation :path="path.pathList"></AppNavigation>
</header> </header>
<RouterView class="page-container" /> <RouterView class="page-container" />
</template> </template>
<style scoped> <style scoped>
.wrapper{ .wrapper{
background-color: var(--primary-background); background-color: var(--primary-background);
padding: 0.2em 0.5em; padding: 0.2em 0.5em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;

View File

@ -2,30 +2,31 @@
<main> <main>
<!-- <h2 v-if="!documentStore.loading && documentStore.error"> {{ documentStore.error }} </h2> --> <!-- <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'"> <div class="carousel-container" v-if="!documentStore.loading && documentStore.mainDocument[0] && documentStore.mainDocument[0].type === 'file'">
<FileCarousel></FileCarousel> <FileCarousel></FileCarousel>
</div> </div>
<a-table <a-table
v-else-if="!documentStore.loading && documentStore.mainDocument" v-else-if="!documentStore.loading && documentStore.mainDocument"
:pagination=false :pagination=false
:row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange: onSelectChange }" :row-selection="{ selectedRowKeys: state.selectedRowKeys, onChange: onSelectChange }"
:columns="columns" :columns="columns"
:data-source="documentStore.mainDocument" :data-source="documentStore.mainDocument"
> >
<template #headerCell="{column}"></template> <template #headerCell="{column}"></template>
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'name'"> <template v-if="column.key === 'name'">
<div class="editable-cell"> <div class="editable-cell" :class="record.type === 'folder' ? 'folder' : 'file'">
<div v-if="editableData[record.key]" class="action-container editable-cell-input-wrapper"> <div v-if="editableData[record.key]" class="action-container editable-cell-input-wrapper">
<a-input class="name" v-model:value="editableData[record.key].name" @pressEnter="save(record.key)" /> <a-input class="name" v-model:value="editableData[record.key].name" @pressEnter="save(record.key)" />
<CheckOutlined class="edit-action editable-cell-icon-check" @click="save(record.key)" /> <CheckOutlined class="edit-action editable-cell-icon-check" @click="save(record.key)" />
</div> </div>
<div v-else class="action-container editable-cell-text-wrapper"> <div v-else class="action-container editable-cell-text-wrapper">
<a class="name" :href="`#${linkBasePath}/${record.name}`">{{record.name}}</a> <a v-if="record.type === 'folder'" class="name" :href="`#${linkBasePath}/${record.name}`">{{record.name}}</a>
<a v-else class="name" :href="`${filesBasePath}/${record.name}`">{{record.name}}</a>
<edit-outlined class="edit-action editable-cell-icon" @click="edit(record.key)" /> <edit-outlined class="edit-action editable-cell-icon" @click="edit(record.key)" />
</div> </div>
</div> </div>
</template> </template>
<template v-if="column.key === 'action'"> <template v-if="column.key === 'action'">
<a-popover trigger="click"> <a-popover trigger="click">
<template #content> <template #content>
@ -34,19 +35,19 @@
<a-button type="text" class="action-button" :icon="h(ImportOutlined)"/>Open <a-button type="text" class="action-button" :icon="h(ImportOutlined)"/>Open
</div> </div>
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(EditOutlined)"/> Rename <a-button type="text" class="action-button" :icon="h(EditOutlined)"/> Rename
</div> </div>
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(LinkOutlined)"/> Share <a-button type="text" class="action-button" :icon="h(LinkOutlined)"/> Share
</div> </div>
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(CopyOutlined)"/> Copy <a-button type="text" class="action-button" :icon="h(CopyOutlined)"/> Copy
</div> </div>
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(ScissorOutlined)"/> Cut <a-button type="text" class="action-button" :icon="h(ScissorOutlined)"/> Cut
</div> </div>
<div class="action-container"> <div class="action-container">
<a-button type="text" class="action-button" :icon="h(DeleteOutlined)"/> Delete <a-button type="text" class="action-button" :icon="h(DeleteOutlined)"/> Delete
</div> </div>
</div> </div>
</template> </template>
@ -54,12 +55,12 @@
</a-popover> </a-popover>
</template> </template>
</template> </template>
</a-table> </a-table>
</main> </main>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, h, computed, reactive, watchEffect } from 'vue' import { ref, h, computed, reactive, watchEffect } from 'vue'
import type { UnwrapRef } from 'vue' import type { UnwrapRef } from 'vue'
@ -82,12 +83,13 @@
}>({ }>({
selectedRowKeys: [], selectedRowKeys: [],
}); });
const linkBasePath = computed(()=>{ const linkBasePath = computed(()=>{
if(Router.currentRoute.value.path === '/') return '' const path = Router.currentRoute.value.path
return Router.currentRoute.value.path return path === '/' ? '' : path
}) })
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
const columns = ref<TableColumnsType>([ const columns = ref<TableColumnsType>([
{ {
title: 'Name', title: 'Name',
@ -95,34 +97,26 @@
width: '70%', width: '70%',
key: 'name', key: 'name',
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
sorter: (a: Document, b: Document, sortOrder) => { sorter: (a: Document, b: Document) => a.name.localeCompare(b.name),
return b.name.localeCompare(a.name)
}
}, },
{ {
title: 'Modified', title: 'Modified',
dataIndex: 'modified', dataIndex: 'modified',
className: 'column-date',
responsive: ['lg'], responsive: ['lg'],
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
defaultSortOrder: 'descend', defaultSortOrder: 'descend',
sorter: (a: FolderDocument, b: FolderDocument) => { sorter: (a: FolderDocument, b: FolderDocument) => a.mtime - b.mtime,
const dateA = new Date(a.modified),
dateB = new Date(b.modified);
if (dateA < dateB) return -1
if (dateA > dateB) return 1
return 0
},
key: 'modified', key: 'modified',
}, },
{ {
// TODO BETTER SORT FOR MULTPLE SIZE OR CUSTOM PIPE TO kB to MB / GB // TODO BETTER SORT FOR MULTPLE SIZE OR CUSTOM PIPE TO kB to MB / GB
title: 'Size', title: 'Size',
dataIndex: 'size', dataIndex: 'size',
className: 'column-size',
responsive: ['lg'], responsive: ['lg'],
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
sorter: (a: FolderDocument, b: FolderDocument) => { sorter: (a: FolderDocument, b: FolderDocument) => a.size - b.size,
return a.size - b.size
},
key: 'size', key: 'size',
}, },
{ {
@ -130,12 +124,12 @@
key: 'action', key: 'action',
}, },
]) ])
const onSelectChange = (selectedRowKeys: Key[]) => { const onSelectChange = (selectedRowKeys: Key[]) => {
const newSelectedRowKeys: Document[] = [] const newSelectedRowKeys: Document[] = []
selectedRowKeys.forEach( key => { selectedRowKeys.forEach( key => {
if(documentStore.mainDocument){ if(documentStore.mainDocument){
const found = documentStore.mainDocument.find( e=> e.key === key ) const found = documentStore.mainDocument.find( e=> e.key === key )
if(found) newSelectedRowKeys.push(found) if(found) newSelectedRowKeys.push(found)
} }
}) })
@ -151,8 +145,11 @@
}; };
</script> </script>
<style scoped> <style>
.column-date, .column-size {
text-align: right;
}
main { main {
padding: 5px; padding: 5px;
height: 100%; height: 100%;
@ -175,11 +172,19 @@
.carousel-container{ .carousel-container{
height: inherit; height: inherit;
} }
.file .name::before {
content: '📄 ';
font-size: 1.5em;
}
.folder .name::before {
content: '📁 ';
font-size: 1.5em;
}
.editable-cell-text-wrapper .editable-cell-icon { .editable-cell-text-wrapper .editable-cell-icon {
visibility: hidden; /* Oculta el ícono de manera predeterminada */ visibility: hidden; /* Oculta el ícono de manera predeterminada */
} }
.editable-cell-text-wrapper:hover .editable-cell-icon { .editable-cell-text-wrapper:hover .editable-cell-icon {
visibility: visible; /* Muestra el ícono al hacer hover en el contenedor */ visibility: visible; /* Muestra el ícono al hacer hover en el contenedor */
} }
</style> </style>

View File

@ -6,13 +6,14 @@ import Client from '@/repositories/Client'
type BaseDocument = { type BaseDocument = {
name: string; name: string;
key?: number; key?: number | string;
}; };
export type FolderDocument = BaseDocument & { export type FolderDocument = BaseDocument & {
type: 'folder' | 'folder-file';
size: number; size: number;
mtime: number;
modified: string; modified: string;
type: 'folder';
}; };
export type FileDocument = BaseDocument & { export type FileDocument = BaseDocument & {
@ -84,4 +85,4 @@ export async function fetchFile(path: string): Promise<FileDocument>{
type: 'file', type: 'file',
ext: getFileExtension(name) ext: getFileExtension(name)
} }
} }

View File

@ -1,15 +1,15 @@
import type { Document } from '@/repositories/Document'; import type { Document, FolderDocument } from '@/repositories/Document';
import type { ISimpleError } from '@/repositories/Client'; import type { ISimpleError } from '@/repositories/Client';
import { fetchFile } from '@/repositories/Document' import { fetchFile } from '@/repositories/Document'
import { formatUnixDate } from '@/utils'; import { formatUnixDate } from '@/utils';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
type FileData = { 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;
}; };
export type FileStructure = {mtime: number, size: number, dir: DirectoryData}; export type FileStructure = {id: string, mtime: number, size: number, dir: DirectoryData};
export type DocumentStore = { export type DocumentStore = {
root: FileStructure, root: FileStructure,
@ -36,7 +36,7 @@ export const useDocumentStore = defineStore({
selectedDocuments: [] as Document[], selectedDocuments: [] as Document[],
error: '' as string, error: '' as string,
}), }),
actions: { actions: {
setActualDocument(location: string){ setActualDocument(location: string){
this.loading = true; this.loading = true;
@ -54,18 +54,20 @@ export const useDocumentStore = defineStore({
}) })
// Transform data // Transform data
let count = 0 for (const [name, attr] of Object.entries(data.dir)) {
for (const key in data.dir) { const {id, size, mtime, dir} = attr
const element: Document = { const element: Document = {
name: key, name,
key: count, key: id,
size: data.dir[key].size, size,
modified: formatUnixDate(data.dir[key].mtime), mtime,
type: 'folder', modified: formatUnixDate(mtime),
type: dir === undefined ? 'folder-file' : 'folder',
} }
count++
dataMapped.push(element) dataMapped.push(element)
} }
// Pre sort directory entries folders first then files, names in natural ordering
dataMapped.sort((a, b) => a.type === b.type ? a.name.localeCompare(b.name) : a.type === "folder" ? -1 : 1)
this.document = dataMapped this.document = dataMapped
this.loading = false; this.loading = false;
}, },
@ -79,15 +81,13 @@ export const useDocumentStore = defineStore({
this.selectedDocuments = document this.selectedDocuments = document
}, },
deleteDocument(document: Document){ deleteDocument(document: Document){
this.document = this.document.filter(e => document.key !== e.key) this.document = this.document.filter(e => document.key !== e.key)
this.selectedDocuments = this.selectedDocuments.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){
this.uploadingDocuments.forEach((document) => { for (const d of this.uploadingDocuments) {
if(document.key === key) { if(d.key === key) d.progress = progress
document.progress = progress }
}
})
}, },
pushUploadingDocuments(name: string){ pushUploadingDocuments(name: string){
this.uploadCount++; this.uploadCount++;
@ -100,7 +100,7 @@ export const useDocumentStore = defineStore({
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)
}, },
getNextDocumentInRoute(direction: number, path: string){ getNextDocumentInRoute(direction: number, path: string){
const locations = path.split('/').slice(1) const locations = path.split('/').slice(1)
@ -116,7 +116,7 @@ export const useDocumentStore = defineStore({
} }
} }
}) })
//Store in a temporary array //Store in a temporary array
for (const key in data.dir) { for (const key in data.dir) {
actualDirArr.push({ actualDirArr.push({
name: key, name: key,
@ -125,7 +125,7 @@ export const useDocumentStore = defineStore({
} }
const actualFileName = decodeURIComponent(this.mainDocument[0].name).split('/').pop() const actualFileName = decodeURIComponent(this.mainDocument[0].name).split('/').pop()
let index = actualDirArr.findIndex(e => e.name === actualFileName) let index = actualDirArr.findIndex(e => e.name === actualFileName)
if(index < 1 && direction === -1 ){ if(index < 1 && direction === -1 ){
index = actualDirArr.length -1 index = actualDirArr.length -1
}else if(index >= actualDirArr.length - 1 && direction === 1){ }else if(index >= actualDirArr.length - 1 && direction === 1){
@ -134,9 +134,13 @@ export const useDocumentStore = defineStore({
index = index + direction index = index + direction
} }
return actualDirArr[index].name return actualDirArr[index].name
},
updateModified() {
for (const d of this.document) {
if ("mtime" in d) d.modified = formatUnixDate(d.mtime)
}
} }
}, },
getters: { getters: {
mainDocument(): Document[] { mainDocument(): Document[] {
return this.document; return this.document;

View File

@ -12,7 +12,9 @@ export function formatUnixDate(t: number) {
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) {
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')
} }
@ -29,7 +31,7 @@ export function formatUnixDate(t: number) {
return formatter.format(Math.round(diff / 86400000), 'day') return formatter.format(Math.round(diff / 86400000), 'day')
} }
return date.toLocaleDateString() return date.toLocaleDateString(undefined, { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' })
} }
export function getFileExtension(filename: string) { export function getFileExtension(filename: string) {
@ -54,4 +56,4 @@ export function getFileType(extension: string): string {
} else { } else {
return "unknown"; return "unknown";
} }
} }

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-1dc06db1.js"></script> <script type="module" crossorigin src="/assets/index-dfc6f58a.js"></script>
<link rel="stylesheet" href="/assets/index-09b10238.css"> <link rel="stylesheet" href="/assets/index-ee545ab1.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>