浏览代码

feat: show update dot on settings gear

Add a small yellow indicator to the top-right Settings gear whenever UpdateController reports an available release. Rebuild the 1.0.0 tester and 1.0.1 release APKs with the indicator; update checksum and docs. Verified on the emulator with 1.0.0 against the 1.0.1 release.
Krasimir Kazakov 1 周之前
父节点
当前提交
920278a9cc

+ 1 - 1
AGENTS.md

@@ -97,7 +97,7 @@ notifyListeners();
 - The Music equalizer follows `AudioManager.isMusicActive`. Now-playing title/progress use the notification-listener component and active MediaSession; never read or store notification content.
 - Night mode is a full-screen black overlay with a dim clock. Long-press only on empty background; tap anywhere to dismiss. It does not change brightness or add a wake lock.
 - GPS speed is a fallback only when no OBD adapter is connected. It starts only with already-granted fine location and GPS; never prompt from that path, and clamp stationary noise below 2 km/h to 0.
-- Updates use a non-blocking public Gogs release check. The Update action downloads the APK to cache and opens Android’s package installer; never install silently. REQUEST_INSTALL_PACKAGES + one-time unknown-apps permission is required.
+- Updates use a non-blocking public Gogs release check. The Update action downloads the APK to cache and opens Android’s package installer; never install silently. REQUEST_INSTALL_PACKAGES + one-time unknown-apps permission is required. Show the yellow dot on the Settings gear whenever `UpdateController.updateAvailable` is true.
 - ADB installs: always try at most twice per connected device. If both attempts fail, stop silently rather than looping or switching install methods.
 - OBD is Bluetooth Classic SPP, bonded-only and read-only, and still needs a real-car test; weather uses Open-Meteo and one-shot location and stores no coordinates.
 - The circular ring is song progress; its centre is play/pause and the Mute pill is separate. Playback uses Android media keys; title/progress need the user-granted notification access, otherwise the card falls back to Playing / Nothing playing.

+ 6 - 5
README.md

@@ -80,7 +80,8 @@ live vehicle data.
   without blocking the dashboard. Settings → Updates shows the current version,
   a Check button, and an Update action that downloads the APK and opens Android’s
   installer. Android still requires confirmation and a one-time “install unknown
-  apps” allowance; silent installation is not attempted.
+  apps” allowance; silent installation is not attempted. A small yellow dot
+  appears on the Settings gear whenever an update is available.
 - **Now-playing access** — Settings → Media grants Android notification access,
   used only to read the active media session’s title, duration and position.
   CarDash never reads or stores notification content.
