Major changes:
- File selections working - CSS more responsive, more consistent use of colors and variables - Keyboard navigation - Added context menu buttons and handler, the menu is still missing - Added download and settings buttons (no functions yet) - Various minor fixes everywhere
This commit is contained in:
@@ -2,11 +2,11 @@
|
||||
<html lang=en>
|
||||
<meta charset=UTF-8>
|
||||
<title>Cista</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Emoji&family=Roboto:wght@400;700&display=swap" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import { watchEffect } from 'vue'
|
||||
import type HeaderMain from '@/components/HeaderMain.vue'
|
||||
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||
import createWebSocket from '@/repositories/WS'
|
||||
import {
|
||||
url_document_watch_ws,
|
||||
@@ -46,17 +47,61 @@ watchEffect(() => {
|
||||
documentStore.wsWatch = wsWatch
|
||||
documentStore.wsUpload = wsUpload
|
||||
})
|
||||
|
||||
const headerMain = ref<typeof HeaderMain | null>(null)
|
||||
let vert = 0
|
||||
let vertInterval: any = null
|
||||
const globalShortcutHandler = (event: KeyboardEvent) => {
|
||||
if (event.repeat) return
|
||||
console.log("key pressed", event)
|
||||
const c = documentStore.fileExplorer.isCursor
|
||||
const keyup = event.type === "keyup"
|
||||
// For up/down implement custom fast repeat
|
||||
if (event.key === "ArrowUp") vert = (keyup ? 0 : event.altKey ? -10 : -1)
|
||||
else if (event.key === "ArrowDown") vert = (keyup ? 0 : event.altKey ? 10 : 1)
|
||||
// Find: process on keydown so that we can bypass the built-in search hotkey
|
||||
else if (!keyup && event.key === 'f' && (event.ctrlKey || event.metaKey)) {
|
||||
headerMain.value!.toggleSearchInput()
|
||||
}
|
||||
// Select all (toggle); keydown to prevent builtin
|
||||
else if (!keyup && event.key === 'a' && (event.ctrlKey || event.metaKey)) {
|
||||
documentStore.fileExplorer.toggleSelectAll()
|
||||
}
|
||||
// Rename
|
||||
else if (c && keyup && !event.ctrlKey && (event.key === 'F2' || event.key === "r")) {
|
||||
documentStore.fileExplorer.cursorRename()
|
||||
}
|
||||
// Toggle selections on file explorer; ignore all spaces to prevent scrolling built-in hotkey
|
||||
else if (c && event.code === 'Space') {
|
||||
if (keyup && !event.altKey && !event.ctrlKey) documentStore.fileExplorer.cursorSelect()
|
||||
}
|
||||
else return
|
||||
event.preventDefault()
|
||||
if (vertInterval !== null) clearInterval(vertInterval)
|
||||
vertInterval = null
|
||||
if (vert) {
|
||||
vertInterval = setInterval(() => { console.log("X"), documentStore.fileExplorer.cursorMove(vert) }, 30)
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", globalShortcutHandler)
|
||||
window.addEventListener("keyup", globalShortcutHandler)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", globalShortcutHandler)
|
||||
window.removeEventListener("keyup", globalShortcutHandler)
|
||||
})
|
||||
export type { Path }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LoginModal />
|
||||
<header>
|
||||
<HeaderMain />
|
||||
<HeaderMain ref="headerMain"/>
|
||||
<BreadCrumb :path="path.pathList" />
|
||||
</header>
|
||||
<RouterView class="page-container" />
|
||||
<main>
|
||||
<RouterView />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,34 +1,69 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
:root {
|
||||
--primary-background: #181818;
|
||||
--secondary-background: #ffffff;
|
||||
--font-color: #333;
|
||||
--primary-color: #000;
|
||||
--primary-background: #eef;
|
||||
--header-background: #000;
|
||||
--table-background: #535353;
|
||||
--primary-color: #ffffff;
|
||||
--secondary-color: #ccc;
|
||||
--blue-color: #66ffeb;
|
||||
--red-color: #ff4d4f;
|
||||
--header-color: #ccc;
|
||||
--primary-color: #000;
|
||||
--secondary-color: #333;
|
||||
--accent-color: #f80;
|
||||
--transition-time: 0.2s;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--primary-background: #333;
|
||||
--secondary-background: #666;
|
||||
--font-color: #ddd;
|
||||
--table-background: #535353;
|
||||
--primary-color: #ffffff;
|
||||
--primary-color: #ddd;
|
||||
--primary-background: #003;
|
||||
--primary-color: #fff;
|
||||
--secondary-color: #ccc;
|
||||
--blue-color: #66ffeb;
|
||||
--red-color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
@media screen and (orientation: portrait) {
|
||||
html {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.size, .modified {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 800px) and (--webkit-min-device-pixel-ratio: 2) {
|
||||
html {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1400px) and (--webkit-min-device-pixel-ratio: 3) {
|
||||
html {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
body {
|
||||
background-color: var(--primary-background);
|
||||
font-size: 1rem;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
color: var(--font-color);
|
||||
color: var(--primary-color);
|
||||
margin: 0;
|
||||
}
|
||||
header {
|
||||
background-color: var(--header-background);
|
||||
color: var(--header-color);
|
||||
}
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
button {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
}
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
a:link,
|
||||
a:visited,
|
||||
a:active,
|
||||
@@ -46,3 +81,11 @@ table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
main {
|
||||
height: calc(100svh - 6rem);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
thead tr {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="448" height="512" viewBox="0 0 448 512"><path d="M223.97 175A81 81 0 0 0 143 256c0 44.7 36.27 81.03 80.97 81.03 44.72 0 80.72-36.34 80.72-81.03 0-44.73-36-81-80.8-81zM386.3 302.53l-14.58 35.16 29.47 57.8-36.1 36.1-59.3-28-35.2 14.4-17.87 54.6-2.28 7.24h-51L177.4 418.2l-35.17-14.5-57.9 29.4-36.1-36.1 27.97-59.2-14.47-35.12L0 282.6v-51l61.7-22.1 14.5-35.1-25.96-51.23-3.43-6.72 36.1-36.03 59.3 27.92 35.1-14.5 17.9-54.6 2.3-7.24h51l22.1 61.73 35.07 14.52 58.04-29.4 36.06 36.03-27.96 59.2 14.42 35.17 61.8 20.13v50.97l-61.67 22.18z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="32" viewBox="0 0 448 512"><path d="M223.97 175A81 81 0 0 0 143 256c0 44.7 36.27 81.03 80.97 81.03 44.72 0 80.72-36.34 80.72-81.03 0-44.73-36-81-80.8-81zM386.3 302.53l-14.58 35.16 29.47 57.8-36.1 36.1-59.3-28-35.2 14.4-17.87 54.6-2.28 7.24h-51L177.4 418.2l-35.17-14.5-57.9 29.4-36.1-36.1 27.97-59.2-14.47-35.12L0 282.6v-51l61.7-22.1 14.5-35.1-25.96-51.23-3.43-6.72 36.1-36.03 59.3 27.92 35.1-14.5 17.9-54.6 2.3-7.24h51l22.1 61.73 35.07 14.52 58.04-29.4 36.06 36.03-27.96 59.2 14.42 35.17 61.8 20.13v50.97l-61.67 22.18z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 587 B After Width: | Height: | Size: 586 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="40" viewBox="10 10 372 468"><path d="M128 344V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zM120 96h112l-12-29.25c-.75-1-3-2.5-4.25-2.75H136.5c-1.5.25-3.5 1.75-4.25 2.75zm232 8v16c0 4.5-3.5 8-8 8h-24v237c0 27.5-18 51-40 51H72c-22 0-40-22.5-40-50V128H8c-4.5 0-8-3.5-8-8v-16c0-4.5 3.5-8 8-8h77.25l17.5-41.75C107.75 42 122.75 32 136 32h80c13.25 0 28.25 10 33.25 22.25L266.75 96H344c4.5 0 8 3.5 8 8z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="10 10 372 468"><path d="M128 344V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zM120 96h112l-12-29.25c-.75-1-3-2.5-4.25-2.75H136.5c-1.5.25-3.5 1.75-4.25 2.75zm232 8v16c0 4.5-3.5 8-8 8h-24v237c0 27.5-18 51-40 51H72c-22 0-40-22.5-40-50V128H8c-4.5 0-8-3.5-8-8v-16c0-4.5 3.5-8 8-8h77.25l17.5-41.75C107.75 42 122.75 32 136 32h80c13.25 0 28.25 10 33.25 22.25L266.75 96H344c4.5 0 8 3.5 8 8z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 670 B After Width: | Height: | Size: 670 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="-1 -3 36 36"><path d="M22.86 15.43q0-.25-.16-.4l-6.3-6.3q-.15-.16-.4-.16t-.4.16L9.3 15q-.18.2-.18.43 0 .25.16.4t.4.17h4v6.3q0 .22.18.4t.4.16h3.44q.23 0 .4-.17t.17-.4V16h4q.22 0 .4-.17t.16-.4zm11.43 5.14q0 2.84-2.06 4.85t-4.85 2H8q-3.3 0-5.65-2.34T0 19.43q0-2.32 1.25-4.3T4.6 12.2l-.03-.77q0-3.8 2.68-6.47t6.47-2.68q2.78 0 5.1 1.56t3.36 4.12q1.27-1.1 2.96-1.1 1.9 0 3.24 1.34t1.34 3.23q0 1.35-.74 2.46 2.32.5 3.82 2.4t1.5 4.23z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="-1 -3 36 36"><path d="M22.86 15.43q0-.25-.16-.4l-6.3-6.3q-.15-.16-.4-.16t-.4.16L9.3 15q-.18.2-.18.43 0 .25.16.4t.4.17h4v6.3q0 .22.18.4t.4.16h3.44q.23 0 .4-.17t.17-.4V16h4q.22 0 .4-.17t.16-.4zm11.43 5.14q0 2.84-2.06 4.85t-4.85 2H8q-3.3 0-5.65-2.34T0 19.43q0-2.32 1.25-4.3T4.6 12.2l-.03-.77q0-3.8 2.68-6.47t6.47-2.68q2.78 0 5.1 1.56t3.36 4.12q1.27-1.1 2.96-1.1 1.9 0 3.24 1.34t1.34 3.23q0 1.35-.74 2.46 2.32.5 3.82 2.4t1.5 4.23z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 507 B After Width: | Height: | Size: 508 B |
@@ -92,10 +92,10 @@ const props = withDefaults(
|
||||
.breadcrumb a:nth-child(even) {
|
||||
background: var(--breadcrumb-background-even);
|
||||
}
|
||||
.breadcrumb a:nth-child(odd):hover {
|
||||
.breadcrumb a:nth-child(odd):hover, .breadcrumb a:focus:nth-child(odd) {
|
||||
background: var(--breadcrumb-hover-background-odd);
|
||||
}
|
||||
.breadcrumb a:nth-child(even):hover {
|
||||
.breadcrumb a:nth-child(even):hover, .breadcrumb a:focus:nth-child(even) {
|
||||
background: var(--breadcrumb-hover-background-even);
|
||||
}
|
||||
.breadcrumb a:hover {
|
||||
|
||||
@@ -1,96 +1,104 @@
|
||||
<template>
|
||||
<main>
|
||||
<table v-if="props.documents.length || editing">
|
||||
<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-if="editing?.key === 'new'" class="folder">
|
||||
<td class="selection"></td>
|
||||
<td class="name">
|
||||
<FileRenameInput
|
||||
:doc="editing"
|
||||
:rename="mkdir"
|
||||
<table v-if="props.documents.length || editing" @blur="cursor = null">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="selection">
|
||||
<input
|
||||
type="checkbox"
|
||||
tabindex="-1"
|
||||
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 modified right"
|
||||
:class="{ sortactive: sort === 'size' }"
|
||||
@click="toggleSort('size')"
|
||||
>
|
||||
Size
|
||||
</th>
|
||||
<th class="menu"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="editing?.key === 'new'" class="folder">
|
||||
<td class="selection"></td>
|
||||
<td class="name">
|
||||
<FileRenameInput
|
||||
:doc="editing"
|
||||
:rename="mkdir"
|
||||
:exit="
|
||||
() => {
|
||||
editing = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
<td class="modified right">{{ editing.modified }}</td>
|
||||
<td class="size right">{{ editing.sizedisp }}</td>
|
||||
<td class="menu"></td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="doc of sorted(props.documents as FolderDocument[])"
|
||||
:key="doc.key"
|
||||
:id="`file-${doc.key}`"
|
||||
:class="{
|
||||
file: doc.type === 'file',
|
||||
folder: doc.type === 'folder',
|
||||
cursor: cursor === doc
|
||||
}"
|
||||
@click="cursor = cursor === doc ? null : doc"
|
||||
@contextmenu.prevent="contextMenu($event, doc)"
|
||||
>
|
||||
<td class="selection" @click.up.stop="cursor = cursor === doc ? doc : null">
|
||||
<input
|
||||
type="checkbox"
|
||||
tabindex="-1"
|
||||
:checked="documentStore.selected.has(doc.key)"
|
||||
@change="($event.target as HTMLInputElement).checked ? documentStore.selected.add(doc.key) : documentStore.selected.delete(doc.key)"
|
||||
/>
|
||||
</td>
|
||||
<td class="name">
|
||||
<template v-if="editing === doc"
|
||||
><FileRenameInput
|
||||
:doc="doc"
|
||||
:rename="rename"
|
||||
:exit="
|
||||
() => {
|
||||
editing = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
<td class="right">{{ editing.modified }}</td>
|
||||
<td class="right">{{ editing.sizedisp }}</td>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="doc of sorted(props.documents as FolderDocument[])"
|
||||
:key="doc.key"
|
||||
:class="doc.type === 'folder' ? 'folder' : 'file'"
|
||||
>
|
||||
<td class="selection">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="doc.key in documentStore.selected"
|
||||
@change="documentStore.selected.add(doc.key)"
|
||||
/>
|
||||
</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>
|
||||
<div v-else>
|
||||
<p>No files</p>
|
||||
</div>
|
||||
</main>
|
||||
/></template>
|
||||
<template v-else>
|
||||
<a :href="url_for(doc)" tabindex="-1" @contextmenu.stop @click.stop @focus.stop="cursor = doc">{{ doc.name }}</a>
|
||||
<button @click="() => (editing = doc)">🖊️</button>
|
||||
</template>
|
||||
</td>
|
||||
<td class="modified right">{{ doc.modified }}</td>
|
||||
<td class="size right">{{ doc.sizedisp }}</td>
|
||||
<td class="menu"><button tabindex="-1" @click.stop="cursor = doc; contextMenu($event, doc)">⋮</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else class="empty-container">Nothing to see here</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watchEffect } from 'vue'
|
||||
import { useDocumentStore } from '@/stores/documents'
|
||||
import type { Document, FolderDocument } from '@/repositories/Document'
|
||||
import FileRenameInput from './FileRenameInput.vue'
|
||||
@@ -113,8 +121,9 @@ const linkBasePath = computed(() => {
|
||||
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
|
||||
const url_for = (doc: FolderDocument) =>
|
||||
doc.type === 'folder'
|
||||
? `#${linkBasePath.value}/${doc.name}`
|
||||
? `#${linkBasePath.value}/${doc.name}/`
|
||||
: `${filesBasePath.value}/${doc.name}`
|
||||
const cursor = ref<FolderDocument | null>(null)
|
||||
// File rename
|
||||
const editing = ref<FolderDocument | null>(null)
|
||||
const rename = (doc: FolderDocument, newName: string) => {
|
||||
@@ -152,6 +161,46 @@ defineExpose({
|
||||
modified: formatUnixDate(now)
|
||||
}
|
||||
},
|
||||
toggleSelectAll() {
|
||||
console.log("Select")
|
||||
allSelected.value = !allSelected.value
|
||||
},
|
||||
isCursor() {
|
||||
return cursor.value !== null && editing.value === null
|
||||
},
|
||||
cursorRename() {
|
||||
editing.value = cursor.value
|
||||
},
|
||||
cursorSelect() {
|
||||
console.log("select", documentStore.selected)
|
||||
const doc = cursor.value
|
||||
if (!doc) return
|
||||
if (documentStore.selected.has(doc.key)) {
|
||||
documentStore.selected.delete(doc.key)
|
||||
} else {
|
||||
documentStore.selected.add(doc.key)
|
||||
}
|
||||
},
|
||||
cursorMove(d: number) {
|
||||
// Move cursor up or down (keyboard navigation)
|
||||
const documents = sorted(props.documents as FolderDocument[])
|
||||
if (documents.length === 0) {
|
||||
cursor.value = null
|
||||
return
|
||||
}
|
||||
const mod = (a: number, b: number) => ((a % b) + b) % b
|
||||
const index = cursor.value !== null ? documents.indexOf(cursor.value) : -1
|
||||
cursor.value = documents[mod(index + d, documents.length + 1)] ?? null
|
||||
const tr = document.getElementById(`file-${cursor.value.key}`) as HTMLTableRowElement | null
|
||||
// @ts-ignore
|
||||
if (tr) tr.scrollIntoView({ block: 'center' })
|
||||
}
|
||||
})
|
||||
watchEffect(() => {
|
||||
if (cursor.value) {
|
||||
const a = document.querySelector(`#file-${cursor.value.key} .name a`) as HTMLAnchorElement | null
|
||||
if (a) a.focus()
|
||||
}
|
||||
})
|
||||
const mkdir = (doc: FolderDocument, name: string) => {
|
||||
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
|
||||
@@ -194,7 +243,7 @@ const selectionIndeterminate = computed({
|
||||
get: () => {
|
||||
return (
|
||||
props.documents.length > 0 &&
|
||||
props.documents.some((doc: Document) => doc.key in documentStore.selected) &&
|
||||
props.documents.some((doc: Document) => documentStore.selected.has(doc.key)) &&
|
||||
!allSelected.value
|
||||
)
|
||||
},
|
||||
@@ -205,10 +254,11 @@ const allSelected = computed({
|
||||
get: () => {
|
||||
return (
|
||||
props.documents.length > 0 &&
|
||||
props.documents.every((doc: Document) => doc.key in documentStore.selected)
|
||||
props.documents.every((doc: Document) => documentStore.selected.has(doc.key))
|
||||
)
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
console.log("Setting allSelected", value)
|
||||
for (const doc of props.documents) {
|
||||
if (value) {
|
||||
documentStore.selected.add(doc.key)
|
||||
@@ -218,26 +268,39 @@ const allSelected = computed({
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const contextMenu = (ev: Event, doc: Document) => {
|
||||
console.log('Context menu', ev, doc)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
table input[type='checkbox'] {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
table .selection {
|
||||
width: 1rem;
|
||||
}
|
||||
table .modified {
|
||||
width: 10em;
|
||||
width: 10rem;
|
||||
}
|
||||
table .size {
|
||||
width: 6em;
|
||||
width: 5rem;
|
||||
}
|
||||
table .menu {
|
||||
width: 1rem;
|
||||
}
|
||||
tbody td {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
table th,
|
||||
table td {
|
||||
padding: 0.5em;
|
||||
padding: 0 0.5rem;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
@@ -246,28 +309,20 @@ table td {
|
||||
}
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
text-overflow: initial;
|
||||
overflow: initial;
|
||||
}
|
||||
.name button {
|
||||
visibility: hidden;
|
||||
padding-left: 1em;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.name:hover button {
|
||||
visibility: visible;
|
||||
}
|
||||
.name button {
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
thead tr {
|
||||
border: 1px solid #ddd;
|
||||
background: #ddd;
|
||||
background: linear-gradient(to bottom, #eee, #fff 30%, #ddd);
|
||||
color: #000;
|
||||
}
|
||||
tbody tr:hover {
|
||||
background: #00f8;
|
||||
tbody tr.cursor {
|
||||
background: var(--accent-color);
|
||||
}
|
||||
.right {
|
||||
text-align: right;
|
||||
@@ -279,7 +334,7 @@ tbody tr:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.sortcolumn:hover::after {
|
||||
color: #f80;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
.sortcolumn {
|
||||
padding-right: 1.7em;
|
||||
@@ -289,16 +344,12 @@ tbody tr:hover {
|
||||
color: #888;
|
||||
margin: 0 1em 0 0.5em;
|
||||
position: absolute;
|
||||
transition: all 0.2s linear;
|
||||
transition: all var(--transition-time) linear;
|
||||
}
|
||||
.sortactive::after {
|
||||
transform: rotate(90deg);
|
||||
color: #000;
|
||||
}
|
||||
main {
|
||||
padding: 5px;
|
||||
height: 100%;
|
||||
}
|
||||
.more-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -325,4 +376,10 @@ main {
|
||||
content: '📁 ';
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.empty-container {
|
||||
padding-top: 3rem;
|
||||
text-align: center;
|
||||
font-size: 3rem;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,5 +43,6 @@ input#FileRenameInput {
|
||||
width: 90%;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { useDocumentStore } from '@/stores/documents'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { ref, nextTick, watchEffect } from 'vue'
|
||||
|
||||
const documentStore = useDocumentStore()
|
||||
const searchQuery = ref<string>('')
|
||||
const showSearchInput = ref<boolean>(false)
|
||||
const search = ref<HTMLInputElement | null>()
|
||||
|
||||
const toggleSearchInput = () => {
|
||||
showSearchInput.value = !showSearchInput.value
|
||||
if (!showSearchInput.value) {
|
||||
searchQuery.value = ''
|
||||
}
|
||||
nextTick(() => {
|
||||
const input = search.value
|
||||
if (input) input.focus()
|
||||
executeSearch()
|
||||
})
|
||||
}
|
||||
|
||||
const executeSearch = (ev: Event) => {
|
||||
// FIXME: Make reactive instead of this update handler
|
||||
const query = (ev.target as HTMLInputElement).value
|
||||
console.log('Searching', query)
|
||||
documentStore.setFilter(query)
|
||||
console.log('Filtered')
|
||||
const executeSearch = () => {
|
||||
documentStore.setFilter(search.value?.value ?? '')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
toggleSearchInput
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -32,26 +29,28 @@ const executeSearch = (ev: Event) => {
|
||||
<div class="buttons">
|
||||
<UploadButton />
|
||||
<SvgButton name="create-folder" @click="() => documentStore.fileExplorer.newFolder()"/>
|
||||
<template v-if="true">
|
||||
<template v-if="documentStore.selected.size > 0">
|
||||
<div class="smallgap"></div>
|
||||
<p>N selected files:</p>
|
||||
<p class="select-text">{{ documentStore.selected.size }} selected ➤</p>
|
||||
<!-- Needs better icons for copy/move/remove -->
|
||||
<SvgButton name="copy" />
|
||||
<SvgButton name="download" />
|
||||
<SvgButton name="copy" />
|
||||
<SvgButton name="paste" />
|
||||
<SvgButton name="trash" />
|
||||
<button @click="documentStore.selected.clear()">❌</button>
|
||||
</template>
|
||||
<div class="spacer"></div>
|
||||
<SvgButton name="find" @click="toggleSearchInput" />
|
||||
<template v-if="showSearchInput">
|
||||
<input
|
||||
ref="search"
|
||||
type="search"
|
||||
v-model="searchQuery"
|
||||
class="margin-input"
|
||||
@keyup.esc="toggleSearchInput"
|
||||
@input="executeSearch"
|
||||
/>
|
||||
</template>
|
||||
<SvgButton name="find" @click="toggleSearchInput" />
|
||||
<SvgButton name="cog" @click="console.log('TODO open settings')" />
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -68,6 +67,9 @@ const executeSearch = (ev: Event) => {
|
||||
.smallgap {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.select-text {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
.search-widget {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<h3 v-if="loginForm.error.length > 0" class="error-text">
|
||||
{{ loginForm.error }}
|
||||
</h3>
|
||||
<input type="submit" class="button-login" />
|
||||
<input id="submit" type="submit" class="button-login" />
|
||||
</form>
|
||||
</ModalDialog>
|
||||
</template>
|
||||
|
||||
@@ -24,14 +24,17 @@ button {
|
||||
transition: all 0.2s ease;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
button:hover {
|
||||
button:hover, button:focus {
|
||||
color: #fff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
svg {
|
||||
fill: #ccc;
|
||||
transform: fill 0.2s ease;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
button:hover svg {
|
||||
button:hover svg, button:focus svg {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,5 +14,4 @@ app.config.errorHandler = err => {
|
||||
app.use(createPinia())
|
||||
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -78,6 +78,7 @@ export const useDocumentStore = defineStore({
|
||||
this.document = dataMapped
|
||||
},
|
||||
setFilter(filter: string) {
|
||||
if (filter === '') return this.updateTable({})
|
||||
function traverseDir(data: DirEntry | FileEntry, path: string) {
|
||||
if (!('dir' in data)) return
|
||||
for (const [name, attr] of Object.entries(data.dir)) {
|
||||
|
||||
@@ -43,14 +43,14 @@ function enter(el: Element, done: () => void) {
|
||||
function leave(el: Element, done: () => void) {
|
||||
const elem = el as HTMLElement
|
||||
elem.style.transform = 'translateX(-100%)'
|
||||
setTimeout(done, 300) // Assuming 300ms is your transition duration
|
||||
setTimeout(done, 200) // Should match --transition-time
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
transition: transform 300ms ease;
|
||||
transition: transform var(--transition-time) linear;
|
||||
}
|
||||
.slide-fade-enter,
|
||||
.slide-fade-leave-to /* .slide-fade-leave-active for <2.1.8 */ {
|
||||
|
||||
Reference in New Issue
Block a user