From 127caeedeab38ef8c9568d8528580ded2b7f4965 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 4 Feb 2026 19:57:31 +0000 Subject: [PATCH] Improved selection toolbar UX. --- frontend/src/components/HeaderSelected.vue | 127 ++++++++++++++++++++- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/HeaderSelected.vue b/frontend/src/components/HeaderSelected.vue index f2b64ef..57476b5 100644 --- a/frontend/src/components/HeaderSelected.vue +++ b/frontend/src/components/HeaderSelected.vue @@ -1,6 +1,19 @@ @@ -19,7 +32,9 @@ import {connect, controlUrl} from '@/repositories/WS' import { useMainStore } from '@/stores/main' import { computed, ref } from 'vue' +import { formatSize } from '@/utils' import CursorTooltip from './CursorTooltip.vue' +import router from '@/router' const unselectTooltip = ref | null>(null) @@ -29,6 +44,77 @@ const props = defineProps({ }) const dst = computed(() => props.path!.join('/')) + +const navigateTo = (path: string) => { + router.push('/' + path) +} + +// Truncate long names to reasonable length +const truncateName = (name: string, maxLen = 20): string => { + if (name.length <= maxLen) return name + return name.slice(0, maxLen - 1) + '…' +} + +interface FolderInfo { + name: string + path: string + count: number +} + +interface SelectionDisplay { + folders: FolderInfo[] + numFolders: number + size: string +} + +const selectionDisplay = computed(() => { + const sel = store.selectedFiles + + // Calculate total size + const totalSize = sel.keys.reduce((sum, key) => { + const doc = sel.docs[key] + return sum + (doc ? doc.size : 0) + }, 0) + const sizeStr = formatSize(totalSize) + + // Group by folder location, storing file names + const folderGroups = new Map() + for (const key of sel.keys) { + const doc = sel.docs[key] + if (!doc) continue + const loc = doc.loc || '' + if (!folderGroups.has(loc)) folderGroups.set(loc, []) + folderGroups.get(loc)!.push(doc.name) + } + + const numFolders = folderGroups.size + + const folders = Array.from(folderGroups.entries()) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([loc, names]) => { + const count = names.length + // For single file, display truncated filename; for multiple, display folder name with count + let displayName: string + if (count === 1) { + displayName = truncateName(names[0]!) + } else { + const folderName = loc ? loc.split('/').pop()! : (store.server.name || 'Root') + displayName = `${truncateName(folderName)} (${count})` + } + return { + name: displayName, + path: loc, + count + } + }) + + return { + folders, + numFolders, + size: sizeStr + } +}) + const op = (opName: string, dst?: string) => { const sel = store.selectedFiles const paths = sel.keys.map(key => { @@ -80,13 +166,46 @@ const op = (opName: string, dst?: string) => { background: transparent; color: var(--header-color); font-size: var(--header-font-size); + gap: 0.3em; + flex-wrap: nowrap; + max-width: 100%; } -.select-text { +.select-info { color: var(--accent-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0; - padding-right: 0.5em; + flex-shrink: 1; + min-width: 0; +} +.select-count { + font-weight: 500; +} +.select-folders { + display: inline; +} +.folder-link, +.folder-link:link, +.folder-link:visited, +.folder-link:active { + color: var(--accent-color); + text-decoration: none; + cursor: pointer; +} +.folder-link:hover { + text-decoration: underline; + color: var(--accent-color); +} +.folder-sep { + color: var(--header-color); + opacity: 0.6; +} +.select-size { + color: var(--header-color); + opacity: 0.8; + font-family: 'Roboto Mono', monospace; + font-size: 0.9em; + margin-left: 0.5em; }