Przeglądaj źródła

feat: use stacked Jellyfin album layout at every width

Drops the wide three-column album body. The cover and control column now sit above a full-width track list at every resolution, with the cover scaling from 200 to 360 px. Includes the album year header from the previous commit. Bumps to 1.2.6+26 with release APK and notes.
Krasimir Kazakov 4 dni temu
rodzic
commit
a87c23007e

+ 2 - 2
AGENTS.md

@@ -107,7 +107,7 @@ notifyListeners();
 - Jellyfin 12.2 API contract: `/Items?userId=...` (there is no `/Users/{userId}/Items` path); `/Genres` requires `parentId` of a `music` view from `/UserViews`; `genres` is pipe-delimited and OR-ed; `/Items/Latest` returns a bare array; auth is the `Authorization: MediaBrowser Client=..., Device=..., DeviceId=..., Version=..., Token=...` header (`X-Emby-Token` and `api_key` were rejected on 12.2). Album `UserData` is not updated by track playback, so "recently played" is derived from played Audio tracks sorted by `DatePlayed`, deduped by `AlbumId`, and capped at 6. Stream URLs use `/Audio/{id}/universal`; the native data source adds the auth header and the token never goes in a URL or log. Favorites are `GET /Items?userId=...&filters=IsFavorite&includeItemTypes=MusicAlbum&recursive=true&enableUserData=true`; toggle with `POST`/`DELETE /UserFavoriteItems/{itemId}?userId=...` (not `/Users/{userId}/FavoriteItems`). The album's `UserData.IsFavorite` is parsed and re-checked on the album page; the Favorites tab refreshes on selection and after an album page closes.
 - Jellyfin playback is Media3 1.11.1 in `JellyfinPlaybackService` (foreground `mediaPlayback`). `JellyfinPlaybackController` sends the queue, polls `getState` every ~700 ms, and keeps the Home card alive; the service owns the `MediaSession`, audio focus and `JellyfinPlaybackReporter`. Focus loss pauses and keeps position, a transient loss resumes on gain, a permanent loss abandons focus and does not resume. The silent `jellyfin_playback` notification needs `POST_NOTIFICATIONS` on Android 13+; the Settings card's **Allow** row requests it, and playback still works when denied. The service keeps playing while CarDash is backgrounded. Progressive/transcoded streams often report no Media3 duration; the bridge falls back to the queued MediaItem duration and `JellyfinPlaybackController.mediaPlayback` falls back to the current queue item so progress and seeking have a denominator.
 - Jellyfin streaming quality is persisted as `jellyfin.audioQuality` (`JellyfinAudioQuality`). Direct (default) omits `MaxStreamingBitrate` and advertises the source codecs; 320/256/192/128 kbps set `Container=mp3`, `AudioCodec=mp3`, `TranscodingContainer=mp3` and `MaxStreamingBitrate`, asking Jellyfin to transcode. `JellyfinLibraryService` reads `audioQualityProvider` when a queue is built, so a change applies to the next album. The dropdown ships in 1.2.2.
