Surface the existing pin (keep-from-cull) and per-file delete actions as visible inline buttons on each offline cache row instead of context-menu-only: a star toggles protection from auto-cull (and restore-if-missing), a trash culls that file early. Aligns wording/icons to the star metaphor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
10 KiB
Watch — Appearance & Winamp skins
Job: Let the operator choose how TVAnarchy looks — default dark UI or
Winamp-style Player chrome, including real classic .wsz skin files.
One-liner: How does the Player (and sidebar tint) feel?
Pillar: Cross settings (shell chrome). Affects Watch Player most visibly; Library/Download tabs use accent tints only.
This doc covers Watch chrome only. Download has its own UI (Search, Downloads tabs); Net has embedded UI (Settings subscriptions, Player merge hints). All three live in the same app shell — see correlation/ui.md.
Scope
| In | Out |
|---|---|
appTheme picker (Standard + 3 built-in Winamp palettes) |
iOS Winamp chrome (macOS only) |
Import / install .wsz (zip of BMP + TXT) |
Full Winamp 2.x window suite (EQ, playlist, MB) |
| Sprite-based Player transport, scrubber, volume, LED time | Federated / shared skins over Net |
VISCOLOR.TXT → spectrum bars; PLEDIT.TXT → LED tints |
Skin authoring tool inside TVAnarchy |
Bundled reference skin base-2.91.wsz |
Shade mode / snap-to-edge layout |
Persist skin id + name in settings.json |
Per-device skin overrides |
Status (v1 → v2 doc)
| Slice | Status | Notes |
|---|---|---|
| Built-in themes | Shipped | standard, winamp-classic, winamp-modern, winamp-llama |
| Theme environment + palette | Shipped | ThemePalette, .themed() on RootView |
| Built-in Winamp Player shell | Shipped | Bevel, LED, spectrum, transport in PlayerView |
.wsz extract + cache |
Shipped | WinampSkinLoader → ~/.local/state/tv-anarchy/skins/<sha256>/ |
| Sprite crop (Webamp coords) | Shipped | WinampSkinSprites + WinampSkinStore |
| Skin-sprite Player chrome | Shipped | When winampSkinId set + skin loads |
| Settings import UI | Shipped | Setup → Appearance → Import / Base / Clear |
| Tests | Shipped | WinampSkinLoaderTests, SettingsStoreTests |
| Extended sheet coverage | Planned | SHUFREP, BALANCE, EQ_EX, PLAYPAUS indicator |
| Skin validation preview | Planned | Thumbnail + missing-sheet report before apply |
MCP / AppLocalAPI patch |
Planned | winampSkinId in settings patch |
| Sidebar chrome from skin | Optional | PLEDIT title colors on nav (Phase 6 UI pass) |
Architecture
flowchart TB
subgraph settings [Settings — Watch]
SS[SettingsStore.appTheme]
SK[winampSkinId + winampSkinName]
end
subgraph core [TVAnarchyCore — Display]
AT[AppTheme]
WL[WinampSkinLoader]
SP[WinampSkinSprites]
end
subgraph app [TVAnarchy — Theme]
WS[WinampSkinStore]
TP[ThemePalette.merging skin]
WV[WinampSkinViews]
WC[WinampComponents fallback]
end
subgraph ui [Surfaces]
RV[RootView.themed]
PV[PlayerView]
SV[SetupView Appearance]
end
SS --> AT
SK --> WL
WL -->|extract .wsz| Cache["skins/<sha256>/"]
Cache --> WS
SP --> WS
WS --> WV
AT --> TP
WL -->|VISCOLOR PLEDIT| TP
TP --> RV
WS --> PV
WV --> PV
WC --> PV
SK --> SV
WS --> SV
Layer rules
- Core stays AppKit-free — loader parses zip + text; sprite rects are data.
- App owns bitmaps —
WinampSkinStoreloadsNSImage, crops sprites. - Fallback always — missing sheet or sprite → built-in
WinampComponents. - Winamp chrome gate —
AppTheme.usesWinampChrome; Standard theme ignores skin sprites on Player (skin cache may remain on disk).
Modules
Core (Sources/TVAnarchyCore/Display/)
| Module | Role |
|---|---|
AppTheme |
Enum: standard + 3 built-in Winamp palettes; usesWinampChrome |
WinampSkinLoader |
Install .wsz, parse VISCOLOR.TXT / PLEDIT.TXT, validate sheets |
WinampSkinSprites |
Webamp-compatible crop rects per sheet name |
WinampSkinPackage |
Installed skin metadata (id, visColors, pledit, availableSheets) |
App (Sources/TVAnarchy/Theme/)
| Module | Role |
|---|---|
ThemePalette |
Resolved colors; merging(skin:) overlays VISCOLOR / PLEDIT |
ThemeEnvironment |
@Environment(\.themePalette), .themed(_:skin:) |
WinampComponents |
Built-in bevel, transport, LED, spectrum, title bar |
WinampSkinStore |
@Observable sheet cache + sprite crop |
WinampSkinViews |
Sprite transport, scrubber, volume, LED, title bar |
Consumers
| Surface | Behavior |
|---|---|
RootView |
WinampSkinStore state, reload on settings change, inject environment |
PlayerView |
winampPlayerShell — skin sprites when winampSkin.isActive |
SetupView |
Theme picker + .wsz import / Base / Clear |
MiniTransport |
Built-in bevel buttons when Winamp theme (no sprites yet) |
State
| Artifact | Path | Writer | Reader |
|---|---|---|---|
| Theme + skin pointers | settings.json → appTheme, winampSkinId, winampSkinName |
SettingsStore |
LibraryController, RootView |
| Extracted skin cache | ~/.local/state/tv-anarchy/skins/<sha256>/ |
WinampSkinLoader.install |
WinampSkinStore |
| Source archive copy | …/skins/<sha256>/source.wsz |
install | re-extract if needed |
| Bundled reference | TVAnarchy.app/Resources/base-2.91.wsz |
build (project.yml) |
Setup “Use Base Skin” |
Override dir for tests: TV_ANARCHY_STATE_DIR (same as other Watch state).
settings.json fields
{
"appTheme": "winamp-classic", // standard | winamp-classic | winamp-modern | winamp-llama
"winampSkinId": "<sha256>", // null = built-in palette only on Player
"winampSkinName": "Base 2.91" // display label
}
Import auto-selects winamp-classic when current theme is standard.
Sprite coverage (TVAnarchy subset)
Coordinates match Webamp skinSprites.ts — required for cross-skin compatibility.
| Sheet | Sprites used | Player surface |
|---|---|---|
CBUTTONS |
prev / play / pause / next (+ active) | Transport row |
POSBAR |
background, thumb | Position scrubber |
VOLUME |
background, thumb | Volume slider |
NUMBERS |
DIGIT_0 … DIGIT_9 |
LED elapsed / duration |
TITLEBAR or MAIN |
title bar / window bg | Player header |
PLAYPAUS |
play / pause / stop indicators | planned — status LED |
VISCOLOR.TXT |
RGB rows | WinampSpectrum bar colors |
PLEDIT.TXT |
Normal, Current, NormalBG, SelectedBG | LED + accent tint |
Minimum compatible skin: CBUTTONS + POSBAR (+ MAIN or TITLEBAR).
v2 build plan (enhancements)
Appearance docs ship in Phase 0 (this file). Code is already on main;
remaining work is a parallel Watch track — does not block Net phases.
Track A — Polish (1 PR, ~2 days)
| Task | Target |
|---|---|
PLAYPAUS indicator next to title |
PlayerView + WinampSkinViews |
| MiniTransport sprite buttons when skin active | MiniTransport.swift |
AppLocalAPI / AppSettingsPatch for skin fields |
AppLocalAPI.swift |
| Missing-sheet banner in Setup | SetupView |
Tests: AppLocalAPITests patch round-trip; UI test optional.
Exit: Operator can set skin via local API; Player shows play/pause LED.
Track B — Validation UX (1 PR, ~1 day)
| Task | Target |
|---|---|
| Post-install compatibility report | WinampSkinPackage + Setup |
| Preview strip (transport + scrubber thumbs) | SetupView |
Reject skins missing CBUTTONS or POSBAR before persist |
already throws; surface message |
Exit: Import shows what works before switching theme.
Track C — Extended chrome (optional, 2–3 PRs)
| Task | Target |
|---|---|
SHUFREP shuffle/repeat |
PlayerView when queue active |
BALANCE pan slider |
low priority |
REGION.TXT window shapes |
defer — high effort, low value |
Read BASE.SKIN coordinates |
only if we add more windows |
Exit: Closer to Winamp 2.x fidelity; still single-window Player.
Track D — Phase 6 UI alignment
Fold into plan.md § Phase 6:
- Sidebar selection tint from imported
PLEDIT.SelectedBGwhen Winamp theme active. - Optional “Appearance” subsection label in sidebar docs (ui.md).
Tests (current + planned)
| Test | Asserts |
|---|---|
WinampSkinLoaderTests.testInstallBaseSkin |
Extract, sheets, vis colors |
WinampSkinLoaderTests.testReloadInstalledSkin |
Cache reload by id |
WinampSkinLoaderTests.testParseVisColors |
r,g,b + comment strip |
WinampSkinLoaderTests.testParsePledit |
hex RGB keys |
WinampSkinLoaderTests.testWinampSkinSettingsPersist |
settings round-trip |
SettingsStoreTests.testAppThemePersists |
theme enum |
planned WinampSkinStoreTests |
BMP crop non-empty for Base skin |
planned AppLocalAPITests |
patch winampSkinId |
Fixture: Sources/TVAnarchy/Resources/base-2.91.wsz (repo path; also app bundle).
Risks
| Risk | Mitigation |
|---|---|
| BMP crop Y-flip wrong on some skins | Webamp coords + unit test against Base skin thumbs |
| Security-scoped import fails silently | startAccessingSecurityScopedResource in Setup |
| Large skin cache on disk | SHA256 dedupe; one cache dir per skin |
Nonstandard .wsz layouts |
Require Webamp sheet names; fallback to built-in chrome |
XcodeGen drops .wsz from bundle |
project.yml → buildPhase: resources on explicit path |
Out of scope (v2)
- Net part for skins — skins are opaque blobs, not TVAnarchy facts; no
contentKey. - iOS — Lilith tokens / native chrome stay default on phone.
- Winamp 3+ / Modern skins — classic 2.x BMP only.
- Skin editor — use external Winamp Skinning Tutorial / Webamp.
Correlation
- Pillar definition: this file (cross chrome + Watch Player)
- Component map: correlation/components.md
- UI surfaces: correlation/ui.md
- State paths: correlation/state.md
- Master schedule: plan.md § 12.4 + full Appendix C (theming deep-dive, not legoblocks, custom sprite system)
Theming architecture summary (see plan Appendix C): Hybrid custom (AppKit sprites from real .wsz + SwiftUI environment/views + WinampComponents fallbacks). Whole shell gets palette; heavy chrome Watch/Player only. iOS standard only. Detailed implementation + extension rules in the plan appendix.