Bladeren bron

fix: align the left home cards with the shortcut tiles

Krasimir Kazakov 36 minuten geleden
bovenliggende
commit
d365170dc6

+ 1 - 1
AGENTS.md

@@ -93,7 +93,7 @@ notifyListeners();
 - The HOME role must be granted for the dashboard to replace the tablet home screen. On the emulator use `cmd role add-role-holder`; the Settings row uses `RoleManager`.
 - There is no Home-role banner. The one-time first-run tip uses `LauncherController.homeRoleHintShown`; do not show it again.
 - The long-press hint uses `onboarding.firstLaunchAt` and `HomeHint`: it is visible only for the first ten minutes after a fresh installation. Upgraded installs are migrated to an expired window and never see it again.
-- Screen-space is deliberately finite: test the 16-button grid at 1280×800, 1280×720 and 1024×600. The shortcut grid is bottom-anchored so a short row sits on the bottom edge; keep that behavior when changing layouts. The grid includes All apps; Settings sits in `StatusCluster` and does not consume a slot. Avoid unbounded `ListView`s inside `Column`s.
+- Screen-space is deliberately finite: test the 16-button grid at 1280×800, 1280×720 and 1024×600. The shortcut grid is bottom-anchored so a short row sits on the bottom edge; keep that behavior when changing layouts. The left card stack is inset by `ShortcutGrid.labelAllowanceFor(compact)` so its bottom aligns with the shortcut tiles, above their labels. The grid includes All apps; Settings sits in `StatusCluster` and does not consume a slot. Avoid unbounded `ListView`s inside `Column`s.
 - `tool/emulator.sh` starts a headless AVD with the **host GPU**; SwiftShader segfaulted on this machine's API 35 image. Use `CARDASH_GPU=swiftshader_indirect` only as a fallback. Use `adb exec-out screencap -p` for screenshots; there may be multiple `adb` binaries on PATH, so use the absolute one.
 - While CarDash is foreground, `MainActivity` consumes hardware volume keys and adjusts `STREAM_MUSIC` so they match the dashboard, unless `AudioManager.isVolumeFixed` is true; on fixed-volume car units the keys pass through to the vehicle amplifier and the on-screen slider is disabled with an explanation. `SystemBridge.setVolume` falls back to relative `adjustStreamVolume` steps when a unit ignores absolute `setStreamVolume`, then to a `FLAG_SHOW_UI` write that may reach the vendor's own volume panel. Long-pressing **Media volume** toggles `SystemController.volumeDiagnostics`, which shows the raw `index/max`/`min`, fixed flag, control source, vendor state, all stream levels and the last write result; the native side logs the same data under `CarDashVolume`. Do not remove this path without providing another sync path; otherwise Android controls the ring stream when no media is playing.
 - FYT/Syu head units (Joying, Junsun, Teyes, Atoto and similar Chinese firmware) own the amplifier in the MCU, so the Android media stream is not what the driver hears. `SyuVolumeBridge` binds `com.syu.ms` (`app.ToolkitService`, action `com.syu.ms.toolkit`), fetches module 4 (`getRemoteModule`, transaction 1) and sends `cmd(0, [level])` for the absolute master volume (command 0 = `FinalSound.C_VOL`, range 0–36); it also subscribes to update 2 (level) and 3 (mute), so knob changes and slider changes stay in sync. `SystemBridge.readVolume` reports `source = syu` while that bridge is connected. This path is verified on a real Android 13 FYT/Syu unit: the on-screen slider and the car's own volume control agree. `MainActivity`'s hardware-key path is unchanged: the car's own knob and steering-wheel volume are handled by the MCU, not by injecting extra steps. No permission, root or manifest change is needed; if the package, service or module is missing the bridge stays disconnected and the `AudioManager` path is used. Never make the vendor bind a hard dependency, and never let a failed binder call crash the app.

+ 1 - 0
README.md

@@ -46,6 +46,7 @@ Jellyfin: [playing on Home](docs/screenshots/home-jellyfin.png) ·
 
 Also: [Home with top-right Settings](docs/screenshots/home-16.png) ·
 [Home at 1024×600](docs/screenshots/home-16-1024.png) ·
+[Aligned home layout](docs/screenshots/home-aligned.png) ·
 [All apps](docs/screenshots/all-apps.png) ·
 [app picker](docs/screenshots/app-picker.png) ·
 [Settings · accent & buttons](docs/screenshots/settings-buttons.png) ·

+ 7 - 2
docs/IMPLEMENTATION_PLAN.md

@@ -1,6 +1,6 @@
 # CarDash implementation plan
 
-_Last updated: 2026-11-01_
+_Last updated: 2026-11-07_
 
 ## 1. What we are building
 
@@ -24,7 +24,8 @@ compatibility:
   Only pinned apps show their own icon by default; the fixed buttons and
   predefined slots stay stylized.
 - The grid adapts from 2 to 6 columns, shrinks its tiles as the count grows, and
-  is anchored to the bottom edge so a short row never floats mid-page.
+  is anchored to the bottom edge so a short row never floats mid-page. The left
+  media/speed stack ends on the tile bottom edge, above the shortcut labels.
 - Every external app opens full-screen through a normal Android `Intent`.
 - Settings: light/dark/automatic theme, six accent colors, battery visibility,
   fixed night window, home-button switches, slot assignment, HOME-role setup,