-- Dashboard layout: the controller asks for 6 albums per row; the screen shows 6 when each of six cards is at least 160 px wide, otherwise 5. Album names/artists share the album art's left edge, the album page cover is top-aligned with the track list/controls, and selecting a Random album re-rolls the Random row after playback starts. The **Favorites** tab sits between Albums and Artists (Artists stays disabled), uses the same 5/6 columns in a scrollable multi-row grid, and shows `No favorites` centered when empty. The album-page heart toggles the server favorite; the Favorites tab reloads after the album page closes. These favorites/Home-shortcut changes ship in 1.2.2. The Home equalizer is exclusive: Jellyfin playing → Jellyfin tile only; another app playing while Jellyfin is paused → Music tile only. The Home `VolumeCard` prefers the Jellyfin state while it is playing, or while it has a queue and no other app is playing; otherwise it uses the system session/media keys.
+- Dashboard layout: the controller asks for 6 albums per row; the screen shows 6 when each of six cards is at least 160 px wide, otherwise 5. Album names/artists share the album art's left edge, and selecting a Random album re-rolls the Random row after playback starts. The **Favorites** tab sits between Albums and Artists (Artists stays disabled), uses the same 5/6 columns in a scrollable multi-row grid, and shows `No favorites` centered when empty. The album-page heart toggles the server favorite; the Favorites tab reloads after the album page closes. These favorites/Home-shortcut changes ship in 1.2.2. The Home equalizer is exclusive: Jellyfin playing → Jellyfin tile only; another app playing while Jellyfin is paused → Music tile only. The Home `VolumeCard` prefers the Jellyfin state while it is playing, or while it has a queue and no other app is playing; otherwise it uses the system session/media keys.
 - Updates use a non-blocking public Gogs release check. The Update action downloads the APK to cache and opens Android’s package installer; never install silently. REQUEST_INSTALL_PACKAGES + one-time unknown-apps permission is required. Show the yellow dot on the Settings gear whenever `UpdateController.updateAvailable` is true.
 - ADB installs: always try at most twice per connected device. If both attempts fail, stop silently rather than looping or switching install methods.
 - OBD is Bluetooth Classic SPP, bonded-only and read-only, and still needs a real-car test; weather uses Open-Meteo and one-shot location and stores no coordinates.
@@ -116,7 +116,7 @@ notifyListeners();
 - Connection attempts are bounded (three failures, 12 s connect watchdog). After they are exhausted the card shows **Retry connection**; do not restore an infinite reconnect loop.
 - When Bluetooth is off, the OBD-II card shows **Open Bluetooth settings**. On resume the controller checks adapter power state and reuses the cached bonded list; it only lists devices when the list is unknown, after **Find**, or when the card opens.
 - Pairing happens in Android settings, never in CarDash.
-- On Home, the circular ring is song progress; its centre is play/pause and the Mute pill is separate. Jellyfin playback title/progress comes from `JellyfinPlaybackController`, so it works without notification access; other apps still need the user-granted notification access, otherwise the card falls back to Playing / Nothing playing. The album page no longer uses `VolumeCard`: it has one seekable progress slider with elapsed/total time, a centered previous / play-pause / next row, and a compact volume card (−, Mute/Unmute, +) beside the now-playing block. The Home card keeps the ring. The progress/transport card is bottom-aligned with the album cover in both the wide and narrow layouts; the transport buttons are 80/62 px and the compact volume steps are 40 px. The album header appends the Jellyfin `ProductionYear` in parentheses to the name when present, e.g. `Ascension (2025)`, and does not duplicate a year already in the name. The album slider seeks on release through `JellyfinPlaybackController.seek`; a local drag value keeps the thumb stable until the next poll.
+- On Home, the circular ring is song progress; its centre is play/pause and the Mute pill is separate. Jellyfin playback title/progress comes from `JellyfinPlaybackController`, so it works without notification access; other apps still need the user-granted notification access, otherwise the card falls back to Playing / Nothing playing. The album page no longer uses `VolumeCard`: it has one seekable progress slider with elapsed/total time, a centered previous / play-pause / next row, and a compact volume card (−, Mute/Unmute, +) beside the now-playing block. The Home card keeps the ring. The album page uses the stacked layout at every width: cover and control column on top, the track list below. The cover is 30% of the body width clamped to 200–360 px; the wide three-column layout was removed. The progress/transport card is bottom-aligned with the album cover; the transport buttons are 80/62 px and the compact volume steps are 40 px. The album header appends the Jellyfin `ProductionYear` in parentheses to the name when present, e.g. `Ascension (2025)`, and does not duplicate a year already in the name. The album slider seeks on release through `JellyfinPlaybackController.seek`; a local drag value keeps the thumb stable until the next poll.
 - Reinstalling the app can clear its HOME role; re-run `tool/install.sh` or use Settings > Home app.
 - CarDash can also run as a normal app without the HOME role. The launcher activity intentionally has no `excludeFromRecents`, so it appears in recents and can be switched back to. `resumeWhilePausing` and `alwaysRetainTaskState` keep the Home transition and task state sane. Android may still kill any background app: there is deliberately no keep-alive foreground service, `persistent` flag or battery-exemption request. On vendor firmware, tell the user to set CarDash to no battery restrictions / allow autostart. When CarDash holds the HOME role, Android may still filter the home task out of recents; use Home to return. Do not re-add `excludeFromRecents` without asking, and never add a launcher keep-alive FGS.
 

