ソースを参照

feat: streamline Jellyfin album controls and chrome

Krasimir Kazakov 10 時間 前
親
コミット
8d32c35f8f

+ 1 - 1
AGENTS.md

@@ -117,7 +117,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 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.
+- 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`: its navigation card has one seekable progress slider with elapsed/total time and one row with the 69 px favorite heart in the left corner, centered previous / play-pause / next, and three separate 60 px volume buttons (−, Mute/Unmute, +) in the right corner; there is no surrounding volume card. 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 21% of the body width clamped to 140–252 px; the wide three-column layout was removed. The navigation card is bottom-aligned with the album cover when the control column fits, otherwise it grows below the cover. The transport buttons are 80/62 px and track rows use 12 px vertical padding. Dashboard and album headers use an 83 px back button and a top-right strip with the time, current speed, current temperature and a Home button that pops to the home route. 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.
 
 

+ 15 - 10
README.md

@@ -103,8 +103,10 @@ live vehicle data.
   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 the cover and controls above a track list, a seekable
   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.
+  song-progress slider, large previous / play-pause / next transport, and three
+  separate volume buttons. The navigation card is bottom-aligned with the album
+  art when the control column fits. Both Jellyfin pages show a top-right strip
+  with the time, current speed, current temperature and a Home button.
   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
@@ -332,14 +334,17 @@ 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**: the stacked layout is used at every screen size — cover and
 - **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.
+  controls on top, track list below. The cover is 21% of the body width, clamped
+  to 140–252 px. The navigation card has one seekable song-progress slider,
+  elapsed / total time, and a row with the favorite heart in the left corner,
+  large previous / play-pause / next buttons in the centre and three separate
+  volume buttons (−, Mute / Unmute, +) in the right corner. Its bottom is aligned
+  with the album cover when the control column fits; otherwise it grows below
+  the cover. 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.
+  The dashboard and album headers use a large back button and a top-right strip
+  with the time, current speed, current temperature and a Home button.
 - **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

+ 6 - 2
docs/IMPLEMENTATION_PLAN.md

@@ -149,8 +149,12 @@ Android (Kotlin)
   network actions.
   network actions.
 - The Jellyfin dashboard asks for 6 albums per row and shows 6 columns when each
 - The Jellyfin dashboard asks for 6 albums per row and shows 6 columns when each
   card is at least 160 px wide, otherwise 5. Album labels share the cover's left
   card is at least 160 px wide, otherwise 5. Album labels share the cover's left
-  edge; the album page cover is top-aligned; a Random selection re-rolls that
-  row after playback starts.
+  edge; the album page cover is top-aligned at 21% of the body width (140–252 px);
+  a Random selection re-rolls that row after playback starts. The album
+  navigation card holds the 69 px favorite heart on the left, the transport in
+  the centre and three 60 px volume buttons on the right. Both Jellyfin headers
+  use an 83 px back button and a top-right time / speed / temperature / Home
+  strip.
 - Jellyfin playback is Media3 1.11.1 in a foreground `mediaPlayback` service.
 - Jellyfin playback is Media3 1.11.1 in a foreground `mediaPlayback` service.
   The Dart controller owns the queue and polls state; the service owns audio
   The Dart controller owns the queue and polls state; the service owns audio
   focus, the notification and playback reporting. It pauses on permanent focus
   focus, the notification and playback reporting. It pauses on permanent focus

BIN
docs/screenshots/jellyfin-album-1024.png


BIN
docs/screenshots/jellyfin-album.png


+ 3 - 3
lib/features/apps/all_apps_screen.dart

@@ -255,8 +255,8 @@ class _BackButton extends StatelessWidget {
       semanticLabel: 'Back',
       semanticLabel: 'Back',
       pressedScale: 0.92,
       pressedScale: 0.92,
       child: Container(
       child: Container(
-        width: 46,
-        height: 46,
+        width: 83,
+        height: 83,
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           color: colors.surface,
           color: colors.surface,
           shape: BoxShape.circle,
           shape: BoxShape.circle,
@@ -265,7 +265,7 @@ class _BackButton extends StatelessWidget {
         alignment: Alignment.center,
         alignment: Alignment.center,
         child: AppIcon(
         child: AppIcon(
           'assets/icons/arrow-left.svg',
           'assets/icons/arrow-left.svg',
-          size: 20,
+          size: 36,
           color: colors.ink,
           color: colors.ink,
         ),
         ),
       ),
       ),

+ 163 - 190
lib/features/jellyfin/jellyfin_album_screen.dart

@@ -12,6 +12,7 @@ import '../../data/services/jellyfin_library_service.dart';
 import '../home/system_controller.dart';
 import '../home/system_controller.dart';
 import 'jellyfin_playback_controller.dart';
 import 'jellyfin_playback_controller.dart';
 import 'widgets/jellyfin_back_button.dart';
 import 'widgets/jellyfin_back_button.dart';
+import 'widgets/jellyfin_status_strip.dart';
 
 
 class JellyfinAlbumScreen extends StatefulWidget {
 class JellyfinAlbumScreen extends StatefulWidget {
   const JellyfinAlbumScreen({super.key, required this.album});
   const JellyfinAlbumScreen({super.key, required this.album});
@@ -165,44 +166,35 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
         crossAxisAlignment: CrossAxisAlignment.stretch,
         crossAxisAlignment: CrossAxisAlignment.stretch,
         mainAxisSize: MainAxisSize.min,
         mainAxisSize: MainAxisSize.min,
         children: [
         children: [
-          Row(
+          Column(
             crossAxisAlignment: CrossAxisAlignment.start,
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
             children: [
-              Expanded(
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    Text(
-                      active
-                          ? (playing ? 'Now playing' : 'Paused')
-                          : playback.loading
-                          ? 'Starting playback…'
-                          : 'Ready',
-                      style: theme.textTheme.labelLarge?.copyWith(
-                        color: colors.inkMuted,
-                      ),
-                    ),
-                    const SizedBox(height: 4),
-                    Text(
-                      currentTitle,
-                      maxLines: 2,
-                      overflow: TextOverflow.ellipsis,
-                      style: theme.textTheme.headlineSmall,
-                    ),
-                    const SizedBox(height: 2),
-                    Text(
-                      currentArtist.isEmpty ? 'Unknown artist' : currentArtist,
-                      maxLines: 1,
-                      overflow: TextOverflow.ellipsis,
-                      style: theme.textTheme.bodyMedium?.copyWith(
-                        color: colors.inkMuted,
-                      ),
-                    ),
-                  ],
+              Text(
+                active
+                    ? (playing ? 'Now playing' : 'Paused')
+                    : playback.loading
+                    ? 'Starting playback…'
+                    : 'Ready',
+                style: theme.textTheme.labelLarge?.copyWith(
+                  color: colors.inkMuted,
+                ),
+              ),
+              const SizedBox(height: 4),
+              Text(
+                currentTitle,
+                maxLines: 2,
+                overflow: TextOverflow.ellipsis,
+                style: theme.textTheme.headlineSmall,
+              ),
+              const SizedBox(height: 2),
+              Text(
+                currentArtist.isEmpty ? 'Unknown artist' : currentArtist,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+                style: theme.textTheme.bodyMedium?.copyWith(
+                  color: colors.inkMuted,
                 ),
                 ),
               ),
               ),
-              const SizedBox(width: 12),
-              _AlbumVolumeCard(system: system),
             ],
             ],
           ),
           ),
           if (error != null) ...[
           if (error != null) ...[
@@ -290,57 +282,77 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
               ],
               ],
             ),
             ),
             const SizedBox(height: 10),
             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()),
-                ),
-              ],
+            SizedBox(
+              height: 80,
+              child: Stack(
+                alignment: Alignment.center,
+                children: [
+                  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()),
+                      ),
+                    ],
+                  ),
+                  Align(
+                    alignment: Alignment.centerLeft,
+                    child: _FavoriteButton(
+                      isFavorite: _isFavorite,
+                      busy: _favoriteBusy,
+                      onTap: _toggleFavorite,
+                    ),
+                  ),
+                  Align(
+                    alignment: Alignment.centerRight,
+                    child: _AlbumVolumeControls(system: system),
+                  ),
+                ],
+              ),
             ),
             ),
           ],
           ],
         ),
         ),
       );
       );
-      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],
-        ),
+      final column = Column(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        mainAxisSize: MainAxisSize.min,
+        mainAxisAlignment: MainAxisAlignment.spaceBetween,
+        children: [
+          content,
+          Padding(padding: const EdgeInsets.only(top: 14), child: card),
+        ],
+      );
+      if (minHeight == null) return column;
+      return ConstrainedBox(
+        constraints: BoxConstraints(minHeight: minHeight),
+        child: column,
       );
       );
     }
     }
 
 
@@ -423,27 +435,26 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
     }
     }
 
 
     Widget body(BoxConstraints constraints) {
     Widget body(BoxConstraints constraints) {
-      final coverWidth = (constraints.maxWidth * 0.30).clamp(200.0, 360.0);
-      return SingleChildScrollView(
-        child: Column(
-          crossAxisAlignment: CrossAxisAlignment.stretch,
-          children: [
-            Row(
-              crossAxisAlignment: CrossAxisAlignment.start,
-              children: [
-                SizedBox(width: coverWidth, child: cover()),
-                const SizedBox(width: 18),
-                Expanded(child: controls(minHeight: coverWidth)),
-              ],
-            ),
-            const SizedBox(height: 16),
-            SizedBox(
+      final coverWidth = (constraints.maxWidth * 0.21).clamp(140.0, 252.0);
+      return Column(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        children: [
+          Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              SizedBox(width: coverWidth, child: cover()),
+              const SizedBox(width: 18),
+              Expanded(child: controls(minHeight: coverWidth)),
+            ],
+          ),
+          const SizedBox(height: 16),
+          Expanded(
+            child: SizedBox.expand(
               key: const ValueKey('jellyfin-album-track-list'),
               key: const ValueKey('jellyfin-album-track-list'),
-              height: 260,
               child: trackList(),
               child: trackList(),
             ),
             ),
-          ],
-        ),
+          ),
+        ],
       );
       );
     }
     }
 
 
@@ -457,12 +468,7 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
             child: Column(
             child: Column(
               crossAxisAlignment: CrossAxisAlignment.stretch,
               crossAxisAlignment: CrossAxisAlignment.stretch,
               children: [
               children: [
-                _AlbumHeader(
-                  album: widget.album,
-                  isFavorite: _isFavorite,
-                  favoriteBusy: _favoriteBusy,
-                  onToggleFavorite: _toggleFavorite,
-                ),
+                _AlbumHeader(album: widget.album),
                 const SizedBox(height: 14),
                 const SizedBox(height: 14),
                 Expanded(
                 Expanded(
                   child: LayoutBuilder(
                   child: LayoutBuilder(
@@ -492,17 +498,9 @@ class _JellyfinAlbumScreenState extends State<JellyfinAlbumScreen> {
 }
 }
 
 
 class _AlbumHeader extends StatelessWidget {
 class _AlbumHeader extends StatelessWidget {
-  const _AlbumHeader({
-    required this.album,
-    required this.isFavorite,
-    required this.favoriteBusy,
-    required this.onToggleFavorite,
-  });
+  const _AlbumHeader({required this.album});
 
 
   final JellyfinAlbum album;
   final JellyfinAlbum album;
-  final bool isFavorite;
-  final bool favoriteBusy;
-  final VoidCallback onToggleFavorite;
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
@@ -547,11 +545,7 @@ class _AlbumHeader extends StatelessWidget {
           ),
           ),
         ),
         ),
         const SizedBox(width: 12),
         const SizedBox(width: 12),
-        _FavoriteButton(
-          isFavorite: isFavorite,
-          busy: favoriteBusy,
-          onTap: onToggleFavorite,
-        ),
+        const JellyfinStatusStrip(),
       ],
       ],
     );
     );
   }
   }
@@ -574,9 +568,10 @@ class _FavoriteButton extends StatelessWidget {
     return Pressable(
     return Pressable(
       onTap: busy ? null : onTap,
       onTap: busy ? null : onTap,
       semanticLabel: isFavorite ? 'Remove from favorites' : 'Add to favorites',
       semanticLabel: isFavorite ? 'Remove from favorites' : 'Add to favorites',
+      pressedScale: 0.94,
       child: Container(
       child: Container(
-        width: 46,
-        height: 46,
+        width: 69,
+        height: 69,
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           color: isFavorite ? colors.accentSoft : colors.surface,
           color: isFavorite ? colors.accentSoft : colors.surface,
           shape: BoxShape.circle,
           shape: BoxShape.circle,
@@ -589,8 +584,8 @@ class _FavoriteButton extends StatelessWidget {
         alignment: Alignment.center,
         alignment: Alignment.center,
         child: busy
         child: busy
             ? SizedBox(
             ? SizedBox(
-                width: 16,
-                height: 16,
+                width: 24,
+                height: 24,
                 child: CircularProgressIndicator(
                 child: CircularProgressIndicator(
                   strokeWidth: 2,
                   strokeWidth: 2,
                   color: colors.accent,
                   color: colors.accent,
@@ -600,7 +595,7 @@ class _FavoriteButton extends StatelessWidget {
                 isFavorite
                 isFavorite
                     ? 'assets/icons/heart-filled.svg'
                     ? 'assets/icons/heart-filled.svg'
                     : 'assets/icons/heart.svg',
                     : 'assets/icons/heart.svg',
-                size: 21,
+                size: 32,
                 color: isFavorite ? colors.accent : colors.inkMuted,
                 color: isFavorite ? colors.accent : colors.inkMuted,
               ),
               ),
       ),
       ),
@@ -608,79 +603,57 @@ class _FavoriteButton extends StatelessWidget {
   }
   }
 }
 }
 
 
-class _AlbumVolumeCard extends StatelessWidget {
-  const _AlbumVolumeCard({required this.system});
+class _AlbumVolumeControls extends StatelessWidget {
+  const _AlbumVolumeControls({required this.system});
 
 
   final SystemController system;
   final SystemController system;
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final colors = context.colors;
     final colors = context.colors;
-    final theme = Theme.of(context);
     final enabled = !system.isVolumeFixed;
     final enabled = !system.isVolumeFixed;
-    return Container(
-      padding: const EdgeInsets.all(8),
-      decoration: BoxDecoration(
-        color: colors.surface,
-        borderRadius: BorderRadius.circular(18),
-        border: Border.all(color: colors.border),
-      ),
-      child: Row(
-        mainAxisSize: MainAxisSize.min,
-        children: [
-          _AlbumVolumeStep(
-            icon: Icons.remove_rounded,
-            semanticLabel: 'Volume down',
-            onTap: enabled ? () => unawaited(system.adjustVolume(-1)) : null,
-          ),
-          const SizedBox(width: 6),
-          Pressable(
-            onTap: enabled ? () => unawaited(system.toggleMute()) : null,
-            semanticLabel: system.isMuted ? 'Unmute' : 'Mute',
-            pressedScale: 0.94,
-            child: Container(
-              height: 40,
-              padding: const EdgeInsets.symmetric(horizontal: 10),
-              decoration: BoxDecoration(
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        _AlbumVolumeStep(
+          icon: Icons.remove_rounded,
+          semanticLabel: 'Volume down',
+          onTap: enabled ? () => unawaited(system.adjustVolume(-1)) : null,
+        ),
+        const SizedBox(width: 8),
+        Pressable(
+          onTap: enabled ? () => unawaited(system.toggleMute()) : null,
+          semanticLabel: system.isMuted ? 'Unmute' : 'Mute',
+          pressedScale: 0.92,
+          child: Container(
+            width: 60,
+            height: 60,
+            decoration: BoxDecoration(
+              color: system.isMuted ? colors.accentSoft : colors.surfaceStrong,
+              shape: BoxShape.circle,
+              border: Border.all(
                 color: system.isMuted
                 color: system.isMuted
-                    ? colors.accentSoft
-                    : colors.surfaceStrong,
-                borderRadius: BorderRadius.circular(999),
-                border: Border.all(
-                  color: system.isMuted
-                      ? colors.accent.withValues(alpha: 0.25)
-                      : colors.border,
-                ),
-              ),
-              child: Row(
-                mainAxisSize: MainAxisSize.min,
-                children: [
-                  AppIcon(
-                    system.isMuted
-                        ? 'assets/icons/volume-x.svg'
-                        : 'assets/icons/volume-2.svg',
-                    size: 18,
-                    color: system.isMuted ? colors.accent : colors.inkMuted,
-                  ),
-                  const SizedBox(width: 6),
-                  Text(
-                    system.isMuted ? 'Unmute' : 'Mute',
-                    style: theme.textTheme.labelMedium?.copyWith(
-                      color: system.isMuted ? colors.accent : colors.inkMuted,
-                    ),
-                  ),
-                ],
+                    ? colors.accent.withValues(alpha: 0.25)
+                    : colors.border,
               ),
               ),
             ),
             ),
+            alignment: Alignment.center,
+            child: AppIcon(
+              system.isMuted
+                  ? 'assets/icons/volume-x.svg'
+                  : 'assets/icons/volume-2.svg',
+              size: 26,
+              color: system.isMuted ? colors.accent : colors.inkMuted,
+            ),
           ),
           ),
-          const SizedBox(width: 6),
-          _AlbumVolumeStep(
-            icon: Icons.add_rounded,
-            semanticLabel: 'Volume up',
-            onTap: enabled ? () => unawaited(system.adjustVolume(1)) : null,
-          ),
-        ],
-      ),
+        ),
+        const SizedBox(width: 8),
+        _AlbumVolumeStep(
+          icon: Icons.add_rounded,
+          semanticLabel: 'Volume up',
+          onTap: enabled ? () => unawaited(system.adjustVolume(1)) : null,
+        ),
+      ],
     );
     );
   }
   }
 }
 }
@@ -704,8 +677,8 @@ class _AlbumVolumeStep extends StatelessWidget {
       semanticLabel: semanticLabel,
       semanticLabel: semanticLabel,
       pressedScale: 0.92,
       pressedScale: 0.92,
       child: Container(
       child: Container(
-        width: 40,
-        height: 40,
+        width: 60,
+        height: 60,
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           color: colors.surfaceStrong,
           color: colors.surfaceStrong,
           shape: BoxShape.circle,
           shape: BoxShape.circle,
@@ -714,7 +687,7 @@ class _AlbumVolumeStep extends StatelessWidget {
         alignment: Alignment.center,
         alignment: Alignment.center,
         child: Icon(
         child: Icon(
           icon,
           icon,
-          size: 20,
+          size: 26,
           color: onTap == null ? colors.inkFaint : colors.inkMuted,
           color: onTap == null ? colors.inkFaint : colors.inkMuted,
         ),
         ),
       ),
       ),
@@ -792,7 +765,7 @@ class _TrackRow extends StatelessWidget {
         borderRadius: BorderRadius.circular(12),
         borderRadius: BorderRadius.circular(12),
         onTap: onTap,
         onTap: onTap,
         child: Padding(
         child: Padding(
-          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 7),
+          padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
           child: Row(
           child: Row(
             children: [
             children: [
               SizedBox(
               SizedBox(

+ 3 - 0
lib/features/jellyfin/jellyfin_dashboard_screen.dart

@@ -14,6 +14,7 @@ import 'jellyfin_album_screen.dart';
 import 'jellyfin_dashboard_controller.dart';
 import 'jellyfin_dashboard_controller.dart';
 import 'jellyfin_playback_controller.dart';
 import 'jellyfin_playback_controller.dart';
 import 'widgets/jellyfin_back_button.dart';
 import 'widgets/jellyfin_back_button.dart';
+import 'widgets/jellyfin_status_strip.dart';
 
 
 class JellyfinDashboardScreen extends StatelessWidget {
 class JellyfinDashboardScreen extends StatelessWidget {
   const JellyfinDashboardScreen({super.key, this.openAlbum});
   const JellyfinDashboardScreen({super.key, this.openAlbum});
@@ -321,6 +322,8 @@ class _DashboardHeader extends StatelessWidget {
             ],
             ],
           ),
           ),
         ),
         ),
+        const SizedBox(width: 12),
+        const JellyfinStatusStrip(),
       ],
       ],
     );
     );
   }
   }

+ 3 - 3
lib/features/jellyfin/widgets/jellyfin_back_button.dart

@@ -15,8 +15,8 @@ class JellyfinBackButton extends StatelessWidget {
       semanticLabel: 'Back',
       semanticLabel: 'Back',
       pressedScale: 0.92,
       pressedScale: 0.92,
       child: Container(
       child: Container(
-        width: 46,
-        height: 46,
+        width: 83,
+        height: 83,
         decoration: BoxDecoration(
         decoration: BoxDecoration(
           color: colors.surface,
           color: colors.surface,
           shape: BoxShape.circle,
           shape: BoxShape.circle,
@@ -25,7 +25,7 @@ class JellyfinBackButton extends StatelessWidget {
         alignment: Alignment.center,
         alignment: Alignment.center,
         child: AppIcon(
         child: AppIcon(
           'assets/icons/arrow-left.svg',
           'assets/icons/arrow-left.svg',
-          size: 20,
+          size: 36,
           color: colors.ink,
           color: colors.ink,
         ),
         ),
       ),
       ),

+ 178 - 0
lib/features/jellyfin/widgets/jellyfin_status_strip.dart

@@ -0,0 +1,178 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
+
+import '../../../core/theme/app_colors.dart';
+import '../../../core/widgets/app_icon.dart';
+import '../../../core/widgets/pressable.dart';
+import '../../home/gps_speed_controller.dart';
+import '../../home/obd_controller.dart';
+import '../../home/telemetry_settings_controller.dart';
+import '../../home/weather_controller.dart';
+
+/// Time, speed, temperature and a Home shortcut for the Jellyfin pages.
+class JellyfinStatusStrip extends StatefulWidget {
+  const JellyfinStatusStrip({super.key});
+
+  @override
+  State<JellyfinStatusStrip> createState() => _JellyfinStatusStripState();
+}
+
+class _JellyfinStatusStripState extends State<JellyfinStatusStrip> {
+  late DateTime _now = DateTime.now();
+  Timer? _timer;
+
+  @override
+  void initState() {
+    super.initState();
+    _scheduleTick();
+  }
+
+  void _scheduleTick() {
+    _timer?.cancel();
+    final now = DateTime.now();
+    final next = DateTime(
+      now.year,
+      now.month,
+      now.day,
+      now.hour,
+      now.minute,
+    ).add(const Duration(minutes: 1));
+    _timer = Timer(next.difference(now) + const Duration(milliseconds: 80), () {
+      if (!mounted) return;
+      setState(() => _now = DateTime.now());
+      _scheduleTick();
+    });
+  }
+
+  @override
+  void dispose() {
+    _timer?.cancel();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = context.colors;
+    final theme = Theme.of(context);
+    final telemetry = context.watch<TelemetrySettingsController>();
+    final obd = context.watch<ObdController>();
+    final gps = context.watch<GpsSpeedController>();
+    final weather = context.watch<WeatherController>();
+
+    final useObdSpeed =
+        telemetry.speedEnabled && telemetry.speedUseObd && obd.hasTelemetry;
+    final speed = !telemetry.speedEnabled
+        ? null
+        : useObdSpeed
+        ? obd.speedKmh
+        : gps.speedKmh;
+    final useObdTemperature =
+        telemetry.temperatureUseObd && obd.ambientCelsius != null;
+    final temperature = !telemetry.temperatureEnabled
+        ? null
+        : useObdTemperature
+        ? obd.ambientCelsius
+        : (weather.hasTemperature ? weather.temperatureCelsius : null);
+
+    final locale = Localizations.localeOf(context).toLanguageTag();
+    final use24Hour = MediaQuery.alwaysUse24HourFormatOf(context);
+    final time = (use24Hour ? DateFormat.Hm(locale) : DateFormat.jm(locale))
+        .format(_now);
+
+    return Container(
+      key: const ValueKey('jellyfin-status-strip'),
+      padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
+      decoration: BoxDecoration(
+        color: colors.surface.withValues(alpha: 0.82),
+        borderRadius: BorderRadius.circular(999),
+        border: Border.all(color: colors.border),
+      ),
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        children: [
+          Text(
+            time,
+            style: theme.textTheme.labelLarge?.copyWith(
+              color: colors.ink,
+              fontSize: 16,
+              fontFeatures: const [FontFeature.tabularFigures()],
+            ),
+          ),
+          const SizedBox(width: 14),
+          _StripMetric(
+            iconAsset: 'assets/icons/gauge.svg',
+            text: speed == null ? '-- km/h' : '${speed.round()} km/h',
+            available: speed != null,
+          ),
+          const SizedBox(width: 14),
+          _StripMetric(
+            iconAsset: 'assets/icons/thermometer.svg',
+            text: temperature == null ? '--°C' : '${temperature.round()}°C',
+            available: temperature != null,
+          ),
+          const SizedBox(width: 10),
+          Pressable(
+            onTap: () =>
+                Navigator.of(context).popUntil((route) => route.isFirst),
+            semanticLabel: 'Go to home',
+            pressedScale: 0.92,
+            child: Container(
+              width: 42,
+              height: 42,
+              decoration: BoxDecoration(
+                color: colors.surfaceStrong,
+                shape: BoxShape.circle,
+                border: Border.all(color: colors.border),
+              ),
+              alignment: Alignment.center,
+              child: AppIcon(
+                'assets/icons/home.svg',
+                size: 21,
+                color: colors.ink,
+              ),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _StripMetric extends StatelessWidget {
+  const _StripMetric({
+    required this.iconAsset,
+    required this.text,
+    required this.available,
+  });
+
+  final String iconAsset;
+  final String text;
+  final bool available;
+
+  @override
+  Widget build(BuildContext context) {
+    final colors = context.colors;
+    final theme = Theme.of(context);
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: [
+        AppIcon(
+          iconAsset,
+          size: 18,
+          color: available ? colors.inkMuted : colors.inkFaint,
+        ),
+        const SizedBox(width: 6),
+        Text(
+          text,
+          style: theme.textTheme.labelLarge?.copyWith(
+            color: available ? colors.ink : colors.inkFaint,
+            fontFeatures: const [FontFeature.tabularFigures()],
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 3 - 3
lib/features/lock/pin_setup_screen.dart

@@ -228,8 +228,8 @@ class _PinSetupScreenState extends State<PinSetupScreen>
                       semanticLabel: 'Back',
                       semanticLabel: 'Back',
                       pressedScale: 0.92,
                       pressedScale: 0.92,
                       child: Container(
                       child: Container(
-                        width: 46,
-                        height: 46,
+                        width: 83,
+                        height: 83,
                         decoration: BoxDecoration(
                         decoration: BoxDecoration(
                           color: colors.surface,
                           color: colors.surface,
                           shape: BoxShape.circle,
                           shape: BoxShape.circle,
@@ -238,7 +238,7 @@ class _PinSetupScreenState extends State<PinSetupScreen>
                         alignment: Alignment.center,
                         alignment: Alignment.center,
                         child: AppIcon(
                         child: AppIcon(
                           'assets/icons/arrow-left.svg',
                           'assets/icons/arrow-left.svg',
-                          size: 20,
+                          size: 36,
                           color: colors.ink,
                           color: colors.ink,
                         ),
                         ),
                       ),
                       ),

+ 3 - 3
lib/features/settings/settings_screen.dart

@@ -60,8 +60,8 @@ class SettingsScreen extends StatelessWidget {
                           semanticLabel: 'Back',
                           semanticLabel: 'Back',
                           pressedScale: 0.92,
                           pressedScale: 0.92,
                           child: Container(
                           child: Container(
-                            width: 46,
-                            height: 46,
+                            width: 83,
+                            height: 83,
                             decoration: BoxDecoration(
                             decoration: BoxDecoration(
                               color: colors.surface,
                               color: colors.surface,
                               shape: BoxShape.circle,
                               shape: BoxShape.circle,
@@ -70,7 +70,7 @@ class SettingsScreen extends StatelessWidget {
                             alignment: Alignment.center,
                             alignment: Alignment.center,
                             child: AppIcon(
                             child: AppIcon(
                               'assets/icons/arrow-left.svg',
                               'assets/icons/arrow-left.svg',
-                              size: 20,
+                              size: 36,
                               color: colors.ink,
                               color: colors.ink,
                             ),
                             ),
                           ),
                           ),

+ 30 - 4
test/jellyfin_album_screen_test.dart

@@ -1,12 +1,18 @@
 import 'package:cardash_launcher/core/theme/app_theme.dart';
 import 'package:cardash_launcher/core/theme/app_theme.dart';
+import 'package:cardash_launcher/data/repositories/settings_repository.dart';
 import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
 import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
+import 'package:cardash_launcher/features/home/gps_speed_controller.dart';
+import 'package:cardash_launcher/features/home/obd_controller.dart';
 import 'package:cardash_launcher/features/home/system_controller.dart';
 import 'package:cardash_launcher/features/home/system_controller.dart';
+import 'package:cardash_launcher/features/home/telemetry_settings_controller.dart';
+import 'package:cardash_launcher/features/home/weather_controller.dart';
 import 'package:cardash_launcher/features/jellyfin/jellyfin_album_screen.dart';
 import 'package:cardash_launcher/features/jellyfin/jellyfin_album_screen.dart';
 import 'package:cardash_launcher/features/jellyfin/jellyfin_playback_controller.dart';
 import 'package:cardash_launcher/features/jellyfin/jellyfin_playback_controller.dart';
 import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
 import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
 
 
 import 'jellyfin_fakes.dart';
 import 'jellyfin_fakes.dart';
 
 
@@ -42,6 +48,17 @@ void main() {
     await playback.playAlbum(albumOne);
     await playback.playAlbum(albumOne);
 
 
     final system = SystemController(FakeSystemPlatform());
     final system = SystemController(FakeSystemPlatform());
+    final prefs = await SharedPreferences.getInstance();
+    final settings = SettingsRepository(prefs);
+    final telemetry = FakeTelemetryPlatform();
+    final obd = ObdController(telemetry, settings)..initialize();
+    final gps = GpsSpeedController(telemetry);
+    final weather = WeatherController(telemetry);
+    final telemetrySettings = TelemetrySettingsController(
+      settings,
+      weather,
+      gps,
+    )..initialize();
 
 
     await tester.pumpWidget(
     await tester.pumpWidget(
       MultiProvider(
       MultiProvider(
@@ -52,6 +69,12 @@ void main() {
             value: playback,
             value: playback,
           ),
           ),
           ChangeNotifierProvider<SystemController>.value(value: system),
           ChangeNotifierProvider<SystemController>.value(value: system),
+          ChangeNotifierProvider<ObdController>.value(value: obd),
+          ChangeNotifierProvider<GpsSpeedController>.value(value: gps),
+          ChangeNotifierProvider<WeatherController>.value(value: weather),
+          ChangeNotifierProvider<TelemetrySettingsController>.value(
+            value: telemetrySettings,
+          ),
         ],
         ],
         child: MaterialApp(
         child: MaterialApp(
           theme: AppTheme.light(),
           theme: AppTheme.light(),
@@ -74,6 +97,8 @@ 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);
+    expect(find.byKey(const ValueKey('jellyfin-status-strip')), findsOneWidget);
+    expect(find.bySemanticsLabel('Go to home'), findsOneWidget);
     final coverRect = tester.getRect(
     final coverRect = tester.getRect(
       find.byKey(const ValueKey('jellyfin-album-cover')),
       find.byKey(const ValueKey('jellyfin-album-cover')),
     );
     );
@@ -124,15 +149,16 @@ void main() {
     final narrowCard = tester.getRect(
     final narrowCard = tester.getRect(
       find.byKey(const ValueKey('jellyfin-album-progress-card')),
       find.byKey(const ValueKey('jellyfin-album-progress-card')),
     );
     );
-    expect(
-      narrowCard.bottom,
-      moreOrLessEquals(narrowCover.bottom, epsilon: 0.5),
-    );
+    expect(narrowCard.bottom, greaterThanOrEqualTo(narrowCover.bottom));
     expect(tester.takeException(), isNull);
     expect(tester.takeException(), isNull);
 
 
     await tester.pumpWidget(const SizedBox.shrink());
     await tester.pumpWidget(const SizedBox.shrink());
     playback.dispose();
     playback.dispose();
     jellyfin.dispose();
     jellyfin.dispose();
     system.dispose();
     system.dispose();
+    obd.dispose();
+    gps.dispose();
+    weather.dispose();
+    telemetrySettings.dispose();
   });
   });
 }
 }

+ 26 - 0
test/jellyfin_dashboard_screen_test.dart

@@ -1,14 +1,20 @@
 import 'package:cardash_launcher/core/theme/app_theme.dart';
 import 'package:cardash_launcher/core/theme/app_theme.dart';
 import 'package:cardash_launcher/data/models/jellyfin_library.dart';
 import 'package:cardash_launcher/data/models/jellyfin_library.dart';
 import 'package:cardash_launcher/data/models/jellyfin_models.dart';
 import 'package:cardash_launcher/data/models/jellyfin_models.dart';
+import 'package:cardash_launcher/data/repositories/settings_repository.dart';
 import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
 import 'package:cardash_launcher/data/services/jellyfin_library_service.dart';
+import 'package:cardash_launcher/features/home/gps_speed_controller.dart';
+import 'package:cardash_launcher/features/home/obd_controller.dart';
 import 'package:cardash_launcher/features/home/system_controller.dart';
 import 'package:cardash_launcher/features/home/system_controller.dart';
+import 'package:cardash_launcher/features/home/telemetry_settings_controller.dart';
+import 'package:cardash_launcher/features/home/weather_controller.dart';
 import 'package:cardash_launcher/features/jellyfin/jellyfin_dashboard_screen.dart';
 import 'package:cardash_launcher/features/jellyfin/jellyfin_dashboard_screen.dart';
 import 'package:cardash_launcher/features/jellyfin/jellyfin_playback_controller.dart';
 import 'package:cardash_launcher/features/jellyfin/jellyfin_playback_controller.dart';
 import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
 import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:provider/provider.dart';
 import 'package:provider/provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
 
 
 import 'jellyfin_fakes.dart';
 import 'jellyfin_fakes.dart';
 
 
@@ -84,6 +90,18 @@ Future<void> _pump(
   addTearDown(fixture.jellyfin.dispose);
   addTearDown(fixture.jellyfin.dispose);
   final system = SystemController(FakeSystemPlatform());
   final system = SystemController(FakeSystemPlatform());
   addTearDown(system.dispose);
   addTearDown(system.dispose);
+  final prefs = await SharedPreferences.getInstance();
+  final settings = SettingsRepository(prefs);
+  final telemetry = FakeTelemetryPlatform();
+  final obd = ObdController(telemetry, settings)..initialize();
+  final gps = GpsSpeedController(telemetry);
+  final weather = WeatherController(telemetry);
+  final telemetrySettings = TelemetrySettingsController(settings, weather, gps)
+    ..initialize();
+  addTearDown(obd.dispose);
+  addTearDown(gps.dispose);
+  addTearDown(weather.dispose);
+  addTearDown(telemetrySettings.dispose);
 
 
   await tester.pumpWidget(
   await tester.pumpWidget(
     MultiProvider(
     MultiProvider(
@@ -96,6 +114,12 @@ Future<void> _pump(
           value: fixture.playback,
           value: fixture.playback,
         ),
         ),
         ChangeNotifierProvider<SystemController>.value(value: system),
         ChangeNotifierProvider<SystemController>.value(value: system),
+        ChangeNotifierProvider<ObdController>.value(value: obd),
+        ChangeNotifierProvider<GpsSpeedController>.value(value: gps),
+        ChangeNotifierProvider<WeatherController>.value(value: weather),
+        ChangeNotifierProvider<TelemetrySettingsController>.value(
+          value: telemetrySettings,
+        ),
       ],
       ],
       child: MaterialApp(
       child: MaterialApp(
         theme: AppTheme.light(),
         theme: AppTheme.light(),
@@ -122,6 +146,8 @@ void main() {
     expect(find.text('Album r1'), findsOneWidget);
     expect(find.text('Album r1'), findsOneWidget);
     expect(find.text('Album r6'), findsOneWidget);
     expect(find.text('Album r6'), findsOneWidget);
     expect(find.bySemanticsLabel('Shuffle random albums'), findsOneWidget);
     expect(find.bySemanticsLabel('Shuffle random albums'), findsOneWidget);
+    expect(find.byKey(const ValueKey('jellyfin-status-strip')), findsOneWidget);
+    expect(find.bySemanticsLabel('Go to home'), findsOneWidget);
     expect(tester.takeException(), isNull);
     expect(tester.takeException(), isNull);
   });
   });
 
 

+ 45 - 0
test/jellyfin_fakes.dart

@@ -1,10 +1,15 @@
 import 'dart:typed_data';
 import 'dart:typed_data';
 
 
 import 'package:cardash_launcher/data/models/app_entry.dart';
 import 'package:cardash_launcher/data/models/app_entry.dart';
+import 'package:cardash_launcher/data/models/bluetooth_device_info.dart';
+import 'package:cardash_launcher/data/models/bluetooth_power_state.dart';
+import 'package:cardash_launcher/data/models/gps_speed.dart';
 import 'package:cardash_launcher/data/models/jellyfin_library.dart';
 import 'package:cardash_launcher/data/models/jellyfin_library.dart';
 import 'package:cardash_launcher/data/models/jellyfin_models.dart';
 import 'package:cardash_launcher/data/models/jellyfin_models.dart';
 import 'package:cardash_launcher/data/models/jellyfin_playback.dart';
 import 'package:cardash_launcher/data/models/jellyfin_playback.dart';
+import 'package:cardash_launcher/data/models/location_reading.dart';
 import 'package:cardash_launcher/data/models/media_playback.dart';
 import 'package:cardash_launcher/data/models/media_playback.dart';
+import 'package:cardash_launcher/data/models/obd_state.dart';
 import 'package:cardash_launcher/data/models/system_status.dart';
 import 'package:cardash_launcher/data/models/system_status.dart';
 import 'package:cardash_launcher/data/models/update_info.dart';
 import 'package:cardash_launcher/data/models/update_info.dart';
 import 'package:cardash_launcher/data/repositories/settings_repository.dart';
 import 'package:cardash_launcher/data/repositories/settings_repository.dart';
@@ -13,6 +18,7 @@ import 'package:cardash_launcher/data/services/jellyfin_playback_platform.dart';
 import 'package:cardash_launcher/data/services/jellyfin_service.dart';
 import 'package:cardash_launcher/data/services/jellyfin_service.dart';
 import 'package:cardash_launcher/data/services/native_bridge.dart';
 import 'package:cardash_launcher/data/services/native_bridge.dart';
 import 'package:cardash_launcher/data/services/secure_store.dart';
 import 'package:cardash_launcher/data/services/secure_store.dart';
+import 'package:cardash_launcher/data/services/telemetry_bridge.dart';
 import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
 import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
 
@@ -168,6 +174,45 @@ class FakeSystemPlatform implements LauncherPlatform {
   }
   }
 }
 }
 
 
+class FakeTelemetryPlatform implements TelemetryPlatform {
+  @override
+  Future<List<BluetoothDeviceInfo>> listBondedDevices() async => const [];
+
+  @override
+  Future<BluetoothPowerState> bluetoothState() async => BluetoothPowerState.off;
+
+  @override
+  Future<bool> openBluetoothSettings() async => true;
+
+  @override
+  Future<bool> ensureBluetoothPermission() async => true;
+
+  @override
+  Future<bool> ensureLocationPermission() async => true;
+
+  @override
+  Future<LocationReading> getLocation() async =>
+      const LocationReading(latitude: 0, longitude: 0, place: '');
+
+  @override
+  Future<void> connectObd(String address) async {}
+
+  @override
+  Future<void> disconnectObd() async {}
+
+  @override
+  Future<ObdState> getObdState() async => const ObdState();
+
+  @override
+  Future<bool> startGpsSpeed() async => false;
+
+  @override
+  Future<void> stopGpsSpeed() async {}
+
+  @override
+  Future<GpsSpeed> getGpsSpeed() async => const GpsSpeed();
+}
+
 class FakeJellyfinLibraryGateway implements JellyfinLibraryGateway {
 class FakeJellyfinLibraryGateway implements JellyfinLibraryGateway {
   List<JellyfinTrack> played = const [];
   List<JellyfinTrack> played = const [];
   List<JellyfinAlbum> added = const [];
   List<JellyfinAlbum> added = const [];