Skip to content

YouTube account settings, faster cold loads, album column - #55

Closed
noFAYZ wants to merge 84 commits into
2latemc:mainfrom
noFAYZ:dev
Closed

YouTube account settings, faster cold loads, album column#55
noFAYZ wants to merge 84 commits into
2latemc:mainfrom
noFAYZ:dev

Conversation

@noFAYZ

@noFAYZ noFAYZ commented Aug 1, 2026

Copy link
Copy Markdown

YouTube account

  • New Settings → Playback → Audio engine toggles: resolve streams as your account, and add plays to YouTube Music history. Both default off.
  • Scrobbling reports watch time on a real clock — YouTube silently ignores a ping that claims a whole track instantly.
  • resolveStreamUrl (playback) and resolveDownloadUrl (downloads) are now separate methods over a shared core. Only playback reads the auth setting, so downloads cannot inherit it.
  • YouTube Music indicator in the header, hideable like the Discord/Last.fm ones. Toggles scrobbling only.

Cold load: ~2.8s → ~1.2s

  • Metadata is fetched alongside the audio instead of before it, so a clicked row no longer waits on a round trip for data already on screen.
  • Media server serves a body while it is still downloading; a 128 KB head chunk means playback starts on the first read. native audio loaded 3449ms → 148ms.
  • Next track is warmed in parallel with the current one, so a fast skip lands warm (~210ms) instead of cold.
  • Playback fetches head + remainder sequentially. Fanning out was drawing 403s from googlevideo and only ever existed to make downloads fast. Ranges retry with backoff before falling back.

Fixes

  • Account selection reverted to default on every launch — restoreSession was clearing the saved channel.
  • Album names were guessed from whatever column was left over, so release years and play counts showed as albums. Only linked album columns count now; cache versions bumped.
  • Scrobbling never fired: the /player request was missing playbackContext, so YouTube returned a stub with no tracking URLs.
  • Connectivity probe fired on every render and pulled 380 KB of HTML each time.

UI

  • Album column in track rows on playlist, history, library and artist pages. Click it to open the album; also in the right-click menu.
  • Library settings split into six labelled cards instead of one flat list.
  • Removed seven always-on backdrop-blur layers sitting behind opaque backgrounds.

Docs updated. 16 Rust tests, 20 checks

noFAYZ and others added 30 commits July 25, 2026 23:36
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>
noFAYZ and others added 19 commits July 30, 2026 10:16
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>
@noFAYZ noFAYZ closed this Aug 1, 2026
@noFAYZ
noFAYZ deleted the dev branch August 1, 2026 09:49
@noFAYZ
noFAYZ restored the dev branch August 1, 2026 09:52
@2latemc

2latemc commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Hey, sorry for my inactivity. Why did you close the PR?

@2latemc

2latemc commented Aug 1, 2026

Copy link
Copy Markdown
Owner

I would love to merge these changes!

@noFAYZ

noFAYZ commented Aug 1, 2026

Copy link
Copy Markdown
Author

:) sorry i mistakemly pushed to parent repo i was pushing to my fork at https://github.com/noFAYZ/zuno

@noFAYZ

noFAYZ commented Aug 1, 2026

Copy link
Copy Markdown
Author

i'd love to but the architecture and the app has been heavily modified

@2latemc

2latemc commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Honestly I might still merge, I have not been working on anything recently!

@noFAYZ

noFAYZ commented Aug 1, 2026

Copy link
Copy Markdown
Author

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

@2latemc

2latemc commented Aug 1, 2026

Copy link
Copy Markdown
Owner

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.

@noFAYZ

noFAYZ commented Aug 1, 2026

Copy link
Copy Markdown
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants