diff options
Diffstat (limited to 'docs/specs')
| -rw-r--r-- | docs/specs/2026-07-06-fancy-music-player-ui-spec.org | 162 | ||||
| -rw-r--r-- | docs/specs/2026-07-06-fancy-music-player-ui.prototype.html | 207 |
2 files changed, 369 insertions, 0 deletions
diff --git a/docs/specs/2026-07-06-fancy-music-player-ui-spec.org b/docs/specs/2026-07-06-fancy-music-player-ui-spec.org new file mode 100644 index 00000000..bf9d8466 --- /dev/null +++ b/docs/specs/2026-07-06-fancy-music-player-ui-spec.org @@ -0,0 +1,162 @@ +#+TITLE: Fancy music-player UI (hi-fi / vinyl) — Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-06 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* DRAFT Fancy music-player UI +:PROPERTIES: +:ID: af4f2688-ce7d-43f5-82e5-595a603e2593 +:END: +- 2026-07-06 Mon @ 14:38:15 -0500 — drafted. + +* Metadata +| Status | draft | +|----------+-------------------------------------------------------------------| +| Owner | Craig Jennings | +|----------+-------------------------------------------------------------------| +| Reviewer | Craig Jennings | +|----------+-------------------------------------------------------------------| +| Related | [[file:2026-07-06-fancy-music-player-ui.prototype.html][prototype (fancy direction)]]; modules/music-config.el | +|----------+-------------------------------------------------------------------| + +* Summary + +Turn the EMMS playlist buffer from a raw-URL text list into a hi-fi "fancy" reading surface: station and track names instead of stream URLs, cover art, a now-playing hero with a serif title and a progress bar, and a warm amber palette, all still inside the Emacs buffer. It degrades cleanly to a plain text version in a terminal frame, where images can't render. + +* Problem / Context + +The music player is an EMMS playlist buffer with a custom multi-line header (Playlist / Current / Mode / Keys / Radio) drawn by cj/music--header-text as an overlay. The track list shows whatever EMMS stores as the track name, which for internet radio is the raw stream URL (https://ice6.somafm.com/groovesalad-256-mp3). It reads as a debug buffer, not a music player. Craig chose the "fancy" direction from the prototype: cover art, serif titles, a now-playing hero, warm amber. + +Two facts shape the design. First, Emacs renders images and variable-pitch faces in a GUI frame but not in a TTY, so the fancy look needs a text fallback. Second, cover art is reachable: radio-browser stations carry a favicon (logo) URL, retrievable at search time from the station plist or later via /json/stations/byuuid/<uuid> (verified live 2026-07-06); local files carry embedded album art. Stations with neither get a generated placeholder. + +* Goals and Non-Goals + +** Goals +- Show human names, not URLs: station name for streams (from the .m3u #EXTINF or a radio-browser lookup), artist and title for local files. +- A now-playing hero: cover art, a serif title, a subtitle (station/album), a progress bar, and a time or "on air" line. +- A track list with small cover thumbnails, serif names, and right-aligned meta; the current row accented. +- A warm amber palette, theme-owned (the dupre theme). +- Degrade to a plain text version (names + a dim glyph + a thin bar) in a TTY or when the fancy view is off. + +** Non-Goals +- No separate EMMS browser pane, library manager, or tag editor. +- No mouse-first chrome, animated equalizers, or transport buttons you click (keys stay the interface). +- No change to the radio lookup, playback engine, or keybindings (the n/t/m radio row and the transport keys are untouched). +- No streaming-service integrations. + +** Scope tiers +- v1: name resolution + the text base (TTY-safe), cover-art fetch + cache, and the fancy GUI render layered on top. +- Out of scope: album/library browsing, playlists-of-playlists UI, per-track ratings. +- vNext (log to todo.org): a scrubbable/seekable progress bar, homepage/link affordances in the hero, animated now-playing transitions, per-station color theming from the cover art. + +* Design + +At a caller's altitude: Craig opens the music player as today (same keys). Instead of URLs he sees named rows with a small logo each; the top of the buffer is a now-playing panel with the current station's cover, its name in a serif face, and a progress bar. In a terminal frame or with the fancy view disabled, the same buffer shows the plain text version (names, a dim glyph, a thin bar) with no images. + +At the implementer's altitude, three layers, each shippable on its own: + +1. Name resolution (pure + text, TTY-safe). A function maps a track to its display name: for a url track, the #EXTINF label from its .m3u if present, else the station name resolved from radio-browser, else a tidied host; for a file track, artist and title from EMMS track info. The row renders name + a dim nerd-icon glyph (broadcast for streams, note for files) + right-aligned meta. The now-playing line under Current gets a thin faces-drawn bar plus a time / "on air" string. This is the plain text base and the fallback; it has no images. + +2. Cover art (image infra). cj/music-art--for-track returns a cached local image path for a track: a station favicon (from the station plist at creation time, or a byuuid lookup for an existing .m3u carrying #RADIOBROWSERUUID), embedded album art for a file, or a generated vinyl placeholder when neither exists. Fetches are asynchronous and written to an on-disk cache keyed by station UUID or file, so a redisplay never blocks on the network and art survives restarts. + +3. Fancy render (GUI). When the frame is graphical and the fancy view is on, cj/music--header-text and the row renderer insert the cover images (create-image / insert-image), remap the title to a serif variable-pitch face, and paint the amber palette and the segmented progress bar. A small timer advances the bar during playback. When the frame is a TTY or the fancy view is off, the render falls through to layer 1's text. + +Pure pieces (name resolution, the m3u-label extraction, the bar-fill computation, the placeholder-vs-art decision) carry the tests; the image insertion, the fetch, and the timer are exercised live. + +* Alternatives Considered + +** Layer the fancy view on top of a text base (chosen) +- Good, because the text base ships value immediately (names beat URLs for everyone), is the honest TTY fallback, and de-risks the image work. +- Bad, because two render paths to keep in sync. +- Neutral, because EMMS already separates track-name from display. + +** Fancy-only, no text fallback +- Good, because one render path. +- Bad, because it breaks in a TTY and forces the image/art work before any value lands. + +** A separate dedicated buffer/major-mode instead of the EMMS playlist overlay +- Good, because full control of layout. +- Bad, because it abandons EMMS's playlist machinery (marking, reorder, the existing keymap) and doubles maintenance. +- Neutral, deferred: the overlay approach reuses everything and is enough for v1. + +* Decisions [/] + +** TODO Cover art for streams — where the favicon comes from +- Owner / by-when: Craig / before Phase 2 +- Context: a radio-browser search result carries a favicon URL, so a newly-created station can capture it. An existing .m3u only has the stream URL and (on 15 of 73) a #RADIOBROWSERUUID; the other 58 have no UUID. A byuuid lookup gets the favicon for the ones that have a UUID. +- Decision: We will (a) write a #RADIOBROWSERFAVICON line into new stations at creation so they need no lookup, (b) for an existing station carrying #RADIOBROWSERUUID, fetch the favicon by UUID once and cache it, and (c) for a station with neither, use the generated vinyl placeholder. +- Consequences: easier — most stations get real art with at most one lookup; harder — a small .m3u format addition (an extra comment line) and a byuuid path for legacy files. + +** TODO Image cache location and refresh +- Owner / by-when: Craig / before Phase 2 +- Context: fetched favicons and extracted album art need to persist so redisplay is instant and offline-safe. +- Decision: We will cache under data/music-art/ (gitignored runtime state), keyed by station UUID or a file hash, fetched once and reused; a manual command clears the cache, and there is no automatic TTL. +- Consequences: easier — instant, offline, no invalidation logic; harder — a stale logo persists until the user clears the cache (acceptable for logos). + +** TODO Serif title face +- Owner / by-when: Craig / before Phase 3 +- Context: the fancy titles want a serif variable-pitch face, theme-owned. +- Decision: We will add a defcustom for the title family (default a widely-available serif, e.g. the same reading serif the nov reading view uses) and a theme-owned face the dupre theme colors. +- Consequences: easier — one knob, consistent with the nov-reading typography choice; harder — another face to register in theme-studio if we want it tunable there (deferred). + +** TODO Progress bar rendering and cadence +- Owner / by-when: Craig / before Phase 3 +- Context: the bar can be drawn with block-character faces or a small SVG; it advances during playback via a timer. +- Decision: We will draw the bar with faces (block characters, accent-colored fill) rather than SVG for v1, and redraw on a ~1s timer only while a track is playing, only when the player buffer is visible. +- Consequences: easier — no SVG dependency, works the moment faces do; harder — a character-cell bar is coarser than an SVG one (fine for v1; SVG is a vNext upgrade). + +** TODO Fancy view toggle and TTY fallback +- Owner / by-when: Craig / before Phase 3 +- Context: images need a GUI frame; a TTY (or a user who wants plain) needs the text version. +- Decision: We will gate the fancy render on (display-graphic-p) AND a defcustom (default on), falling through to the layer-1 text render otherwise, decided per-redisplay so a frame on a TTY and a frame on a GUI can differ live. +- Consequences: easier — never broken in a TTY, user can opt out; harder — both render paths stay maintained (already a chosen tradeoff). + +* Implementation phases + +** Phase 1 — Name resolution + text base (TTY-safe, no images) +Map a track to a display name (url -> #EXTINF label / radio-browser name / tidy host; file -> artist and title), render rows as name + dim glyph + right-aligned meta, and add the thin now-playing bar + time/on-air line and the header spacing + rule. Unit-test the pure name/label/bar-fill helpers. Ships the "names not URLs" win on its own and is the fallback for later phases. + +** Phase 2 — Cover art fetch + cache (image infra) +cj/music-art--for-track returning a cached local image path: favicon capture at station creation (a #RADIOBROWSERFAVICON line), byuuid favicon fetch for legacy UUID stations, embedded album art for files, and a generated vinyl placeholder. Async fetch into data/music-art/. Unit-test the art-vs-placeholder decision and the cache-key logic; the fetch is a smoke test. + +** Phase 3 — Fancy GUI render +The now-playing hero (cover image + serif title + subtitle + segmented bar) and the thumbnailed serif list with the amber palette, gated on (display-graphic-p) + the toggle, degrading to Phase 1's text. The ~1s playback timer. Live verification (images, faces, the bar advancing) since it can't run headless. + +* Acceptance criteria +- [ ] A radio row shows the station name, not the stream URL; a local track shows artist and title. +- [ ] Each row shows a dim glyph (broadcast for streams, note for files) and right-aligned meta. +- [ ] In a GUI frame with the fancy view on, the now-playing hero shows the current station's cover art, a serif title, and a progress bar that advances during playback. +- [ ] A station with no favicon or embedded art shows the generated vinyl placeholder, not a broken image. +- [ ] In a TTY frame, or with the fancy view off, the same buffer renders the text version (names + glyph + thin bar) with no images and no errors. +- [ ] Art is fetched once and cached under data/music-art/; a second open is instant and works offline. +- [ ] The name/label, bar-fill, and art-decision helpers have Normal/Boundary/Error tests. + +* Readiness dimensions +- Data model & ownership: display names are derived (never stored); .m3u files gain an optional #RADIOBROWSERFAVICON comment (Craig-owned, written at creation); cached art under data/music-art/ is generated and disposable. +- Errors, empty states & failure: a fetch failure or missing art yields the placeholder, never a broken image; a TTY yields text; an empty playlist yields the header with no rows. +- Security & privacy: fetching a station's favicon URL hits a third-party host the user chose; fetch with a timeout and cache locally; no credentials; nothing sensitive logged. +- Observability: the player messages when it can't fetch art (once, quietly); a clear-art-cache command exists. +- Performance & scale: art fetch is async and cached, never blocking redisplay; the bar timer runs ~1s only while playing and only when the buffer is visible; the list is small (a playlist), so per-row images are cheap. +- Reuse & lost opportunities: reuses the EMMS playlist buffer + keymap, the existing header overlay (cj/music--header-text / update-header), cj/music--track-description, nerd-icons (already used by dashboard/dirvish), the dupre theme, and the nov-reading serif choice. Images via built-in create-image/insert-image. Nothing new is invented that Emacs already provides. +- Architecture fit & weak points: layers on music-config.el's existing header/render path; the seam is the header overlay + a per-row renderer. Weak point: keeping the text and fancy render paths in sync — mitigated by making the text render the base and the fancy render an image/face overlay on the same rows. +- Config surface: cj/music-fancy-ui (default on), the serif title face + family, the amber palette (theme-owned), the art cache dir, the bar redraw interval. All with defaults and doc. +- Documentation plan: a note in the module commentary and the keybinding/header list; no separate doc. +- Dev tooling: existing make test / test-file; no new tooling. +- Rollout, compatibility & rollback: additive; cj/music-fancy-ui off restores the plain text render; the #RADIOBROWSERFAVICON line is an ignorable comment in older readers; deleting data/music-art/ is a safe reset. +- External APIs & deps: radio-browser favicon field and /json/stations/byuuid/<uuid> VERIFIED live 2026-07-06. Local album-art extraction depends on an image being embedded (or a folder cover.jpg) — the fallback is the placeholder. + +* Risks, Rabbit Holes, and Drawbacks +- Local album-art extraction is the likeliest rabbit hole (embedded art via a tag reader vs a sibling cover.jpg). v1 can start with sibling-cover-file + placeholder and defer embedded-tag extraction if it balloons. +- Image sizing across frame DPIs: pick a fixed pixel size for the hero and thumbnails, scaled by the frame, and don't chase per-monitor perfection. +- Two render paths drifting: the text base must stay the source of truth for row content; the fancy path only adds images/faces, never different text. + +* References / Appendix +- Prototype (open in a browser): [[file:2026-07-06-fancy-music-player-ui.prototype.html][2026-07-06-fancy-music-player-ui.prototype.html]] — three directions (minimal / fancy / modern); this spec builds the fancy one, with minimal as the text fallback. +- radio-browser favicon + byuuid verified live 2026-07-06 against de1.api.radio-browser.info. + +* Review and iteration history +** 2026-07-06 Mon @ 14:38:15 -0500 — Claude (for Craig) — author +- What: initial draft, after Craig picked the fancy direction from the three-way prototype. +- Why: the player shows raw stream URLs and reads as a debug buffer; the fancy look needs cover art, serif titles, and a now-playing hero, which is real infrastructure (image fetch/cache, GUI-vs-TTY) worth a spec. +- Artifacts: the fancy prototype; radio-browser favicon/byuuid verified live. diff --git a/docs/specs/2026-07-06-fancy-music-player-ui.prototype.html b/docs/specs/2026-07-06-fancy-music-player-ui.prototype.html new file mode 100644 index 00000000..8b7ddd21 --- /dev/null +++ b/docs/specs/2026-07-06-fancy-music-player-ui.prototype.html @@ -0,0 +1,207 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Music player UI — three directions</title> +<style> + :root { + --page-bg: #f3f0ea; --page-fg: #29261f; --page-dim: #6b6459; --card-line: #ded8cc; --accent: #9a6b2f; + } + @media (prefers-color-scheme: dark) { + :root { --page-bg: #131210; --page-fg: #d6cfc0; --page-dim: #8a8272; --card-line: #2b271f; --accent: #d8a24f; } + } + :root[data-theme="light"] { --page-bg: #f3f0ea; --page-fg: #29261f; --page-dim: #6b6459; --card-line: #ded8cc; --accent: #9a6b2f; } + :root[data-theme="dark"] { --page-bg: #131210; --page-fg: #d6cfc0; --page-dim: #8a8272; --card-line: #2b271f; --accent: #d8a24f; } + + * { box-sizing: border-box; } + body { margin: 0; background: var(--page-bg); color: var(--page-fg); + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; line-height: 1.55; + padding: 3rem 1.25rem 5rem; } + .wrap { max-width: 64rem; margin: 0 auto; } + .eyebrow { text-transform: uppercase; letter-spacing: .14em; font-size: .72rem; color: var(--accent); font-weight: 600; margin: 0 0 .5rem; } + h1 { font-size: 1.85rem; margin: 0 0 .6rem; text-wrap: balance; font-weight: 650; } + header p { margin: .3rem 0; max-width: 64ch; color: var(--page-dim); } + header p strong { color: var(--page-fg); font-weight: 600; } + + .dir { margin: 2.6rem 0 0; } + .dir > .head { display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap; margin-bottom: .7rem; } + .dir .n { font-variant-numeric: tabular-nums; color: var(--accent); font-weight: 700; } + .dir h2 { font-size: 1.15rem; margin: 0; font-weight: 640; } + .dir .sub { color: var(--page-dim); font-size: .92rem; } + .feas { margin: .55rem 0 0; font-size: .86rem; color: var(--page-dim); } + .feas b { color: var(--page-fg); font-weight: 600; } + .cost { display: inline-block; font-size: .72rem; letter-spacing: .04em; text-transform: uppercase; + border: 1px solid var(--card-line); border-radius: 999px; padding: .06rem .55rem; margin-right: .4rem; color: var(--page-dim); } + + /* shared editor frame */ + .frame { border-radius: 9px; overflow: hidden; box-shadow: 0 16px 40px -22px rgba(0,0,0,.65); border: 1px solid #0000; } + .mono { font-family: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace; } + + /* ---------- CURRENT (reference) ---------- */ + .cur-ref { background: #15140f; color: #b9b1a1; border-color: #322d25; font-size: 12.5px; } + .cur-ref .buf { padding: .6rem 0; } + .cur-ref .r { white-space: pre; padding: .05rem 1rem; } + .cur-ref .lbl { color: #8a8170; } + .cur-ref .play { color: #e6c98a; font-weight: 700; } + + /* ---------- 1 · MINIMAL ---------- */ + .min { background: #15140f; color: #cfc8b8; border-color: #322d25; font-size: 13px; } + .min .buf { padding: .7rem 0 .8rem; } + .min .r { white-space: pre; padding: .07rem 1.1rem; } + .min .lbl { color: #9a917f; } + .min .dim { color: #6f685b; } + .min .on { color: #e6c98a; } .min .off { color: #4a453b; } + .min .keys { color: #6f685b; } .min .keys .k { color: #9a917f; } + .min .rule { color: #4a453b; padding: .12rem 1.1rem .28rem; white-space: pre; } + .min .bar-full { color: #8faf7f; } .min .bar-empty { color: #43403a; } .min .time { color: #6f685b; } + .min .track { display: grid; grid-template-columns: 1.4ch 1fr auto; gap: .7ch; align-items: baseline; padding: .13rem 1.1rem; } + .min .g-radio { color: #8fb0c4; } .min .g-note { color: #a7b58f; } + .min .track .name { color: #cfc8b8; } .min .track .meta { color: #6f685b; white-space: pre; } + .min .track.cur { background: #262119; } .min .track.cur .name { color: #e6c98a; font-weight: 700; } + + /* ---------- 2 · FANCY (hi-fi / vinyl) ---------- */ + .fan { background: radial-gradient(120% 90% at 12% 0%, #241a10 0%, #17110a 55%, #120d07 100%); color: #e8d9bd; border-color: #3c2c17; } + .fan .top { display: flex; gap: 1rem; padding: 1.1rem 1.2rem 1rem; align-items: center; border-bottom: 1px solid #3a2b18; } + .fan .cover { width: 78px; height: 78px; border-radius: 6px; flex: none; position: relative; + background: radial-gradient(circle at 50% 50%, #3a2c18 0 22%, #1a130b 23% 26%, #2a2012 27% 46%, #17110a 47% 50%, #2a2012 51%); + box-shadow: inset 0 0 0 1px #4a3820, 0 6px 16px -8px #000; } + .fan .cover::after { content: ""; position: absolute; inset: 45% 45% auto auto; width: 8px; height: 8px; border-radius: 50%; + background: #e6c98a; box-shadow: 0 0 0 3px #17110a; top: 46%; left: 46%; } + .fan .np-title { font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif; font-size: 1.5rem; font-weight: 600; color: #f2e4c6; line-height: 1.15; } + .fan .np-sub { color: #b99a63; font-size: .9rem; margin-top: .15rem; letter-spacing: .02em; } + .fan .seg { display: flex; gap: 3px; margin-top: .7rem; } + .fan .seg i { height: 6px; flex: 1; border-radius: 2px; background: #3a2c18; } + .fan .seg i.on { background: linear-gradient(#f0cf8a, #d8a24f); } + .fan .tline { display: flex; justify-content: space-between; color: #a98c58; font-size: .74rem; margin-top: .3rem; font-variant-numeric: tabular-nums; } + .fan .list { padding: .55rem .4rem .7rem; } + .fan .t { display: grid; grid-template-columns: 30px 1fr auto; gap: .7rem; align-items: center; padding: .34rem .8rem; border-radius: 7px; } + .fan .t .th { width: 26px; height: 26px; border-radius: 4px; background: #241a10; box-shadow: inset 0 0 0 1px #4a3820; display: grid; place-items: center; color: #c79a54; font-size: 12px; } + .fan .t .nm { font-family: "Iowan Old Style", Georgia, serif; font-size: 1.02rem; color: #ecdcbc; } + .fan .t .mt { color: #9c8154; font-size: .8rem; font-variant-numeric: tabular-nums; } + .fan .t.cur { background: linear-gradient(90deg, #2a1f10, #201810); box-shadow: inset 2px 0 0 #e6c98a; } + .fan .t.cur .nm { color: #f7e7c4; } + + /* ---------- 3 · MODERN (streaming card) ---------- */ + .mod { background: #0e1013; color: #cdd3da; border-color: #1c2027; } + .mod .top { display: flex; gap: .95rem; padding: 1.05rem 1.1rem; align-items: center; } + .mod .cover { width: 66px; height: 66px; border-radius: 12px; flex: none; + background: linear-gradient(145deg, #2a6f5a, #1b3b6b); box-shadow: 0 8px 20px -10px #000; display: grid; place-items: center; color: #bfeede; font-size: 20px; } + .mod .np-title { font-size: 1.28rem; font-weight: 680; color: #f2f5f8; letter-spacing: -.01em; } + .mod .np-sub { color: #8b94a0; font-size: .88rem; margin-top: .1rem; } + .mod .prog { display: flex; align-items: center; gap: .6rem; padding: 0 1.1rem .2rem; } + .mod .track-bar { position: relative; height: 4px; border-radius: 4px; background: #232830; flex: 1; } + .mod .track-bar > i { position: absolute; left: 0; top: 0; bottom: 0; width: 42%; border-radius: 4px; background: #5fd0a8; } + .mod .track-bar > b { position: absolute; left: 42%; top: 50%; width: 11px; height: 11px; border-radius: 50%; background: #eafaf3; transform: translate(-50%, -50%); box-shadow: 0 0 0 3px rgba(95,208,168,.25); } + .mod .tm { color: #7f8894; font-size: .72rem; font-variant-numeric: tabular-nums; } + .mod .ctl { display: flex; gap: .5rem; padding: .55rem 1.1rem .1rem; } + .mod .ctl .pill { border: 1px solid #262c34; background: #171b21; border-radius: 999px; padding: .2rem .7rem; font-size: .82rem; color: #aeb6c0; } + .mod .ctl .pill.main { background: #5fd0a8; color: #08130e; border-color: #5fd0a8; font-weight: 700; } + .mod .list { padding: .5rem .45rem .7rem; } + .mod .t { display: grid; grid-template-columns: 34px 1fr auto; gap: .7rem; align-items: center; padding: .4rem .7rem; border-radius: 10px; } + .mod .t .th { width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; font-size: 13px; color: #dfe6ee; } + .mod .t .th.radio { background: linear-gradient(145deg, #2a6f5a, #1b3b6b); } .mod .t .th.note { background: linear-gradient(145deg, #3a3f4a, #23272e); } + .mod .t .nm { color: #dfe4ea; font-weight: 550; } .mod .t .mt { color: #7f8894; font-size: .8rem; font-variant-numeric: tabular-nums; } + .mod .t.cur { background: #151b22; } .mod .t.cur .nm { color: #eafaf3; } + .mod .t.cur .eq { color: #5fd0a8; } + + .foot { margin-top: 2.4rem; padding-top: 1.2rem; border-top: 1px solid var(--card-line); color: var(--page-dim); font-size: .9rem; } + .foot b { color: var(--page-fg); } +</style> +</head> +<body> +<div class="wrap"> + <header> + <p class="eyebrow">EMMS playlist buffer · three directions</p> + <h1>Music player: pick a look</h1> + <p>Same playlist, three treatments. All are achievable inside an Emacs buffer, but they cost different amounts. Today it shows raw stream URLs (below); each direction fixes that and goes further. Mockups, not live renders.</p> + <div class="frame cur-ref mono" style="margin-top:1rem; max-width:34rem"> + <div class="buf"> + <div class="r"><span class="lbl">Current :</span> https://ice6.somafm.com/groovesalad-256-mp3</div> + <div class="r play">https://ice6.somafm.com/groovesalad-256-mp3</div> + <div class="r">https://ice1.somafm.com/groovesalad-256-mp3</div> + <div class="r">https://ice2.somafm.com/groovesalad-256-mp3</div> + </div> + </div> + </header> + + <!-- 1 · MINIMAL --> + <section class="dir"> + <div class="head"><span class="n">1</span><h2>Minimal</h2><span class="sub">refined terminal — names, one dim glyph, thin now-playing bar</span></div> + <div class="frame min mono"> + <div class="buf"> + <div class="r"><span class="lbl">Playlist</span> <span class="lbl">:</span> Evening mix <span class="dim">(5)</span></div> + <div class="r"><span class="lbl">Current </span> <span class="lbl">:</span> <span style="color:#8faf7f">▶ </span>Groove Salad <span class="dim">· SomaFM</span></div> + <div class="r"><span class="dim"> </span><span class="bar-full">━━━━━━━━━━━━</span><span class="bar-empty">────────────────</span> <span class="time">live · 256k</span></div> + <div class="r keys"><span class="k">Keys </span> <span class="k">:</span> a:add c:clear L:load S:stop <>:skip</div> + <div class="r keys"><span class="k">Radio </span> <span class="k">:</span> n:by name t:by tag m:enter manually</div> + <div class="rule"> ────────────────────────────────────────</div> + <div class="track cur"><span class="g-radio">◉</span><span class="name">Groove Salad</span><span class="meta">SomaFM · 256k</span></div> + <div class="track"><span class="g-radio">◉</span><span class="name">Drone Zone</span><span class="meta">SomaFM · 256k</span></div> + <div class="track"><span class="g-note">♪</span><span class="name">Miles Davis — So What</span><span class="meta">9:22</span></div> + <div class="track"><span class="g-note">♪</span><span class="name">Bill Evans — Peace Piece</span><span class="meta">6:41</span></div> + <div class="track"><span class="g-radio">◉</span><span class="name">Jazz Radio Blues</span><span class="meta">FR · 128k</span></div> + </div> + </div> + <p class="feas"><span class="cost">low lift</span><b>Faces + text only.</b> Names come from the .m3u #EXTINF and local tags; the glyph is a nerd-icon; the bar is one timer-driven line. No images. Sits naturally in the buffer you already have.</p> + </section> + + <!-- 2 · FANCY --> + <section class="dir"> + <div class="head"><span class="n">2</span><h2>Fancy</h2><span class="sub">hi-fi / vinyl — cover art, serif titles, warm amber, a real now-playing hero</span></div> + <div class="frame fan"> + <div class="top"> + <div class="cover"></div> + <div> + <div class="np-title">Groove Salad</div> + <div class="np-sub">SOMA FM · AMBIENT / DOWNTEMPO · 256K</div> + <div class="seg"><i class="on"></i><i class="on"></i><i class="on"></i><i class="on"></i><i class="on"></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div> + <div class="tline"><span>on air</span><span>live stream</span></div> + </div> + </div> + <div class="list"> + <div class="t cur"><span class="th">◉</span><span class="nm">Groove Salad</span><span class="mt">SomaFM</span></div> + <div class="t"><span class="th">◉</span><span class="nm">Drone Zone</span><span class="mt">SomaFM</span></div> + <div class="t"><span class="th">♪</span><span class="nm">Miles Davis — So What</span><span class="mt">9:22</span></div> + <div class="t"><span class="th">♪</span><span class="nm">Bill Evans — Peace Piece</span><span class="mt">6:41</span></div> + <div class="t"><span class="th">◉</span><span class="nm">Jazz Radio Blues</span><span class="mt">FR</span></div> + </div> + </div> + <p class="feas"><span class="cost">medium lift</span><b>Cover art + variable-pitch serif.</b> Emacs shows images (station favicons, embedded album art) and a serif face for titles via `display` and face remaps. The vinyl cover and segmented bar are drawn with faces/SVG. Needs an art fetch-and-cache layer; the warmth is a theme overlay.</p> + </section> + + <!-- 3 · MODERN --> + <section class="dir"> + <div class="head"><span class="n">3</span><h2>Modern</h2><span class="sub">streaming-app card — rounded art, sans title, a slim seek bar with a handle, pill controls</span></div> + <div class="frame mod"> + <div class="top"> + <div class="cover">♪</div> + <div> + <div class="np-title">Groove Salad</div> + <div class="np-sub">SomaFM · ambient · 256 kbps</div> + </div> + </div> + <div class="prog"><span class="tm">LIVE</span><span class="track-bar"><i></i><b></b></span><span class="tm">∞</span></div> + <div class="ctl"><span class="pill">shuffle</span><span class="pill">⏮</span><span class="pill main">⏸ pause</span><span class="pill">⏭</span><span class="pill">repeat</span></div> + <div class="list"> + <div class="t cur"><span class="th radio">◉</span><span class="nm">Groove Salad <span class="eq">▎▍▎</span></span><span class="mt">SomaFM</span></div> + <div class="t"><span class="th radio">◉</span><span class="nm">Drone Zone</span><span class="mt">SomaFM</span></div> + <div class="t"><span class="th note">♪</span><span class="nm">Miles Davis — So What</span><span class="mt">9:22</span></div> + <div class="t"><span class="th note">♪</span><span class="nm">Bill Evans — Peace Piece</span><span class="mt">6:41</span></div> + <div class="t"><span class="th radio">◉</span><span class="nm">Jazz Radio Blues</span><span class="mt">FR</span></div> + </div> + </div> + <p class="feas"><span class="cost">higher lift</span><b>Pushes hardest against the buffer model.</b> Rounded art, pill controls, the seek-bar dot handle, and rounded rows are easy in a browser but need SVG-rendered widgets or `svg-lib`-style images in Emacs, redrawn on playback. Doable in GUI Emacs, but it's the most code and the least "text buffer." A cool accent (mint) instead of the theme's amber to read contemporary.</p> + </section> + + <div class="foot"> + <p>My read: <b>Minimal</b> is the honest sweet spot for an Emacs player you live in, and it kills the primitive feeling on its own. <b>Fancy</b> is worth it if you want the now-playing moment to feel like hi-fi and you're happy to add cover art. <b>Modern</b> is the most striking but fights Emacs the most, so it's the biggest build for a look that a browser does more naturally. All three keep the n/t/m radio row and the same keys.</p> + <p style="margin:.7rem 0 0">Left out of every direction to stay tasteful: animated equalizers everywhere, a separate browser pane, mouse-first chrome. Glyphs shown here (◉ ♪) stand in for nerd-icons.</p> + </div> +</div> +<script> + (function () { var r = document.documentElement; + try { var t = localStorage.getItem('theme'); if (t === 'dark' || t === 'light') r.setAttribute('data-theme', t); } catch (e) {} })(); +</script> +</body> +</html> |
