Skip to content

Multi-service UI polish: queue + settings + sidebar (OnTheSpot-inspired, codebase-verified) #911

@Salem874

Description

@Salem874

Summary

As MeedyaDL approaches the multi-service rollout (#100 EPIC → M8 BBC iPlayer → M9 Spotify → M10 YouTube), the queue / download / settings UX needs polish that scales beyond the current Apple-Music-only assumption. This issue captures a researched, codebase-verified design proposal inspired by OnTheSpot (a similar multi-service music downloader, PyQt6-based) — borrowing its cleanest patterns while deliberately rejecting its rougher edges.

Methodology: 11 specific polish recommendations + 6 net-new from a completeness pass were each verified against the current codebase (file paths, existing helpers, effort estimate) before landing in this issue. Each item below carries its concrete "files to change" pointer and effort tier.

Revision (2026-06-07): Following user feedback, original anti-pattern 1 ("cap visible queue columns at 4-5") was replaced with a more nuanced responsive-column-visibility rule that allows up to ~8 columns at wide widths but tier-hides them at narrower widths. New Phase 1 item 0 ("Responsive column system") captures the framework. Other recommendations (per-row platform icon, art thumb, status pill etc.) now land into that column system rather than as ad-hoc row additions.

What OnTheSpot does well (worth borrowing)

  • Service column per row — small platform icon + name on every queue / search row, instantly identifying which service the item came from
  • Album-art thumbnails — ~48×48 cover art on every row, making 100+ row queues visually scannable
  • Many columns of useful data — codec / bitrate / format / type / progress, all available for quick scan when window is wide
  • Unified Accounts table in Settings — one place that shows every connected service with auth status, account tier, last-validated timestamp
  • Status text in a dedicated column — single source of truth, not buried inside a coloured icon
  • Action buttons grouped at far right — consistent action vocabulary across rows (copy / retry / locate / open / delete)

What OnTheSpot does poorly (deliberately rejecting)

  • Generic Qt aesthetic — flat stock buttons, no hover affordance, no micro-interactions
  • Status badges as plain text rather than coloured pills
  • Fixed column density at every window width — all 8 columns visible even when window is narrow, becoming unreadable
  • Conflating service brand colour with download status (deuteranopia users can't tell "Spotify queued" from "Spotify failed")
  • Settings as a single dense form with no progressive disclosure

Anti-patterns to avoid (load-bearing — preserve in future Claude sessions)

  1. Use responsive column visibility, never fixed density at all widths. The queue may surface up to ~8 columns at wide widths (album art, platform icon, Artist — Album — Track, status pill, progress, speed/ETA, codec/quality, file path, actions), but visibility must tier at Tailwind breakpoints so a sidebar-collapsed narrow window stays scannable. Tier 1 (essentials: art, identifier, status, actions) always visible; subsequent tiers reveal at md / lg / xl / 2xl. Provide a hover-tooltip or click-to-expand affordance so users on narrow windows can still see hidden data on demand. Don't dump everything onto the row at every width.
  2. Don't make service brand colours the primary status signal. Status pills (recommendation 4) and brand colours (recommendation 7) must use different hue ranges — green / amber / red for state, brand colour for the service icon only.
  3. Don't auto-open service-specific Settings tabs on URL paste. Becomes hostile mid-flow on a 50-URL batch. Surface a preview card under the textarea (recommendation 9) instead.
  4. Don't gate basic queue functionality behind multi-select mode. Selection should augment per-row actions, never replace them.

Phase 1 — Strongly recommend, can land pre-M8 (Apple-Music-only era still benefits)

These don't depend on multi-service code landing first.

0. Responsive column visibility system (foundation — lands first)

Build the queue row as a CSS-Grid-based layout with up to ~8 columns whose visibility tiers at Tailwind breakpoints. Subsequent Phase 1 items (per-row platform icon, art thumb, status pill, codec, file path…) all slot into this system rather than each adding ad-hoc width.

Tier hierarchy (always-visible at the top, least-essential hides first):

Tier Visibility from Columns
Tier 1 always (≥ 320px) Album art thumbnail · Artist — Album — Track · Status pill · Action buttons (hover-revealed)
Tier 2 md (≥ 768px) Platform / service icon
Tier 3 lg (≥ 1024px) Inline progress bar (during downloads) · Speed / ETA
Tier 4 xl (≥ 1280px) Codec / quality (ALAC, Atmos, 4K HDR) · Content type (Album / Single / Music Video / Playlist)
Tier 5 2xl (≥ 1536px) File path · Submitted-at timestamp · Estimated file size

Tailwind's default breakpoints (md 768, lg 1024, xl 1280, 2xl 1536) align with typical Tauri-window-on-desktop widths (sidebar-collapsed narrow ~800px → big-monitor maximised ~2400px+). No custom screens config needed — confirmed tailwind.config.js uses defaults.

Click-to-expand affordance: even at narrow widths, the user can click/tap the row to reveal a secondary inline detail panel showing the hidden columns (codec, file path, timestamps, speed/ETA) for that one row. Reuses the existing fallback-warning expandable panel pattern in QueueItem.tsx.

Header row: in tabular mode, render a sticky column-header strip with the same responsive visibility — labels appear/disappear with their columns.

  • Files:
    • QueueItem.tsx (rewrite layout into grid with grid-cols-[…] + per-cell hidden md:flex / hidden lg:flex / hidden xl:flex / hidden 2xl:flex classes)
    • DownloadQueue.tsx (add sticky column header strip above the virtualised list)
    • new src/components/download/QueueRowExpansion.tsx (click-to-expand secondary panel)
    • QueueListVirtualized.tsx (track expanded-row IDs; variable row height when expanded)
  • Effort: one-PR medium
  • Verdict: strongly recommend (this is the framework all other Phase 1 row work builds on; ship first)
  • Cross-ref: anti-pattern 1 — must use this system, not ad-hoc width

1. Per-row platform icon in queue

The detectPlatform() helper + PlatformIcon SVG-cached component already exist in GlobalProgressBar.tsx (lines ~76-87, 99-188) but are scoped to that one file. Surface the same icon in the queue row's Tier 2 column (≥ md), left of the status pill.

  • Files: extract detectPlatform() + PlatformEntry into a new src/lib/platform-config.ts (avoid naming collision with the existing OS-detection usePlatform() hook); slim integration in QueueItem.tsx Tier 2 column
  • Effort: one-PR small
  • Verdict: strongly recommend (infrastructure proven, removes duplication)

2. Album-art thumbnails on queue rows (Tier 1)

AlbumMetadata already exposes artwork_square_url and artwork_tall_url (apple_music_api.rs lines 237-243), fetched at enqueue via try_fetch_metadata(). URLs are currently consumed only by the animated-artwork service — never exposed to the frontend.

  • Files: add artwork_url: Option<String> to QueueItemStatus in models/download.rs; pass it through in download_queue.rs; mirror in types/index.ts (lines 964-1031); render lazy-loaded <img> ~48×48 in QueueItem.tsx Tier 1 column
  • Effort: one-PR small
  • Verdict: strongly recommend (pure layering, no new backend capability)

3. Render "Artist — Album — Track" instead of raw URL (Tier 1)

album_name and artist_name already exist on QueueItemStatus (types/index.ts lines 979-982) — populated at enqueue per CLAUDE.md, used in progress-caption.ts formatter for the global progress bar — but QueueItem.tsx line 541 still shows the raw URL. Same metadata, different rendering policy.

  • Files: QueueItem.tsx Tier 1 primary identifier column; optionally extract reusable formatter from progress-caption.ts
  • Effort: one-PR small
  • Verdict: strongly recommend (pure rendering switch, gracefully falls back to URL when metadata absent)

4. Status pills (icon + label inside a coloured rounded-full)

STATE_CONFIG mapping in QueueItem.tsx (lines 205-231) already has icon + colorClass + label — pill rendering already used in the same file for the "Retry without Wrapper" pattern (lines 708-716) and in YouTubeTab.tsx line 38. Trivial swap. Lives in the Tier 1 status column.

  • Files: QueueItem.tsx (replace icon-only div with pill in Tier 1 status column); optionally extract <StatusPill> into src/components/common/
  • Effort: one-PR small
  • Verdict: strongly recommend

5. Hover-reveal row actions

At 50+ rows, always-visible action icons clutter visual scan. group + group-hover:opacity-100 pattern already used in ActivityLog.tsx line ~330. Tab-focused buttons must remain visible for keyboard a11y — the existing aria-label / title annotations are already in place. Lives in the Tier 1 actions column.

  • Files: QueueItem.tsx row container + action button container in Tier 1 actions column
  • Effort: one-PR small
  • Verdict: strongly recommend

6. Micro-animations (row insert + status transition + expand-row slide)

prefers-reduced-motion is fully wired in globals.css lines 331-340 (suppresses to 0.01ms). Existing transition-colors / transition-all duration-300 patterns already in QueueItem + ProgressBar. Safe to add a row-insert fade-slide, a status-transition icon swap, and a click-to-expand slide-down animation (paired with recommendation 0) without touching motion-sensitive users.

  • Files: new @keyframes in globals.css around line 260-270; conditional class on QueueItem row; track newly-inserted IDs in QueueListVirtualized.tsx
  • Effort: one-PR medium
  • Verdict: strongly recommend (perfect low-risk test case for polish animations)

7. WCAG-AA-verified service brand colour tokens

Per CLAUDE.md, MeedyaDL has 4 platform themes + a11y-high-contrast.css + a11y-colour-blind.css. No --service-* tokens exist today. Add tokens (5 services × 4 themes × 2 modes ≈ 40 declarations) with WCAG AA contrast verified against each background, plus CVD-safe variants in a11y-colour-blind.css.

8. Undo for destructive queue operations

Clear All / Abort All / multi-row Cancel are all immediate and irreversible — accidental clicks on a 200-row queue are real frustration. Snapshot affected items in memory; surface a 5-second toast: "Cleared 47 items · Undo". Undo re-enqueues in original order with mv_companion_override etc. preserved. Toast respects notification_auto_dismiss_seconds.

9. Per-service "what gets downloaded" preview card on Download page

Different services produce wildly different artifacts (Apple Music: lyrics + animated art + companion codecs; BBC iPlayer: video + subtitles; Spotify: Ogg + lyrics). Surface a small accent card under the URL textarea once detectService(url) resolves, listing what this service will download with the current settings + per-line links to the controlling settings.

  • Files: DownloadForm.tsx; reuse useSettingsField hook
  • Effort: small
  • Verdict: strongly recommend (sets correct user expectations across services)

Phase 2 — Strongly recommend, but tie to multi-service landing (M8+)

These need at least one non-Apple-Music service to be functional before they land cleanly.

10. Service filter chips above queue

service field already exists on QueueItemStatus (types/index.ts line 970). Status-filter chip pattern already implemented in DownloadQueue.tsx lines 1139-1160 with toggle buttons, counts, reset logic — direct copy-paste shape. Conditional rendering: show only when ≥4 distinct services present in queue to avoid clutter pre-M8.

  • Files: DownloadQueue.tsx (filter row + state + memo extension); ensure backend populates service field reliably on QueueItemStatus
  • Effort: one-PR small
  • Verdict: strongly recommend, gated on M8 landing

11. Friendly multi-service empty state on Queue + Download

Both DownloadForm.tsx lines 871-875 and QueueListVirtualized.tsx lines 109-122 have generic empty states today. MEDIA_SERVICE_LABELS constant exists in types/index.ts but is unused in the UI. Multi-service empty state shows enabled-service icons + "Paste a URL or drop a link".

  • Files: DownloadForm.tsx, QueueListVirtualized.tsx; optional new src/components/common/ServiceIconBadge.tsx
  • Effort: one-PR medium
  • Verdict: recommend, gated on multi-service landing
  • Risk: shipping in v1.x with Apple-Music-only would show 5 service icons but only 1 works — user confusion. Either gate behind enabled_services setting (see recommendation 14) or ship only after M8

12. Unified Settings > Accounts page

Backend PerServiceSettings stubs for AppleMusicSettings / SpotifySettings / YouTubeSettings already exist (settings.rs lines 565-590). Frontend has placeholder SpotifyTab / YouTubeTab / BBCiPlayerTab with "Coming Soon" + @ts-nocheck. CookiesTab (lines 1-1268) holds Apple-Music-specific validation logic that needs to be decomposed into shared utilities before the unified table is viable.

  • Files: new src/components/settings/tabs/AccountsTab.tsx; decompose CookiesTab.tsx into shared validators (StatusBadge, BrowserInstructions, ExpiryWarning); extend settings.rs schema to surface per-service AccountStatus
  • Effort: one-PR medium
  • Verdict: recommend, defer until at least Spotify (M9) has real integration — otherwise the unified abstraction may need reshaping. Start with the CookiesTab decomposition now so the eventual Accounts work is straightforward.

13. Sidebar per-service connection status strip

Below the sidebar's existing update button: one small dot per configured service, green/amber/red driven by the same preflight checks (check_cookies_before_download, check_wrapper_health, MusicKit token validity). Click opens Settings > Accounts. Amber gently pulses (prefers-reduced-motion respected) when a token is within 7 days of expiry — proactive renewal before a failed download.

  • Files: Sidebar.tsx footer; new helper polling existing preflight IPCs; tie into serviceStatusStore.ts (already staged)
  • Effort: medium
  • Verdict: recommend, gated on M8+ (one dot for Apple Music pre-M8 is fine but uninteresting)

14. First-launch service picker

Wizard Step 1 becomes "Which services will you use?" — checkboxes per service. Subsequent dependency-install + credential-prompt steps iterate only over selected services. Stores enabled_services: Vec<MediaServiceId> in settings; sidebar filter chips (#10) and connection strip (#13) auto-hide unselected services.

  • Files: SetupWizard components; settings schema + migration; sidebar conditional rendering
  • Effort: medium
  • Verdict: recommend, gated on M8+

Phase 3 — Medium-term polish

15. Cmd/Ctrl+K command palette / cross-store search

A persistent top-of-window search across Queue + History + Library Scan with scope chips (All / Queue / History / Library) and service chips (reuse #10). Results render with the same per-row vocabulary established in #0-3 (responsive columns, platform icon, art thumb, Artist — Album — Track) so users learn one row format everywhere. Reuses @tanstack/react-virtual already in ActivityLog.tsx.

  • Effort: medium
  • Verdict: recommend (transforms findability once data grows; OnTheSpot's standout pattern that none of the other recommendations replicate)

16. Batch row selection + bulk-actions toolbar

Click-to-select / Shift+click range / Cmd-Ctrl+click toggle. When ≥1 row selected, queue header morphs into a contextual toolbar ("3 selected · Cancel · Retry · Move to top · Export · Clear"). Existing abort_all_downloads IPC (#620) becomes one button on that toolbar instead of a separate red action. Pairs cleanly with #5 (hovering an unselected row reveals individual actions; selection mode supersedes hover).

  • Effort: medium
  • Verdict: recommend; respect anti-pattern 4 (selection augments, never replaces, single-row actions)

Phase 4 — Defer

17. Sidebar service grouping (Queue → expandable per-service)

Adds virtualizer row-height complexity for marginal benefit over the cheaper filter chips (#10). Wait for user feedback from #100 multi-service rollout to confirm grouping is actually wanted.

  • Verdict: defer

Suggested rollout

Recommendation 0 (responsive column system) lands first as the framework — it's the foundation everything else snaps into. Once 0 is in place, recommendations 1-9 fit comfortably across 2-3 small follow-up PRs in the same release window — they don't conflict and don't depend on multi-service code. Bundle as a "queue polish" pre-release tagged 1.10.0-alpha.NN.

Phase 2 work begins as soon as #102 (M8 BBC iPlayer) lands and reuses the column system to drop the platform-icon column into a populated state. Phase 3 sequentially after that. Phase 4 evaluated post-multi-service-launch based on real user feedback.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions