From be1facdec3cc78caaedc5abd389894be2f6974b4 Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Tue, 15 Sep 2026 10:26:48 +1000 Subject: [PATCH] Drag out from file tabs, plate chips and a Drag out handle; only offer dragging in Chromium browsers Co-Authored-By: Claude Opus 5 --- public/app.js | 33 ++++++++++++++++++++++++--------- public/index.html | 1 + public/styles.css | 3 ++- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/public/app.js b/public/app.js index 18dd519..576f0d4 100644 --- a/public/app.js +++ b/public/app.js @@ -401,7 +401,7 @@ function cardHtml(m) { const trashed = Boolean(m.deleted_at); const when = trashed ? `trashed ${formatDate(m.deleted_at)}` : formatDate(m.date_added); return ` -
+
${thumbHtml(m)}
${escapeHtml(m.name)}
@@ -758,10 +758,11 @@ function setupEventListeners() { const target = card && modelDragTarget(findModel(card.dataset.modelId)); if (target) setDragOut(e.dataTransfer, target); }); - document.getElementById('detailFileInfo').addEventListener('dragstart', (e) => { - const row = e.target.closest('[data-drag-url]'); - if (row) setDragOut(e.dataTransfer, { url: row.dataset.dragUrl, name: row.dataset.dragName }); + fileDetailModal.addEventListener('dragstart', (e) => { + const el = e.target.closest && e.target.closest('[data-drag-url]'); + if (el) setDragOut(e.dataTransfer, { url: el.dataset.dragUrl, name: el.dataset.dragName }); }); + document.getElementById('dragOutBtn').addEventListener('click', (e) => followLink(e.currentTarget.dataset.dragUrl)); document.getElementById('detailFileInfo').addEventListener('click', (e) => { const slice = e.target.closest('[data-slice-kind]'); if (slice) { @@ -1000,7 +1001,7 @@ function openModelDetail(modelId) { ? `Download all ${model.files.length} files (.zip)` : ''; fileInfoEl.innerHTML = downloadAll + model.files.map((f) => ` -
+
${escapeHtml(f.originalName)} (${formatFileSize(f.size)}${f.volume ? `, ${Number(f.volume).toFixed(2)} cm³` : ''}) ${sliceButtonHtml('file', f.id, f.format)} @@ -1050,7 +1051,7 @@ function renderViewerTabs(activeKey) { activeItemKey = activeKey; renderSlicerBar(); document.getElementById('detailFileTabs').innerHTML = detailItems.map((item) => ` - `).join(''); + `).join(''); } function openViewerItem(item) { @@ -1116,7 +1117,7 @@ async function loadArchiveEntries(modelId, fileId) { const name = e.path.split('/').pop(); const folder = e.path.replace(/^\.\//, '').slice(0, -name.length); return ` -
+
${escapeHtml(name)}${folder ? `${escapeHtml(folder)}` : ''} ${formatFileSize(e.size)} ${PREVIEWABLE.includes(e.format) && e.format !== 'mtl' ? `` : ''} @@ -1207,6 +1208,13 @@ function handoffItem(kind, id, archiveId) { function renderSlicerBar() { const item = detailItems.find((i) => itemKey(i) === activeItemKey); + const handle = document.getElementById('dragOutBtn'); + handle.hidden = !(item && CAN_DRAG_OUT); + if (item) { + handle.dataset.dragUrl = rawUrlFor(item); + handle.dataset.dragName = item.name; + handle.title = `Drag ${item.name} onto your desktop or a folder to save it`; + } const bar = document.getElementById('slicerBar'); bar.hidden = !(item && SLICEABLE.includes(item.format)); const slicer = SLICERS[prefs.slicer]; @@ -1355,6 +1363,13 @@ function handleSaveSlicer(e) { // ---------------------------------------------------------------- drag-out // Chromium browsers save a dragged element as a file when it carries DownloadURL // ("type:name:url"); the download goes through the browser, session included. +// Safari and Firefox have no equivalent. Only Chromium has navigator.userAgentData. +const CAN_DRAG_OUT = 'userAgentData' in navigator; + +// draggable + data for anything that drags out as a file. +function dragAttrs(url, name) { + return CAN_DRAG_OUT ? ` draggable="true" data-drag-url="${escapeHtml(url)}" data-drag-name="${escapeHtml(name)}"` : ''; +} function setDragOut(dataTransfer, { url, name }) { dataTransfer.effectAllowed = 'copy'; dataTransfer.setData('DownloadURL', `application/octet-stream:${String(name).replace(/[:\\/]/g, '-')}:${new URL(url, window.location.origin).href}`); @@ -1519,12 +1534,12 @@ function renderProjectPanels(item) { } else { platesEl.hidden = false; platesEl.innerHTML = project.plates.map((p) => ` - `).join('') - + ``; + + ``; } const filaments = ((project && project.filaments) || []) diff --git a/public/index.html b/public/index.html index b2f7d25..3ec8380 100644 --- a/public/index.html +++ b/public/index.html @@ -425,6 +425,7 @@ +
diff --git a/public/styles.css b/public/styles.css index 8cc7cef..6f62190 100644 --- a/public/styles.css +++ b/public/styles.css @@ -256,7 +256,8 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut .file-row:last-child { border-bottom: none; } .file-row em { color: var(--text-dim); font-style: normal; font-family: var(--mono); font-size: 12px; } .file-row > span:first-child { min-width: 0; overflow-wrap: anywhere; } -.file-row[draggable="true"], .entry-row[draggable="true"] { cursor: grab; } +.file-row[draggable="true"], .entry-row[draggable="true"], .drag-out-btn { cursor: grab; } +.drag-out-btn:active { cursor: grabbing; } /* ---- slicer hand-off ---- */ .preview-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; }