Tabs
- Replace SVG icons with mockup-style numeric badges (01..05)
- Hide the legacy .tab-indicator (was rendering as a long copper
bar above the strip); active tab gets its own copper underline
via .tab-btn.active::after
- Numbers turn copper on the active tab
Player view
- Volume control restored: mute button + slim copper slider live
inside the VU cluster, on the right of the readout, separated
by a hairline. Slider is the existing #volume-slider so all JS
hooks (bidirectional sync, drag, etc.) keep working.
- Track title font scaled down (clamp 34..64) and clamped to 3
lines with ellipsis so long YouTube-style titles don't dominate
the masthead. Adds word-break + overflow-wrap.
- #artist:empty and #album:empty are now display:none so blank
rows don't leave a gap when the source provides no metadata.
Mini player
- Forced display: grid with 4 columns: track / controls / progress
/ volume. Was inheriting legacy display:flex which pushed elements
into a single non-aligned row.
- Position locked: position: fixed, bottom: 0, left/right: 0 with
!important. The strip is firmly anchored to the viewport bottom.
- Top edge progress (::before) painted in copper with glow.
- Responsive collapse: hide progress at <=880px, hide volume at
<=540px, leaving track + controls on phones.
i18n
- tab.player default text aligned to "Now Spinning" (matches
existing en/ru values added earlier).
Wholesale replacement of the player view markup + a verbatim mockup
CSS block scoped to .now-playing. Previous approach kept restyling
legacy classes which left a layered, inconsistent result. This is a
clean snap: same DOM structure as the mockup, same CSS rules, mapped
onto existing JS-touched IDs.
Markup
- One <section class="now-playing"> with vinyl-stage on left and
track-masthead on right
- Vinyl spins via data-playstate and contains album art as the
circular center label (existing #album-art img preserved)
- SVG tonearm pivots in/out by data-playstate
- Track masthead: .kicker (copper italic mono), large italic-serif
.track-title, italic .track-byline, mono .track-album
- Meta grid: 2 cells (State / Source) with mono labels + italic
serif values
- 30 .spectrum bars between metadata and transport
- .transport: progress-row (timecode + .progress-track + timecode)
and .controls (3 .btn-trans buttons + .vu-cluster)
- Volume slider, mute button, visualizer toggle moved to a
.visually-hidden block — they remain functional for JS / a11y
but no longer compete for visual real estate. Volume control
happens via the always-visible mini player.
VU cluster (mockup-faithful)
- 140x60 VU meter with conic-gradient grid background, copper
needle, "VU" label
- Stacked readout: "OUT <strong>SYS</strong>" / "VOL <strong>72%</strong>"
- Click anywhere on cluster toggles mute (calls toggleMute())
- When muted: needle turns rust, readout strong turns rust,
OUT label switches to MUTE
JS hooks
- updatePlaybackState already sets :root[data-playstate] (drives
spin + tonearm)
- Volume tick now updates #vu-vol and #vu-out
- updateMuteIcon updates #vu-out + .vu-cluster.muted class
Scoping
- All new CSS is .now-playing-prefixed so other tabs and dialogs
are untouched
- Legacy .progress-bar:hover scaleY and ::after scale(0) are
defeated with !important inside .now-playing
- Meta-grid reduced from 4 cells to 2 (State / Source). Elapsed and
Length were duplicates of the timecodes already flanking the
timeline. Added .meta-grid-2 modifier with a 2-column layout.
- Timeline was distorted by legacy rules: .progress-bar:hover did
scaleY(1.4) (inflating the 2px hairline on hover) and the
progress-fill::after handle defaulted to scale(0). Both are now
forced via !important to keep the hairline flat and the copper
handle always visible.
- progress-row uses minmax(0, 1fr) for the bar cell so it shrinks
cleanly inside the grid.
- Removed unused meta.elapsed / meta.length keys from en.json + ru.json.
- Dead JS lookups for #meta-elapsed / #meta-length stay (cheap if-checks,
no-op when DOM elements gone).
Side-by-side comparison surfaced several layout regressions vs. the
mockup. This commit lands all of them at once.
Header
- Restore centered "Media Server / Studio Reference Edition" wordmark
in italic Fraunces
- Move folio marks to fixed page corners (visible on every tab):
TL = green pulse + "Connected · Local 8765"
TR = "Vol. I — Studio Reference · v0.x.x"
- Replace boxed version-label badge with copper mono inline in folio.tr
- Reduce header-to-content gap (container padding-top 28→56 with the
folio now anchored above)
Player view
- Spectrum bars: smaller height (32px), centered with max-width so
they don't span the whole right column
- Spectrogram canvas: hidden by default (opacity 0); reveals only when
visualizer toggle is active. No more leaking into bottom-left.
- VU cluster volume controls: strip legacy box (background, padding,
border-radius); compact stacked layout with thin slider, small mute
button, mono "VOL · XX%" readout
- Disable legacy applyVinylMode() — the .vinyl class added a SECOND
rotation animation on top of the structural .vinyl-stage spin,
causing visible compounding. Vinyl is now purely structural.
Toggles
- Remove vinyl mode toggle button (vinyl is always on)
- Keep audio visualizer (spectrum vis) toggle — still shown by JS
when supported
Mini player
- Force always-visible on non-player tabs regardless of scroll, by
short-circuiting setMiniPlayerVisible when activeTab !== 'player'
i18n
- New keys: header.connected, header.volume, header.edition,
header.edition_sub
- Removed unused: player.folio_left, player.folio_right
- en.json + ru.json updated
Restructures the player tab DOM to actually look like the editorial
mockup, not just inherit new fonts. The previous commit only swapped
tokens & typography on the legacy Spotify-clone layout.
DOM additions (all preserve existing JS-touched IDs):
- Vinyl stage: rotating vinyl wrapping the existing #album-art as a
circular center label; spins only when state=playing via CSS hook
- SVG tonearm: pivots in/out based on data-playstate
- Kicker line: copper italic mono header above the track title
- Editorial 4-cell metadata grid: State / Source / Elapsed / Length
- Decorative spectrum bars (30, CSS-only animation, paused when idle)
- VU meter cluster: needle visual driven by volume %, alongside the
preserved volume slider for a11y
- Folio marks: top-left and top-right of the player container
JS hooks (small, additive):
- updatePlaybackState now sets :root[data-playstate] for CSS
- progress tick mirrors timecode into meta-grid cells
- volume update rotates the VU needle
- folio-version mirrors the version label
i18n:
- new keys: player.kicker, player.modes, player.folio_*, meta.*
- added to both en.json and ru.json
Restored: media_server/static/redesign-mockup.html (Studio Reference
visual reference; deleting it in the prior commit was a mistake).
Replaces the Spotify-clone dark theme with a warm editorial design
language inspired by hi-fi audio mastering and magazine layouts.
- Self-host Fraunces, Geist, and Geist Mono as variable WOFF2 files
(Latin + Latin-ext + Cyrillic subsets, OFL-licensed)
- New design tokens: warm charcoal + copper accent (dark) /
cream paper + hunter emerald (light)
- Editorial typography: Fraunces serif for display + masthead,
Geist for UI, Geist Mono for technical readouts (timecodes, bitrates)
- Player view restyled as magazine spread with framed album art
- Mini player as glassy console strip with copper hairline glow
- Tabs as italic editorial nav with copper underline
- Browser items as gallery cards with editorial typography
- Settings as numbered sections with refined tables
- Quick Access as console rail
- Dialogs and auth modal as paper cards with mono kickers
- Subtle film-grain overlay for analog warmth
- Localized tab labels: Player → Now Spinning, Browser → Library
- Generate numpy/_distributor_init_local.py during build so libopenblas
can be located when running from the Windows installer
- Add os.add_dll_directory() call at runtime as a fallback for embedded Python
- Broaden audio import errors from ImportError to Exception, log at warning
- Move visualizer WS re-subscription into loadAudioDevices() so it runs
after availability is confirmed from the API
- Show/hide the visualizer toggle button based on fetched availability
Instead of waiting for the next poll cycle, new clients now get the
current playback status immediately on connect by calling get_status_func
if no cached status is available yet.
Two root causes for the 'imaging extension was built for another version
of Pillow' error users hit after install:
1) cleanup_site_packages ran 'find ... -name "*.py" ! -name "__init__.py"
-delete' with a comment claiming 'keep .pyc only' — but no compileall
step exists. Result: the dist shipped __init__.py + .pyd only, missing
every submodule (Image.py, ImageDraw.py, _version.py, ...). Fresh
installs were broken; in-place upgrades produced a half-old/half-new
site-packages. Removed the deletion entirely.
2) NSIS installer extracted over the previous install without cleaning
python/, app/, scripts/. Upgrades left stale files (old PIL/_version.py
next to new PIL/_imaging.pyd) which raised the Pillow ABI mismatch.
Wipe those subtrees before File /r, preserving config.yaml at the
install root.
pystray in WIN_DEPS (per-dep loop) downloaded its own Pillow version,
which overwrote the one resolved alongside CORE_DEPS during unzip.
Result at runtime: '_imaging extension was built for another version
of Pillow'.
Move pystray into VIS_DEPS so it's resolved in the single cross-deps
pip-download call and shares one consistent Pillow version.
uvicorn[standard] pulls uvloop via a 'sys_platform != win32' marker.
pip evaluates env markers against the HOST (Linux in CI), so uvloop
is requested even in a --platform win_amd64 resolve. No uvloop wheel
exists for Windows, so pip backtracks across every uvicorn[standard]
version and fails with ResolutionImpossible.
Use plain uvicorn plus the Windows-compatible extras we actually need
(httptools, websockets, python-dotenv).
The per-dep loop regressed pydantic/pydantic-core compatibility:
each dep resolves transitive versions independently, so 'pydantic'
brings core 2.41.5 while 'pydantic-settings' brings core 2.45.0,
and the later wheel overwrites the earlier during site-packages
unzip, producing:
SystemError: pydantic-core 2.45.0 is incompatible with
pydantic, which requires 2.41.5
Fix: single pip-download call for CORE_DEPS + VIS_DEPS so pip
resolves compatible transitive versions. Keep the per-dep loop
with --pre only for WIN_DEPS, where each dep needs its own
platform/non-platform fallback and winsdk requires --pre for
its beta wheels.
Single pip-download call fails because the second fallback branch
(without --platform) tries to resolve Windows-only deps like winsdk
on Linux, where no wheels exist. The original per-dep loop isolates
each failure so the platform-specific branch handles each dep
independently. Add --pre throughout for winsdk (1.0.0bNN beta).
If a tag or CI ref is not PEP 440 compliant (e.g. 'dev', 'nightly',
'snapshot-2024'), the previous detect_version stamped it raw into
pyproject.toml, which then broke 'pip install' with:
configuration error: project.version must be pep440
Add a regex check after stripping the leading 'v'. If the result
is not PEP 440, substitute '0.0.0.dev0' and warn.
Pattern from ClaudeCodeFacts/gitea-python-ci-cd.md §3.
The single pip-download call regressed winsdk fetching because pip
won't pick up pre-releases (1.0.0bNN) without --pre. The old per-dep
loop hid this via its fallback branch. Add --pre to both branches.
setup-node and actions/cache@v4 hang trying to talk to a missing
cache server, adding 1-3min per step. Drop the cache: directives
and explicit cache blocks. Keep the single pip-download call in
build-dist-windows.sh which is independent of any cache backend.
- Add pip and npm caching to build-windows and build-linux jobs
- Cache embedded Python zip and Windows wheels across runs
- Collapse per-dep pip download loop into a single resolver call
First run after this lands populates the caches; subsequent
release builds should drop from ~11min to ~3-5min.
Patches HTMLDialogElement.prototype.showModal globally to move focus
onto the dialog element itself instead of the first focusable
descendant. On touch devices the previous behavior popped up the
on-screen keyboard whenever a modal opened, which was confusing.
- Root folder cards with hero-style layout and SVG icons
- Full-width thumbnails with aspect-ratio grid items
- List view column headers (Name, Bitrate, Duration, Size)
- Modernized breadcrumb with pill segments and overflow handling
- Proper skeleton shimmer replacing emoji hourglass on thumbnails
- Pagination shows "Showing X-Y of Z" item count
- Refined hover effects, animations, and visual hierarchy
- Download button revealed on row hover in list view
- Type badges hidden by default, shown on hover
- Localized new keys in en.json and ru.json
workflow_dispatch-triggered build.yml that produces Windows
installer/portable and Linux tarball as CI artifacts without
creating a release. Trigger from Gitea UI → Actions → Run.
- Rename GITEA_TOKEN to DEPLOY_TOKEN in release workflow
- Extract shared version detection into build-common.sh
- Use importlib.metadata for runtime version instead of hardcoded string
- Use PEP 440 parsing (packaging lib) for update version comparison
- Add packaging>=23.0 to dependencies
- Fix update banner close button alignment (CSS)
- Update CLAUDE.md with versioning docs and frontend rebuild notes
- Abstract ReleaseProvider protocol for platform-agnostic version checking
- GiteaReleaseProvider implementation using stdlib urllib
- UpdateChecker service with periodic background checks and WS broadcast
- Persistent dismissible banner in Web UI when a new version is detected
- Health endpoint now returns cached update info
- Configurable via update_check_enabled and update_check_interval settings
- i18n support (EN/RU)