Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 68 additions & 18 deletions static/css/daw.css
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,8 @@ input, textarea { font-family: inherit; }
transition: background var(--t-fast), color var(--t-fast);
user-select: none;
}
.folder-head:hover { background: var(--panel); color: var(--fg); }
/* Unsorted (.system) has no controls to reveal, so hovering it does not light it up (#695). */
.folder-head:not(.system):hover { background: var(--panel); color: var(--fg); }
.folder-head .f-chevron { width: 11px; height: 11px; transition: transform 180ms ease; flex-shrink: 0; color: var(--muted); }
.folder.collapsed .folder-head .f-chevron { transform: rotate(-90deg); }
.folder-head .f-icon { width: 13px; height: 13px; flex-shrink: 0; color: var(--folder-color); }
Expand Down Expand Up @@ -2527,11 +2528,23 @@ input, textarea { font-family: inherit; }
}
/* The clusters, and only the clusters, scroll. Favourite and Export Mix sit
outside this box so a window too narrow for the row cannot carry them off
the right-hand edge. */
the right-hand edge.

Sideways only. overflow-x: auto alone computes overflow-y to auto too, and
the strip is sized to its content, so a vertical scrollbar has nothing to
scroll. It can still appear: at a fractional device pixel ratio (250%
Windows scaling) content and box heights round differently, and a sub-pixel
of phantom overflow puts a classic scrollbar down the strip's right edge.
That takes its width out of the row, space-between at the "click" level
spreads the loss across every gap, and footerFit's observer re-measures.
That matches the breathing row of #633 exactly: every gap moving, Export
Mix not. hidden clips exactly what auto already clipped, and never takes
width from the row. */
.footer-clusters {
flex: 1; min-width: 0;
display: flex; align-items: flex-start; flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
gap: 16px;
}
/* Acts on the track, not on playback, so it is pinned opposite the clusters
Expand Down Expand Up @@ -3017,8 +3030,8 @@ input, textarea { font-family: inherit; }
Nothing is removed and nothing changes behaviour -- the same panel, one
click away, and back inline the moment the row has room for it again.

Fixed, not absolute: .footer-clusters is overflow-x: auto, which computes
overflow-y to auto as well, so an absolutely positioned panel would be
Fixed, not absolute: .footer-clusters scrolls sideways and clips
vertically (overflow-y: hidden), so an absolutely positioned panel would be
clipped by the strip it belongs to. Fixed escapes that (no ancestor here
creates a containing block), and transport.js sets left/top from the
trigger's rect -- the same shape as .search-panel below. */
Expand All @@ -3029,8 +3042,8 @@ input, textarea { font-family: inherit; }
gap: 9px;
position: fixed;
z-index: 300;
/* 260px takes the count-in and meter selects side by side on the first row,
so the popover is two short rows rather than one control per line. */
/* 260px keeps the popover to a few short rows: count-in with its hint, then
the meter selects side by side, rather than one control per line. */
width: 260px;
padding: 10px;
background: var(--panel); border: 1px solid var(--border-strong);
Expand All @@ -3041,6 +3054,21 @@ input, textarea { font-family: inherit; }
.footer-clusters.collapse-click .footer-metro-panel:not(.open) { display: none; }
.footer-clusters.collapse-click .footer-metro-panel.hidden { display: none !important; }

/* The count-in plays with the click off too (#696), so in the popover it sits
apart: its own row, a caption saying so, and a rule between it and the
click-only settings below. A full-width item is what breaks the row, so the
meter select moves down beside Grid. Popover only: inline it would be one
more flex item on a row footerFit is already measuring to the pixel. */
.metro-countin-hint { display: none; }
.footer-clusters.collapse-click .metro-countin-hint {
display: block;
flex-basis: 100%;
margin-top: -4px;
padding-bottom: 9px;
border-bottom: 1px solid var(--border);
font-size: 11px; line-height: 1.35; color: var(--muted);
}