+ 22 - 19
README.md

@@ -101,9 +101,9 @@ live vehicle data.
   button in Settings → General → Home buttons. The tile opens a full-screen
   dashboard with **Recently played**, **Recently added** and **Random** album
   rows (six cards when they fit, five otherwise), grouped genre selection and a
-  full album page with track list, a seekable song-progress slider, large
-  previous / play-pause / next transport, and a compact volume card. The
-  navigation card is bottom-aligned with the album art.
+  full album page with the cover and controls above a track list, a seekable
+  song-progress slider, large previous / play-pause / next transport, and a
+  compact volume card. The navigation card is bottom-aligned with the album art.
   Playback runs in CarDash through Media3, keeps playing in the background, shows
   a media notification when allowed, pauses on audio-focus loss and reports to
   Jellyfin. Selecting a Random album re-rolls the row. The home equalizer is
@@ -330,11 +330,12 @@ the app resumes.
   shows the server-side favorites in a scrollable grid, six columns when they fit
   and five otherwise; an empty list shows **No favorites**. Artists is shown but
   disabled for a later release.
-- **Album page**: large cover and track list, plus a control card with one
-  seekable song-progress slider, elapsed / total time, and large previous /
-  play-pause / next buttons below it. The card is bottom-aligned with the album
-  cover. A compact volume card (−, Mute / Unmute, +) sits to the right of the
-  now-playing block. The heart adds or removes the album through the Jellyfin
+- **Album page**: the stacked layout is used at every screen size — cover and
+  controls on top, track list below. The cover grows on larger screens. The
+  control card has one seekable song-progress slider, elapsed / total time, and
+  large previous / play-pause / next buttons; its bottom is aligned with the
+  album cover. A compact volume card (−, Mute / Unmute, +) sits to the right of
+  the now-playing block. The heart adds or removes the album through the Jellyfin
   API; the Favorites tab refreshes when you return. The album heading includes
   the release year in parentheses, for example `Ascension (2025)`, when Jellyfin
   provides it.
@@ -443,8 +444,9 @@ speed fallback and its permission prompt, speed/temperature source persistence,
 weather fallback, the update version/availability logic and the Settings update
 dot. Jellyfin coverage includes genre grouping, recent-played dedupe, the
 12.2 request contract, the dashboard controller and widget states, the genre
-picker and five-family cap, album page/track list/transport, the seek slider and
-compact album volume card, playback queue and state mapping, the settings
+picker and five-family cap, album page/track list/transport, the stacked album
+layout, the seek slider and compact album volume card, playback queue and state
+mapping, the settings
 gating, the Home tile width clipping, the equalizer's stale-session guard, the
 streaming-quality persistence, the direct/MP3 transcode URL parameters, Jellyfin
 favorite toggling, the Favorites tab/grid/empty state, the playing-album Home
@@ -458,15 +460,16 @@ real 12.2 server (Quick Connect, grouped genres, 6/5-card dashboard, album page,
 background playback, notification, media keys, exclusive equalizers, audio-focus
 handover, Random re-shuffle) and against a local 12.2 container. The OBD hardware
 path compiles and is covered by the parser/economy design, but no ELM327 adapter
-is available on this machine; socket behavior still needs a car test. The 1.2.5
-universal APK is the current published release. It bottom-aligns the Jellyfin
-progress/transport card with the album cover, enlarges the transport and compact
-volume buttons, and includes the 1.2.4 album redesign, the 1.2.3
-recents/normal-app work, the FYT/Syu MCU volume bridge, the no-snap-back volume
-handling, the streaming-quality setting, Favorites, the playing-album Home
-shortcut and the Jellyfin progress-duration fallback. The emulator has verified
-the in-app update path, and the MCU volume path is verified on the car: the
-on-screen slider and the car's own volume control now agree.
+is available on this machine; socket behavior still needs a car test. The 1.2.6
+universal APK is the current published release. It uses the stacked album
+layout at every resolution (cover and controls above the track list), shows the
+album year, and includes the 1.2.5 bottom alignment and larger controls, the
+1.2.4 album redesign, the 1.2.3 recents/normal-app work, the FYT/Syu MCU volume
+bridge, the no-snap-back volume handling, the streaming-quality setting,
+Favorites, the playing-album Home shortcut and the Jellyfin progress-duration
+fallback. The emulator has verified the in-app update path, and the MCU volume
+path is verified on the car: the on-screen slider and the car's own volume
+control now agree.
 
 ## Troubleshooting the emulator
 

