YouTube account settings, faster cold loads, album column - #55
Conversation
Replace all nine settings checkboxes with the beUI Switch and the native <select> with the beUI Select. Both are vendored to src/components/motion with lucide swapped for the project's Solar icon chokepoint, since lucide-react is deliberately not a dependency. Introduce SettingRow / SettingToggle so every row shares one layout instead of repeating the same markup nine times. The wrapper is a div rather than a label because the controls are now buttons (role="switch", role="listbox"), and a button inside a label has its activation swallowed by the label's click forwarding; the association is made with aria-labelledby instead. This also drops a dead empty <span aria-hidden> that each row carried. Version 1.2.81 -> 1.0.0 across package.json, Cargo.toml, tauri.conf.json and both lockfiles: first release under the Zuno name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Point the updater at the Zuno signing key (id 8252494F509AA25C) instead of the inherited upstream one (D300FF458A9F6333). Verified the value matches zuno.key.pub for the generated pair, so releases signed with the private key will actually verify on clients. `tauri signer generate -w ~/.tauri/...` does not expand `~` on Windows: it created a literal `~/` directory in the repo root holding the private key, unignored. It was never committed, and the key has been moved to the real home directory, but the ignore rules now make that class of mistake impossible to commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tauri derives the app-data directory from the bundle identifier, so renaming it points a returning user at an empty folder: every preference, keyboard binding, mini-player position and user-created local playlist would appear to have vanished. migrate_legacy_app_data copies the old directory across on first run. It is a copy rather than a move so an interrupted run cannot destroy the only copy of the user's settings; the old directory is left for them to delete. The presence of settings-v1.json in the new location is the "already migrated" marker, so it is a no-op on every subsequent start. Only the roaming directory is migrated — caches, logs and the webview profile regenerate on their own and would mean copying hundreds of megabytes for nothing. Sign-in is unaffected: credentials live in the OS keyring under a service name already decoupled from the identifier. Verified end to end: settings-v1.json arrived byte-identical (1382 bytes, theme preserved), the old directory survived, and the app restored the signed-in library afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The v1.0.0 build failed on Windows and Linux with "incorrect updater private key password". Both compiled and bundled fine; only the signing step failed, because the password secret did not match the key. macOS passed only because its matrix built `dmg`, which is not an updater target, so signing never ran there at all. Regenerate the key without a password (verified locally: it signs with an empty password, which is exactly what CI supplies) and point the config at the new public key, id 32044213B9118FCC. The password secret is deleted, so the workflow's reference resolves to an empty string. Also build the `app` bundle on both macOS targets. Tauri's macOS updater needs it to emit the .app.tar.gz that gets signed — with `dmg` alone the job reported success while silently producing no updater artifacts, meaning Mac users would never have received an update. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three pages had drifted into three different rows: only the artist page showed artwork, only the playlist page showed what was playing, and each styled its title differently. Extract TrackRow so the now-playing treatment is defined once and cannot fall out of sync again, and give album and artist pages the indicator they never had. The row is memoised deliberately. Each list subscribes to player state to mark the current track, so every track change re-renders the list; without memo a 500-row playlist would rebuild every row to repaint two of them. useNowPlaying narrows that subscription to two primitives, which keeps the comparison cheap and meaningful. Playback position stays out of these components entirely — it lives in SeekBar's local state. Album rows opt out of artwork: every row would repeat the cover already in the header, which is noise rather than information. Also removes three copies of a stagger map that fed a --track-enter-delay custom property nothing consumed, plus the state and helpers feeding it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three pages hand-rolled the same artwork + title + shuffle arrangement, which is how they ended up at different sizes with different affordances. MediaHeader replaces all three. Two design changes beyond unification: - Play is now the primary action. These pages only offered Shuffle, so the obvious intent — play this, in order — had no button at all. - The header is tinted by its own artwork: the already-loaded image, blown up and blurred behind the text under a gradient mask. No palette extraction and no second request, and each collection carries its own colour. Headers also gained a metadata line (song count, and total duration where the source provides one). Paginated playlists render "120+ songs" rather than implying the loaded page is the whole thing. The artist page keeps its specifics through slots: circular artwork with its own retry chain, a title that copies the artist URL, and the Subscribe control, all passed in rather than special-cased inside the header. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wash previously lived inside MediaHeader, so it could only ever cover the header: the page renders inside a scroll container that clips at its own top edge, well below the search bar. Move it to Layout, which sits above that container. The page publishes its artwork through a small store rather than drilling it up through Layout's props — the same useSyncExternalStore pattern the player and settings stores use — and clears it on unmount so the tint leaves with the page. Ordering is done with DOM position, not z-index: the wash is the first positioned child and everything after it is positioned too, so the search bar and page paint above it without a stacking context that would trap the blur. Vibrancy raised substantially: opacity 25% -> 70%, saturation 1.5 -> 2.2, and a wider blur over an oversized source so the colour reads as a gradient rather than a blurred thumbnail. It fades out through a mask before reaching the track list. Album rows show artwork again, reverting an earlier call that the repeated cover was redundant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header Play button was write-only: it started playback and then kept saying "Play" no matter what. It now tracks whether the track being played belongs to *this* collection, and becomes a Pause control only then — playing something else elsewhere leaves it reading "Play", which is what pressing it would do. Pressing it while this collection is already loaded resumes rather than restarts, so pausing a playlist and pressing Play does not jump back to track one. Membership is a memoised Set rather than a scan, since these lists run to several hundred rows. The button also holds a minimum width and shows a spinner while the collection is starting, so the label swapping between Play, Pause and Loading does not resize it mid-interaction. Loading indicators inside buttons are now SpinnerSteps: the header, the like button, the mini player's transport and the artist Subscribe button. All pass color="currentColor" — SpinnerSteps defaults to #fff, which would be invisible on the light theme and on the mini player's white transport button. Toast and page-level spinners stay on Loader; those are status surfaces, not buttons. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sidebar now opens as a 72px icon rail and stays there. That width sits below the Sidebar's own text-hide threshold, so every row renders as artwork alone and the existing collapsed-layout branches drive the whole thing — no new layout mode was introduced. Rows explain themselves on hover through the beUI Tooltip rather than a native `title`: it appears immediately instead of after the OS delay, is styled with the rest of the app, and has room for the owner on a second line. The wrapper returns its child untouched when the rail is wide enough to show text, so it costs nothing if the width is ever restored. The resize grip is removed, along with the drag state and the document-level mousemove/mouseup listeners it needed — `noUnusedLocals` does not allow keeping them once nothing renders the grip. What survives is everything that matters for reversing this: the `width` prop, `onWidthChange` on the props interface, and every collapsed-vs-expanded branch in the markup. Restoring a resizable sidebar means re-adding the grip and its handler, not rebuilding the layout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
YouTube Music returns no artwork for Liked Songs, so three separate surfaces each drew their own heart glyph on a tinted square — the sidebar album rail, the sidebar playlist rail, and the playlist header, at two different sizes and two different icon weights. They now share assets/img/liked.jpg through one module, so the cover cannot drift between surfaces and Vite fingerprints it like any other asset. The "is this Liked Songs" test moves there too: it was duplicated inline at four call sites, each spelling out the kind/id check by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tabs get the same hover labels the rows have, so the collapsed rail explains every control rather than only its list items. A "New playlist" button sits under the tabs and opens a popover to the right with a name field. Local playlists could previously only be created from Settings, which is a strange place to look for "new playlist". Two fixes this needed: - The popover only supported top/bottom. `side` now accepts left/right too: the offset axis and the align axis simply swap, and the goo layer is two rounded rects in a shared box, so the neck stretches sideways unchanged. - The popover root is `inline-flex`, so it shrink-wrapped the trigger and the button rendered as a small circle instead of spanning the rail. The caller now overrides the display, and a z-index lifts the isolated popover above the scrolling list that follows it. Separately, the header meta showed "98 songs · 3 min": YouTube omits durationSec on most playlist entries, so summing whatever was present gave badly wrong totals. The duration is now shown only when every track in a fully-loaded list reported one. A missing total is honest; a wrong one is not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel never appeared. The goo Popover positions itself inside the trigger's own box, which cannot work here: the rail is 72px wide and sits inside two nested overflow-hidden shells — the window root that cuts the rounded corners, and the layout row — with the content column as a later sibling painting its own opaque background over anything that escapes. The earlier z-index was a guess that did not address any of that. RailPopover portals the panel to document.body and positions it fixed from the trigger's measured rect, so no ancestor can clip or cover it by construction. Position is recomputed on scroll and resize, with capture so the app's inner scrollers are included, since fixed coordinates are otherwise frozen at paint time. The first attempt still mispositioned to the window corner: the ref wrapper used `display: contents`, which generates no box, so getBoundingClientRect returned zeros. It is a real flex box now. The vendored popover is reverted to upstream — its left/right support was added for this and is no longer used, so it is not worth carrying untested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar carried a permanently visible 96px slider for a control most people touch rarely. It collapses to the icon, which returns that width to the track title: click toggles mute, the wheel still nudges the level, and hovering opens a panel above with the beUI RangeSlider and a percentage readout. The icon already reflected muted/quiet/loud and continues to. RailPopover becomes FloatingPanel, since the same problem applies in the player bar: it sits inside the window's overflow-hidden root, so a panel positioned within it would be clipped. It gained a `side` of "top", hover opening with a delay, and a grace period on leave — without that the panel would close as the pointer crossed the gap and the slider would be unreachable. It also gained a ResizeObserver on the panel. Position was computed once, in the same commit the panel mounts, so its height was still zero; a top-side panel was therefore placed as though it had none and landed under the window edge. Re-placing once it has real dimensions fixes that, and keeps it correct when the content resizes. Drops ~140 lines of hand-rolled pointer, drag and rAF-easing code that the RangeSlider provides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Features - Minimize to tray: closing the window keeps Zuno running and playing. - Switch between the YouTube channels on the signed-in account, from the toolbar avatar or Settings. - Queue panel rebuilt: collapses to an artwork rail, end-queue-here, generate-queue-from-here, shuffle and clear. - Play in loop on album and playlist pages, backed by a real repeat-all mode. - Add a song or a whole album/playlist to the queue, plus hover actions on every track row. Fixes - Account name and photo were always "YouTube Music": the channel list was requested on the music client (which the WEB-only endpoint answers with a stub), WEB requests went out unsigned, and the profile that did come back was overwritten by the placeholder during dedupe. - Settings rows and card headers now space label and control apart. - Toolbar tooltips no longer render outside the window and get clipped. CI - Push a v* tag to start a release; release:published never fired for v1.0.0 or v1.0.1 and both had to be dispatched by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fixes - Downloads work again. googlevideo serves only the first 1 MiB of a URL minted by an unattested session and refuses the rest with a bare 403, so every download past the opening megabyte failed. Sessions are now attested with a proof-of-origin token bound to the video id, which is the binding the CDN actually accepts. - Playlist actions cover the whole playlist. Download, add-to-queue and add-to-playlist previously acted only on the rows that had scrolled into view, so "download this playlist" quietly fetched the visible hundred and looked finished. - Artwork no longer blinks to the placeholder while dragging a song. Reordering changed every row's key, remounting the rows and restarting each image from scratch. - Thumbnails are cached properly: one request per cover instead of one per row, failures are not retried forever, resolutions survive a restart, and a cover that stops working is re-resolved instead of staying broken. - The onboarding tour no longer blocks the control it asks you to click, and its card is positioned next to what it highlights rather than in the corner. Features - Like and dislike on every song row, in the player and in the right-click menu. Dislike is new; it was not previously possible anywhere in the app. - Download from the right-click menu, with cancel and remove. - Skip any onboarding step, or the whole tour, with Back and progress. - Discord presence and Last.fm scrobbling toggle from the toolbar, and Discord now has a Settings entry. Turning Discord off clears the presence rather than leaving your last track pinned to your profile. - Download button on album and playlist headers, showing how many songs it would fetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reference dumps and working images that accumulate in docs/ are local working files, not part of the project — they now stay out of git. The committed documentation and the README's screenshots are deliberately still tracked, because the README links to all of them and ignoring the folder wholesale would break ten references on GitHub. The feature table had fallen behind several releases: offline downloads, ratings, batch selection, queue control, tray and auto-updates all shipped without being listed, while the cylinder carousel was given a row of its own despite being a detail of the home page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Shuffle and repeat are now independent: shuffle toggles on its own, repeat cycles in-order -> repeat-all -> repeat-one. Sessions saved with the old combined mode still restore correctly. - Sidebar display mode setting: expand on hover (default), always collapsed, or always expanded. - Discord Rich Presence and playback fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KeyValueMap serialization panics on an empty sequence or map entry. Lockfile-only
bump, within the existing semver range.
The other two Dependabot alerts are pinned upstream and left alone:
- esbuild (low): vite 7.3.5 requires ^0.27.0, so the patched 0.28.1 is out of
range. Dev-server-only, Windows-only file read — not worth an override that
forces an untested esbuild under vite.
- glib 0.18.5 (moderate): held by tauri's gtk/webkit stack. Linux-only, and it
moves when tauri moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chore: track the main branch ruleset in the repo
Vite 7.3.6 widened its esbuild range to `^0.27.0 || ^0.28.0`, but npm keeps a satisfying version once resolved, so the tree stayed on 0.27.7 and the advisory with it. Lockfile only — `vite: ^7.0.4` already allowed this. The bug lets any website read arbitrary files through the dev server on Windows. Dev-only and never shipped, but the dev server is exactly what runs while `npm run tauri dev` is open. landing/ was already on 0.28.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `rust` job runs on Windows, chosen because building Tauri on Ubuntu needs webkit2gtk and friends installed first. The cost of that choice was invisible: every line behind `#[cfg(target_os = linux)]` was never handed to a compiler, so a pull request adding a whole MPRIS backend can go green without the code having been built once. `cargo check`, not `cargo test` — the tests are platform-independent and the Windows job already runs them. The compiler is what was missing here, not the assertions. `--locked` catches the other quiet failure, a Cargo.toml change whose Cargo.lock was not committed with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Linux notes in the README already tell people to install the `.deb` or `.rpm`. Only the first of those was ever built, so half that sentence was fiction and Fedora and openSUSE users were left with the AppImage. Tauri 2 bundles rpm in pure Rust, so the runner needs no rpmbuild and the build args are the whole change on the CI side. The dependency list is not: the deb bundler derives Depends from the linked libraries, while the rpm bundler ships whatever Requires it is given, so the Fedora package names are spelled out here. GStreamer is recommends rather than depends, matching how the AUR package treats codecs as optional. The updater is untouched. It only consumes the AppImage on Linux, so latest.json is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Puts the mark above the wordmark in the hero, swaps the plain "Windows - macOS - Linux" line for glass badges carrying the same brand marks as the download tiles, and ports the app mini player to the page as a draggable capsule driving an <audio> element. The Apple glyph shipped without a fill, so SVG resolved it to black and every text-foreground on it was dead; it now takes currentColor. Download buttons wear the app primary instead of white-on-black, and the demo video gets a flat tint to settle it into the page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…atar The capsule was pointing at the demo video as a stand-in source. It now plays Low Tide (Tom Rhodes) from public/, and the disc is the Zuno character loop rather than the app icon. Autoplay is attempted on mount and falls back to the visitor's first click or keypress, because every engine refuses audible playback until the page has been interacted with — the rejection is a promise, not an error, so it has to be caught rather than checked for. The record spin is left off the avatar: the clip already animates, and rotating it as well reads as a glitch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Hey, sorry for my inactivity. Why did you close the PR? |
|
I would love to merge these changes! |
|
:) sorry i mistakemly pushed to parent repo i was pushing to my fork at https://github.com/noFAYZ/zuno |
|
i'd love to but the architecture and the app has been heavily modified |
|
Honestly I might still merge, I have not been working on anything recently! |
|
sure but that'll completely change the app idk if that's the direction you want to go..might want to look at the repo |
|
Hey thats super cool, I just had a look at the client. I thought you just made some changes but I didn't know you where making an entirely new one, super nice to see though I kind of stopped worked on the client but would love to pick the project up again some day, can I take some of your code for some playback backend features? as that has been something I have been struggeling with. |
|
Hey sure why not and thankyou I liked your app which is why i built upon it especialy the tabs. Would love to send some PR if you want to port any feature |
YouTube account
resolveStreamUrl(playback) andresolveDownloadUrl(downloads) are now separate methods over a shared core. Only playback reads the auth setting, so downloads cannot inherit it.Cold load: ~2.8s → ~1.2s
native audio loaded3449ms → 148ms.Fixes
restoreSessionwas clearing the saved channel./playerrequest was missingplaybackContext, so YouTube returned a stub with no tracking URLs.UI
backdrop-blurlayers sitting behind opaque backgrounds.Docs updated. 16 Rust tests, 20 checks