Frontend created and rewritten a few times, with some backend fixes #1
|
@ -1,227 +1,215 @@
|
|||
<template>
|
||||
<main>
|
||||
<context-holder />
|
||||
<!-- <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>
|
||||
<main>
|
||||
<context-holder />
|
||||
<!-- <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>
|
||||
<tr>
|
||||
<th class="selection"><input type="checkbox" v-model="allSelected" :indeterminate="selectionIndeterminate"></th>
|
||||
<th class="sortcolumn" :class="{sortactive: sort === 'name'}" @click="toggleSort('name')">Name</th>
|
||||
<th class="sortcolumn modified right" :class="{sortactive: sort === 'modified'}" @click="toggleSort('modified')">Modified</th>
|
||||
<th class="sortcolumn size right" :class="{sortactive: sort === 'size'}" @click="toggleSort('size')">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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="name">
|
||||
<template v-if="editing === doc"><FileRenameInput :doc="doc" :rename="rename" :exit="() => { editing = null}"/></template>
|
||||
<template v-else>
|
||||
<a :href="url_for(doc)">{{doc.name}}</a>
|
||||
<button @click="() => editing = doc">🖊️</button>
|
||||
</template>
|
||||
</td>
|
||||
<td class="right">{{doc.modified}}</td>
|
||||
<td class="right">{{doc.sizedisp}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table v-else-if="!documentStore.loading && documentStore.mainDocument">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="selection"><input type="checkbox" v-model="allSelected" :indeterminate="selectionIndeterminate"></th>
|
||||
<th class="sortcolumn" :class="{sortactive: sort === 'name'}" @click="toggleSort('name')">Name</th>
|
||||
<th class="sortcolumn modified right" :class="{sortactive: sort === 'modified'}" @click="toggleSort('modified')">Modified</th>
|
||||
<th class="sortcolumn size right" :class="{sortactive: sort === 'size'}" @click="toggleSort('size')">Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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="name">
|
||||
<template v-if="editing === doc"><FileRenameInput :doc="doc" :rename="rename" :exit="() => { editing = null}"/></template>
|
||||
<template v-else>
|
||||
<a :href="url_for(doc)">{{doc.name}}</a>
|
||||
<button @click="() => editing = doc">🖊️</button>
|
||||
</template>
|
||||
</td>
|
||||
<td class="right">{{doc.modified}}</td>
|
||||
<td class="right">{{doc.sizedisp}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</main>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, h, computed, reactive, watchEffect } from 'vue'
|
||||
import type { UnwrapRef } from 'vue'
|
||||
import { useDocumentStore } from '@/stores/documents'
|
||||
import Router from '@/router/index';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { Document, FolderDocument } from '@/repositories/Document';
|
||||
import FileCarousel from './FileCarousel.vue';
|
||||
import FileRenameInput from './FileRenameInput.vue'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useDocumentStore } from '@/stores/documents'
|
||||
import Router from '@/router/index';
|
||||
import type { Document, FolderDocument } from '@/repositories/Document';
|
||||
import FileCarousel from './FileCarousel.vue';
|
||||
import FileRenameInput from './FileRenameInput.vue'
|
||||
import createWebSocket from '@/repositories/WS';
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
type Key = string | number;
|
||||
const documentStore = useDocumentStore()
|
||||
const editableData: UnwrapRef<Record<string, Document>> = reactive({});
|
||||
const state = reactive<{
|
||||
selectedRowKeys: Key[];
|
||||
}>({
|
||||
selectedRowKeys: [],
|
||||
});
|
||||
|
||||
const linkBasePath = computed(()=>{
|
||||
const path = Router.currentRoute.value.path
|
||||
return path === '/' ? '' : path
|
||||
})
|
||||
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
|
||||
const url_for = (doc: FolderDocument) => (
|
||||
doc.type === "folder" ?
|
||||
`#${linkBasePath.value}/${doc.name}` :
|
||||
`${filesBasePath.value}/${doc.name}`
|
||||
)
|
||||
// File rename
|
||||
const editing = ref<FolderDocument | null>(null)
|
||||
const rename = (doc: FolderDocument, newName: string) => {
|
||||
const oldName = doc.name
|
||||
const control = createWebSocket("/api/control", (ev: MessageEvent) => {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if ("error" in msg) {
|
||||
console.error("Rename failed", msg.error.message, msg.error)
|
||||
doc.name = oldName
|
||||
} else {
|
||||
console.log("Rename succeeded", msg)
|
||||
}
|
||||
})
|
||||
control.onopen = () => {
|
||||
control.send(JSON.stringify({
|
||||
"op": "rename",
|
||||
"path": `${linkBasePath.value}/${oldName}`,
|
||||
"to": newName
|
||||
}))
|
||||
}
|
||||
doc.name = newName // We should get an update from watch but this is quicker
|
||||
}
|
||||
|
||||
// Column sort
|
||||
const toggleSort = (name: string) => { sort.value = sort.value === name ? "" : name }
|
||||
const sort = ref<string>("")
|
||||
const sortCompare = {
|
||||
"name": (a: Document, b: Document) => a.name.localeCompare(b.name),
|
||||
"modified": (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
|
||||
"size": (a: FolderDocument, b: FolderDocument) => b.size - a.size
|
||||
}
|
||||
const sorted = (documents: FolderDocument[]) => {
|
||||
const cmp = sortCompare[sort.value as keyof typeof sortCompare]
|
||||
const sorted = [...documents]
|
||||
if (cmp) sorted.sort(cmp)
|
||||
return sorted
|
||||
}
|
||||
const selectionIndeterminate = computed({
|
||||
get: () => {
|
||||
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.some((doc: Document) => doc.selected) && !allSelected.value
|
||||
},
|
||||
set: (value: boolean) => {}
|
||||
})
|
||||
const allSelected = computed({
|
||||
get: () => {
|
||||
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.every((doc: Document) => doc.selected)
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (documentStore.mainDocument) {
|
||||
documentStore.mainDocument.forEach((doc: Document) => doc.selected = value)
|
||||
}
|
||||
const documentStore = useDocumentStore()
|
||||
const linkBasePath = computed(()=>{
|
||||
const path = Router.currentRoute.value.path
|
||||
return path === '/' ? '' : path
|
||||
})
|
||||
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
|
||||
const url_for = (doc: FolderDocument) => (
|
||||
doc.type === "folder" ?
|
||||
`#${linkBasePath.value}/${doc.name}` :
|
||||
`${filesBasePath.value}/${doc.name}`
|
||||
)
|
||||
// File rename
|
||||
const editing = ref<FolderDocument | null>(null)
|
||||
const rename = (doc: FolderDocument, newName: string) => {
|
||||
const oldName = doc.name
|
||||
const control = createWebSocket("/api/control", (ev: MessageEvent) => {
|
||||
const msg = JSON.parse(ev.data)
|
||||
if ("error" in msg) {
|
||||
console.error("Rename failed", msg.error.message, msg.error)
|
||||
doc.name = oldName
|
||||
} else {
|
||||
console.log("Rename succeeded", msg)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
control.onopen = () => {
|
||||
control.send(JSON.stringify({
|
||||
"op": "rename",
|
||||
"path": `${linkBasePath.value}/${oldName}`,
|
||||
"to": newName
|
||||
}))
|
||||
}
|
||||
doc.name = newName // We should get an update from watch but this is quicker
|
||||
}
|
||||
|
||||
<style>
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
// Column sort
|
||||
const toggleSort = (name: string) => { sort.value = sort.value === name ? "" : name }
|
||||
const sort = ref<string>("")
|
||||
const sortCompare = {
|
||||
"name": (a: Document, b: Document) => a.name.localeCompare(b.name),
|
||||
"modified": (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
|
||||
"size": (a: FolderDocument, b: FolderDocument) => b.size - a.size
|
||||
}
|
||||
const sorted = (documents: FolderDocument[]) => {
|
||||
const cmp = sortCompare[sort.value as keyof typeof sortCompare]
|
||||
const sorted = [...documents]
|
||||
if (cmp) sorted.sort(cmp)
|
||||
return sorted
|
||||
}
|
||||
const selectionIndeterminate = computed({
|
||||
get: () => {
|
||||
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.some((doc: Document) => doc.selected) && !allSelected.value
|
||||
},
|
||||
set: (value: boolean) => {}
|
||||
})
|
||||
const allSelected = computed({
|
||||
get: () => {
|
||||
return documentStore.mainDocument && documentStore.mainDocument.length > 0 && documentStore.mainDocument.every((doc: Document) => doc.selected)
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
if (documentStore.mainDocument) {
|
||||
documentStore.mainDocument.forEach((doc: Document) => doc.selected = value)
|
||||
}
|
||||
}
|
||||
table input[type=checkbox] {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
table .modified { width: 10em; }
|
||||
table .size { width: 6em; }
|
||||
table th, table td {
|
||||
padding: .5em;
|
||||
font-weight: normal;
|
||||
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 0.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;
|
||||
}
|
||||
</style>
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
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: normal;
|
||||
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 0.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;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue
Block a user