+ 7 - 4
docs/IMPLEMENTATION_PLAN.md

@@ -331,10 +331,13 @@ Android (Kotlin)
 
 ## 8. Change log
 
-- 2026-11-01 (unreleased): the Jellyfin album header shows the release year in
-  parentheses after the album name, e.g. `Ascension (2025)`, when Jellyfin
-  provides `ProductionYear`. It does not add a duplicate year already present in
-  the name. No version bump, APK or release has been made.
+- 2026-11-01: CarDash 1.2.6+26 uses the stacked Jellyfin album layout at every
+  resolution: cover and controls on top, track list below. The cover is 30% of
+  the body width clamped to 200–360 px, so larger screens get a larger cover; the
+  wide three-column layout is gone. The album header shows the release year in
+  parentheses after the name, e.g. `Ascension (2025)`, without duplicating a year
+  already present. Includes the 1.2.5 bottom alignment and larger controls. 150
+  tests pass; release `releases/cardash-android-1.2.6.apk`.
 - 2026-11-01: CarDash 1.2.5+25 aligns the Jellyfin album progress/transport card
   with the bottom of the album cover in both wide and narrow layouts. The
   previous/play-pause/next buttons grow to 62/80 px and the compact volume steps

+ 1 - 1
lib/core/app_version.dart

@@ -1,2 +1,2 @@
 /// Keep this in sync with the `version` in pubspec.yaml.
-const String appVersion = '1.2.5';
+const String appVersion = '1.2.6';

+ 8 - 27
lib/features/jellyfin/jellyfin_album_screen.dart

@@ -422,27 +422,8 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
       );
     }
 
-    Widget wideBody(BoxConstraints constraints) {
-      final coverWidth = (constraints.maxWidth * 0.30).clamp(280.0, 380.0);
-      final controlsWidth = (constraints.maxWidth * 0.44).clamp(520.0, 600.0);
-      return Row(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          SizedBox(width: coverWidth, child: cover()),
-          const SizedBox(width: 22),
-          Expanded(child: trackList()),
-          const SizedBox(width: 22),
-          SizedBox(
-            width: controlsWidth,
-            height: coverWidth,
-            child: controls(minHeight: coverWidth),
-          ),
-        ],
-      );
-    }
-
-    Widget narrowBody(BoxConstraints constraints) {
-      final coverWidth = (constraints.maxWidth * 0.34).clamp(200.0, 260.0);
+    Widget body(BoxConstraints constraints) {
+      final coverWidth = (constraints.maxWidth * 0.30).clamp(200.0, 360.0);
       return SingleChildScrollView(
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -456,7 +437,11 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
               ],
             ),
             const SizedBox(height: 16),
-            SizedBox(height: 260, child: trackList()),
+            SizedBox(
+              key: const ValueKey('jellyfin-album-track-list'),
+              height: 260,
+              child: trackList(),
+            ),
           ],
         ),
       );
@@ -481,11 +466,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
                 const SizedBox(height: 14),
                 Expanded(
                   child: LayoutBuilder(
-                    builder: (context, constraints) {
-                      return constraints.maxWidth >= 1180
-                          ? wideBody(constraints)
-                          : narrowBody(constraints);
-                    },
+                    builder: (context, constraints) => body(constraints),
                   ),
                 ),
               ],

+ 1 - 1
pubspec.yaml

@@ -1,7 +1,7 @@
 name: cardash_launcher
 description: "CarDash — a calm, configurable Android car dashboard launcher."
 publish_to: "none"
-version: 1.2.5+25
+version: 1.2.6+26
 
 environment:
   sdk: ^3.13.7

