Frontend created and rewritten a few times, with some backend fixes #1
|
@ -1,227 +1,215 @@
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<context-holder />
|
<context-holder />
|
||||||
<!-- <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>
|
||||||
|
|
||||||
<table v-else-if="!documentStore.loading && documentStore.mainDocument">
|
<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>
|
||||||
<th class="sortcolumn" :class="{sortactive: sort === 'name'}" @click="toggleSort('name')">Name</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 modified right" :class="{sortactive: sort === 'modified'}" @click="toggleSort('modified')">Modified</th>
|
||||||
<th class="sortcolumn size right" :class="{sortactive: sort === 'size'}" @click="toggleSort('size')">Size</th>
|
<th class="sortcolumn size right" :class="{sortactive: sort === 'size'}" @click="toggleSort('size')">Size</th>
|
||||||
</tr>
|
</tr>
|
||||||
</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" v-model="doc.selected"></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>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, h, computed, reactive, watchEffect } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { UnwrapRef } 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 { message } from 'ant-design-vue';
|
import FileCarousel from './FileCarousel.vue';
|
||||||
import type { Document, FolderDocument } from '@/repositories/Document';
|
import FileRenameInput from './FileRenameInput.vue'
|
||||||
import FileCarousel from './FileCarousel.vue';
|
|
||||||
import FileRenameInput from './FileRenameInput.vue'
|
|
||||||
import createWebSocket from '@/repositories/WS';
|
import createWebSocket from '@/repositories/WS';
|
||||||
|
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
const documentStore = useDocumentStore()
|
||||||
|
const linkBasePath = computed(()=>{
|
||||||
type Key = string | number;
|
const path = Router.currentRoute.value.path
|
||||||
const documentStore = useDocumentStore()
|
return path === '/' ? '' : path
|
||||||
const editableData: UnwrapRef<Record<string, Document>> = reactive({});
|
})
|
||||||
const state = reactive<{
|
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
|
||||||
selectedRowKeys: Key[];
|
const url_for = (doc: FolderDocument) => (
|
||||||
}>({
|
doc.type === "folder" ?
|
||||||
selectedRowKeys: [],
|
`#${linkBasePath.value}/${doc.name}` :
|
||||||
});
|
`${filesBasePath.value}/${doc.name}`
|
||||||
|
)
|
||||||
const linkBasePath = computed(()=>{
|
// File rename
|
||||||
const path = Router.currentRoute.value.path
|
const editing = ref<FolderDocument | null>(null)
|
||||||
return path === '/' ? '' : path
|
const rename = (doc: FolderDocument, newName: string) => {
|
||||||
})
|
const oldName = doc.name
|
||||||
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
|
const control = createWebSocket("/api/control", (ev: MessageEvent) => {
|
||||||
const url_for = (doc: FolderDocument) => (
|
const msg = JSON.parse(ev.data)
|
||||||
doc.type === "folder" ?
|
if ("error" in msg) {
|
||||||
`#${linkBasePath.value}/${doc.name}` :
|
console.error("Rename failed", msg.error.message, msg.error)
|
||||||
`${filesBasePath.value}/${doc.name}`
|
doc.name = oldName
|
||||||
)
|
} else {
|
||||||
// File rename
|
console.log("Rename succeeded", msg)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</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>
|
// Column sort
|
||||||
table {
|
const toggleSort = (name: string) => { sort.value = sort.value === name ? "" : name }
|
||||||
width: 100%;
|
const sort = ref<string>("")
|
||||||
table-layout: fixed;
|
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;
|
</script>
|
||||||
height: 1em;
|
|
||||||
}
|
<style>
|
||||||
table .modified { width: 10em; }
|
table {
|
||||||
table .size { width: 6em; }
|
width: 100%;
|
||||||
table th, table td {
|
table-layout: fixed;
|
||||||
padding: .5em;
|
}
|
||||||
font-weight: normal;
|
table input[type=checkbox] {
|
||||||
text-align: left;
|
width: 1em;
|
||||||
white-space: nowrap;
|
height: 1em;
|
||||||
overflow: hidden;
|
}
|
||||||
text-overflow: ellipsis;
|
table .modified { width: 10em; }
|
||||||
}
|
table .size { width: 6em; }
|
||||||
.name {
|
table th, table td {
|
||||||
white-space: nowrap;
|
padding: .5em;
|
||||||
text-overflow: initial;
|
font-weight: normal;
|
||||||
overflow: initial;
|
text-align: left;
|
||||||
}
|
white-space: nowrap;
|
||||||
.name button {
|
overflow: hidden;
|
||||||
visibility: hidden;
|
text-overflow: ellipsis;
|
||||||
padding-left: 1em;
|
}
|
||||||
}
|
.name {
|
||||||
.name:hover button {
|
white-space: nowrap;
|
||||||
visibility: visible;
|
text-overflow: initial;
|
||||||
}
|
overflow: initial;
|
||||||
.name button {
|
}
|
||||||
cursor: pointer;
|
.name button {
|
||||||
border: 0;
|
visibility: hidden;
|
||||||
background: transparent;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
thead tr {
|
.name:hover button {
|
||||||
border: 1px solid #ddd;
|
visibility: visible;
|
||||||
background: #ddd;
|
}
|
||||||
}
|
.name button {
|
||||||
tbody tr {
|
cursor: pointer;
|
||||||
background: #444;
|
border: 0;
|
||||||
color: #ddd;
|
background: transparent;
|
||||||
}
|
}
|
||||||
tbody tr:hover {
|
thead tr {
|
||||||
background: #00f8;
|
border: 1px solid #ddd;
|
||||||
}
|
background: #ddd;
|
||||||
.right {
|
}
|
||||||
text-align: right;
|
tbody tr {
|
||||||
}
|
background: #444;
|
||||||
.selection {
|
color: #ddd;
|
||||||
width: 2em;
|
}
|
||||||
}
|
tbody tr:hover {
|
||||||
.sortcolumn:hover {
|
background: #00f8;
|
||||||
cursor: pointer;
|
}
|
||||||
}
|
.right {
|
||||||
.sortcolumn:hover::after {
|
text-align: right;
|
||||||
color: #f80;
|
}
|
||||||
}
|
.selection {
|
||||||
.sortcolumn {
|
width: 2em;
|
||||||
padding-right: 1.7em;
|
}
|
||||||
}
|
.sortcolumn:hover {
|
||||||
.sortcolumn::after {
|
cursor: pointer;
|
||||||
content: "▸";
|
}
|
||||||
color: #888;
|
.sortcolumn:hover::after {
|
||||||
margin: 0 1em 0 .5em;
|
color: #f80;
|
||||||
position: absolute;
|
}
|
||||||
transition: transform 0.2s linear;
|
.sortcolumn {
|
||||||
}
|
padding-right: 1.7em;
|
||||||
.sortactive::after {
|
}
|
||||||
transform: rotate(90deg);
|
.sortcolumn::after {
|
||||||
color: #000;
|
content: "▸";
|
||||||
}
|
color: #888;
|
||||||
main {
|
margin: 0 1em 0 .5em;
|
||||||
padding: 5px;
|
position: absolute;
|
||||||
height: 100%;
|
transition: transform 0.2s linear;
|
||||||
}
|
}
|
||||||
.more-action{
|
.sortactive::after {
|
||||||
display: flex;
|
transform: rotate(90deg);
|
||||||
flex-direction: column;
|
color: #000;
|
||||||
justify-content: start;
|
}
|
||||||
}
|
main {
|
||||||
.action-container{
|
padding: 5px;
|
||||||
display: flex;
|
height: 100%;
|
||||||
align-items: center;
|
}
|
||||||
}
|
.more-action{
|
||||||
.edit-action{
|
display: flex;
|
||||||
min-width: 5%;
|
flex-direction: column;
|
||||||
}
|
justify-content: start;
|
||||||
.carousel-container{
|
}
|
||||||
height: inherit;
|
.action-container{
|
||||||
}
|
display: flex;
|
||||||
.name a {
|
align-items: center;
|
||||||
text-decoration: none;
|
}
|
||||||
}
|
.edit-action{
|
||||||
.file .name::before {
|
min-width: 5%;
|
||||||
content: '📄 ';
|
}
|
||||||
font-size: 1.5em;
|
.carousel-container{
|
||||||
}
|
height: inherit;
|
||||||
.folder .name::before {
|
}
|
||||||
content: '📁 ';
|
.name a {
|
||||||
font-size: 1.5em;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
</style>
|
.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