@@ -511,3 +512,7 @@ Android (Kotlin)
   connection also stays alive while the app is backgrounded and is stopped only
   on detach, so returning from navigation keeps OBD speed instead of falling
   back to GPS. Bumped to 1.2.10+30; no APK build was requested.
+- 2026-11-07: Raised the left Home card stack by the shortcut grid's label
+  allowance so the volume card's bottom edge lines up with the bottom of the
+  shortcut tiles, above their labels, at both regular and compact sizes. No
+  version bump or APK build.

BIN
docs/screenshots/home-aligned.png


+ 40 - 32
lib/features/home/home_screen.dart

@@ -71,6 +71,7 @@ class _HomeScreenState extends State<HomeScreen> {
         ? (screen.width * 0.31).clamp(300.0, 400.0)
         : (screen.width * 0.37).clamp(300.0, 440.0);
     final gap = compact ? 26.0 : 40.0;
+    final shortcutLabelAllowance = ShortcutGrid.labelAllowanceFor(compact);
 
     final obdTemperature = obd.ambientCelsius;
     final useObdTemperature =
@@ -257,41 +258,48 @@ class _HomeScreenState extends State<HomeScreen> {
                         children: [
                           SizedBox(
                             width: leftWidth,
-                            child: Column(
-                              crossAxisAlignment: CrossAxisAlignment.stretch,
-                              mainAxisAlignment: MainAxisAlignment.end,
-                              children: [
-                                if (showObdSpeed) ...[
-                                  ObdCard(
-                                    speedKmh: obd.speedKmh,
-                                    throttlePercent: obd.throttlePercent,
-                                    compact: compact,
-                                  ),
-                                  SizedBox(height: compact ? 12 : 14),
-                                ] else if (showGpsSpeed) ...[
-                                  GpsSpeedCard(
-                                    speedKmh: gps.speedKmh ?? 0,
+                            child: Padding(
+                              padding: EdgeInsets.only(
+                                bottom: shortcutLabelAllowance,
+                              ),
+                              child: Column(
+                                crossAxisAlignment: CrossAxisAlignment.stretch,
+                                mainAxisAlignment: MainAxisAlignment.end,
+                                children: [
+                                  if (showObdSpeed) ...[
+                                    ObdCard(
+                                      speedKmh: obd.speedKmh,
+                                      throttlePercent: obd.throttlePercent,
+                                      compact: compact,
+                                    ),
+                                    SizedBox(height: compact ? 12 : 14),
+                                  ] else if (showGpsSpeed) ...[
+                                    GpsSpeedCard(
+                                      speedKmh: gps.speedKmh ?? 0,
+                                      compact: compact,
+                                    ),
+                                    SizedBox(height: compact ? 12 : 14),
+                                  ],
+                                  VolumeCard(
+                                    volume: system.volume,
                                     compact: compact,
+                                    onSetVolume: system.setVolume,
+                                    onToggleMute: system.toggleMute,
+                                    isMusicPlaying: cardPlaying,
+                                    onPlayPause: jellyfinActive
+                                        ? () => unawaited(
+                                            jellyfinPlayback.toggle(),
+                                          )
+                                        : () =>
+                                              unawaited(_toggleMusic(context)),
+                                    media: cardMedia,
+                                    diagnostics: system.volumeDiagnostics,
+                                    onToggleDiagnostics:
+                                        system.toggleVolumeDiagnostics,
+                                    unconfirmed: system.volumeUnconfirmed,
                                   ),
-                                  SizedBox(height: compact ? 12 : 14),
                                 ],
-                                VolumeCard(
-                                  volume: system.volume,
-                                  compact: compact,
-                                  onSetVolume: system.setVolume,
-                                  onToggleMute: system.toggleMute,
-                                  isMusicPlaying: cardPlaying,
-                                  onPlayPause: jellyfinActive
-                                      ? () =>
-                                            unawaited(jellyfinPlayback.toggle())
-                                      : () => unawaited(_toggleMusic(context)),
-                                  media: cardMedia,
-                                  diagnostics: system.volumeDiagnostics,
-                                  onToggleDiagnostics:
-                                      system.toggleVolumeDiagnostics,
-                                  unconfirmed: system.volumeUnconfirmed,
-                                ),
-                              ],
+                              ),
                             ),
                           ),
                           SizedBox(width: gap),

+ 3 - 1
lib/features/home/widgets/shortcut_grid.dart

@@ -42,6 +42,8 @@ class ShortcutGrid extends StatelessWidget {
   final List<ShortcutSpec> shortcuts;
   final bool compact;
 
+  static double labelAllowanceFor(bool compact) => compact ? 46.0 : 50.0;
+
   @override
   Widget build(BuildContext context) {
     return LayoutBuilder(
@@ -59,7 +61,7 @@ class ShortcutGrid extends StatelessWidget {
         final rowGap = dense
             ? (compact ? 12.0 : 16.0)
             : (compact ? 18.0 : 26.0);
-        final labelAllowance = compact ? 46.0 : 50.0;
+        final labelAllowance = labelAllowanceFor(compact);
         final maxTileSize = compact ? 168.0 : 196.0;
 
         var bestSize = 0.0;