Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
99f0fb7
Error handling, Test source, modal timing, delete race fix (QA pending)
jerryagenyi Feb 2, 2026
8ff8765
feature: Play after device change, WhatsApp country code, stream labe…
jerryagenyi Feb 2, 2026
9f7936a
fix: Play proxy timeout handling, stream order deduplication
jerryagenyi Feb 2, 2026
1412494
Merge main into dev (sync)
jerryagenyi Feb 2, 2026
aa34433
docs: TODO updates (notification tasks, WhatsApp/Event/Contact notes)
jerryagenyi Feb 2, 2026
23bd205
feat: notification types, update UX, contact/event fixes, single scro…
jerryagenyi Feb 2, 2026
61e8b9c
PR fixes: ContactManager fetch-before-save, scoped duplicate modal, T…
jerryagenyi Feb 2, 2026
9fa0c62
UI/UX v2: glass cards, header-glass on streams, EventManager/ContactM…
jerryagenyi Feb 3, 2026
089a71b
Merge main into dev: resolve conflicts (keep glass UI, body-bg)
jerryagenyi Feb 3, 2026
388532a
TODO: mark Source validation and Error alert UX done (structured erro…
jerryagenyi Feb 3, 2026
820241c
TODO: add refactor items as checkboxes at top (notifications, theming)
jerryagenyi Feb 3, 2026
9c7f376
Fix: guard config.icecast.paths in _checkEnvironmentPaths to avoid ex…
jerryagenyi Feb 3, 2026
8162d78
Merge branch 'main' of https://github.com/jerryagenyi/LANStreamer int…
jerryagenyi Feb 3, 2026
65986e3
docs: Add robustness plan (layers 1-8, failure modes, refactor path)
jerryagenyi Feb 3, 2026
3d4c155
Merge remote-tracking branch 'origin/main' into dev
jerryagenyi Oct 28, 2026
4d7df2a
Merge branch 'fix/error-handling-refactor' into dev
jerryagenyi Oct 28, 2026
87a1c6e
chore: remove dead services (SystemService, WebSocketService) and ana…
jerryagenyi Oct 28, 2026
cd7742a
feat: vendor Tailwind + fonts locally — UI now works fully offline
jerryagenyi Oct 28, 2026
3b7cb7d
chore: remove FFmpegService.js — assigned but never called (per review)
jerryagenyi Oct 28, 2026
53988f2
feat: Electron thin-wrapper scaffold (Phase 1)
jerryagenyi Oct 28, 2026
c19142b
fix: stale-hostname stats polls, log spam, dismissible notices, edita…
jerryagenyi Oct 28, 2026
363c716
feat: PostHog telemetry (opt-in), dark/light theming foundation, app-…
jerryagenyi Oct 28, 2026
05a6e25
feat: listener-page redesign (broadcast rows + mini-player), tool upg…
jerryagenyi Oct 28, 2026
0ff9914
chore: keep config/ runtime state untracked (telemetry.json slipped in)
jerryagenyi Oct 28, 2026
7c099ba
fix: auto-start Icecast on stream start; FFmpeg 9 device parsing; mod…
jerryagenyi Oct 28, 2026
3f5c3f7
feat: dashboard redesign pass — broadcast rows matching the listener …
jerryagenyi Oct 28, 2026
b93e977
feat: QR sharing, auth removal, latency guide, UX polish, screenshot …
jerryagenyi Oct 28, 2026
e31cbd3
fix: UI declutter, light-mode root fix, play-failure notices, share card
jerryagenyi Oct 28, 2026
11983f3
feat: listener auto-resume, notification center, toast stacking, PWA,…
jerryagenyi Oct 28, 2026
da6ff79
chore(telemetry): app=lanstreamer tag on every event
jerryagenyi Oct 28, 2026
ccd13c0
devops cross-pollination: enforce-prs, pressure-checklist gate, appen…
jerryagenyi Oct 28, 2026
8bc73be
feat: component showcase + tokenized add-modal; remove PWA; GUI test …
jerryagenyi Oct 28, 2026
f8e6c27
feat: adopt external designer redesign (P1-P4)
jerryagenyi Oct 28, 2026
35b1e07
fix: feedback round — stale-cache busting, on-demand QR, stable row a…
jerryagenyi Oct 28, 2026
2892f2d
fix: round-3 critical fixes — restart route hang, zombie ffmpeg, draw…
jerryagenyi Oct 29, 2026
db39bec
feat: admin login gate + last zombie-ffmpeg fixes (round 4)
jerryagenyi Oct 29, 2026
9f01dbb
fix: listener play button, stale LAN IP in share links, autostart wit…
jerryagenyi Oct 29, 2026
34fc3c6
ui: pin stream-row actions to a fixed left column (no more jitter)
jerryagenyi Oct 29, 2026
165cd51
docs: split README into user guide + developer guide
jerryagenyi Oct 29, 2026
e1f6ae9
fix: audio-source dropdown black-on-black in light mode
jerryagenyi Oct 29, 2026
742aaa0
feat: event banner cover + click-to-open lightbox
jerryagenyi Oct 29, 2026
9a6e743
ui: flat cards — no hover/shadow/animation; header keeps the only (sm…
jerryagenyi Oct 29, 2026
c076471
fix: sticky header hides nothing — toasts/menus above it, opaque bar
jerryagenyi Oct 29, 2026
5ad7788
fix: notification panel scrollbar — header's whitespace-nowrap leaked in
jerryagenyi Oct 29, 2026
89498a9
chore: add offline-capability regression check
jerryagenyi Oct 29, 2026
6a340e3
ui: remove decorative title icons from the dashboard sidebar
jerryagenyi Oct 29, 2026
68bb309
fix: audio-inputs header icons misplaced — nested buttons broke the row
jerryagenyi Oct 29, 2026
8842c8b
fix: listener header was never actually sticky — overflow-x wrapper k…
jerryagenyi Oct 29, 2026
4d470ce
feat: first-run banner tells the operator the default login and how t…
jerryagenyi Oct 29, 2026
93da1e7
feat: report default-credentials status on token verify
jerryagenyi Oct 29, 2026
9f45f8b
docs: capture the 2026-10-29 rounds — first-run banner, auto-start, f…
jerryagenyi Oct 29, 2026
5b1fe11
docs: leftover server instance — what the stop buttons do and don't c…
jerryagenyi Oct 29, 2026
d3a710b
fix: dashboard sticky header — drop overflow-x wrapper, stick the hea…
jerryagenyi Oct 29, 2026
3993d47
fix: listener row icon flips back to play on pause
jerryagenyi Oct 29, 2026
64cdadb
ui: stream-row actions pinned right (play + ⋯ in fixed 80px trailing …
jerryagenyi Oct 29, 2026
27da113
ui: Audio inputs card moved back under the streams card (left column)
jerryagenyi Oct 29, 2026
9350771
ui: remove drop-shadow glow under the header logo
jerryagenyi Oct 29, 2026
001956a
feat: bundled default event banner replaces the logo placeholder
jerryagenyi Oct 29, 2026
a32ad43
docs: refreshed flat-surface evidence screenshots (logo-glow removal …
jerryagenyi Oct 29, 2026
4c71f8d
fix: dashboard header logo renders the light variant in light mode
jerryagenyi Oct 29, 2026
cf4aed9
feat: listener failure telemetry + distributed-install key bundling
jerryagenyi Oct 29, 2026
37e667a
docs: Electron-first alignment + go-live checklist (test matrix, pack…
jerryagenyi Oct 29, 2026
e9cb779
feat: bundle FFmpeg + Icecast with the installer — nothing to pre-ins…
jerryagenyi Oct 29, 2026
4afdf93
fix: equalizer bars freeze when the listener pauses
jerryagenyi Oct 29, 2026
a2ff029
docs: fold external review into go-live checklist — blockers + test a…
jerryagenyi Oct 29, 2026
721d281
fix: Electron shell diagnostics round — lifecycle, crash, power edge …
jerryagenyi Oct 29, 2026
85b1b43
docs: repo cleanup for the Electron era — archive stale docs, dual-au…
jerryagenyi Oct 29, 2026
6e2318c
chore: drop stray commit-message helper file
jerryagenyi Oct 29, 2026
c855d3e
docs: restore deleted images as archive/legacy-images — project visua…
jerryagenyi Oct 30, 2026
47bb845
assets: app icon regenerated from the current brand mark
jerryagenyi Oct 30, 2026
8d3d109
docs: ship Icecast exactly as a stock install — advise, don't diverge
jerryagenyi Oct 30, 2026
07f2d6e
build: en-US-only Electron locales + recompressed default banner
jerryagenyi Oct 30, 2026
5e5f6b8
docs: binary strategy + shipped size items on the go-live checklist
jerryagenyi Oct 30, 2026
68ca137
feat: functional binary probe + essentials bundle + zombie-proof icec…
jerryagenyi Oct 30, 2026
2a1b381
chore: drop stray commit-message helper
jerryagenyi Oct 30, 2026
f6cf368
feat: in-app admin login settings (dashboard card, data-dir store)
jerryagenyi Oct 30, 2026
a2fa9f8
feat: close the remaining go-live blockers — pinned binaries, per-ins…
jerryagenyi Oct 30, 2026
fc8f8a3
docs: UX audit 2026-10 (IxDF framework) — B 68/85, latency is the top…
jerryagenyi Oct 30, 2026
3575f23
feat: credential policy per owner decision + bundled-Icecast hardening
jerryagenyi Oct 30, 2026
16dc3b3
chore: drop stray commit-message helper file
jerryagenyi Oct 30, 2026
7bc4381
feat: low-latency playback — listener delay cut from 4-5s to ~1s class
jerryagenyi Oct 31, 2026
4bfd7d9
fix: periodic audio break eliminated + bitrate control + menu z-order
jerryagenyi Oct 31, 2026
34bb66b
fix: editing a LIVE stream no longer errors — restart stops the old F…
jerryagenyi Oct 31, 2026
93cf3d4
fix: smoke-test findings — sweep quoting, shutdown null crash, reject…
jerryagenyi Oct 31, 2026
514a60b
build: v2.0.0 Windows build green — fix electron-builder 26 config + …
jerryagenyi Oct 31, 2026
f8162db
ui: eye-icon password visibility toggles on every password field
jerryagenyi Nov 1, 2026
560983c
ui: header order + telemetry switch — and the consent prompt Jerry's …
jerryagenyi Nov 1, 2026
43fc485
feat: telemetry ON by default (opt-out) + devops-practices pass fixes
jerryagenyi Nov 1, 2026
5231168
feat: header Minimize + real Exit — app control buttons (owner spec)
jerryagenyi Nov 1, 2026
76e312d
feat: Remember me + Forgot-password link + Standalone artifact name
jerryagenyi Nov 1, 2026
b611a5f
docs: v2.0.0 release prep — changelog versioned, README status, relea…
jerryagenyi Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .augment/README.md

This file was deleted.

59 changes: 0 additions & 59 deletions .claude/settings.local.json

This file was deleted.

3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Integrity check (design tokens + UI rules)
run: node scripts/check-design-tokens.mjs

- name: Run tests
run: npm test
57 changes: 57 additions & 0 deletions .github/workflows/enforce-prs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: enforce-prs

# Free-tier branch protection (adopted from the devops repo, 2026-10-28):
# every commit that lands on main must (a) belong to a merged PR and (b)
# have a green CI run for that exact commit. Direct pushes and red-CI
# merges are auto-reverted (GITHUB_TOKEN pushes do not re-trigger
# workflows, so the revert itself doesn't loop). dev stays open for the
# documented local-merge workflow; only main (the release branch) is
# protected. Replace with native branch protection if the repo upgrades
# to GitHub Pro or goes public.

on:
workflow_run:
workflows: ["CI"]
types: [completed]

jobs:
verify-pr:
if: >
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.event == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Require green CI and PR association for this commit
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.event.workflow_run.head_sha }}
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
run: |
if [ "$CONCLUSION" != "success" ]; then
echo "::error::CI concluded '$CONCLUSION' for $SHA on main. This commit will be reverted."
exit 78
fi
n=$(gh api "repos/$REPO/commits/$SHA/pulls" --jq 'length')
echo "associated PRs: $n"
if [ "$n" -eq 0 ]; then
echo "::error::Direct push to main is not allowed. Open a PR (CI must pass, then merge). This commit will be reverted."
exit 78
fi
- name: Revert the offending push
if: failure()
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.event.workflow_run.head_sha }}
run: |
git config --global user.name "ci-guard"
git config --global user.email "ci-guard@users.noreply.github.com"
git clone "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" repo
cd repo
git revert --no-edit "$SHA" || { echo "revert failed (merge conflict?) — MANUAL ACTION REQUIRED"; exit 1; }
git push origin main
echo "Reverted $SHA on main."
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ dev-docs/
config/streams.json
config/device-config.json
config/contact-details.json
config/security.json
config/web/
config/admin/

Expand All @@ -133,3 +134,15 @@ Desktop.ini

# Windows reserved name (can appear as accidental file)
nul
/release/
.claude/settings.local.json
.claude-consult-prompt.md
# Root config/ holds per-install runtime state — anchored so src/config/ (source
# code) is never caught by this pattern (it silently blocked `git add` there).
/config/
/config/binary-probe.json
/config/telemetry.json
electron/telemetry-key.json

# Bundled FFmpeg/Icecast binaries for the installer (fetched, never committed)
/vendor/
42 changes: 40 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Context

**Project Type:** Node.js audio streaming server
**Key Technologies:** FFmpeg, Icecast, Express.js, Socket.io
**Project Type:** Electron desktop app (LAN audio streaming; Express server embedded in the Electron main process)
**Key Technologies:** Electron, FFmpeg, Icecast, Express.js, PostHog (voluntary telemetry)

**Architecture:**
- `src/services/StreamingService.js` → Core FFmpeg process management
Expand All @@ -20,6 +20,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **dev** is the integration branch. All changes land here (via merge or PR) before they can go to main.
- **Feature branches** are created from **main** (or **dev**) when starting new work. Pull from main into your feature branch if you need latest: `git fetch origin main && git merge origin/main` (or rebase).
- **Flow:** main → create `feature/xyz` → work → **merge feature into dev locally** → test → push dev → merge dev into main (release).
- **main is now PR-protected (2026-10-28):** `enforce-prs.yml` auto-reverts any main commit that wasn't merged via a PR with green CI. Releases go dev → PR → main (pushing main directly gets reverted). dev stays open for the local-merge workflow above.

### Pre-review gate for streaming features (enforced)

Every PR that adds or changes streaming behavior (stream lifecycle,
proxying, device capture, listener fanout, start-all, queueing) must
answer the 7 hostile conditions in **docs/pressure-checklist.md**
(uneven load, slow-not-down dependency, 10x data, machine dies mid-write,
poor network, duplicate request, hostile traffic) in the PR description
for the changed code path. Adopted from the devops practices DB.

### Integrity linter (CI-enforced)

`node scripts/check-design-tokens.mjs` fails CI on NEW violations of the
UI rules below (hardcoded hex outside token sources, gray-* classes,
native dialogs, runtime CDN refs). Existing debt is grandfathered in
`scripts/design-token-baseline.json`; to accept new debt deliberately run
`--update-baseline` and say why in the commit.

### Merge feature into dev locally (recommended)

Expand Down Expand Up @@ -54,6 +72,26 @@ Example: `git merge feature/start-all-and-unique-names`. After pushing dev, you
- FFmpeg exit codes are unsigned, require conversion to signed
- Example: 0xA7F00008 needs special handling

## UI rules (enforced)

- **Design tokens are the single source of truth**: `public/assets/css/tokens.css` (`--tok-*`, dark default + `[data-theme="light"]`). Tailwind maps them (`tailwind.config.js` → `bg-surface-1`, `text-muted`, `border-line`, `text-accent`, `rounded-pill`, `shadow-glow`…). **Never hardcode hex colors or dark-only Tailwind grays in new UI code** — use the semantic tokens so both themes work.
- **Component showcase (dev-only) at `/showcase.html`**: every approved component variant, rendered from live tokens in both themes. New UI must match a pattern shown there — if it doesn't fit, extend the showcase first, then build. Check both themes on that page when touching tokens or `ui.css`.
- **Shared components live in `public/assets/css/ui.css`**: `.btn-primary/secondary/ghost/danger`, `.play-btn`, `.copy-btn` (IconButton), `.channel-badge`, `.stream-row`, `.live-chip`, `.eq`, `.mq` (marquee), `.tok-label`, `.toast`/`.toast-stack` (via `LSToasts`), `.stream-error` (collapsed row error), `.ls-collapse` (remembered collapsibles via `LSCollapse`). New variants need a shared definition there first — no one-off button styles.
- **Toasts must stack**: all transient notifications go through `LSToasts.show(message, type, {tag, linkUrl, linkText})` — it appends into `#toast-stack` so simultaneous toasts push down instead of overlapping; same-`tag` toasts replace each other. Never hand-roll `position:fixed` toasts.
- **One icon family**: Material Symbols Rounded, locally served. Icon-only buttons must have `aria-label` + `title`. Replace decorative emoji used as UI icons; content emoji is fine.
- **Sharing the listener page happens in ONE place** (header share card: open / copy / QR via `LSShare`). Never per-stream share buttons — we share the page, not individual mounts.
- **Notifications**: transient confirmations via the page toast; stream failures stay visible near the stream; never `alert()`. Don't report success before the server confirms.
- **Offline rule**: no runtime CDN dependencies — fonts/icons/styles all self-hosted (`build:css`, `vendor:fonts`).
- After changing classes anywhere: run `npm run build:css`.
- **No PWA on the listener page** (decision, 2026-10-28): installed shortcuts are bound to the origin they were made from, so a DHCP IP change kills every listener's shortcut. The page stays a plain bookmarkable URL; `streams.html` actively unregisters any leftover service worker. Don't re-add a manifest/SW without solving dynamic IPs first.

## Telemetry (PostHog)

- `src/services/TelemetryService.js` — opt-in only; **never** capture personal data, audio, stream names, or paths. Events: `app_start`, `stream_started` (format), `stream_stopped`, `stream_failed` (error category), `telemetry_enabled/disabled`. Distinct ID = anonymous install UUID from `config/telemetry.json`.
- Enabled only when user consented (first-run Electron prompt; toggle in tray menu) **and** `POSTHOG_KEY` env is set (`POSTHOG_HOST` optional). No-op otherwise — keep it that way for any new events.
- Skipped entirely under `NODE_ENV=test`. All calls are fire-and-forget with catch: telemetry must never break the app.
- Docs: README "Telemetry & Privacy" (user-facing) + this section (dev-facing).

## Configuration

- `.env` - Environment variables (use `env.example` as template)
Expand Down
36 changes: 36 additions & 0 deletions LANStreamer-redesign/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# LANStreamer — UI/UX redesign

Open `dashboard.html` or `listener.html` in any browser (no build step). Use the moon icon to switch light/dark; resize to see mobile.

Files
- `styles.css` — shared design tokens (light + dark) and components. Drop-in replacement for the current token sheet.
- `app.js` — theme memory, stacked/tagged toasts, icons.
- `dashboard.html` — admin dashboard (interactive mock: start/stop, filter, new-stream drawer, overflow menu, empty state).
- `listener.html` — public listener page.

## What changed and why

Admin dashboard
1. **Status strip at the top** — on-air count, listeners, Icecast status and "needs attention" are visible at a glance instead of buried in collapsed sidebar cards.
2. **Security banner is actionable** — plain-language risk + a direct "Change password" button, in warning (not error) colour so it doesn't compete with real stream failures.
3. **Live streams grouped first** — "On air" and "Ready" sections; no more hunting for the one live row among seven.
4. **Full source names** — the old rows truncated the *start* of device names ("tput (VB-Audio…"). Names now truncate at the end with a tooltip.
5. **Errors explain the fix** — the error row shows cause + "Change input" / "Retry" inline, instead of a collapsed "Audio Device Not Found" toggle.
6. **Fewer icons per row** — one play/stop button + a "⋯" menu (copy link, edit, duplicate, delete). Delete no longer sits one click from Play. Up/down arrows replaced by a drag handle on hover.
7. **Stop is visually distinct from Play** (red square vs neutral play), so the live row is unmistakable.
8. **New stream as a side drawer** — input list shows which device is already in use and a live level meter; quality chosen by purpose (Speech / Standard / Music); optional auto-start.
9. **Share card** — QR + copyable LAN address replaces the cramped "Listen to streams:" header pill.
10. **Audio inputs always visible** — status (in use / idle / missing) per device instead of a "Component unavailable" collapsed panel.
11. **Filters** (All / Live / Issues) and a friendlier empty state with a 3-step guide.

Listener page
1. **One big play button** with the current channel, live timer, listener count, waveform and volume.
2. **Channel picker** for multi-room / multi-language events (designed for interpretation use).
3. **Help line** for the most common "no sound" causes.
4. **Mobile mini-player** stays pinned at the bottom after scrolling.
5. Event artwork is themed from tokens instead of a fixed illustration; About/Contact are native, keyboard-accessible disclosures.

Design system
- Typography: Space Grotesk (UI) + JetBrains Mono (IDs, bitrates, timers).
- Warning colour now separate from error; `--onair` reserved for the ON AIR badge.
- Visible focus rings, `prefers-reduced-motion` respected, colour never the only status signal (text labels on every chip).
26 changes: 26 additions & 0 deletions LANStreamer-redesign/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Shared: theme toggle (remembered), toasts, icons
(function(){
const saved = localStorage.getItem('ls-theme');
const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = saved || (prefersDark ? 'dark' : 'light');
})();
window.LS = {
toggleTheme(){
const t = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = t; localStorage.setItem('ls-theme', t);
},
toast(msg, tag){
let box = document.querySelector('.toasts');
if(!box){ box = document.createElement('div'); box.className='toasts'; box.setAttribute('aria-live','polite'); document.body.appendChild(box); }
if(tag){ const old = box.querySelector(`[data-tag="${tag}"]`); if(old) old.remove(); }
const t = document.createElement('div'); t.className='toast'; t.textContent=msg; if(tag) t.dataset.tag=tag;
box.appendChild(t); while(box.children.length>4) box.firstChild.remove();
setTimeout(()=>t.remove(), 3200);
}
};
const I = {
logo:'<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><rect x="4" y="13" width="3" height="7" rx="1.5"/><rect x="10" y="9" width="3" height="11" rx="1.5"/><rect x="16" y="5" width="3" height="15" rx="1.5"/></svg>',
play:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5z"/></svg>',
stop:'<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>',
pause:'<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><rect x="6" y="5" width="4" height="14" rx="1.2"/><rect x="14" y="5" width="4" height="14" rx="1.2"/></svg>',
};
Loading
Loading