Ver Fonte

fix: align album controls and enlarge buttons

Bottoms the Jellyfin album progress/transport card against the album cover in both wide and narrow layouts, enlarges previous/next to 62 px, play/pause to 80 px and the compact volume controls to 40 px. Adds alignment assertions at 1280x800 and 1024x600. Bumps to 1.2.5+25 with release APK and notes.
Krasimir Kazakov há 5 dias atrás
pai
commit
3a7aae6037

+ 1 - 1
AGENTS.md

@@ -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 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 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 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.
 
 

+ 16 - 13
README.md

@@ -101,8 +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, previous /
-  play-pause / next transport, and a compact volume card.
+  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.
   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,10 +331,11 @@ the app resumes.
   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
 - **Album page**: large cover and track list, plus a control card with one
-  seekable song-progress slider, elapsed / total time, and previous /
-  play-pause / next below it. 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.
+  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
+  API; the Favorites tab refreshes when you return.
 - **Home tile**: if a Jellyfin album is active, the Home tile opens that album's
 - **Home tile**: if a Jellyfin album is active, the Home tile opens that album's
   page with the dashboard underneath, so Back returns to the Albums tab.
   page with the dashboard underneath, so Back returns to the Albums tab.
 - **Playback**: in-app Media3 with background playback, a media notification on
 - **Playback**: in-app Media3 with background playback, a media notification on
@@ -454,13 +456,14 @@ 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.4
-universal APK is the current published release. It redesigns the Jellyfin album
-controls and includes 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; the MCU volume path
-still needs the real-car test.
+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; the MCU volume path still needs the real-car test.
 
 
 ## Troubleshooting the emulator
 ## Troubleshooting the emulator
 
 

+ 6 - 0
docs/IMPLEMENTATION_PLAN.md

@@ -331,6 +331,12 @@ Android (Kotlin)
 
 
 ## 8. Change log
 ## 8. Change log
 
 
+- 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
+  to 40 px. The album page still has one seekable slider with elapsed / total
+  time and the compact −/Mute/+ card beside the now-playing block. 150 tests
+  pass; release `releases/cardash-android-1.2.5.apk`.
 - 2026-11-01: CarDash 1.2.4+24 redesigns the Jellyfin album controls. The old
 - 2026-11-01: CarDash 1.2.4+24 redesigns the Jellyfin album controls. The old
   ring card is replaced by one seekable song-progress slider with elapsed /
   ring card is replaced by one seekable song-progress slider with elapsed /
   total time, and a centered previous / play-pause / next row. A compact volume
   total time, and a centered previous / play-pause / next row. A compact volume

+ 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.4';
+const String appVersion = '1.2.5';

+ 119 - 103
lib/features/jellyfin/jellyfin_album_screen.dart

