Credential enable/disable + descriptions, artist casing settings page, playlist compare view
Credentials page now shows a fact-checked one-line description per service and marks Spotify/Soulseek/Navidrome as Required (no toggle -- they're load-bearing for every playlist sync). The four optional integrations (Bandcamp, AzuraCast, Qobuz, Telegram) get a real on/off switch: Bandcamp pauses its scheduled job (same mechanism as the Jobs page), the other three just remove their rendered credential file, which the scripts that read them already treat as "not configured, skip" -- no script changes needed. Disabled optional scopes also drop off the dashboard's credential status row. New Settings > Artist Casing page to view/add/remove entries in artist-canonical.list without shelling in -- adding a name that already matches case-insensitively replaces its casing in place instead of duplicating. Playlist detail page's status section is now two side-by-side columns: the original Spotify tracklist on the left, and a tabbed Downloaded / Waiting-to-download view on the right (client-side tabs via Alpine), both with an internal scroll area so long playlists don't pull the columns out of sync. Uses the existing status_service reconciliation (sldl index + beets + quarantine) -- no backend changes needed there.
This commit is contained in:
@@ -44,6 +44,8 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
[x-cloak] { display: none !important; }
|
||||
|
||||
html { color-scheme: dark; }
|
||||
|
||||
body {
|
||||
@@ -515,6 +517,61 @@ tbody td.actions-cell { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.status-legend .item strong { color: var(--fg); }
|
||||
.status-legend .dot { width: 0.6em; height: 0.6em; border-radius: 50%; box-shadow: 0 0 7px currentColor; }
|
||||
|
||||
/* ---- playlist compare (original vs. downloaded/waiting) ---- */
|
||||
|
||||
.playlist-compare {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 860px) {
|
||||
.playlist-compare { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.compare-col-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.compare-col-header h3 { margin: 0; font-size: 1rem; }
|
||||
|
||||
/* Fixed-height scroll area so a 150+ track playlist doesn't make the two
|
||||
columns drift out of vertical sync -- the point of side-by-side columns
|
||||
is comparing them at a glance, which only works if both stay on screen
|
||||
together. Sticky header keeps column labels visible while scrolling. */
|
||||
.compare-scroll { max-height: 30rem; overflow-y: auto; }
|
||||
.compare-scroll thead th { position: sticky; top: 0; background: var(--bg-soft); z-index: 1; }
|
||||
|
||||
.seg-tabs {
|
||||
display: inline-flex;
|
||||
gap: 0.2rem;
|
||||
padding: 0.2rem;
|
||||
border-radius: 999px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 0.9rem;
|
||||
}
|
||||
.seg-tabs button {
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.4rem 0.9rem;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.seg-tabs button:hover { color: var(--fg); }
|
||||
.seg-tabs button.active { color: #08090d; background: var(--gradient-holo); background-size: 200% auto; }
|
||||
|
||||
.status-dot-info { background: var(--status-info); box-shadow: 0 0 6px var(--status-info); }
|
||||
|
||||
/* ---- misc ---- */
|
||||
|
||||
.notice {
|
||||
@@ -586,6 +643,54 @@ tbody td.actions-cell { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.cred-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.cred-scope-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
.cred-scope-header h2 { margin: 0 0 0.35rem; text-transform: capitalize; }
|
||||
.cred-desc { font-size: 0.85rem; margin: 0; max-width: 46ch; }
|
||||
.cred-scope.cred-disabled .panel { opacity: 0.55; }
|
||||
.cred-disabled-note { font-size: 0.78rem; color: var(--status-warning); margin: -0.5rem 0 1.1rem; }
|
||||
|
||||
/* ---- toggle switch ---- */
|
||||
|
||||
.switch { position: relative; display: inline-flex; width: 38px; height: 21px; flex-shrink: 0; cursor: pointer; }
|
||||
.switch input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.switch-track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--panel-strong);
|
||||
border: 1px solid var(--border-strong);
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
.switch-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
transition: transform 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
.switch input:checked ~ .switch-track { background: rgba(167, 139, 250, 0.22); border-color: var(--iris-violet); }
|
||||
.switch input:checked ~ .switch-track .switch-thumb {
|
||||
transform: translateX(17px);
|
||||
background: var(--iris-violet);
|
||||
box-shadow: 0 0 8px rgba(167, 139, 250, 0.6);
|
||||
}
|
||||
.switch input:focus-visible ~ .switch-track { outline: 2px solid var(--iris-cyan); outline-offset: 2px; }
|
||||
|
||||
/* ---- settings layout (sidebar sub-nav) ---- */
|
||||
|
||||
.settings-layout {
|
||||
|
||||
Reference in New Issue
Block a user