From 6fec48e084046fa59950735f1b725cde5b8d2661 Mon Sep 17 00:00:00 2001
From: Thales <>
Date: Sun, 4 Oct 2026 10:50:07 +0100
Subject: [PATCH] feat(trash): hold a row for a second to restore it, pick
several, and a count on the icon
Restoring from the Trash used to mean dragging one row at a time onto the
Library button, which nothing on screen explained, and a click on a
trashed row opened it in the studio as if it were not trashed.
Hovering a Trash row now turns its second line into "Hold left click for
1 second to restore". A click selects a row, Ctrl+click adds or removes
one, Shift+click takes a range. Holding the mouse button, or Enter, for a
second restores the row, or the whole selection when the row is part of
it; letting go early does nothing. Every row the hold will restore fills
while it is held.
Dragging onto Library still restores, the selection too. Restore never
queues a song again (#748).
The Trash icon carries the same count badge as the Queue, and its label
says how many songs are in it, so a full Trash is visible from anywhere.
Closes #749
Closes #755
---
static/css/daw.css | 31 +++-
static/index.html | 3 +-
static/js/catalog.js | 161 +++++++++++++++++++--
static/js/i18n.js | 20 +++
tests/e2e/trash-restore.spec.mjs | 240 +++++++++++++++++++++++++++++++
5 files changed, 441 insertions(+), 14 deletions(-)
create mode 100644 tests/e2e/trash-restore.spec.mjs
diff --git a/static/css/daw.css b/static/css/daw.css
index db6cde71..cb2090e9 100644
--- a/static/css/daw.css
+++ b/static/css/daw.css
@@ -578,8 +578,9 @@ input, textarea { font-family: inherit; }
.rail-btn > span { white-space: nowrap; max-width: 56px; overflow: hidden; text-overflow: ellipsis; }
/* Queue rail button. Only present while something is importing, so the rail
- stays as it is today for anyone not using the queue. */
-.rail-queue { position: relative; }
+ stays as it is today for anyone not using the queue. The Trash button wears
+ the same badge, with how many songs are in it (#749). */
+.rail-queue, .rail-trash { position: relative; }
.rail-badge {
position: absolute; top: 2px; right: 2px;
min-width: 15px; height: 15px; padding: 0 4px;
@@ -931,6 +932,32 @@ input, textarea { font-family: inherit; }
.cat-fav.active, .cat-fav.active:hover { color: #e54e4e; }
/* A stopped song (#748) offers Extract at rest: it is the row's only way on. */
.cat-item.is-stopped .cat-actions { opacity: 1; pointer-events: auto; }
+
+/* Trash rows look like library rows. On hover the second line becomes the
+ hint, and a hold fills the row from the left over --hold-ms (set from
+ TRASH_HOLD_MS in catalog.js), then restores. */
+.cat-item.in-trash { user-select: none; -webkit-user-select: none; overflow: hidden; }
+.cat-item.in-trash .trash-hint { display: none; color: var(--fg-2); }
+.cat-item.in-trash:hover .trash-hint,
+.cat-item.in-trash:focus-visible .trash-hint,
+.cat-item.in-trash.holding .trash-hint { display: block; overflow: hidden; text-overflow: ellipsis; }
+.cat-item.in-trash:hover .cat-sub:not(.trash-hint),
+.cat-item.in-trash:focus-visible .cat-sub:not(.trash-hint),
+.cat-item.in-trash.holding .cat-sub:not(.trash-hint) { display: none; }
+.trash-hold { position: absolute; inset: 0; pointer-events: none; }
+.trash-hold::before {
+ content: ""; position: absolute; inset: 0;
+ background: color-mix(in srgb, var(--accent) 18%, transparent);
+ transform: scaleX(0); transform-origin: left center;
+}
+.cat-item.in-trash.holding .trash-hold::before {
+ transform: scaleX(1);
+ transition: transform var(--hold-ms, 1000ms) linear;
+}
+.cat-item.in-trash.selected {
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
+ border-color: color-mix(in srgb, var(--accent) 45%, transparent);
+}
.cat-extract {
height: 20px; padding: 0 8px; border: 1px solid var(--accent); border-radius: 5px;
background: transparent; color: var(--accent); cursor: pointer;
diff --git a/static/index.html b/static/index.html
index 7baf25c0..56490e23 100644
--- a/static/index.html
+++ b/static/index.html
@@ -323,11 +323,12 @@
Queue
-