gui: native-backend session work + fix shim crashes; add page smoke test

Bundles the pending native-UI session (dark shell, style overhaul, audio
page) with review fixes:
- native Row: set_subtitle_lines + set_activatable_widget (store and
  online-accounts pages crashed with AttributeError under TANINUX_UI=native)
- wine page routed through the ui seam (last direct Adw import removed)
- console: delete no-op log_expander/collapsible_log and .clear() call sites
- ui/__init__ docstring now states the actual default backend (native)
- tests/test_pages_native.py builds every registered page under the native
  backend (29 pass; skips headless)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 21:21:52 +02:00
parent e64e3d69fe
commit 30899de5a3
13 changed files with 1058 additions and 94 deletions
+114
View File
@@ -177,3 +177,117 @@ Installed to `~/.local/share/icons/Tanin-*` (user dir, not pacman).
- `camel.toml` `rebuilt_aur` list is stale (missing librewolf-bin, arch-update,
timeshift-autosnap, xdg-terminal-exec).
- Manual icon picker in `desktop.py` still lists all 5 `Tanin-*` themes (minor).
---
## SESSION 2026-06-27 — TANINUX Music scaffold (native TIDAL client, "Weg 3")
New package `src/taninux/music/` — a native Apple-Music-style TIDAL client. Strategy:
**reuse High Tide's UI-free backend, build our own native shell** (no libadwaita fork).
High Tide's shell is built on heavy Adw layout widgets (NavigationView, OverlaySplitView,
MultiLayoutView, Breakpoints, Carousel) that have no plain-GTK4 equivalent and that our
`native.py` does NOT cover — so forking its UI was rejected. Its *backend* (~1650 LOC:
GStreamer player, tidalapi session, token store) is cleanly decoupled and worth reusing.
### Structure
- `backend/player.py` — copied verbatim from High Tide's `lib/player_object.py` (GPL-3.0),
except ONE edit: `from . import discord_rpc, utils``from . import discord_rpc` +
`from . import state as utils`. Player's only coupling to the old god-`utils` was 4 names
(`CACHE_DIR`, `MUSIC_DIR`, `session`, `send_toast`) — all provided by `state.py`.
- `backend/state.py` — slim shim providing those 4 names + `init()`/`set_session()`/`set_toast_sink()`.
- `backend/secret_store.py` — copied from High Tide (LGPL-3.0), schema/key renamed to TANINUX.
- `backend/auth.py` — extracted PKCE flow (was in an Adw.Dialog): `Auth.try_restore()` /
`pkce_login_url()` / `finish_pkce_login()` + `login_async()`.
- `backend/discord_rpc.py` — no-op stub (player calls `set_activity`/`disconnect`).
- `ui/transport.py` — bottom now-playing bar, binds to player GObject signals
(`song-changed`, `update-slider`, `notify::playing`, `can-go-*`). Async cover fetch.
- `ui/library.py` — favorites-tracks list; row-activate → `player.play_this(tracks, i)`.
- `ui/login_dialog.py` — native `ui.Dialog` PKCE login (tidalapi imported lazily inside).
- `window.py` — shell: sidebar (nav) + `Gtk.Stack` content + transport bar. `tn-dark`.
- `app.py` — entry `tmusic`. Lazy-imports player/auth (so app loads without tidalapi → shows
"backend missing" StatusPage). Flow: CSS → state.init → Auth → try_restore? → shell ±login.
- CSS appended to `gui/ui/style_apple.css` (section "TANINUX Music"): `.tn-transport`,
`.tn-content`, `.tn-track-list`, `.tn-music-sidebar`. Reuses existing `.tn-sidebar`.
- Entry point added to `pyproject.toml`: `tmusic = "taninux.music.app:main"`.
### Verified
- All new files `py_compile` clean. Backend `state`/`discord_rpc` import without tidalapi/Adw.
- All UI+app modules import without tidalapi (lazy imports hold).
### NOT verified (needs real env)
- End-to-end playback: `tidalapi` is NOT installed here, and there's no TIDAL account/display
in the sandbox. **To run:** `pacman -S python-tidalapi` (or `pip install tidalapi`), then
`PYTHONPATH=src python -m taninux.music` (or `tmusic` after install). Expect: login dialog →
paste redirect URL → library loads → click track → GStreamer plays → transport updates.
### Next building blocks (all go into the content `Gtk.Stack`)
- Home page (mixes/recommendations carousels) and Search page (currently placeholders).
- The "few missing native layout widgets" if richer nav is wanted: a NavigationStack
(push/pop with back button) and horizontal carousels. v1 deliberately uses a flat `Gtk.Stack`.
- Toast sink: `state.set_toast_sink(...)` is wired but app currently leaves the default (logs).
---
## SESSION 2026-06-27 (cont.) — TANINUX Music FULL build + playback verified
User gave full autonomy ("entwickle die app komplett aus"). The scaffold above is now a
working app. **Playback verified end-to-end** via a headless test (logged-in PKCE session →
favorite track → GStreamer pipeline reached PLAYING, position advanced to 8.2s).
### Login: must be PKCE, not device-flow (IMPORTANT)
- tidalapi has TWO clients. The OAuth **device-flow** (`session.login_oauth()`, the smooth
`link.tidal.com/XXXXX` flow) logs in and can BROWSE, but **cannot stream** — playback hits
`429 Too Many Requests` on `playbackinfopostpaywall`. tidalapi docstring: PKCE "is the only
way" to stream.
- So login uses **PKCE** (`auth.pkce_login_url()` → browser → copy the redirect/"Oops" URL →
`finish_pkce_login`). `LoginDialog` auto-opens the browser, shows a clickable link, AND
watches the clipboard (`Gdk.Clipboard.read_text_async` poll) so pasting is automatic.
- `auth.try_restore()` discards any stored NON-pkce token (`is-pkce` flag in keyring) to force
a PKCE re-login. Tokens persist in libsecret → normally no dialog on subsequent launches.
### Runtime deps installed this session (pacman)
- `python-tidalapi` (extra) — the API/player lib.
- `gst-libav` + `gst-plugins-bad` + `gst-plugins-ugly`**required for audio**. Without them
GStreamer can't decode TIDAL's AAC/DASH streams ("No suitable plugins found", no sound).
Key elements now present: `avdec_aac`, `avdec_flac`, `faad`, `dashdemux2`.
- These are NOT expressible in pyproject; package them as distro deps for the [tanin] repo.
### Architecture (all under src/taninux/music/)
- `backend/api.py` — thin tidalapi data layer (UI-free): `home/explore/for_you_categories`,
`search`, favorites/playlists, `album_tracks`/`playlist_tracks`/`mix_items`/`artist_*`,
item classification (`item_kind/title/subtitle`), `IdCache`. NOTE: `session.home()` returns
400 sometimes → `home_categories()` falls back to legacy endpoint, and HomePage also pulls
`explore()` + `for_you()`.
- `ui/images.py` — async cover loader, disk cache under `state.IMG_DIR`, sets `Gdk.Texture`
on `Gtk.Picture`.
- `ui/nav.py``NavigationStack` (back button + title + content; set_root/push/pop). Replaces
Adw.NavigationView.
- `ui/context.py``MusicContext(session, player, nav, toast)`; `open_item(item)` classifies +
pushes the right detail page; `play/shuffle`. Passed to all pages/widgets (no globals).
- `ui/widgets.py``Card` (cover+title+sub, click→open_item), `Carousel` (titled h-scroll),
`TrackList` (row-activate → play).
- `ui/pages_base.py``LoadablePage` (spinner → thread fetch() → main-thread build()).
- `ui/pages_home.py`, `pages_search.py` (live clipboard-less SearchEntry → debounced search),
`pages_detail.py` (Album/Playlist/Mix share `_TrackDetailPage`; ArtistPage), `pages_collection.py`
(Bibliothek: playlists/albums/artists/tracks), `pages_queue.py`.
- `window.py` — shell: sidebar (Home/Suchen/Bibliothek) → `nav.set_root`; Gtk.Overlay for
toasts; `TransportBar` at bottom. `reload_after_login()`.
- `ui/transport.py` — now-playing bar: cover, prev/play/next, seek, shuffle/repeat/queue, volume.
- `backend/mpris.py` — ported from High Tide, DECOUPLED: `MPRIS(player, app_id, raise_cb, quit_cb)`,
cover path via `state.IMG_DIR`. Verified on bus as `org.mpris.MediaPlayer2.ch.gabrielevarano.Taninux.Music`.
- `app.py` — entry `tmusic`. CSS → state.init → Auth → try_restore? → shell ±PKCE login; wires
toast sink + starts MPRIS (both non-fatal).
- CSS: "TANINUX Music" section in `gui/ui/style_apple.css` (cards/covers/carousel/transport/
toast/nav + title-1/title-3/dim-label/error fallbacks).
### Known minor issues / TODO
- Cosmetic GTK warning "GtkGizmo (slider) reported min width -4" from a `Gtk.Scale` — harmless.
- Per-section nav stacks are not preserved: switching sidebar resets that section to its root
(Apple keeps a stack per tab). Acceptable for v1.
- Queue page is read-only (no reorder/remove). No lyrics page. `discord_rpc` is a no-op stub.
- DONE (2026-06-27 cont.): track→album/artist navigation — TrackRow has a "⋯" MenuButton +
right-click popover (Zum Album / Zum Interpreten / Als Nächstes / Zur Warteschlange); the
transport bar's title→album and artist→artist are clickable (`.tn-link`). Carousel cards
already opened detail pages. Sidebar switched to the Settings `.tn-nav` look (glassy accent
wash + a frost gradient on `window.tn-dark .tn-sidebar`).
- Search is debounced via SearchEntry's `search-changed`; could add a small delay/throttle.