a2d17d693c6057b1e8dc7c42c0bfcbcfa02ce7cb
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
92c305bab9 |
fix(nav): a navigation closes every overlay before it pushes
The overlay history stack let a navigation land ABOVE open overlays and "buried" their entries with a counter. Measured in Chrome: Shift+W over About navigated under the still-open dialog, the next Back both undid the navigation and closed About, and the Back after that did nothing; Shift+W on Home with the player open did nothing at all. The counter could also be spent by a different overlay closing first, and callers that could not report whether they pushed were assumed to have. setPage and openChannel now close every open overlay and push only once the history is back in sync (one entry per live overlay, no programmatic back() in flight, no reconcile queued), so nothing is ever buried and the whole mechanism goes: navigateOverOverlays, closeThenNavigate, buryUnder, setPage's boolean, and the three test files that pinned them. The palette closes only itself before running a row, so a theme picked over the player keeps the player. Shift+W on Home with the player open now closes it (owner decision 2026-09-09). The wait is on state, not on "a popstate": the old 250 ms fallback could run with back()s still in flight. An overlay that refuses to close within 1.5 s drops the command instead of being navigated out from under. |
||
|
|
545996f807 |
fix(ui): a toast host ranks by when its overlay opened, and an overlay keeps its Escape, its dialog and its navigation straight
Deep-review round 4, the overlays. - Toast hosts were ranked by mount time, and PlayerModal swaps its host on every fullscreen change: leaving true fullscreen with the download dialog open put the player's host back on top, so toasts were drawn under that dialog's scrim and a click on one closed the dialog. Each overlay takes a rank once, when it opens (`useOverlayOrder`), and hands it to every host it renders. - A dialog open while fullscreen is left keeps a stable portal container (moved, not re-created): React tore the portal down when `fsRoot` changed, resetting a half-filled form and dropping focus to <body>. Focus is put back after the move. - The onboarding wizard is a layer, not a raw document listener: one Escape used to dismiss the error dialog over it AND end onboarding, skipping the write-scope step. - `navigateTo` (a toast's "View", clicked inside the player's own host) goes over the overlays it unmounts, so the player's Back-to-close cleanup no longer pops the page it just opened — the same fix Shift+W already had. The access-requests toast action too. - The live region arms itself a task after it takes over inherited toasts: React flushes effects in the same task as that render, so the "silent inheritance" arming was a no-op. - The overlay-host guard matches class tokens in any order (PlexPlayer's root was never checked), globs `ui/` too (ui/Modal was not checked either), and requires the rank. |
||
|
|
f76a2afbf1 |
feat(ui): the four small gaps — player delete, repeat-move, a real channel link, YT search sorting
Items 2, 4, 5 and 7 of the bug hunt, one round. They are independent, but they are all the same kind of thing: an action that exists somewhere else in the app and is missing where you actually want it. **5 — the channel name in the YT player is a link again.** It was a `<button>`, so middle-click and Ctrl/Cmd-click did nothing on a thing that looks exactly like a link (the YouTube icon beside it IS an anchor and opens a tab). It is now a real `<a>` whose href is the same `?chan=<id>` URL `openChannel` pushes, with a plain left click still taking the in-app path — that path does more than navigate, since the player must close and reclaim its own Back-to-close history entry first. The "is this click ours or the browser's" test is now ONE predicate (`isPlainLeftClick`), which VideoCard's card link also uses, so a second such link cannot forget the middle-click arm. **2 — "Remove" from inside the player.** The parent owns the removal (the SAME `removeItem` the row menu and the inline ✕ use: one confirm, one optimistic update, one undo-stack rebase), and the player owns what happens next, because it is the only side that knows which item is playing — its launch queue is frozen at mount. It drops the item, plays what followed, and closes when the list runs out. That forced a real fix in Playlists: the player's mount was derived from `shown` every render, so anything that dropped the launch row yanked the player off the screen mid-playback. That was a latent edge (change the view and it happens) and the normal case for this feature. The parent now snapshots what it launched with, and only onClose takes the player down. **4 — "Move to <the last list you moved to>".** The submenu is two levels deep and moving a run of videos into the same list is the shape this gets used in. The remembered id is a per-account pref; the row is derived from `moveTargets`, which already excludes the list you are looking at, so it cannot offer a move to where the video already is. Remembered only on a SUCCESSFUL move — a shortcut to a move that failed is a shortcut to failing again. Absent, not disabled, when there is nothing to repeat: there is nothing to explain. **7 — the live-YouTube results can be sorted.** Client-side, over the page already loaded. Not a new `order=` param, and this is the decision worth recording: the owner's case is "even one page of results", i.e. reorder what is on screen; a re-query would REPLACE the results, which is not what "sort these" means; and the zero-quota scrape source has no order parameter at all, so a server-side control would silently do nothing (or quietly cost 100 quota units) depending on which source served the page. Only keys a search hit carries are offered — `subscribers`/`priority` describe a subscription, which a search hit is not, and `shuffle` on a ranked list is noise. Its own pref, so sorting a search page never re-sorts the library behind it. Verified in the browser on localdev against the real account: the channel name renders as `<a href="…&chan=…">` and a ctrl-click / middle-click is left to the browser while a plain click still opens the channel in-app; removing from the player stepped 1/3 → 1/2 onto the next video and closed on the last one; the row menu grew "Move to “…”" only after a move and only on a list that is not the target; and the results sort reordered by views 41.7M → 29.8M → 24.2M → 15.8M. (The pane's own ctrl-click arrives without the modifier — a harness limit, so the modifier cases were driven as real MouseEvents.) Scratch playlists and the touched prefs were cleaned up afterwards. 22 vitest, mutation-checked against 14 mutations (no href at all · href without the rest of the query · every click hijacked in-app · the middle-click arm dropped from the predicate · plain click no longer in-app · always jumping to the top of the list · the removed item left in the queue · no clamp when the last item goes · an empty queue not reported as "nothing left" · sort-then-reverse flipping ties · unknown values first · titles compared numerically · shuffle offered on search results · direction ignored) — each red on exactly the test that names it. Item 4 has no test of its own: its rule is enforced by construction, not by a check. |
||
|
|
d39fccd399 |
feat(downloads): M5 — Download Center frontend
- api.ts: download types + methods (profiles, enqueue, list/manage, file url, share, usage, index, admin storage/quota) - format.ts: formatBytes / formatSpeed - i18n: en/hu/de downloads.json (trilingual) - Downloads nav module (Download icon, active-count badge, hidden for demo) placed after Messages; urlState page + App render + Header title - DownloadButton: self-contained per-card/player affordance (demo-hidden, reflects the per-user download index: downloaded / in-queue), opens the preset dialog - DownloadDialog: preset picker + optional display name -> enqueue + 'View' toast - ProfileEditor: manage built-in + custom formats (full spec form incl. SponsorBlock) - DownloadCenter: Queue / Library / Shared / (admin) System tabs, live progress via useLiveQuery, usage bar, pause/resume/cancel/delete, save-to-device, rename, share, admin storage dashboard + per-user quota editor - wired DownloadButton into VideoCard + PlayerModal - lib/nav.ts: decoupled navigator so the download toast can jump to the page tsc + vite build clean. Verified in a headless authed browser: page + tabs render, Library shows a real download with usage bar + actions, no console errors. |