+ 39 - 0
releases/1.2.6.md

@@ -0,0 +1,39 @@
+CarDash 1.2.6 makes the Jellyfin album page use the stacked layout on every screen.
+
+- **Package:** `com.wasp.cardash_launcher`
+- **Version:** 1.2.6 (versionCode 26)
+- **ABIs:** arm64-v8a, armeabi-v7a, x86_64 (universal APK)
+- **Signing:** project debug key matching all prior releases
+- **SHA-256:** `d664e9dff1ba499df93e03c0a44fe274b78ac438c665471a781c698c8650100f`
+
+**What changed**
+- The album page now uses the same stacked layout as the smaller tablet at
+  every resolution: cover and controls on top, track list below. The wide
+  three-column layout has been removed.
+- The cover scales with the screen (30% of the body, clamped to 200–360 px), so
+  a 1280-wide dashboard gets a larger cover instead of a small centre column.
+- The progress/transport card remains bottom-aligned with the cover; the track
+  list gets the full width underneath.
+- The album heading now shows the Jellyfin release year in parentheses, for
+  example `Ascension (2025)`; a year already present in the name is not
+  duplicated.
+- Includes the 1.2.5 larger 80/62 px transport buttons and 40 px compact volume
+  controls, the 1.2.4 album controls and all earlier Jellyfin, volume and
+  recents work.
+
+**Verified**
+- `flutter analyze`, formatting and the full 150-test suite.
+- Widget tests assert the track list is below the cover at 1280×800 and
+  1024×600 and that the progress card stays bottom-aligned with the cover.
+- Universal release APK built with JDK 21.
+
+**[Download cardash-android-1.2.6.apk](https://git.krasi.net/kkazakov/car-interface-android/raw/main/releases/cardash-android-1.2.6.apk)**
+
+Devices with the in-app updater can use Settings → Updates to download this APK
+and open Android's package installer. Android requires install confirmation and a
+one-time unknown-apps allowance; no silent install is attempted. A yellow dot
+appears on the Settings gear when this release is available.
+
+This build is debug-signed for sideloading/testing, not Play Store distribution.
+Gogs release attachments are limited to 32 MB, so the universal APK is linked
+directly from the repository.

+ 13 - 0
releases/README.md

@@ -1,5 +1,18 @@
 # CarDash releases
 
+## 1.2.6 — stacked Jellyfin album layout at every resolution
+
+- **File:** `cardash-android-1.2.6.apk`
+- **Version:** 1.2.6 (versionCode 26)
+- **ABIs:** arm64-v8a, armeabi-v7a, x86_64
+- **SHA-256:** `d664e9dff1ba499df93e03c0a44fe274b78ac438c665471a781c698c8650100f`
+- **Signing:** project debug key matching all releases.
+- **Changes:** uses the stacked album layout everywhere — cover and controls on
+  top, track list below — with the cover scaling from 200 to 360 px. Adds the
+  album year in the header. Includes all 1.2.5 bottom alignment, larger controls
+  and earlier Jellyfin work.
+- **Verified:** analyze, formatting, 150 tests and universal release build.
+
 ## 1.2.5 — album card alignment and larger controls
 
 - **File:** `cardash-android-1.2.5.apk`

BIN
releases/cardash-android-1.2.6.apk


+ 1 - 0
releases/cardash-android-1.2.6.apk.sha256

@@ -0,0 +1 @@
+d664e9dff1ba499df93e03c0a44fe274b78ac438c665471a781c698c8650100f  cardash-android-1.2.6.apk

+ 4 - 0
test/jellyfin_album_screen_test.dart

@@ -81,6 +81,10 @@ void main() {
       find.byKey(const ValueKey('jellyfin-album-progress-card')),
     );
     expect(cardRect.bottom, moreOrLessEquals(coverRect.bottom, epsilon: 0.5));
+    final trackListRect = tester.getRect(
+      find.byKey(const ValueKey('jellyfin-album-track-list')),
+    );
+    expect(trackListRect.top, greaterThan(coverRect.bottom));
     expect(tester.takeException(), isNull);
 
     await tester.drag(find.byType(Slider), const Offset(120, 0));