diff --git a/plugins/slides/frontend/views/SlideManager.vue b/plugins/slides/frontend/views/SlideManager.vue index ee8604f..e6a3903 100644 --- a/plugins/slides/frontend/views/SlideManager.vue +++ b/plugins/slides/frontend/views/SlideManager.vue @@ -33,8 +33,16 @@
No slides yet. Upload some images.
-
+
+ ⋮⋮ {{ idx + 1 }} @@ -69,6 +77,57 @@ import { withBase } from '@/utils/basePath' // Slide currently hovered, shown enlarged. Null when the cursor is elsewhere. const previewSlide = ref(null) +// Drag reordering. dragIndex is the row being carried, dropIndex the row it is +// currently over. Both null when no drag is in progress. +const dragIndex = ref(null) +const dropIndex = ref(null) + +function onDragStart(idx, event) { + dragIndex.value = idx + // The hover preview would otherwise sit over the list for the whole drag. + previewSlide.value = null + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move' + // Firefox starts no drag at all unless some data is set. + event.dataTransfer.setData('text/plain', String(idx)) + } +} + +function onDragLeave(idx) { + // Only clear when leaving the row that is actually marked, or the row being + // entered next would clear its own highlight. + if (dropIndex.value === idx) dropIndex.value = null +} + +function onDragEnd() { + dragIndex.value = null + dropIndex.value = null +} + +async function onDrop(target) { + const from = dragIndex.value + onDragEnd() + if (from === null || from === target) return + await reorderTo(from, target) +} + +// Shared by the drag handler and the up/down buttons, so both persist the same +// way and a failure recovers the same way. +async function reorderTo(from, to) { + const arr = slides.value.slice() + const [item] = arr.splice(from, 1) + arr.splice(to, 0, item) + slides.value = arr + try { + await slidesApi.reorder(surface.value, arr.map(s => s.filename)) + } catch (err) { + console.error('Reorder failed:', err) + // The list on screen no longer matches the server; reload rather than + // leave an order that looks saved and is not. + await load() + } +} + const surfaces = [ { key: 'lobby', label: 'Lobby Display' }, { key: 'shopfloor', label: 'Shopfloor Screensaver' } @@ -120,16 +179,7 @@ async function onUpload(event) { async function move(idx, delta) { const target = idx + delta if (target < 0 || target >= slides.value.length) return - const arr = slides.value.slice() - const [item] = arr.splice(idx, 1) - arr.splice(target, 0, item) - slides.value = arr - try { - await slidesApi.reorder(surface.value, arr.map(s => s.filename)) - } catch (err) { - console.error('Reorder failed:', err) - await load() - } + await reorderTo(idx, target) } async function deleteSelected() { @@ -200,6 +250,28 @@ onMounted(load) font-weight: 700; color: var(--text-light); } +.grip { + cursor: grab; + color: var(--text-light); + letter-spacing: -3px; + user-select: none; + padding: 0 0.25rem; +} + +.slide-row { + transition: background 0.12s, opacity 0.12s; +} + +.slide-row.dragging { + opacity: 0.4; +} + +/* A line on the edge being dropped against, rather than moving rows around + under the cursor - that reads as the list fighting the drag. */ +.slide-row.drop-target { + box-shadow: inset 0 2px 0 0 var(--primary); +} + .thumb { width: 120px; height: 68px;