@@ -127,6 +127,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
           child: AspectRatio(
           child: AspectRatio(
             aspectRatio: 1,
             aspectRatio: 1,
             child: Container(
             child: Container(
+              key: const ValueKey('jellyfin-album-cover'),
               clipBehavior: Clip.antiAlias,
               clipBehavior: Clip.antiAlias,
               decoration: BoxDecoration(
               decoration: BoxDecoration(
                 borderRadius: BorderRadius.circular(24),
                 borderRadius: BorderRadius.circular(24),
@@ -146,7 +147,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
       );
       );
     }
     }
 
 
-    Widget controls() {
+    Widget controls({double? minHeight}) {
       final error = playback.error;
       final error = playback.error;
       final durationMs = media.durationMs > 0
       final durationMs = media.durationMs > 0
           ? media.durationMs
           ? media.durationMs
@@ -160,7 +161,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
       final canSeek = active && durationMs > 0;
       final canSeek = active && durationMs > 0;
       final canToggle = !playback.loading;
       final canToggle = !playback.loading;
 
 
-      return Column(
+      final content = Column(
         crossAxisAlignment: CrossAxisAlignment.stretch,
         crossAxisAlignment: CrossAxisAlignment.stretch,
         mainAxisSize: MainAxisSize.min,
         mainAxisSize: MainAxisSize.min,
         children: [
         children: [
@@ -226,106 +227,120 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
               ],
               ],
             ),
             ),
           ],
           ],
-          const SizedBox(height: 14),
-          Container(
-            padding: const EdgeInsets.fromLTRB(18, 12, 18, 14),
-            decoration: BoxDecoration(
-              color: colors.surface,
-              borderRadius: BorderRadius.circular(20),
-              border: Border.all(color: colors.border),
-            ),
-            child: Column(
-              mainAxisSize: MainAxisSize.min,
+        ],
+      );
+      final card = Container(
+        key: const ValueKey('jellyfin-album-progress-card'),
+        padding: const EdgeInsets.fromLTRB(18, 12, 18, 14),
+        decoration: BoxDecoration(
+          color: colors.surface,
+          borderRadius: BorderRadius.circular(20),
+          border: Border.all(color: colors.border),
+        ),
+        child: Column(
+          mainAxisSize: MainAxisSize.min,
+          children: [
+            Row(
               children: [
               children: [
-                Row(
-                  children: [
-                    Expanded(
-                      child: SliderTheme(
-                        data: SliderTheme.of(context).copyWith(
-                          trackHeight: 4,
-                          activeTrackColor: colors.accent,
-                          inactiveTrackColor: colors.border,
-                          thumbColor: colors.accent,
-                          overlayColor: colors.accent.withValues(alpha: 0.12),
-                          thumbShape: const RoundSliderThumbShape(
-                            enabledThumbRadius: 7,
-                          ),
-                          overlayShape: const RoundSliderOverlayShape(
-                            overlayRadius: 16,
-                          ),
-                        ),
-                        child: Slider(
-                          min: 0,
-                          max: durationMs > 0 ? durationMs.toDouble() : 1,
-                          value: durationMs > 0 ? rawPosition : 0,
-                          onChanged: canSeek
-                              ? (value) {
-                                  setState(() => _seekDragMs = value);
-                                }
-                              : null,
-                          onChangeEnd: canSeek
-                              ? (value) {
-                                  setState(() => _seekDragMs = null);
-                                  unawaited(
-                                    playback.seek(
-                                      Duration(milliseconds: value.round()),
-                                    ),
-                                  );
-                                }
-                              : null,
-                        ),
+                Expanded(
+                  child: SliderTheme(
+                    data: SliderTheme.of(context).copyWith(
+                      trackHeight: 4,
+                      activeTrackColor: colors.accent,
+                      inactiveTrackColor: colors.border,
+                      thumbColor: colors.accent,
+                      overlayColor: colors.accent.withValues(alpha: 0.12),
+                      thumbShape: const RoundSliderThumbShape(
+                        enabledThumbRadius: 8,
                       ),
                       ),
-                    ),
-                    const SizedBox(width: 10),
-                    Text(
-                      '${_formatDuration(position)} / ${_formatDuration(total)}',
-                      style: theme.textTheme.bodySmall?.copyWith(
-                        color: colors.inkMuted,
-                        fontFeatures: const [FontFeature.tabularFigures()],
+                      overlayShape: const RoundSliderOverlayShape(
+                        overlayRadius: 18,
                       ),
                       ),
                     ),
                     ),
-                  ],
-                ),
-                const SizedBox(height: 10),
-                Row(
-                  mainAxisAlignment: MainAxisAlignment.center,
-                  children: [
-                    _AlbumTransportButton(
-                      asset: 'assets/icons/skip-back.svg',
-                      semanticLabel: 'Previous track',
-                      enabled: active && playback.canGoPrevious,
-                      onTap: () => unawaited(playback.previous()),
-                    ),
-                    const SizedBox(width: 18),
-                    _AlbumTransportButton(
-                      asset: playing
-                          ? 'assets/icons/pause.svg'
-                          : 'assets/icons/play.svg',
-                      semanticLabel: playing ? 'Pause music' : 'Play music',
-                      primary: true,
-                      enabled: canToggle,
-                      onTap: () {
-                        if (playback.loading) return;
-                        if (active) {
-                          unawaited(playback.toggle());
-                        } else {
-                          unawaited(playback.playAlbum(widget.album));
-                        }
-                      },
+                    child: Slider(
+                      min: 0,
+                      max: durationMs > 0 ? durationMs.toDouble() : 1,
+                      value: durationMs > 0 ? rawPosition : 0,
+                      onChanged: canSeek
+                          ? (value) {
+                              setState(() => _seekDragMs = value);
+                            }
+                          : null,
+                      onChangeEnd: canSeek
+                          ? (value) {
+                              setState(() => _seekDragMs = null);
+                              unawaited(
+                                playback.seek(
+                                  Duration(milliseconds: value.round()),
+                                ),
+                              );
+                            }
+                          : null,
                     ),
                     ),
-                    const SizedBox(width: 18),
-                    _AlbumTransportButton(
-                      asset: 'assets/icons/skip-forward.svg',
-                      semanticLabel: 'Next track',
-                      enabled: active && playback.canGoNext,
-                      onTap: () => unawaited(playback.next()),
-                    ),
-                  ],
+                  ),
+                ),
+                const SizedBox(width: 10),
+                Text(
+                  '${_formatDuration(position)} / ${_formatDuration(total)}',
+                  style: theme.textTheme.bodySmall?.copyWith(
+                    color: colors.inkMuted,
+                    fontFeatures: const [FontFeature.tabularFigures()],
+                  ),
                 ),
                 ),
               ],
               ],
             ),
             ),
-          ),
-        ],
+            const SizedBox(height: 10),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                _AlbumTransportButton(
+                  asset: 'assets/icons/skip-back.svg',
+                  semanticLabel: 'Previous track',
+                  enabled: active && playback.canGoPrevious,
+                  onTap: () => unawaited(playback.previous()),
+                ),
+                const SizedBox(width: 20),
+                _AlbumTransportButton(
+                  asset: playing
+                      ? 'assets/icons/pause.svg'
+                      : 'assets/icons/play.svg',
+                  semanticLabel: playing ? 'Pause music' : 'Play music',
+                  primary: true,
+                  enabled: canToggle,
+                  onTap: () {
+                    if (playback.loading) return;
+                    if (active) {
+                      unawaited(playback.toggle());
+                    } else {
+                      unawaited(playback.playAlbum(widget.album));
+                    }
+                  },
+                ),
+                const SizedBox(width: 20),
+                _AlbumTransportButton(
+                  asset: 'assets/icons/skip-forward.svg',
+                  semanticLabel: 'Next track',
+                  enabled: active && playback.canGoNext,
+                  onTap: () => unawaited(playback.next()),
+                ),
+              ],
+            ),
+          ],
+        ),
+      );
+      if (minHeight == null) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          mainAxisSize: MainAxisSize.min,
+          children: [content, const SizedBox(height: 14), card],
+        );
+      }
+      return SizedBox(
+        height: minHeight,
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: [content, const Spacer(), card],
+        ),
       );
       );
     }
     }
 
 
