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
38ed5d095f

+ 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 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 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.
 - 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.
 - 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.
 - 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.
 - 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.
 - 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.
 - 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.
 - 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.
 - 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.
 - 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
   button in Settings → General → Home buttons. The tile opens a full-screen
   dashboard with **Recently played**, **Recently added** and **Random** album
   dashboard with **Recently played**, **Recently added** and **Random** album
   rows (six cards when they fit, five otherwise), grouped genre selection and a
   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
   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
   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
   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
   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
   and five otherwise; an empty list shows **No favorites**. Artists is shown but
   disabled for a later release.
   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
   API; the Favorites tab refreshes when you return. The album heading includes
   the release year in parentheses, for example `Ascension (2025)`, when Jellyfin
   the release year in parentheses, for example `Ascension (2025)`, when Jellyfin
   provides it.
   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
 weather fallback, the update version/availability logic and the Settings update
 dot. Jellyfin coverage includes genre grouping, recent-played dedupe, the
 dot. Jellyfin coverage includes genre grouping, recent-played dedupe, the
 12.2 request contract, the dashboard controller and widget states, the genre
 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
 gating, the Home tile width clipping, the equalizer's stale-session guard, the
 streaming-quality persistence, the direct/MP3 transcode URL parameters, Jellyfin
 streaming-quality persistence, the direct/MP3 transcode URL parameters, Jellyfin
 favorite toggling, the Favorites tab/grid/empty state, the playing-album Home
 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
 background playback, notification, media keys, exclusive equalizers, audio-focus
 handover, Random re-shuffle) and against a local 12.2 container. The OBD hardware
 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
 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
 ## Troubleshooting the emulator
 
 

+ 7 - 4
docs/IMPLEMENTATION_PLAN.md

@@ -331,10 +331,13 @@ Android (Kotlin)
 
 
 ## 8. Change log
 ## 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
 - 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
   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
   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.
 /// 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(
       return SingleChildScrollView(
         child: Column(
         child: Column(
           crossAxisAlignment: CrossAxisAlignment.stretch,
           crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -456,7 +437,11 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
               ],
               ],
             ),
             ),
             const SizedBox(height: 16),
             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),
                 const SizedBox(height: 14),
                 Expanded(
                 Expanded(
                   child: LayoutBuilder(
                   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
 name: cardash_launcher
 description: "CarDash — a calm, configurable Android car dashboard launcher."
 description: "CarDash — a calm, configurable Android car dashboard launcher."
 publish_to: "none"
 publish_to: "none"
-version: 1.2.5+25
+version: 1.2.6+26
 
 
 environment:
 environment:
   sdk: ^3.13.7
   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
 # 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
 ## 1.2.5 — album card alignment and larger controls
 
 
 - **File:** `cardash-android-1.2.5.apk`
 - **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')),
       find.byKey(const ValueKey('jellyfin-album-progress-card')),
     );
     );
     expect(cardRect.bottom, moreOrLessEquals(coverRect.bottom, epsilon: 0.5));
     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);
     expect(tester.takeException(), isNull);
 
 
     await tester.drag(find.byType(Slider), const Offset(120, 0));
     await tester.drag(find.byType(Slider), const Offset(120, 0));