/* The popover frees more width than the row was short -- the click cluster
goes from about 450px to about 140px in one step, and there is no smaller
step to take. Left-aligned, the surplus pools as a hole between the last
Expand Down Expand Up @@ -3099,8 +3127,8 @@ input, textarea { font-family: inherit; }
.metro-vol-icon { flex-shrink: 0; }

/* Fixed rather than absolute for the same reason the options popover is:
.footer-clusters is overflow-x: auto, which computes overflow-y to auto as
well, so an absolutely positioned panel would be clipped by the strip its
.footer-clusters scrolls sideways and clips vertically (overflow-y: hidden),
so an absolutely positioned panel would be clipped by the strip its
button sits in. transport.js sets left/top from that button's rect. */
.metro-vol-panel {
position: fixed; z-index: 320;
Expand Down Expand Up @@ -4541,7 +4569,9 @@ input, textarea { font-family: inherit; }
display: flex;
flex-direction: column;
justify-content: center;
gap: 6px;
/* 4px, not 6: with the meta line on two rows (#698) that is what keeps the
card inside the bar's height rather than growing it by 2px. */
gap: 4px;
padding: 0 14px;
background: var(--panel-2, var(--panel));
border: 1px solid var(--border);
Expand Down Expand Up @@ -4630,18 +4660,24 @@ input, textarea { font-family: inherit; }
color: var(--muted);
}
#nowPlayingPanel .daw-track-meta-line {
/* Indented to start under the title rather than under the artwork, and kept
to one line so the card holds its shape whatever the track is called. The
footer lets it wrap, because its gutter is narrow and tall; this one is
wide and short.

Not indented under the title either: the 51px that would cost is the
difference between the line fitting and ending in an ellipsis. */
/* Not indented under the title: the 51px that would cost is the difference
between the line fitting on one row and needing two.

It was kept to one row with nowrap and an ellipsis, but the ellipsis never
fired: every child is a flex item, so the last one, the extracted date,
was simply cut off at the card's edge (#698). It wraps instead, a whole
item at a time, so the date moves to a second row when the first is full.
The 40px cover already makes the card tall enough for two rows, with the
tighter gap on the card above. Never more than two: just above the
breakpoint that hides the card, a long language leaves it under 250px
wide, and a third row would make the whole bar taller. */
margin: 0;
flex-wrap: nowrap;
flex-wrap: wrap;
row-gap: 0;
line-height: 1.35;
max-height: 2.7em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

/* 7. The whole waveform strip in the bottom bar.
Expand Down Expand Up @@ -4757,6 +4793,20 @@ input, textarea { font-family: inherit; }
opacity: 0.4;
pointer-events: none;
}
/* Except when the panel is the popover (#697). There it has a box of its own,
* and fading that box with its contents left a 40% ghost of a panel floating
* over the lanes, their lines showing through it and no edge to tie it to the
* button, which read as a popover in the wrong place. It also let a click
* inside fall through to whatever was under it. So the box stays solid and
* takes its clicks, and only the controls in it go quiet. */
.footer-clusters.collapse-click .footer-metro-panel.unavailable {
opacity: 1;
pointer-events: auto;
}
.footer-clusters.collapse-click .footer-metro-panel.unavailable > * {
opacity: 0.4;
pointer-events: none;
}

