Drag out from file tabs, plate chips and a Drag out handle; only offer dragging in Chromium browsers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f3a69c82ec
commit
be1facdec3
3 changed files with 27 additions and 10 deletions
|
|
@ -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 `
|
||||
<div class="file-card" data-model-id="${m.id}"${!trashed && m.files.length ? ' draggable="true" title="Drag out to save the file"' : ''}>
|
||||
<div class="file-card" data-model-id="${m.id}"${CAN_DRAG_OUT && !trashed && m.files.length ? ' draggable="true" title="Drag out to save the file"' : ''}>
|
||||
<div class="file-thumbnail">${thumbHtml(m)}</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${escapeHtml(m.name)}</div>
|
||||
|
|
@ -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) {
|
|||
? `<a class="btn btn-secondary btn-sm download-all" href="/api/models/${model.id}/download">Download all ${model.files.length} files (.zip)</a>`
|
||||
: '';
|
||||
fileInfoEl.innerHTML = downloadAll + model.files.map((f) => `
|
||||
<div class="file-row" draggable="true" data-drag-url="/api/files/${f.id}/raw" data-drag-name="${escapeHtml(f.originalName)}">
|
||||
<div class="file-row"${dragAttrs(`/api/files/${f.id}/raw`, f.originalName)}>
|
||||
<span>${escapeHtml(f.originalName)} <em>(${formatFileSize(f.size)}${f.volume ? `, ${Number(f.volume).toFixed(2)} cm³` : ''})</em></span>
|
||||
<span class="row-actions">
|
||||
${sliceButtonHtml('file', f.id, f.format)}
|
||||
|
|
@ -1050,7 +1051,7 @@ function renderViewerTabs(activeKey) {
|
|||
activeItemKey = activeKey;
|
||||
renderSlicerBar();
|
||||
document.getElementById('detailFileTabs').innerHTML = detailItems.map((item) => `
|
||||
<button class="file-tab${itemKey(item) === activeKey ? ' active' : ''}" data-key="${itemKey(item)}">${escapeHtml(item.name)}</button>`).join('');
|
||||
<button class="file-tab${itemKey(item) === activeKey ? ' active' : ''}" data-key="${itemKey(item)}"${dragAttrs(rawUrlFor(item), item.name)}>${escapeHtml(item.name)}</button>`).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 `
|
||||
<div class="entry-row" draggable="true" data-drag-url="/api/entries/${e.id}/raw" data-drag-name="${escapeHtml(name)}">
|
||||
<div class="entry-row"${dragAttrs(`/api/entries/${e.id}/raw`, name)}>
|
||||
<span title="${escapeHtml(e.path)}">${escapeHtml(name)}${folder ? `<span class="entry-folder">${escapeHtml(folder)}</span>` : ''} <em>${formatFileSize(e.size)}</em></span>
|
||||
<span class="row-actions">
|
||||
${PREVIEWABLE.includes(e.format) && e.format !== 'mtl' ? `<button type="button" class="link-btn" data-view-entry="${e.id}" data-archive="${fileId}">view</button>` : ''}
|
||||
|
|
@ -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) => `
|
||||
<button type="button" class="plate-chip${item.plate === p.index ? ' active' : ''}" data-plate="${p.index}"
|
||||
<button type="button" class="plate-chip${item.plate === p.index ? ' active' : ''}" data-plate="${p.index}"${dragAttrs(rawUrlFor(item), item.name)}
|
||||
title="${escapeHtml(p.name || `Plate ${p.index}`)} · ${p.objectCount} object${p.objectCount === 1 ? '' : 's'}">
|
||||
${p.thumbnailUrl ? `<img src="${escapeHtml(p.thumbnailUrl)}" alt="">` : ''}
|
||||
<span>${p.index}${p.name ? ` · ${escapeHtml(p.name)}` : ''}</span>
|
||||
</button>`).join('')
|
||||
+ `<button type="button" class="plate-chip${item.plate ? '' : ' active'}" data-plate="all"><span>All plates</span></button>`;
|
||||
+ `<button type="button" class="plate-chip${item.plate ? '' : ' active'}" data-plate="all"${dragAttrs(rawUrlFor(item), item.name)}><span>All plates</span></button>`;
|
||||
}
|
||||
|
||||
const filaments = ((project && project.filaments) || [])
|
||||
|
|
|
|||
|
|
@ -425,6 +425,7 @@
|
|||
<button type="button" class="btn btn-primary btn-sm" id="openInSlicerBtn">Open in slicer</button>
|
||||
<button type="button" class="link-btn slicer-change" id="changeSlicerBtn">change slicer</button>
|
||||
</span>
|
||||
<button type="button" class="btn btn-secondary btn-sm drag-out-btn" id="dragOutBtn" draggable="true" hidden>⠿ Drag out</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="regenThumbBtn">Regenerate thumbnail</button>
|
||||
</div>
|
||||
<div id="slicerNotice" class="slicer-notice" role="status" hidden></div>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue