feat(ui): collapsible ⓘ help, rank-based toggle, tidier quality-profile settings
Quality-profile settings UI cleanup:
- Add the "Rank-based download order" toggle (priority mode). It's hidden when
Best quality is active, since that mode always ranks by quality.
- Plain-language search-strategy options ("fast" / "thorough"); load + save the
new rank_candidates_by_quality flag.
- Move the long help texts behind a dim ⓘ icon that sits on the (fixed) label
row and toggles a collapsible body below — the trigger no longer moves on
open. Applied to: search strategy, rank-based order, off-list fallback,
AcoustID-verified, and the "How it works" ranked-targets explainer.
toggleSettingHelp walks to the next .setting-help-body sibling so it works
regardless of wrapper or an in-between control.
- Fix the "Search strategy" label: zero the flex-row margin so it aligns with
the ⓘ, and bump it to 12px/brighter so it doesn't read as dim/undersized.
- Remove the duplicate "🎵 Quality Profile" heading inside the tile body.
- Replace the inline "Reset to defaults" link with a proper ↺ button.
- Restore the gap between the "Quality priority" label and the target list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ab05508acc
commit
2668980872
3 changed files with 191 additions and 49 deletions
141
webui/index.html
141
webui/index.html
|
|
@ -5067,7 +5067,6 @@
|
|||
|
||||
<!-- Quality Profile Settings (global — applies to all download sources) -->
|
||||
<div class="settings-group" id="quality-profile-section" data-stg="downloads">
|
||||
<h3>🎵 Quality Profile</h3>
|
||||
|
||||
<!-- Presets -->
|
||||
<div class="quality-presets">
|
||||
|
|
@ -5086,17 +5085,38 @@
|
|||
💾 Space Saver
|
||||
</button>
|
||||
</div>
|
||||
<div class="help-text" style="margin-top:6px">
|
||||
Edits you make below are saved <em>per preset</em> — switch away and back and
|
||||
your changes are still there. Use
|
||||
<a href="#" onclick="resetActiveQualityPreset(); return false;">Reset to defaults</a>
|
||||
to restore the selected preset's factory settings.
|
||||
<div class="quality-presets-footer">
|
||||
<span class="help-text" style="padding:0;border:0;background:none;margin:0;flex:1;">
|
||||
Edits below are saved <em>per preset</em> — switch away and back and your
|
||||
changes are still there.
|
||||
</span>
|
||||
<button type="button" class="preset-reset-btn" onclick="resetActiveQualityPreset()"
|
||||
title="Restore the selected preset's factory settings">
|
||||
<span class="preset-reset-icon">↺</span> Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ranked target priority list (v3) -->
|
||||
<div class="ranked-targets-editor">
|
||||
<label class="ranked-targets-label">Quality priority (drag to reorder — 1st = most preferred):</label>
|
||||
<div class="setting-row">
|
||||
<label class="ranked-targets-label" style="margin:0;">Quality priority (drag to reorder — 1st = most preferred):</label>
|
||||
<span class="info-icon" role="button" tabindex="0" title="What's this?"
|
||||
onclick="toggleSettingHelp(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleSettingHelp(this);}">ⓘ</span>
|
||||
</div>
|
||||
<div class="help-text setting-help-body" hidden>
|
||||
<strong>How it works:</strong> Each download source is checked against this list
|
||||
top-down. The first target a source can satisfy wins; a source that meets no target
|
||||
is skipped for the next one (source priority still decides between sources that can).
|
||||
For lossless, bit depth + sample rate decide the match. For MP3/AAC the bitrate is a
|
||||
<em>minimum</em> threshold (≥), so VBR and mono files aren't falsely rejected. After
|
||||
download the real file is verified against the same list. With fallback off, a track
|
||||
is left missing rather than accepting a quality below every target.
|
||||
<br><br>
|
||||
<strong>Note:</strong> the <em>Downsample hi-res</em> option (under Lossy Copy) also
|
||||
bypasses this gate — if off-list files keep slipping through with fallback off, check
|
||||
that setting too.
|
||||
</div>
|
||||
<div id="ranked-targets-list" class="ranked-targets-list">
|
||||
<!-- rows injected by renderRankedTargets() -->
|
||||
</div>
|
||||
|
|
@ -5154,45 +5174,82 @@
|
|||
|
||||
<!-- Search strategy -->
|
||||
<div class="form-group">
|
||||
<label for="quality-search-mode" style="display:block;margin-bottom:4px;font-weight:600;">Search strategy</label>
|
||||
<select id="quality-search-mode" style="width:100%;max-width:420px;">
|
||||
<option value="priority">Source priority — fastest, stops at first good source</option>
|
||||
<option value="best_quality">Best quality — search all sources, pick the highest</option>
|
||||
<div class="setting-row" style="margin-bottom:4px;">
|
||||
<label for="quality-search-mode" style="font-weight:600;">Search strategy</label>
|
||||
<span class="info-icon" role="button" tabindex="0" title="What's this?"
|
||||
onclick="toggleSettingHelp(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleSettingHelp(this);}">ⓘ</span>
|
||||
</div>
|
||||
<select id="quality-search-mode" style="width:100%;max-width:420px;" onchange="onSearchModeChange()">
|
||||
<option value="priority">Source priority — fast (use the first source that has it)</option>
|
||||
<option value="best_quality">Best quality — thorough (check every source, take the best)</option>
|
||||
</select>
|
||||
<div class="help-text" style="margin-top:4px">
|
||||
<strong>Source priority</strong> takes the first source in your chain that meets
|
||||
a target — fast, fewer lookups. <strong>Best quality</strong> searches
|
||||
<em>every</em> source for each track, pools the results, and downloads them
|
||||
best→worst by actual audio quality (source order only breaks ties). Slower and
|
||||
more API calls, but it won't settle for a passable Soulseek file when HiFi/Qobuz
|
||||
has a higher-resolution version. Retry budgets are unchanged: a source that spends
|
||||
its budget is dropped from the whole pool.
|
||||
<div class="help-text setting-help-body" hidden>
|
||||
<strong>Source priority</strong> goes through your sources in order and
|
||||
downloads from the <em>first one</em> that has the track. Fastest, fewest
|
||||
lookups.<br><br>
|
||||
<strong>Best quality</strong> asks <em>every</em> source, then downloads the
|
||||
highest-quality copy it found — even if a faster source also had a lower-quality
|
||||
one. Slower and more API calls, but you never settle for a worse file when a
|
||||
better one exists.<br><br>
|
||||
Either way, the per-source retry limits stay the same.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rank candidates by quality (priority mode only) -->
|
||||
<div class="form-group" id="quality-rank-candidates-group">
|
||||
<div class="setting-row">
|
||||
<label class="checkbox-label" style="margin:0;">
|
||||
<input type="checkbox" id="quality-rank-candidates">
|
||||
Rank-based download order
|
||||
</label>
|
||||
<span class="info-icon" role="button" tabindex="0" title="What's this?"
|
||||
onclick="toggleSettingHelp(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleSettingHelp(this);}">ⓘ</span>
|
||||
</div>
|
||||
<div class="help-text setting-help-body" hidden>
|
||||
A single source often returns several copies of the same track.<br><br>
|
||||
<strong>Off</strong> (default): the most likely-correct / fastest copy is
|
||||
downloaded first — the original behaviour.<br><br>
|
||||
<strong>On</strong>: the highest-quality copy (per your target list above) is
|
||||
downloaded first instead; correctness and speed only break ties.<br><br>
|
||||
Nothing is skipped and the source order doesn't change, so a track can't go
|
||||
missing — it just prefers the better copy. (This is always how
|
||||
<strong>Best quality</strong> works, which is why this option only appears for
|
||||
<strong>Source priority</strong>.)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fallback Option -->
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="quality-fallback-enabled" checked>
|
||||
Accept off-list quality when nothing in the list is available
|
||||
</label>
|
||||
<div class="help-text" style="margin-top:4px">
|
||||
⚠️ This does <strong>not</strong> mean "walk down my list" — the list already
|
||||
does that on its own. When <strong>on</strong>, a file that matches
|
||||
<em>none</em> of your targets (e.g. a 16-bit FLAC or MP3 when you only listed
|
||||
24-bit) is <strong>accepted anyway</strong> as a last resort. Turn it
|
||||
<strong>off</strong> to strictly enforce your list — anything off-list is then
|
||||
quarantined instead of completed.
|
||||
<div class="setting-row">
|
||||
<label class="checkbox-label" style="margin:0;">
|
||||
<input type="checkbox" id="quality-fallback-enabled" checked>
|
||||
Accept off-list quality when nothing in the list is available
|
||||
</label>
|
||||
<span class="info-icon" role="button" tabindex="0" title="What's this?"
|
||||
onclick="toggleSettingHelp(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleSettingHelp(this);}">ⓘ</span>
|
||||
</div>
|
||||
<div class="help-text setting-help-body" hidden>
|
||||
This does <strong>not</strong> mean "walk down my list" — the list already
|
||||
does that on its own.<br><br>
|
||||
When <strong>on</strong>, a file that matches <em>none</em> of your targets
|
||||
(e.g. a 16-bit FLAC or an MP3 when you only listed 24-bit) is
|
||||
<strong>accepted anyway</strong> as a last resort.<br><br>
|
||||
Turn it <strong>off</strong> to strictly enforce your list — anything off-list
|
||||
is then quarantined instead of completed.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Require hard AcoustID verification — only shown when AcoustID is enabled -->
|
||||
<div class="form-group" id="acoustid-require-verified-group" style="display:none;">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="acoustid-require-verified">
|
||||
Only import AcoustID-verified tracks (reject "could not confirm")
|
||||
</label>
|
||||
<div class="help-text" style="margin-top:4px">
|
||||
<div class="setting-row">
|
||||
<label class="checkbox-label" style="margin:0;">
|
||||
<input type="checkbox" id="acoustid-require-verified">
|
||||
Only import AcoustID-verified tracks (reject "could not confirm")
|
||||
</label>
|
||||
<span class="info-icon" role="button" tabindex="0" title="What's this?"
|
||||
onclick="toggleSettingHelp(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleSettingHelp(this);}">ⓘ</span>
|
||||
</div>
|
||||
<div class="help-text setting-help-body" hidden>
|
||||
When <strong>on</strong>, a track that AcoustID runs but <em>cannot confirm</em>
|
||||
(no fingerprint match / cross-script metadata — the ⚠ "unverified" case) is
|
||||
<strong>quarantined</strong> instead of imported. Downloads then try the next
|
||||
|
|
@ -5210,20 +5267,6 @@
|
|||
outage) always import, so an AcoustID outage never stalls you. Requires AcoustID enabled.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-text">
|
||||
<strong>How it works:</strong> Each download source is checked against this list
|
||||
top-down. The first target a source can satisfy wins; a source that meets no target
|
||||
is skipped for the next one (source priority still decides between sources that can).
|
||||
For lossless, bit depth + sample rate decide the match. For MP3/AAC the bitrate is a
|
||||
<em>minimum</em> threshold (≥), so VBR and mono files aren't falsely rejected. After
|
||||
download the real file is verified against the same list. With fallback off, a track
|
||||
is left missing rather than accepting a quality below every target.
|
||||
<br><br>
|
||||
<strong>Note:</strong> the <em>Downsample hi-res</em> option (under Lossy Copy) also
|
||||
bypasses this gate — if off-list files keep slipping through with fallback off, check
|
||||
that setting too.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end Quality Profile body -->
|
||||
|
|
|
|||
|
|
@ -2001,6 +2001,32 @@ function populateQualityProfileUI(profile) {
|
|||
|
||||
const searchModeSelect = document.getElementById('quality-search-mode');
|
||||
if (searchModeSelect) searchModeSelect.value = profile.search_mode === 'best_quality' ? 'best_quality' : 'priority';
|
||||
|
||||
const rankCandidatesCheckbox = document.getElementById('quality-rank-candidates');
|
||||
if (rankCandidatesCheckbox) rankCandidatesCheckbox.checked = profile.rank_candidates_by_quality === true;
|
||||
|
||||
onSearchModeChange();
|
||||
}
|
||||
|
||||
// Hide the "rank-based download order" toggle when Best quality is active —
|
||||
// that mode always ranks by quality, so the toggle would be meaningless there.
|
||||
function onSearchModeChange() {
|
||||
const mode = document.getElementById('quality-search-mode')?.value;
|
||||
const group = document.getElementById('quality-rank-candidates-group');
|
||||
if (group) group.style.display = mode === 'best_quality' ? 'none' : '';
|
||||
}
|
||||
|
||||
// Toggle the collapsible help text below a setting's ⓘ icon. Walks forward from
|
||||
// the icon's row to the next .setting-help-body sibling, so it works whether the
|
||||
// body is the immediate next element or sits after a control (e.g. a <select>),
|
||||
// and regardless of any wrapping container.
|
||||
function toggleSettingHelp(iconEl) {
|
||||
const row = iconEl.closest('.setting-row') || iconEl;
|
||||
let el = row.nextElementSibling;
|
||||
while (el && !el.classList.contains('setting-help-body')) {
|
||||
el = el.nextElementSibling;
|
||||
}
|
||||
if (el) el.hidden = !el.hidden;
|
||||
}
|
||||
|
||||
function renderRankedTargets() {
|
||||
|
|
@ -2211,6 +2237,7 @@ function collectQualityProfileFromUI() {
|
|||
preset: (currentQualityProfile && currentQualityProfile.preset) || 'custom',
|
||||
fallback_enabled: document.getElementById('quality-fallback-enabled')?.checked ?? true,
|
||||
search_mode: document.getElementById('quality-search-mode')?.value === 'best_quality' ? 'best_quality' : 'priority',
|
||||
rank_candidates_by_quality: document.getElementById('quality-rank-candidates')?.checked ?? false,
|
||||
ranked_targets,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3921,6 +3921,41 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
/* Setting label + inline ⓘ info icon. The icon sits on the (fixed) label row;
|
||||
clicking it only toggles the help text below, so the trigger never moves. */
|
||||
.setting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* Kill the default .form-group label margin-bottom so the label centers on the
|
||||
icon row instead of sitting high. */
|
||||
.setting-row > label { margin: 0; }
|
||||
/* Field-heading labels (e.g. "Search strategy") sit next to a 12px control and
|
||||
the brighter checkbox labels — match that weight/brightness so they don't look
|
||||
dim or undersized. */
|
||||
.setting-row > label:not(.checkbox-label) {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.info-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
color: rgba(var(--accent-rgb), 0.85);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
.info-icon:hover { color: rgba(var(--accent-rgb), 1); }
|
||||
.setting-help-body { margin-top: 4px; }
|
||||
|
||||
/* Supported Formats */
|
||||
.supported-formats {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
|
@ -4109,6 +4144,39 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
box-shadow: 0 4px 12px rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
/* Presets footer: explanation on the left, a tidy Reset button on the right */
|
||||
.quality-presets-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.preset-reset-btn {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.preset-reset-btn:hover {
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
border-color: rgba(var(--accent-rgb), 0.35);
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
}
|
||||
.preset-reset-icon {
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Quality tier cards - upgraded inner cards */
|
||||
/* ── Ranked-targets editor (v3 quality profile) ───────────────────────── */
|
||||
.ranked-targets-editor {
|
||||
|
|
@ -4123,6 +4191,10 @@ body.helper-mode-active #dashboard-activity-feed:hover {
|
|||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* The label now sits in a .setting-row (alongside the ⓘ), whose label margin is
|
||||
zeroed — restore a healthy gap before the draggable target list below. */
|
||||
.ranked-targets-editor > .setting-row { margin-bottom: 10px; }
|
||||
|
||||
.ranked-targets-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
Loading…
Reference in a new issue