@@ -289,13 +290,13 @@ flutter test
 ./tool/check.sh
 ```
 
-The 50 tests cover PIN hashing/persistence, settings persistence, theme/accent,
+The 51 tests cover PIN hashing/persistence, settings persistence, theme/accent,
 dark accent tinting, battery visibility, first-run state, slot visibility,
 pinned apps, custom labels/icons, the 16-button cap at 1280×800, 1280×720 and
 1024×600, the dense shortcut grid, the equalizer animation, OBD economy, the OBD
 card, media-key playback, media metadata/progress, the marquee, the play/pause
 ring, the keep-awake preference, the night overlay, GPS speed fallback and the
-update version/availability logic. Native package
+update version/availability logic and the Settings update dot. Native package
 enumeration, the HOME role, volume, app launching and PIN flows were verified on
 API 35 and API 29 emulators. Location permission, reverse geocoding and the
 Open-Meteo fallback were verified live on the API 35 emulator. Now-playing
@@ -304,8 +305,8 @@ MediaSession. The OBD hardware path compiles and is covered by the parser/econom
 design, but no ELM327 adapter is available on this machine; socket behavior still
 needs a car test. Release
 The 1.0.1 universal APK is built from this tree. The emulator completed a live
-1.0.0 → 1.0.1 in-app update; the physical tablet has the 1.0.0 installer build
-ready for its own test.
+1.0.0 → 1.0.1 in-app update and shows the yellow update dot; the tablet is on
+the 1.0.1 dot build.
 
 ## Troubleshooting the emulator
 

+ 2 - 1
docs/IMPLEMENTATION_PLAN.md

@@ -77,7 +77,7 @@ compatibility:
 | Economy | 5 km MAF rolling window, petrol constants, fuel-cut guard | Matches the design note; show `—` until 0.5 km. |
 | Weather fallback | Open-Meteo + one-shot device location, Settings toggle | No API key/account; hide the row on any failure. |
 | GPS speed fallback | Poll GPS updates only when no OBD is connected and fine location is already granted | No permission prompt; 0 below 2 km/h to suppress stationary drift. |
-| Update check | Public Gogs releases API, non-blocking startup check, Settings card | Update downloads the APK and opens Android’s installer; installation stays user-confirmed. |
+| Update check | Public Gogs releases API, non-blocking startup check, Settings card | Update downloads the APK and opens Android’s installer; installation stays user-confirmed. Yellow dot on Settings gear when available. |
 | Temperature priority | OBD ambient, else location weather | OBD wins whenever a fresh value exists. |
 | Accent background | Dark canvas/surfaces/borders blend toward the accent | Fixes the green base showing through when Blue is selected. |
 | Screen | `FLAG_KEEP_SCREEN_ON` controlled by the Settings keep-awake switch | Default on; no wake-lock permission, and off restores the system display timeout. |
@@ -198,6 +198,7 @@ Android (Kotlin)
 - [x] 1.0.0 updater build installed on emulator/tablet; it reported up to date before 1.0.1.
 - [x] 1.0.1 universal APK built and added to `releases/`; release published for update testing.
 - [x] Emulator completed the full in-app 1.0.0 → 1.0.1 download/installer flow.
+- [x] Yellow update dot appears on the Settings gear; 51 tests pass.
 - [x] Tablet has the 1.0.0 installer build; unknown-app confirmation is user-controlled.
 - [x] Screenshots in `docs/screenshots/`; README/AGENTS/plan/OBD doc updated.
 

+ 7 - 0
lib/core/theme/app_colors.dart

@@ -22,6 +22,7 @@ class AppColors extends ThemeExtension<AppColors> {
     required this.accentSoft,
     required this.border,
     required this.danger,
+    required this.warning,
     required this.shadow,
   });
 
@@ -37,6 +38,7 @@ class AppColors extends ThemeExtension<AppColors> {
   final Color accentSoft;
   final Color border;
   final Color danger;
+  final Color warning;
   final Color shadow;
 
   static const light = AppColors(
@@ -52,6 +54,7 @@ class AppColors extends ThemeExtension<AppColors> {
     accentSoft: Color(0xFFDCEAD4),
     border: Color(0xFFDDDCD4),
     danger: Color(0xFFA74736),
+    warning: Color(0xFFD89B00),
     shadow: Color(0xFF141A12),
   );
 
@@ -107,6 +110,7 @@ class AppColors extends ThemeExtension<AppColors> {
     accentSoft: Color(0xFF22321B),
     border: Color(0xFF293026),
     danger: Color(0xFFE08A76),
+    warning: Color(0xFFFFC928),
     shadow: Color(0xFF000000),
   );
 
@@ -124,6 +128,7 @@ class AppColors extends ThemeExtension<AppColors> {
     Color? accentSoft,
     Color? border,
     Color? danger,
+    Color? warning,
     Color? shadow,
   }) {
     return AppColors(
@@ -139,6 +144,7 @@ class AppColors extends ThemeExtension<AppColors> {
       accentSoft: accentSoft ?? this.accentSoft,
       border: border ?? this.border,
       danger: danger ?? this.danger,
+      warning: warning ?? this.warning,
       shadow: shadow ?? this.shadow,
     );
   }
@@ -159,6 +165,7 @@ class AppColors extends ThemeExtension<AppColors> {
       accentSoft: Color.lerp(accentSoft, other.accentSoft, t)!,
       border: Color.lerp(border, other.border, t)!,
       danger: Color.lerp(danger, other.danger, t)!,
+      warning: Color.lerp(warning, other.warning, t)!,
       shadow: Color.lerp(shadow, other.shadow, t)!,
     );
   }

+ 3 - 0
lib/features/home/home_screen.dart

@@ -17,6 +17,7 @@ import 'obd_controller.dart';
 import 'pinned_app_actions.dart';
 import 'system_controller.dart';
 import 'weather_controller.dart';
+import '../settings/update_controller.dart';
 import 'widgets/clock_block.dart';
 import 'widgets/gps_speed_card.dart';
 import 'widgets/night_mode_overlay.dart';
@@ -55,6 +56,7 @@ class _HomeScreenState extends State<HomeScreen> {
     final obd = context.watch<ObdController>();
     final gps = context.watch<GpsSpeedController>();
     final weather = context.watch<WeatherController>();
+    final update = context.watch<UpdateController>();
     final screen = MediaQuery.sizeOf(context);
     final compact = screen.height < 700 || screen.width < 1100;
     final horizontalPadding = compact ? 30.0 : 44.0;
@@ -191,6 +193,7 @@ class _HomeScreenState extends State<HomeScreen> {
                           compact: compact,
                           showBattery: appearance.showBattery,
                           onOpenSettings: () => _openSettings(context),
+                          showUpdateAvailable: update.updateAvailable,
                         ),
                       ],
                     ),

+ 36 - 14
lib/features/home/widgets/status_cluster.dart

@@ -14,12 +14,14 @@ class StatusCluster extends StatelessWidget {
     this.compact = false,
     this.showBattery = true,
     this.onOpenSettings,
+    this.showUpdateAvailable = false,
   });
 
   final SystemStatus status;
   final bool compact;
   final bool showBattery;
   final VoidCallback? onOpenSettings;
+  final bool showUpdateAvailable;
 
   @override
   Widget build(BuildContext context) {
@@ -56,20 +58,40 @@ class StatusCluster extends StatelessWidget {
               onTap: onOpenSettings!,
               semanticLabel: 'Open settings',
               pressedScale: 0.92,
-              child: Container(
-                width: compact ? 28 : 30,
-                height: compact ? 28 : 30,
-                decoration: BoxDecoration(
-                  color: colors.surfaceStrong,
-                  shape: BoxShape.circle,
-                  border: Border.all(color: colors.border),
-                ),
-                alignment: Alignment.center,
-                child: AppIcon(
-                  'assets/icons/settings.svg',
-                  size: compact ? 15 : 17,
-                  color: colors.inkMuted,
-                ),
+              child: Stack(
+                clipBehavior: Clip.none,
+                children: [
+                  Container(
+                    width: compact ? 28 : 30,
+                    height: compact ? 28 : 30,
+                    decoration: BoxDecoration(
+                      color: colors.surfaceStrong,
+                      shape: BoxShape.circle,
+                      border: Border.all(color: colors.border),
+                    ),
+                    alignment: Alignment.center,
+                    child: AppIcon(
+                      'assets/icons/settings.svg',
+                      size: compact ? 15 : 17,
+                      color: colors.inkMuted,
+                    ),
+                  ),
+                  if (showUpdateAvailable)
+                    Positioned(
+                      top: -2,
+                      right: -2,
+                      child: Container(
+                        key: const Key('settings-update-dot'),
+                        width: 9,
+                        height: 9,
+                        decoration: BoxDecoration(
+                          color: colors.warning,
+                          shape: BoxShape.circle,
+                          border: Border.all(color: colors.surface, width: 1.5),
+                        ),
+                      ),
+                    ),
+                ],
               ),
             ),
           ],

+ 1 - 1
releases/README.md

@@ -5,7 +5,7 @@
 - **File:** `cardash-android-1.0.1.apk`
 - **Version:** 1.0.1 (versionCode 13)
 - **ABIs:** arm64-v8a, armeabi-v7a, x86_64
-- **SHA-256:** `e7fee1eecb428d05003d01688ad561c614a392012629f96e701eb36ce972f6a7`
+- **SHA-256:** `f229414c8c406a2f6754f9ee1ccc4763ba1742acd277765962b61ecaaf356329`
 - **Changes:** version bump only; no functional changes from the 1.0.0 updater build.
 - The in-app Updates action downloads this APK and opens Android’s installer; the
   user still confirms the installation and allows unknown-app installs once.

二进制
releases/cardash-android-1.0.1.apk


+ 1 - 1
releases/cardash-android-1.0.1.apk.sha256

@@ -1 +1 @@
-e7fee1eecb428d05003d01688ad561c614a392012629f96e701eb36ce972f6a7  releases/cardash-android-1.0.1.apk
+f229414c8c406a2f6754f9ee1ccc4763ba1742acd277765962b61ecaaf356329  releases/cardash-android-1.0.1.apk

+ 28 - 0
test/status_cluster_test.dart

@@ -0,0 +1,28 @@
+import 'package:cardash_launcher/core/theme/app_theme.dart';
+import 'package:cardash_launcher/data/models/system_status.dart';
+import 'package:cardash_launcher/features/home/widgets/status_cluster.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('shows the yellow update dot on the Settings gear', (
+    tester,
+  ) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        theme: AppTheme.dark(),
+        home: Scaffold(
+          body: Center(
+            child: StatusCluster(
+              status: const SystemStatus(batteryLevel: 80, wifi: true),
+              showUpdateAvailable: true,
+              onOpenSettings: () {},
+            ),
+          ),
+        ),
+      ),
+    );
+
+    expect(find.byKey(const Key('settings-update-dot')), findsOneWidget);
+  });
+}