Frontend created and rewritten a few times, with some backend fixes #1
|
@ -13,6 +13,7 @@
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@imengyu/vue3-context-menu": "^1.3.3",
|
||||||
"@vueuse/core": "^10.4.1",
|
"@vueuse/core": "^10.4.1",
|
||||||
"esbuild": "^0.19.5",
|
"esbuild": "^0.19.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
@ -57,9 +57,9 @@
|
||||||
<td class="menu"></td>
|
<td class="menu"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<template
|
<template
|
||||||
v-for="doc of sorted(props.documents as Document[])"
|
v-for="(doc, index) in sortedDocuments"
|
||||||
:key="doc.key">
|
:key="doc.key">
|
||||||
<tr class="folder-change" v-if="doc.loc !== prevloc && ((prevloc = doc.loc) || true)">
|
<tr class="folder-change" v-if="showFolderBreadcrumb(index)">
|
||||||
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
|
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
@ -188,6 +188,12 @@ const rename = (doc: Document, newName: string) => {
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
const sortedDocuments = computed(() => sorted(props.documents as Document[]))
|
||||||
|
const showFolderBreadcrumb = (i: number) => {
|
||||||
|
const docs = sortedDocuments.value
|
||||||
|
const docloc = docs[i].loc
|
||||||
|
return i === 0 ? docloc !== loc.value : docloc !== docs[i - 1].loc
|
||||||
|
}
|
||||||
defineExpose({
|
defineExpose({
|
||||||
newFolder() {
|
newFolder() {
|
||||||
const now = Date.now() / 1000
|
const now = Date.now() / 1000
|
||||||
|
@ -230,7 +236,7 @@ defineExpose({
|
||||||
},
|
},
|
||||||
cursorMove(d: number, select = false) {
|
cursorMove(d: number, select = false) {
|
||||||
// Move cursor up or down (keyboard navigation)
|
// Move cursor up or down (keyboard navigation)
|
||||||
const documents = sorted(props.documents as Document[])
|
const documents = sortedDocuments.value
|
||||||
if (documents.length === 0) {
|
if (documents.length === 0) {
|
||||||
cursor.value = null
|
cursor.value = null
|
||||||
return
|
return
|
||||||
|
@ -358,8 +364,6 @@ const allSelected = computed({
|
||||||
})
|
})
|
||||||
|
|
||||||
const loc = computed(() => props.path.join('/'))
|
const loc = computed(() => props.path.join('/'))
|
||||||
let prevloc = ''
|
|
||||||
onBeforeUpdate(() => { prevloc = loc.value })
|
|
||||||
|
|
||||||
const contextMenu = (ev: Event, doc: Document) => {
|
const contextMenu = (ev: Event, doc: Document) => {
|
||||||
cursor.value = doc
|
cursor.value = doc
|
||||||
|
|
|
@ -20,14 +20,26 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<SvgButton ref="searchButton" name="find" @click.prevent="toggleSearchInput" />
|
<SvgButton ref="searchButton" name="find" @click.prevent="toggleSearchInput" />
|
||||||
<SvgButton name="cog" @click="console.log('settings menu')" />
|
<SvgButton name="cog" @click="settingsMenu" />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<context-menu v-model:show="showMenu">
|
||||||
|
<context-menu-item label="Simple item" @click="onMenuClick(1)" />
|
||||||
|
<context-menu-sperator /><!--use this to add sperator-->
|
||||||
|
<context-menu-group label="Menu with child">
|
||||||
|
<context-menu-item label="Item1" @click="onMenuClick(2)" />
|
||||||
|
<context-menu-item label="Item2" @click="onMenuClick(3)" />
|
||||||
|
<context-menu-group label="Child with v-for 50">
|
||||||
|
<context-menu-item v-for="index of 50" :key="index" :label="'Item3-'+index" @click="onLoopMenuClick(index)" />
|
||||||
|
</context-menu-group>
|
||||||
|
</context-menu-group>
|
||||||
|
</context-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDocumentStore } from '@/stores/documents'
|
import { useDocumentStore } from '@/stores/documents'
|
||||||
import { ref, nextTick } from 'vue'
|
import { ref, nextTick } from 'vue'
|
||||||
|
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||||
|
|
||||||
const documentStore = useDocumentStore()
|
const documentStore = useDocumentStore()
|
||||||
const showSearchInput = ref<boolean>(false)
|
const showSearchInput = ref<boolean>(false)
|
||||||
|
@ -50,6 +62,17 @@ const toggleSearchInput = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settingsMenu = (e: Event) => {
|
||||||
|
// show the context menu
|
||||||
|
ContextMenu.showContextMenu({
|
||||||
|
// @ts-ignore
|
||||||
|
x: e.target.getBoundingClientRect().right, y: e.target.getBoundingClientRect().bottom,
|
||||||
|
items: [
|
||||||
|
{ label: "Logout", onClick: () => { documentStore.logout() } },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
toggleSearchInput,
|
toggleSearchInput,
|
||||||
closeSearch,
|
closeSearch,
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<button v-if="store.isUserLogged" @click="logout" class="action-button">
|
<ModalDialog v-if="store.user.isOpenLoginModal" title="Authentication required">
|
||||||
Logout {{ store.user.username }}
|
|
||||||
</button>
|
|
||||||
<ModalDialog v-if="store.user.isOpenLoginModal" title="Login">
|
|
||||||
<form @submit.prevent="login">
|
<form @submit.prevent="login">
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<label for="username">Username:</label>
|
<label for="username">Username:</label>
|
||||||
|
@ -26,7 +23,10 @@
|
||||||
<h3 v-if="loginForm.error.length > 0" class="error-text">
|
<h3 v-if="loginForm.error.length > 0" class="error-text">
|
||||||
{{ loginForm.error }}
|
{{ loginForm.error }}
|
||||||
</h3>
|
</h3>
|
||||||
<input id="submit" type="submit" class="button-login" />
|
<div class="dialog-buttons">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<input id="submit" type="submit" value="Login" class="button-login" />
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</template>
|
</template>
|
||||||
|
@ -81,10 +81,19 @@ const login = async () => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
.dialog-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.button-login {
|
.button-login {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 0;
|
||||||
|
border-radius: .5rem;
|
||||||
|
padding: .5rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
background-color: var(--secondary-color);
|
background-color: var(--accent-color);
|
||||||
color: var(--secondary-background);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
.ant-btn-primary:not(:disabled):hover {
|
.ant-btn-primary:not(:disabled):hover {
|
||||||
background-color: var(--blue-color);
|
background-color: var(--blue-color);
|
||||||
|
|
|
@ -46,6 +46,8 @@ body:has(dialog[open])::before {
|
||||||
|
|
||||||
/* Hide the dialog by default */
|
/* Hide the dialog by default */
|
||||||
dialog[open] {
|
dialog[open] {
|
||||||
|
background: #ddd;
|
||||||
|
color: black;
|
||||||
display: block;
|
display: block;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
@ -58,8 +60,8 @@ dialog[open] {
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog[open] > h1 {
|
dialog[open] > h1 {
|
||||||
background: #00f;
|
background: var(--accent-color);
|
||||||
color: #fff;
|
color: black;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin: -1rem -1rem 0 -1rem;
|
margin: -1rem -1rem 0 -1rem;
|
||||||
padding: 0.5rem 1rem 0.5rem 1rem;
|
padding: 0.5rem 1rem 0.5rem 1rem;
|
||||||
|
|
|
@ -8,6 +8,9 @@ import router from './router'
|
||||||
|
|
||||||
import piniaPluginPersistedState from 'pinia-plugin-persistedstate'
|
import piniaPluginPersistedState from 'pinia-plugin-persistedstate'
|
||||||
|
|
||||||
|
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||||
|
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.config.errorHandler = err => {
|
app.config.errorHandler = err => {
|
||||||
/* handle error */
|
/* handle error */
|
||||||
|
@ -17,6 +20,6 @@ app.config.errorHandler = err => {
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
pinia.use(piniaPluginPersistedState)
|
pinia.use(piniaPluginPersistedState)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(ContextMenu)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
|
@ -4,7 +4,6 @@ export type Document = {
|
||||||
loc: string
|
loc: string
|
||||||
name: string
|
name: string
|
||||||
key: FUID
|
key: FUID
|
||||||
type: 'folder' | 'file'
|
|
||||||
size: number
|
size: number
|
||||||
sizedisp: string
|
sizedisp: string
|
||||||
mtime: number
|
mtime: number
|
||||||
|
|
|
@ -3,7 +3,6 @@ import type {
|
||||||
DirEntry,
|
DirEntry,
|
||||||
FileEntry,
|
FileEntry,
|
||||||
FUID,
|
FUID,
|
||||||
DirList,
|
|
||||||
SelectedItems
|
SelectedItems
|
||||||
} from '@/repositories/Document'
|
} from '@/repositories/Document'
|
||||||
import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
|
import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
|
||||||
|
@ -67,8 +66,10 @@ export const useDocumentStore = defineStore({
|
||||||
// Recurse but replace recursive structure with boolean
|
// Recurse but replace recursive structure with boolean
|
||||||
loc = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
loc = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
||||||
queue.push(...Object.entries(doc.dir).map(mapper))
|
queue.push(...Object.entries(doc.dir).map(mapper))
|
||||||
|
// @ts-ignore
|
||||||
doc.dir = true
|
doc.dir = true
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
else doc.dir = false
|
else doc.dir = false
|
||||||
}
|
}
|
||||||
// Pre sort directory entries folders first then files, names in natural ordering
|
// Pre sort directory entries folders first then files, names in natural ordering
|
||||||
|
@ -77,7 +78,7 @@ export const useDocumentStore = defineStore({
|
||||||
b.dir - a.dir ||
|
b.dir - a.dir ||
|
||||||
collator.compare(a.name, b.name)
|
collator.compare(a.name, b.name)
|
||||||
)
|
)
|
||||||
this.document = docs
|
this.document = docs as Document[]
|
||||||
},
|
},
|
||||||
updateUploadingDocuments(key: number, progress: number) {
|
updateUploadingDocuments(key: number, progress: number) {
|
||||||
for (const d of this.uploadingDocuments) {
|
for (const d of this.uploadingDocuments) {
|
||||||
|
@ -107,6 +108,12 @@ export const useDocumentStore = defineStore({
|
||||||
this.user.privileged = privileged
|
this.user.privileged = privileged
|
||||||
this.user.isLoggedIn = true
|
this.user.isLoggedIn = true
|
||||||
this.user.isOpenLoginModal = false
|
this.user.isOpenLoginModal = false
|
||||||
|
},
|
||||||
|
async logout() {
|
||||||
|
const res = await fetch('/logout', { method: 'POST' })
|
||||||
|
if (!res.ok) throw Error(`Logout failed: ${res.statusText}`)
|
||||||
|
this.$reset()
|
||||||
|
history.go() // Reload page
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user