The grid read the window's size whenever React read its snapshot. React
re-reads the snapshot after every commit and re-renders if it changed, and
rendering the grid can change the window's size: on Windows, a window size
where the full-width grid gives the page a scrollbar shrinks clientWidth by
the scrollbar, and the narrower grid takes the scrollbar away again. That
went round until React gave up ("Maximum update depth exceeded", #185),
on load or on resizing to such a size.
The size is now measured only when the grid subscribes and on resize, and
the snapshot just returns it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The grid always shows a vertical scrollbar but sized its columns to the full
window width. Where scrollbars take up room (Windows, about 17px) the right
column ran under the scrollbar and the grid scrolled sideways. The columns
now fit in the window's width less the scrollbar, measured again as the
window's size (or zoom) changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Archive refuses an identifier that differs only in case from an
existing item, but we only checked for an exact match, so the same skins
picked the same refused name and failed every night. Now a refused name
moves on to the next number.
A name that already holds this skin (an upload that timed out after the
Archive had it) is adopted instead of uploaded again, with its screenshot
added if missing.
Screenshot names also replace an uppercase .WSZ/.ZIP extension, which
had been left in place, so the PNG was uploaded posing as a skin.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168RMPEf7DAZeo7G6so4a6e
/dev/events loaded the whole event log into the live server's memory (All
time by default). The log is 2.8M events and growing ~145k a day; loading it
all took over 4GB when it was smaller, on a 4GB server that's already using
swap. So one visit could take down the museum.
Gone: the page, its charts and styles, the token login (/dev/login) and the
middleware gate in front of /dev, and loadSessions, which read the whole log
at once. /dev/* is a plain 404 now. The event logger no longer has to ignore
tabs opened from the dashboard.
The analysis the nightly Downloads order needs stays, moved from app/dev/events
to events/: analyze() and botReasons, and forEachSessionBatch, which reads the
log a batch of sessions at a time. The dashboard's chart and stats helpers are
dropped with it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168RMPEf7DAZeo7G6so4a6e
Its max width was the feed column's (56.25dvh, from the window's height), so
in a short window it shrank to ~240px and the logo, search box, sort button
and menu piled on top of each other. It's now the window's width less the
gutter, up to 34rem: about what it was in a typical desktop window.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168RMPEf7DAZeo7G6so4a6e
The position correction compared each of the museum's first skins with
itself: those positions always show the same skin, so each had its own
"average" rate, scored ~1.00, and sat around #2,680 (base-2.91, the Classified
skins, Garfield, Zelda...). Random skins, which appear at many positions, get
about as many downloads per view anywhere (0.29% at position 14, 0.19% at
10,000), so position isn't corrected for at all now.
Also:
- Grid views in every order count, not just the museum's own; searches still
don't.
- Smoothing goes from 2 to 10 expected downloads, so a skin needs ~2,800
views before its own record outweighs the average.
- Skins with no downloads aren't scored; they follow in the museum's order.
(28.5k of the 32k scored skins had none, ordered fewest views first.)
On production's events through Nov 3: 3,969 skins scored, led by Winamp5
Classified, base-2.91, Sony CDX-MP3 and Bento Classified.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168RMPEf7DAZeo7G6so4a6e
The div Webamp appends to <body> for skin window shapes held an inline svg,
which sat in a line box and made the page ~18px taller than the viewport. In
the museum's feed, wheeling over a fixed element (or scrollIntoView) scrolled
the window by that much, pushing each skin's title off the top.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168RMPEf7DAZeo7G6so4a6e
Opening a skin from the grid, the backdrop fades in over 0.8s, but the
fixed fade above the floating bar (over the readme) and the shadow along
the top appeared at once, so for most of that time they were a dark band
on the page's plain background. They now fade in with the backdrop, so
they're never darker than what they blend into.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168RMPEf7DAZeo7G6so4a6e
The classic museum's search syntax (has:ani, has:transparency,
-filter:nsfw, ...; legacy-client/src/queryParser.js) works in the new grid
again: it was being searched for as words, so "has:avs" found 560 skins
that mention AVS rather than the 43,937 that have it. Filters combine with
words ("zelda has:transparency").
A filters button in the search box (beside the sort button, or the clear
button once there's text) opens a menu of them as toggle pills, in groups:
Windows, Plugins, Cursors, Other. It stays open, so several can be picked;
each adds its has: term to the box (or takes it out), which is where people
see the syntax, and the search runs as the text changes. Picked pills fill
in at the same size, so nothing moves. has:likes (nearly every skin) and the
NSFW filters work if typed but aren't in the menu. The menu and the sort
menu close each other, and on Escape or a press outside.
A link to a search that uses the syntax skips the server's first results
(its SQLite search can only apply the NSFW filter) and the grid searches
Algolia, as when typing. If Algolia is down, the SQLite fallback still
ignores filters other than NSFW.
Event schema version 10: search_filters_open and search_filter_toggle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0168RMPEf7DAZeo7G6so4a6e
It stays unlisted until there's enough data behind it for the results to be
good: /?sort=downloads still shows it (and then the menu lists it, checked),
and the nightly job keeps rebuilding it. To list it, take "downloads" out of
UNLISTED_SORTS.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The first production run of the Downloads order failed with "database is
locked": the site's servers, the bot and the nightly jobs all write to
skins.sqlite3, and no connection set a busy timeout, so a write that met
another in progress failed at once. That also means the site's own writes
(mostly logged events) have been failing for the moment each nightly
rebuild holds the write lock.
Every connection now waits up to 5s (knexfile's pool.afterCreate), and the
Downloads job up to 30s. Its transaction also starts with its write: in WAL
mode, a transaction that reads and then writes fails at once if another
write lands in between, however long it would wait, so the skins it may list
are read before it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The museum's sort menu gets "Downloads" (?sort=downloads), between Top and
Newest: skins by how often seeing them in the museum grid led to a download,
against how often it would for an average skin at the same positions. It was
/sorts' "downloads per grid view". Checked on production's events (Oct
14-26): a skin's score from one half of the sessions predicts downloads per
feed view in the other half well (Spearman +0.62 above a shuffled baseline,
against +0.19 for direct page loads), and ~14.8k skins have the 20+ grid
views it takes to be scored.
The nightly museum-order job (--compute-museum-order) rebuilds it after the
museum's own order, into museum_downloads_order: scored skins best first,
then the rest in the museum's own order, leaving out NSFW skins like Newest
and Random. Positions count from 1, so a page is a range of them. It reads
the event log 500 sessions at a time (forEachSessionBatch, using the new
session_id index): ~13s and ~600MB on a copy of production's log, where
loading it all at once, as /sorts did, took 4.3GB on a 4GB server. It's
written in one transaction; if it fails, the previous order stays and the
museum order still updates.
/sorts is gone, and its other rankings with it: the scoring this needs moved
to tasks/computeDownloadsOrder.ts, with its test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sort button at the end of the grid's empty search box opens a menu, like
the "…" menu, of the museum's orders: Top (its own), Newest and Random. The
button names the chosen order. Choosing one closes the menu, scrolls the grid
to the start and puts it in the URL (/?sort=newest), so it can be shared and
Back returns to it. Searches aren't sorted (a search is its own order, best
match first), so while there's text in the box the sort button gives way to
the clear button; clearing the search goes back to the chosen order. The logo
goes back to Top.
Newest is by skins.id. Random is ordered by skins.id times a seed, mod a
prime: one seed per visit, so the order holds between the grid and the feed
and on Back, and a reload shuffles it again. Both leave out skins reviewed as
NSFW, like the museum's random picks. The feed follows whichever order it was
opened from (listPosition now carries the sort).
Each page of Newest or Random sorts the whole museum: ~125ms for the first
page on production data, ~220ms deep in, against ~1ms for Top's first page,
which reads museum_sort_order in its stored order. Fine for now; the fix is a
numbered table of these skins, written by the nightly museum-order job.
Event schema version 9: lists carry `sort` (null for a search) on
session_start, view_switch, skin views, grid_impressions and grid_skin_click,
and there are sort_open and grid_sort events. Grid positions mean something
different in each order, so /sorts and the dashboard's position stats keep
to Top.
Both menus now close on a pointerdown outside them, not a mousedown: the
search box's buttons cancel their pointerdown (to leave focus where it is),
which also cancels the mousedown, so opening the sort menu (or tapping the
clear button) left the "…" menu open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bottom menu bar (Grid and Feed buttons, plus a menu) is gone. Every page
now has a bar floating at the bottom: the museum's logo, then the grid's
search box or, elsewhere, a Back button, then a "…" menu (About, Upload,
Feedback, Donate, GitHub). Back goes to the previous page in the tab,
animating into the grid if that's where it was, or offers "Browse skins"
when there's nothing on the site to go back to. It replaces the feed's back
arrow beside each skin's title. On the grid, the logo scrolls back to the
start of the museum.
The feed's like button is gone too. It wasn't saved anywhere: it logged
skin_like and added one to the skin's Twitter like count until the page
reloaded. Over Oct 14-26 it got 5.8 likes per 1,000 feed views from 2.3% of
viewing sessions (downloads: 134 per 1,000, half of sessions), and among
skins with similar download rates, being liked didn't predict more
downloads by others. The feed no longer fetches tweets for like counts.
Event schema version 8 covers both: no "menu" view switches, menu_open
counts the "…" menu, "back" means the Back button, and no skin_like.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Our quota reset on Oct 16 (billing periods run from the 16th to the 15th),
and the limit is ~300k search requests a period. Before the Oct 5-12 spike
we used ~200k; the new grid logs ~6k debounced searches a day, ~180k a
period. Both search boxes already wait for SEARCH_DEBOUNCE_MS, and the grid
falls back to SQLite if Algolia fails, so this is just flipping USE_ALGOLIA.
A page loaded with ?query= still renders the SQLite results on the server,
so crawlers and link previews don't spend Algolia quota.
Algolia writes no longer throw. Running out of quota last time made uploads
fail, so a failed write is now logged and skipped. And algolia_field_updates
is only written when the write actually happened: while writes were off it
went on recording updates that never reached Algolia, which hid ~2,500
skins missing from the index.
The mark_skin_nsfw test enables writes itself; it had failed since writes
were gated behind ALGOLIA_ENABLED.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
storeHas(), which skinIsLoaded() waits on for every skin, registered a
teardown with the instance's Disposable and never removed it once the
wait was over. The museum keeps one instance for as long as the page is
open, so each skin it showed kept a few closures and a promise, about
0.26 KB, forever. Unsubscribing from onTrackDidChange() or
__onStateChange() left a teardown behind the same way.
Disposable gains child(): a scope with one owner, disposed along with
it, that removes itself from its owner when disposed on its own. Each
of these now gets one. Disposing twice is now a no-op rather than an
error, since a child can be disposed both on its own and by its owner.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Disposing an instance left several things holding on to it or to what it
had loaded:
- The AudioContext stayed open, keeping its audio thread running and every
node in it, including the <audio> element, alive. It's now closed.
- The <audio> element kept what it had downloaded and decoded. Its source
node is now disconnected and its src removed and reloaded.
- The media session's action handlers closed over the instance, so the
media keys kept controlling it and kept it alive. enableMediaSession()
now returns a teardown that clears them, unless another instance has
taken the session over since.
- Object URLs made for blob tracks and album art were never revoked. They
now go through a createObjectURL extra that revokes them on dispose.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The search box was mostly see-through, with a faint edge and a dim
placeholder, so it blended into the busy screenshots behind it. It's now
nearly opaque, with a deeper shadow, a light edge and a magnifying glass.
Chrome's own clear button was small, blue and only appeared on hover, and
Firefox has none. In its place is a round X that shows whenever there's a
query, with the spinner beside it. Tapping it doesn't move focus, so it
doesn't bring up the keyboard on mobile.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/og/museum.jpg, /og/search.jpg?query= and /og/skin/:md5.png replace the
skin-museum-og service on Vercel, with the same layouts. A skin's image
goes from about 650 KB to about 65 KB, and the grids from about 1.2 MB to
about 160 KB:
- The renderer scales images smoothly whatever the CSS says, blurring the
pixel art into colors PNG compresses badly. Screenshots are now scaled
with nearest neighbor to the size they're drawn at, and drawn 1:1.
- Its PNGs are poorly compressed, so they're re-encoded with sharp, and
the grids (whose screenshots are drawn rotated) as JPEG.
They come straight from the database rather than through GraphQL, and are
cached for a few hours per server process, as skins.webamp.org isn't
behind a CDN.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012wifNHvrLbRC9MT5mRuyPW
Next streams metadata into the body for crawlers it doesn't know, and
Mastodon, Bluesky and other link preview crawlers read only <head>, so
their previews had no title or image. Add them to Next's list of crawlers
that get it in <head>.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012wifNHvrLbRC9MT5mRuyPW
The OG service fetched skins from api.webampskins.org, which loops on
redirects (Cloudflare reaches the server over HTTP, and Apache redirects
HTTP to HTTPS), so every image was a 500. Fetch from skins.webamp.org.
Search images also failed for searches that match a modern skin, which
search_skins returns as {} with no screenshot. Leave those out.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012wifNHvrLbRC9MT5mRuyPW
Shows the museum's order next to a random one and four inferred from the
event log: direct loads, clicks and downloads per grid view (against what
an average skin gets at the same positions), and downloads per feed view.
Public, but not linked and not indexed.
Skins opened from it (or from /dev) aren't logged, so looking through a
ranking doesn't count as direct loads of its skins.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012wifNHvrLbRC9MT5mRuyPW
The dashboard counted every session without a session_end as "discarded
in the background", 21% of them. Most were: the page was hidden, which
sends everything queued, and the browser later discarded it, which sends
nothing. Those lost no events. The rest stopped mid-activity with no hide
either (the browser quit or crashed, the app was killed), and may be
missing their last events.
Sessions now end one of three ways (session_end, hidden or cut_off), and
the dashboard shows the last two separately: 12.4% and 9.6% today.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bottom bar's menu gets "Support the museum", linking to Ko-fi for
thank-you donations, and the repo gets a FUNDING.yml so GitHub shows a
Sponsor button for the same page.
Opening the menu wasn't logged, only following a link in it, so we
couldn't tell how many people look at the menu. Add menu_open (event
schema version 7). Following the Ko-fi link is already logged as a
link_click with its href, like the menu's other links.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
FTS5's rank (BM25) scores a match lower the longer the whole row is, so
a long readme buries a skin even when the search is for its own file
name: "Atlas" put Skinner_Atlas.wsz, with a 49 KB readme, 136th of 165.
Weighting the file_names column doesn't help, since the length penalty
is for the whole row. So order skins with every word of the search in
their file names first, then by rank. It's now 7th, and the broadest
search ("winamp") takes 141ms instead of 44ms.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The search_index migration was one multi-statement knex.raw(), and the
sqlite3 driver runs only the first statement of a string, so all it did
was create the skin_search table. Production's index was filled by hand
once and never got the triggers meant to maintain it, so none of the
2,334 skins added since (ids after 220686) can be found by search, and
13 rows are for skins since deleted.
A new migration does the rest, one statement at a time: the triggers
(plus one to drop a deleted skin from the index), then adds the missing
skins and removes the orphans. It can be rerun. The original migration
now creates just the table, which is all it ever did.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
/dev/events was development-only. In production it's now served to a
browser that has signed in at /dev/login with DEV_DASHBOARD_TOKEN (set in
.env.production.local on the server), which sets a year-long HttpOnly
cookie. Everyone else, and everyone when no token is configured, gets a
404. The middleware checks /dev/*, and the page checks again in case the
matcher ever misses it. The token is posted from a form, not put in the
URL, so it stays out of access logs and history.
deploy.sh --classic/--new rewrote .env.production.local with just
MUSEUM_UI, which would now drop the token; it replaces that one line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since clip paths started carrying their window's size, a region.txt with
a section other than the four Webamp clips (say a stray [Playlist])
crashed rendering: the size lookup came back undefined and destructuring
it threw "undefined is not iterable". Skip those sections, as was
effectively the case before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Download rate per view and session, time from opening a skin to
downloading it, downloads per session, rate by device, referrer and
depth into the feed, and the most downloaded skins.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QzNErwA6wVTLaUTNMARPQt
Firefox draws an image's alt text until it loads, so the grid filled
with skin names in link blue. Make it transparent (screen readers still
read it), in the grid and on the feed's screenshot.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QzNErwA6wVTLaUTNMARPQt
- Record each session's user agent (a new session.user_agent column, from
its first batch), and in session_start whether the browser is
automated (navigator.webdriver), whether the page was prerendered, and
whether it was visible. A prerendered page sends nothing until it's
opened, so it no longer leaves a session nobody saw. Event schema
version 6.
- Don't log the Download button's blob: link as a link followed out.
- Dashboard: a Traffic filter (likely people by default) that leaves out
suspected bots by rule (no start, bot user agent, automated browser,
never seen) and says how many each caught; merge searches typed letter
by letter; ignore blob: links already logged; line up bars and
histogram columns; reopen the pulled copy when it changes. The pull
script copies the session table too.
The migration has to run on the server before this is deployed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QzNErwA6wVTLaUTNMARPQt
The museum depends on this repo's Webamp (workspace:*), whose package
entry points are built files. The server never built them, so the
museum's build silently fell back to a published [email protected] elsewhere
in node_modules and none of Webamp's later changes reached it. The
deploy now builds the two bundles the museum imports, with the same
heap cap as the museum's build.
Webamp's rollup script can now build only named bundles
(`node ./scripts/rollup.mjs webamp.bundle.min.mjs ...`); with no
arguments it builds them all, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QzNErwA6wVTLaUTNMARPQt
Webamp clips the main and equalizer windows with SVG clip paths built
from a skin's region.txt, in pixels. When a page scales Webamp with CSS
zoom, Firefox applies those clip paths at the unscaled size (Chromium
scales them), so only part of each window showed. Define them relative
to the window's own box (clipPathUnits="objectBoundingBox", with the
pixel polygons scaled down by the window's size), so they scale with
the window however it's scaled. Unchanged at normal size.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QzNErwA6wVTLaUTNMARPQt
Guessing whether each touch on Webamp was meant for it or for scrolling
the feed (and replaying synthetic events into Webamp) was never reliable
across browsers. Now, on touch screens, Webamp is either seen or used:
- Seen (default): a transparent shield covers it, so every swipe scrolls
the feed and no touch reaches Webamp.
- Used: tapping the skin removes the shield and stops the feed scrolling,
and Webamp gets every touch unaltered. Done, tapping outside the skin,
or Escape go back to scrolling.
Mouse input is unchanged. Event schema version 5 adds webamp_use_start
and webamp_use_end (how long, and how it ended); the dashboard shows them.
Also make the grid's hover glow switch instantly, drawn as an overlay
rather than a filter on the image: the animated filter put the tile on
its own GPU layer, which re-sampled it and its neighbors' pixels
slightly differently.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QzNErwA6wVTLaUTNMARPQt
The plan is to ship with thorough logging and no ranking algorithm, look
at the data, and then decide what a ranking should learn from.
- The museum list (grid and feed) keeps the museum's top 10, then one
place in five is a random skin from anywhere in the museum (not NSFW),
picked by the server for each page (museumList.ts). Searches have none.
The server marks the random skins, and every view, grid impression and
grid tap is logged with its source ("list" or "random").
- Event schema version 4 adds grid_impressions (tiles on screen once
scrolling stops), link_click (links out of the grid and feed),
skin_load_failure, and skin_view_end's readyMs (how long Webamp took
to show the skin). Webamp failing to load a skin is detected and the
screenshot stays up instead of the previous skin.
- Remove the ranking task (tasks/computeScrollRanking.ts) and its CLI
command: nothing derives a rank from events for now.
- A dev-only dashboard at /dev/events reports on the event log (session
length, feed retention, dwell, outcomes by candidate signal, random vs
list, position effects, coverage, grid, Webamp, data quality). It reads
the dev database or a copy of production's events made by
scripts/pull-events.sh, and 404s in production.
- Fix a scroll event that lands just after tapping a grid tile replacing
the tapped skin as the feed's place in the list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QzNErwA6wVTLaUTNMARPQt
* Add setTracks instance method
setTracksToPlay always starts playing the first track, so embedders who want
to swap the source without autoplaying had to dispose() and create a new
instance. setTracks replaces the playlist, stops any current playback, and
loads the first track (like initialTracks) without playing it.
* Add changelog entry and cite version in docs
The feed was its own shuffled list (museum favorites plus random skins).
Now it continues through whatever the grid shows, the museum or a search:
tapping a skin opens the feed on it with the list before and after it,
and going back lands the grid on the skin you were on. Feed URLs are
plain permalinks; the list lives in the tab (listPosition.ts), and a
permalink opened fresh is followed by the museum. /feed redirects to the
museum's first skin.
- Event logging v3: one session per tab visit spanning both views, with
view_switch events and each feed view's list, list index and depth.
Older events are still accepted.
- The feed loads readmes and like counts on demand.
- Keyboard paging steps from the skin actually on screen, so it can't get
stuck after a scroll is interrupted.
- Going back to the grid animates the skin into its tile. React's
<ViewTransition> can't animate Back with Next's router, so it's done
by hand.
- Grid tiles and view transition snapshots render pixelated.
- Download saves the skin under its file name, not its md5.
- A feed URL for a missing skin returns a 404 status.
- The list position is kept apart from the server actions, so pages with
only the menu bar don't each compile another copy of the server code;
this kept the build within the server's memory.
- Declare reselect (used by legacy-client) and give knexfile.ts named
exports.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QzNErwA6wVTLaUTNMARPQt
* Add onMediaError instance method
When a track fails to load or decode, Webamp logs the error and continues as
if the track had ended, so embedders could not tell an unplayable file from
the end of a track.
ElementSource now emits an "error" event with the MediaError code, which
Media re-emits and the media middleware dispatches as MEDIA_ERROR. The new
webamp.onMediaError(cb) subscribes to it. The existing behavior of treating
the error as the end of the track is unchanged.
* Fix lint and type errors in onMediaError test
* Add changelog entry and cite version in docs
* Expose toggle state of toggle buttons with aria-pressed
Shuffle, Repeat, the EQ and PL window toggles, and the equalizer's On and Auto
buttons show their state visually via the skin's selected sprite. Mirror that
state with aria-pressed so screen readers announce it.
Originally #1367, which was merged into its stacked base branch instead of
master.
* Add changelog entry for aria-pressed
npm install -g npm@latest now installs npm 12, which requires Node 22+, so the
release job fails on Node 20 before publishing anything. Every push to master
since npm 12's release has failed to publish webamp@next.
npm 11 supports Node 20 and includes trusted publishing (11.5.1+).
Each page drew its own fade over the bottom of its readme, so it scrolled
along with the page. One fixed fade now sits above the menu bar; the expanded
readme keeps its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>