@@ -419,7 +434,8 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
           const SizedBox(width: 22),
           const SizedBox(width: 22),
           SizedBox(
           SizedBox(
             width: controlsWidth,
             width: controlsWidth,
-            child: SingleChildScrollView(child: controls()),
+            height: coverWidth,
+            child: controls(minHeight: coverWidth),
           ),
           ),
         ],
         ],
       );
       );
@@ -436,7 +452,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
               children: [
               children: [
                 SizedBox(width: coverWidth, child: cover()),
                 SizedBox(width: coverWidth, child: cover()),
                 const SizedBox(width: 18),
                 const SizedBox(width: 18),
-                Expanded(child: controls()),
+                Expanded(child: controls(minHeight: coverWidth)),
               ],
               ],
             ),
             ),
             const SizedBox(height: 16),
             const SizedBox(height: 16),
@@ -638,7 +654,7 @@ class _AlbumVolumeCard extends StatelessWidget {
             semanticLabel: system.isMuted ? 'Unmute' : 'Mute',
             semanticLabel: system.isMuted ? 'Unmute' : 'Mute',
             pressedScale: 0.94,
             pressedScale: 0.94,
             child: Container(
             child: Container(
-              height: 36,
+              height: 40,
               padding: const EdgeInsets.symmetric(horizontal: 10),
               padding: const EdgeInsets.symmetric(horizontal: 10),
               decoration: BoxDecoration(
               decoration: BoxDecoration(
                 color: system.isMuted
                 color: system.isMuted
@@ -658,7 +674,7 @@ class _AlbumVolumeCard extends StatelessWidget {
                     system.isMuted
                     system.isMuted
                         ? 'assets/icons/volume-x.svg'
                         ? 'assets/icons/volume-x.svg'
                         : 'assets/icons/volume-2.svg',
                         : 'assets/icons/volume-2.svg',
-                    size: 16,
+                    size: 18,
                     color: system.isMuted ? colors.accent : colors.inkMuted,
                     color: system.isMuted ? colors.accent : colors.inkMuted,
                   ),
                   ),
                   const SizedBox(width: 6),
                   const SizedBox(width: 6),
@@ -703,8 +719,8 @@ class _AlbumVolumeStep extends StatelessWidget {
       semanticLabel: semanticLabel,
       semanticLabel: semanticLabel,
       pressedScale: 0.92,
       pressedScale: 0.92,
       child: Container(
       child: Container(
-        width: 36,
-        height: 36,
+        width: 40,
+        height: 40,
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           color: colors.surfaceStrong,
           color: colors.surfaceStrong,
           shape: BoxShape.circle,
           shape: BoxShape.circle,
@@ -713,7 +729,7 @@ class _AlbumVolumeStep extends StatelessWidget {
         alignment: Alignment.center,
         alignment: Alignment.center,
         child: Icon(
         child: Icon(
           icon,
           icon,
-          size: 18,
+          size: 20,
           color: onTap == null ? colors.inkFaint : colors.inkMuted,
           color: onTap == null ? colors.inkFaint : colors.inkMuted,
         ),
         ),
       ),
       ),
@@ -739,7 +755,7 @@ class _AlbumTransportButton extends StatelessWidget {
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final colors = context.colors;
     final colors = context.colors;
-    final size = primary ? 68.0 : 54.0;
+    final size = primary ? 80.0 : 62.0;
     return Opacity(
     return Opacity(
       opacity: enabled ? 1 : 0.4,
       opacity: enabled ? 1 : 0.4,
       child: Pressable(
       child: Pressable(
@@ -757,7 +773,7 @@ class _AlbumTransportButton extends StatelessWidget {
           alignment: Alignment.center,
           alignment: Alignment.center,
           child: AppIcon(
           child: AppIcon(
             asset,
             asset,
-            size: primary ? 28 : 22,
+            size: primary ? 32 : 26,
             color: primary ? colors.accentInk : colors.ink,
             color: primary ? colors.accentInk : colors.ink,
           ),
           ),
         ),
         ),

+ 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.4+24
+version: 1.2.5+25
 
 
 environment:
 environment:
   sdk: ^3.13.7
   sdk: ^3.13.7

+ 37 - 0
releases/1.2.5.md

@@ -0,0 +1,37 @@
+CarDash 1.2.5 polishes the Jellyfin album controls.
+
+- **Package:** `com.wasp.cardash_launcher`
+- **Version:** 1.2.5 (versionCode 25)
+- **ABIs:** arm64-v8a, armeabi-v7a, x86_64 (universal APK)
+- **Signing:** project debug key matching all prior releases
+- **SHA-256:** `47ba0960bf9bf16122f482f65941f27f166283d8aeeb0eb6ae718216aaf0dad5`
+
+**What changed**
+- The progress/transport card is now bottom-aligned with the album cover in
+  both the wide three-column layout and the narrower two-column layout.
+- Previous and next grow from 54 to 62 px; play/pause grows from 68 to 80 px.
+  The compact volume −/+ and Mute/Unmute pill grow from 36 to 40 px, so all
+  controls are easier to hit in a moving car.
+- Includes all 1.2.4 work: the seekable song-progress slider with elapsed/total
+  time, the centered transport row and the compact volume card. The Home card is
+  unchanged.
+- Includes the 1.2.3 normal-app/recents and Jellyfin progress-duration work, and
+  all 1.2.2 FYT/Syu volume, Favorites and streaming-quality work.
+
+**Verified**
+- `flutter analyze`, formatting and the full 150-test suite.
+- A widget test asserts the progress card's bottom edge matches the album
+  cover's bottom edge at 1280×800 and 1024×600, and covers the slider seek and
+  transport.
+- Universal release APK built with JDK 21.
+
+**[Download cardash-android-1.2.5.apk](https://git.krasi.net/kkazakov/car-interface-android/raw/main/releases/cardash-android-1.2.5.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.

+ 12 - 0
releases/README.md

@@ -1,5 +1,17 @@
 # CarDash releases
 # CarDash releases
 
 
+## 1.2.5 — album card alignment and larger controls
+
+- **File:** `cardash-android-1.2.5.apk`
+- **Version:** 1.2.5 (versionCode 25)
+- **ABIs:** arm64-v8a, armeabi-v7a, x86_64
+- **SHA-256:** `47ba0960bf9bf16122f482f65941f27f166283d8aeeb0eb6ae718216aaf0dad5`
+- **Signing:** project debug key matching all releases.
+- **Changes:** bottoms the album progress/transport card against the album cover;
+  enlarges previous/next to 62 px, play/pause to 80 px and the compact volume
+  controls to 40 px. Includes all 1.2.4 album controls and prior Jellyfin work.
+- **Verified:** analyze, formatting, 150 tests and universal release build.
+
 ## 1.2.4 — Jellyfin album controls redesign
 ## 1.2.4 — Jellyfin album controls redesign
 
 
 - **File:** `cardash-android-1.2.4.apk`
 - **File:** `cardash-android-1.2.4.apk`

BIN
releases/cardash-android-1.2.5.apk


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

@@ -0,0 +1 @@
+47ba0960bf9bf16122f482f65941f27f166283d8aeeb0eb6ae718216aaf0dad5  cardash-android-1.2.5.apk

+ 21 - 0
test/jellyfin_album_screen_test.dart

@@ -73,6 +73,13 @@ void main() {
     expect(find.bySemanticsLabel('Volume down'), findsOneWidget);
     expect(find.bySemanticsLabel('Volume down'), findsOneWidget);
     expect(find.bySemanticsLabel('Volume up'), findsOneWidget);
     expect(find.bySemanticsLabel('Volume up'), findsOneWidget);
     expect(find.bySemanticsLabel('Remove from favorites'), findsOneWidget);
     expect(find.bySemanticsLabel('Remove from favorites'), findsOneWidget);
+    final coverRect = tester.getRect(
+      find.byKey(const ValueKey('jellyfin-album-cover')),
+    );
+    final cardRect = tester.getRect(
+      find.byKey(const ValueKey('jellyfin-album-progress-card')),
+    );
+    expect(cardRect.bottom, moreOrLessEquals(coverRect.bottom, epsilon: 0.5));
     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));
@@ -104,6 +111,20 @@ void main() {
     expect(platform.commands, contains('previous'));
     expect(platform.commands, contains('previous'));
     expect(playback.currentItem?.trackId, 't2');
     expect(playback.currentItem?.trackId, 't2');
 
 
+    await tester.binding.setSurfaceSize(const Size(1024, 600));
+    await tester.pumpAndSettle();
+    final narrowCover = tester.getRect(
+      find.byKey(const ValueKey('jellyfin-album-cover')),
+    );
+    final narrowCard = tester.getRect(
+      find.byKey(const ValueKey('jellyfin-album-progress-card')),
+    );
+    expect(
+      narrowCard.bottom,
+      moreOrLessEquals(narrowCover.bottom, epsilon: 0.5),
+    );
+    expect(tester.takeException(), isNull);
+
     await tester.pumpWidget(const SizedBox.shrink());
     await tester.pumpWidget(const SizedBox.shrink());
     playback.dispose();
     playback.dispose();
     jellyfin.dispose();
     jellyfin.dispose();