Two iPad 2 issues from screenshot:
1. Button text was left-aligned. On iOS 9 a <button> with display:flex
ignores justify-content:center. Added text-align:center to .btn as a
fallback (harmless when flex works).
2. Play symbol rendered as broken-glyph tofu. ▶ (U+25B6) gets emoji
presentation which fails on iOS 9. Switched legacy play to ► (U+25BA),
a text-presentation pointer in the same Geometric Shapes block as the
■ stop glyph that already renders fine. Canvas PLAY label now uses
SYM.play too.
Modern browsers unaffected (still ▶ emoji, flex centering).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In ECMAScript 5 strict mode, function declarations inside blocks
(if/for/while) are a syntax error. Since app.js uses 'use strict',
the legacySym function defined inside the if(isLegacyIOS) block could
prevent the entire script from parsing, killing connect() on all browsers.
Moved legacySym() to module scope. The if-block now only contains
the call sites, which is valid everywhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-char symbols (<< >>) get 26px bold, single-char (■ ▶) get 36px bold.
letter-spacing: 2px spreads the chars slightly for better visual weight.
Mute button (vol/mut) gets 16px bold to stay readable in its 48px container.
Flex centering already guaranteed by .btn layout -- no extra work needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Media-control emoji (U+23EE U+23F8 U+23F9 U+23ED) and 🔊 🔇 📋 🚫
are absent from the iOS 9 system font and render as empty boxes on iPad 2.
Added isLegacyIOS detection (iOS <= 9 via UA) and a SYM symbol table
that picks between emoji (modern) and plain-text fallbacks for iOS 9:
prev=<< pause=|| stop=■ next=>> volOn=vol volOff=mut playlist=PL skip=✕
Static buttons replaced at boot; dynamic updates (play/pause, mute)
use SYM throughout. Modern browsers completely unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NodeList.prototype.forEach was added in Safari 10 / iOS 10.
On iOS 9 it throws 'TypeError: forEach is not a function' at
init time, before connect() is called — leaving the app stuck
on 'Nicht verbunden' despite the async/await fix.
Added qsa(selector, root?) helper that wraps querySelectorAll
in Array.prototype.slice.call() to produce a real Array, then
replaced all three call sites:
- document.querySelectorAll('.btn-seek').forEach
- starEls (stored NodeList).forEach x2
- playlistList.querySelectorAll('li').forEach
Array.prototype.forEach has been safe since iOS 4.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
async/await, catch{} without binding, and ?? are all syntax errors in
Safari 9 (iOS 9). A single syntax error prevents the entire script from
parsing, so connect() never ran — explaining 'nicht verbunden'.
Removed / replaced every incompatible construct:
- async/await → .then()/.catch() Promise chains
- catch {} → catch(_e) {} (optional catch binding is ES2019)
- ?? → explicit != null ternary (nullish coalescing is ES2020)
- padStart() → pad2() helper (String.prototype.padStart is ES2017,
not available in iOS < 10)
- scrollIntoView({block,behavior}) → scrollIntoView(true) (options
object not supported in iOS 9, boolean form works everywhere)
Template literals, arrow functions, const/let, forEach, classList,
dataset, Promise, WebSocket, Canvas, performance.now() and
requestAnimationFrame are all fine in iOS 9.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS:
- #seek-row and #controls-row: flexbox base (flex: 1 on children,
margin-left gaps) + CSS Grid enhancement via @supports (display: grid)
- #playlist-overlay: replaced inset: 0 with explicit top/right/bottom/left
(inset shorthand not available before iOS 14.5)
- All flex-gap usages: adjacent-sibling margin fallbacks as the base;
gap values restored via a single @supports (gap: 1px) block at the end
(flex gap not available before iOS 14.5 / Safari 14.1)
- Added -webkit- prefixes for user-select, flex, transition, transform
throughout to be safe on older WebKit
JS:
- Added apiFetch() wrapper: uses native fetch() when available (iOS 10.3+),
falls back to a minimal XMLHttpRequest shim for iOS 9 and older.
Matches the exact subset of the fetch API the app uses: .json(),
method, headers, body.
- Replaced all four fetch() call sites with apiFetch()
Result: layout and all API calls (rating, killist, playlist) work on
iOS 9 / iPad 2. Modern browsers get the exact same behaviour as before
via the @supports enhancements.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- winamp.GetCurrentFile() reads file path via IPC_GETPLAYLISTFILE (211)
+ ReadProcessMemory, same pattern as playlist titles
- internal/rating: Get/Set POPM frame via bogem/id3v2
- Email: rating@winamp.com (Winamp standard)
- Byte scale: 0/1/64/128/196/255 = 0-5 stars
- Compatible with Windows Explorer and Winamp
- GET /api/rating -> {stars: N}
- POST /api/rating {stars: N} -> writes POPM, returns {stars: N}
Frontend:
- 5 stars in track-info, gold when lit
- Fetched automatically on track change
- Tap to rate; tap same star again to remove rating
- Optimistic update with revert on error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace per-frame hsl(val*120) with a static createLinearGradient
(green->yellow->red, bottom to top) shared across all bars. Colour is
now position-based, not amplitude-based, so it never flickers. Peak
decay slightly slower (0.008/frame) for smoother hold.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three modes on canvas click:
viz -> spectrum analyser (default)
actual -> hh:mm:ss elapsed in white, ELAPSED label
remaining -> -hh:mm:ss countdown in accent red, REMAINING label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Time display now shows elapsed / -remaining instead of elapsed / total,
matching the Delphi original (reversetime mode). Progress-bar click seek
derives total from current + remaining to stay correct.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sw.js uses __BUILDVER__ token; build.ps1 replaces it with the git
version before go build (which embeds the patched file), then restores
the template via the finally block. Installed PWA clients receive new
assets after every release.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Playlist browser:
- GET /api/playlist returns all track titles via ReadProcessMemory
- WS command {cmd:jump, index:N} jumps to track (0-based)
- Full-screen overlay with scrollable list
- Current track highlighted, auto-scrolls into view on open
- Live highlight update when track changes while panel is open
PWA:
- manifest.json with standalone display + theme colour
- sw.js: cache-first service worker for shell files
- icon.svg: music-note icon on dark background
- Apple/Android meta tags for Add to Homescreen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Canvas / viz:
- Idle animation when Winamp stopped/paused: slow sine-wave breathing
across bars at low amplitude, dim colours — canvas is never dead-black
- 'PLAY' label overlay when stopped, 'NO SIGNAL' when WS disconnected
- hasSignal check: viz data must be < 1.5s old AND Winamp playing
- DPR resize uses Math.round() to avoid sub-pixel canvas size mismatch
- Peak indicators fade quickly in idle mode
winamp.go:
- GetPosition: clamp 0xFFFFFFFF sentinel at source (> 0xF0000000 → 0)
instead of in server.go; also removed redundant clamp from statusMsg
- GetTitle: use strings.LastIndex/Index instead of hand-rolled helpers
Returns empty string when window title is just 'Winamp' (stopped/empty)
Playlist prefix strip is now bounded (dot <= 4) so track titles with
'. ' in them are not accidentally trimmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WebSocket (/ws):
- Replaces 2s HTTP polling with persistent WS connection
- Server pushes status updates every 500ms to all clients
- Server pushes FFT spectrum frames at ~30fps
- Bidirectional: client sends commands as JSON ({cmd, delta, level, ...})
- Auto-reconnect on disconnect (3s backoff)
- Status pushed immediately on connect
Visualisation (internal/viz/capture.go):
- WASAPI loopback capture via IAudioClient (same COM approach as volume)
- Captures whatever is playing through the default render device
- 2048-sample Hanning-windowed FFT (pure Go, no deps)
- 64 log-spaced bars, 40Hz-20kHz
- Fast attack / slow decay smoothing per bar
Canvas renderer (app.js):
- requestAnimationFrame loop, DPR-aware resize
- Green->yellow->red HSL gradient by amplitude
- Peak-hold indicators with 1.2%/frame decay
- Graceful: canvas stays dark if no viz data (Winamp paused/stopped)
Architecture:
- internal/server/hub.go: gorilla/websocket hub (register/broadcast/unregister)
- internal/server/server.go: full //go:build windows, REST API kept for debug
- frontend uses WS for all commands, REST only for killist list fetch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- internal/volume: IAudioEndpointVolume via COM (pure Go, no CGO)
- Get/Set master volume (0-100%)
- GetMute/SetMute
- Uses WASAPI instead of deprecated MMSystem mixer API
- server: /api/volume now uses system volume (0-100, was Winamp 0-255)
- server: new /api/mute endpoint (GET + POST ?muted=true|false)
- server: volume+mute included in /api/status response
- frontend: mute button with visual state (🔊/🔇, red bar when muted)
- frontend: volume display as percentage label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>