/* Vocals and its two mode buttons, as one thing.
*
Expand Down
7 changes: 6 additions & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -880,14 +880,19 @@
</div>
<div class="footer-metro-panel unavailable" id="t-metro-panel" role="group" aria-label="Click track options" data-i18n-aria-label="click.optionsAria">
<span class="metro-select-wrap">
<select id="t-metro-countin" class="metro-select" title="Bars of click before the song starts when you press play" data-i18n-title="click.countInTitle" aria-label="Count-in" data-i18n-aria-label="click.countIn">
<select id="t-metro-countin" class="metro-select" title="Bars of click before the song starts when you press play. Plays even with the click track off." data-i18n-title="click.countInTitle" aria-label="Count-in" data-i18n-aria-label="click.countIn">
<option value="0" selected data-i18n="click.countInOff">Count-in: off</option>
<option value="1" data-i18n="click.countIn1">Count-in: 1 bar</option>
<option value="2" data-i18n="click.countIn2">Count-in: 2 bars</option>
<option value="3" data-i18n="click.countIn3">Count-in: 3 bars</option>
<option value="4" data-i18n="click.countIn4">Count-in: 4 bars</option>
</select>
</span>
<!-- The count-in plays with the click off as well as on (#696),
which reads as a fault in a popover titled for the click.
Popover only (see .metro-countin-hint in daw.css): inline,
the tooltip says it and the row has no room for a caption. -->
<span class="metro-countin-hint" data-i18n="click.countInHint">Plays even with the click track off</span>
<span class="metro-select-wrap">
<select id="t-metro-bar" class="metro-select" aria-label="Accent every N beats" data-i18n-aria-label="click.barAccentAria">
<option value="-1" selected data-i18n="click.auto">Auto (detected)</option>
Expand Down
62 changes: 42 additions & 20 deletions static/js/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,13 +527,14 @@ function deriveQuality(track) {
// "local:<title>" with the extension removed, so "Lossless (WAV)" never
// appeared for a real upload, and a title with a dot in it was misread
// (#690).
const format = track.sourceFormat;
if (format === "wav") return i18nT("track.losslessWav");
if (format === "mp3") return i18nT("track.compressedMp3");
return i18nT("track.localFile");
// Just the format ("MP3", "FLAC"): the line is short and ends in the date,
// which a longer label pushed out of the card (#698). No format known, as
// for a track from before #690, returns "" and the chip is hidden: it used
// to fall back to "Local file", repeating the source chip beside it.
return trackFormat(track).toUpperCase();
}
if (sourceUrl.includes("youtube.com") || sourceUrl.includes("youtu.be")) return i18nT("track.qualityHigh");
if (sourceUrl.includes("soundcloud.com")) return i18nT("track.compressedMp3");
if (sourceUrl.includes("soundcloud.com")) return "MP3";
return "—";
}

Expand Down Expand Up @@ -612,7 +613,14 @@ function applyTrackInfoToPanel(track) {
if (metaDuration) metaDuration.textContent = track.duration ? fmtTime(track.duration) : "—";
if (trackExtracted) trackExtracted.textContent = fmtExtracted(track.createdAt);
if (trackSource) trackSource.textContent = deriveSource(track.sourceUrl);
if (trackQuality) trackQuality.textContent = deriveQuality(track);
if (trackQuality) {
const quality = deriveQuality(track);
trackQuality.textContent = quality;
// Hidden with the separator in front of it, so there is no empty "· ·".
trackQuality.hidden = !quality;
const sep = trackQuality.previousElementSibling;
if (sep?.classList.contains("daw-track-sep")) sep.hidden = !quality;
}
// The now-playing square shows the same format icon the library row does.
paintNowPlayingArt(trackFormat(track));
if (favBtn) {
Expand Down Expand Up @@ -1435,6 +1443,10 @@ function renderTrackItem(trackId, { inTrash = false } = {}) {
function renderFolder(folder) {
const isTrash = folder.id === TRASH_ID;
const isUnsorted = folder.id === UNSORTED_ID;
// Unsorted is the system folder every new import lands in: it has no grip,
// no New subfolder and no delete, so its row has nothing to reveal on hover
// (#695). The Stem Collections header already has New Folder.
const isSystem = isTrash || isUnsorted;
const isSubfolder = Boolean(folder.parentId);
if (!isTrash) folder.color = normalizeFolderColor(folder.color);

Expand All @@ -1443,15 +1455,15 @@ function renderFolder(folder) {
el.dataset.id = folder.id;

const head = document.createElement("div");
head.className = "folder-head";
head.className = isUnsorted ? "folder-head system" : "folder-head";
if (!isTrash) head.style.setProperty("--folder-color", folder.color);

const folderIcon = isTrash
? `<svg class="f-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"></path></svg>`
: `<svg class="f-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>`;

head.innerHTML = `
${isTrash ? "" : `<span class="f-grip" title="${esc(i18nT("folder.dragToReorder"))}">
${isSystem ? "" : `<span class="f-grip" title="${esc(i18nT("folder.dragToReorder"))}">
<svg viewBox="0 0 24 24" width="10" height="10" fill="currentColor" aria-hidden="true">
<circle cx="9" cy="5" r="1.5"/><circle cx="15" cy="5" r="1.5"/>
<circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/>
Expand All @@ -1462,16 +1474,16 @@ function renderFolder(folder) {
${folderIcon}
<span class="f-name">${esc(folder.name)}</span>
<span class="f-count">${folder.items.length}</span>
${isTrash ? "" : `
${isSystem ? "" : `
<button class="f-subfolder" type="button" aria-label="${esc(i18nT("folder.newSubfolder"))}" title="${esc(i18nT("folder.newSubfolder"))}">
<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<path d="M12 11v6M9 14h6"/>
</svg>
</button>
${isUnsorted ? "" : `<button class="f-del" type="button" aria-label="${esc(i18nT("folder.deleteFolder"))}" title="${esc(i18nT("folder.deleteFolder"))}">
<button class="f-del" type="button" aria-label="${esc(i18nT("folder.deleteFolder"))}" title="${esc(i18nT("folder.deleteFolder"))}">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"></path></svg>
</button>`}
</button>
`}
`;

Expand Down Expand Up @@ -1565,11 +1577,10 @@ function renderFolder(folder) {
if (isFolderDescendant(folderDragId, folder.id)) return; // prevent cycle
e.preventDefault();
e.dataTransfer.dropEffect = "move";
const rect = head.getBoundingClientRect();
const rel = (e.clientY - rect.top) / rect.height;
el.classList.toggle("drop-before", rel < 0.25);
el.classList.toggle("drop-into", rel >= 0.25 && rel < 0.75);
el.classList.toggle("drop-after", rel >= 0.75);
const zone = folderDropZone(e, head, isUnsorted);
el.classList.toggle("drop-before", zone === "before");
el.classList.toggle("drop-into", zone === "into");
el.classList.toggle("drop-after", zone === "after");
return;
}
if (!isTrackDragEvent(e)) return;
Expand All @@ -1585,11 +1596,10 @@ function renderFolder(folder) {
el.addEventListener("drop", (e) => {
e.preventDefault();
if (folderDragId && folderDragId !== folder.id && !isTrash) {
const rect = head.getBoundingClientRect();
const rel = (e.clientY - rect.top) / rect.height;
const zone = folderDropZone(e, head, isUnsorted);
el.classList.remove("drop-before", "drop-after", "drop-into");
if (rel < 0.25) reorderFolder(folderDragId, folder.id, true);
else if (rel >= 0.75) reorderFolder(folderDragId, folder.id, false);
if (zone === "before") reorderFolder(folderDragId, folder.id, true);
else if (zone === "after") reorderFolder(folderDragId, folder.id, false);
else reparentFolder(folderDragId, folder.id);
return;
}
Expand All @@ -1600,6 +1610,18 @@ function renderFolder(folder) {
return el;
}

// Where a dragged folder lands relative to a folder row: the top quarter
// reorders before it, the bottom quarter after it, the middle nests inside.
// Unsorted takes no subfolders, so it splits into before/after only.
function folderDropZone(e, head, noNesting) {
const rect = head.getBoundingClientRect();
const rel = (e.clientY - rect.top) / rect.height;
if (noNesting) return rel < 0.5 ? "before" : "after";
if (rel < 0.25) return "before";
if (rel >= 0.75) return "after";
return "into";
}

function renderStrip(strip, nonTrash) {
if (!strip) return;
const folderTrackIds = new Set(folders.flatMap((folder) => folder.items));
Expand Down
Loading
Loading