From 15d227fda29c9db0276642612d0baafca1a996e4 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 1 Feb 2026 06:33:34 +0000 Subject: [PATCH] Implement a larger set of file menu options. --- frontend/src/components/FileExplorer.vue | 93 ++++++++++++++++++++++-- frontend/src/components/Gallery.vue | 93 ++++++++++++++++++++++-- 2 files changed, 176 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/FileExplorer.vue b/frontend/src/components/FileExplorer.vue index 3cf0142..83e9aac 100644 --- a/frontend/src/components/FileExplorer.vue +++ b/frontend/src/components/FileExplorer.vue @@ -292,13 +292,96 @@ const allSelected = computed({ const loc = computed(() => props.path.join('/')) +const downloadFile = (doc: Doc) => { + const path = doc.loc ? `${doc.loc}/${doc.name}` : doc.name + if (doc.dir) { + // Download folder as ZIP + const a = document.createElement('a') + a.href = `/zip/${doc.key}/${doc.name}.zip` + a.download = '' + a.click() + store.showToast(`Downloading ${doc.name}.zip`) + } else { + // Download single file + const a = document.createElement('a') + a.href = `/files/${path}` + a.download = '' + a.click() + store.showToast(`Downloading ${doc.name}`) + } +} + +const copyLink = async (doc: Doc) => { + const url = new URL(doc.url, window.location.origin).href + try { + await navigator.clipboard.writeText(url) + store.showToast('📋 Link copied!') + } catch { + store.showToast('Failed to copy link') + } +} + +const copyImage = async (doc: Doc) => { + const path = doc.loc ? `${doc.loc}/${doc.name}` : doc.name + try { + store.showToast('Copying image...') + const res = await fetch(`/files/${path}`) + const blob = await res.blob() + // Convert to PNG if needed (clipboard only supports PNG) + if (blob.type !== 'image/png') { + const img = new Image() + img.src = URL.createObjectURL(blob) + await new Promise(r => img.onload = r) + const canvas = document.createElement('canvas') + canvas.width = img.naturalWidth + canvas.height = img.naturalHeight + canvas.getContext('2d')!.drawImage(img, 0, 0) + const pngBlob = await new Promise(r => canvas.toBlob(b => r(b!), 'image/png')) + URL.revokeObjectURL(img.src) + await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })]) + } else { + await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) + } + store.showToast('📋 Image copied!') + } catch (e) { + console.error('Copy image failed', e) + store.showToast('Failed to copy image') + } +} + +const deleteFile = (doc: Doc) => { + const path = doc.loc ? `${doc.loc}/${doc.name}` : doc.name + doc.ghost = true + const control = connect(controlUrl, { + message(ev: MessageEvent) { + const res = JSON.parse(ev.data) + if ('error' in res) { + console.error('Delete failed', res.error) + doc.ghost = false + store.showToast(res.error.message || 'Delete failed') + } else if (res.status === 'ack') { + store.showToast(`🗑️ Deleted ${doc.name}`) + control.close() + } + } + }) + control.onopen = () => { + control.send(JSON.stringify({ op: 'rm', sel: [path] })) + } +} + const contextMenu = (ev: MouseEvent, doc: Doc) => { store.cursor = doc.key - ContextMenu.showContextMenu({ - x: ev.x, y: ev.y, items: [ - { label: 'Rename', onClick: () => { editing.value = doc } }, - ], - }) + const items = [ + { label: '📥 Download', onClick: () => downloadFile(doc) }, + { label: '🔗 Copy Link', onClick: () => copyLink(doc) }, + ] + if (doc.img) items.push({ label: '📋 Copy Image', onClick: () => copyImage(doc) }) + items.push( + { label: '✏️ Rename', onClick: () => { editing.value = doc } }, + { label: '🗑️ Delete', onClick: () => deleteFile(doc) }, + ) + ContextMenu.showContextMenu({ x: ev.x, y: ev.y, items }) } diff --git a/frontend/src/components/Gallery.vue b/frontend/src/components/Gallery.vue index 0fc5c80..27f32b3 100644 --- a/frontend/src/components/Gallery.vue +++ b/frontend/src/components/Gallery.vue @@ -236,13 +236,96 @@ const allSelected = computed({ const loc = computed(() => props.path.join('/')) +const downloadFile = (doc: Doc) => { + const path = doc.loc ? `${doc.loc}/${doc.name}` : doc.name + if (doc.dir) { + // Download folder as ZIP + const a = document.createElement('a') + a.href = `/zip/${doc.key}/${doc.name}.zip` + a.download = '' + a.click() + store.showToast(`Downloading ${doc.name}.zip`) + } else { + // Download single file + const a = document.createElement('a') + a.href = `/files/${path}` + a.download = '' + a.click() + store.showToast(`Downloading ${doc.name}`) + } +} + +const copyLink = async (doc: Doc) => { + const url = new URL(doc.url, window.location.origin).href + try { + await navigator.clipboard.writeText(url) + store.showToast('📋 Link copied!') + } catch { + store.showToast('Failed to copy link') + } +} + +const copyImage = async (doc: Doc) => { + const path = doc.loc ? `${doc.loc}/${doc.name}` : doc.name + try { + store.showToast('Copying image...') + const res = await fetch(`/files/${path}`) + const blob = await res.blob() + // Convert to PNG if needed (clipboard only supports PNG) + if (blob.type !== 'image/png') { + const img = new Image() + img.src = URL.createObjectURL(blob) + await new Promise(r => img.onload = r) + const canvas = document.createElement('canvas') + canvas.width = img.naturalWidth + canvas.height = img.naturalHeight + canvas.getContext('2d')!.drawImage(img, 0, 0) + const pngBlob = await new Promise(r => canvas.toBlob(b => r(b!), 'image/png')) + URL.revokeObjectURL(img.src) + await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })]) + } else { + await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) + } + store.showToast('📋 Image copied!') + } catch (e) { + console.error('Copy image failed', e) + store.showToast('Failed to copy image') + } +} + +const deleteFile = (doc: Doc) => { + const path = doc.loc ? `${doc.loc}/${doc.name}` : doc.name + doc.ghost = true + const control = connect(controlUrl, { + message(ev: MessageEvent) { + const res = JSON.parse(ev.data) + if ('error' in res) { + console.error('Delete failed', res.error) + doc.ghost = false + store.showToast(res.error.message || 'Delete failed') + } else if (res.status === 'ack') { + store.showToast(`🗑️ Deleted ${doc.name}`) + control.close() + } + } + }) + control.onopen = () => { + control.send(JSON.stringify({ op: 'rm', sel: [path] })) + } +} + const contextMenu = (ev: MouseEvent, doc: Doc) => { store.cursor = doc.key - ContextMenu.showContextMenu({ - x: ev.x, y: ev.y, items: [ - { label: 'Rename', onClick: () => { editing.value = doc } }, - ], - }) + const items = [ + { label: '📥 Download', onClick: () => downloadFile(doc) }, + { label: '🔗 Copy Link', onClick: () => copyLink(doc) }, + ] + if (doc.img) items.push({ label: '📋 Copy Image', onClick: () => copyImage(doc) }) + items.push( + { label: '✏️ Rename', onClick: () => { editing.value = doc } }, + { label: '🗑️ Delete', onClick: () => deleteFile(doc) }, + ) + ContextMenu.showContextMenu({ x: ev.x, y: ev.y, items }) }