diff --git a/lib/main.dart b/lib/main.dart index a74b0a2..ec11e43 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'pages/battery_health_page.dart'; import 'services/saf_storage_service.dart'; +import 'theme/app_colors.dart'; +import 'theme/theme_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await SafStorageService().init(); + await ThemeService.init(); // 🆕 runApp(const MyApp()); } @@ -15,53 +18,60 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - title: '电池健康度', - theme: ThemeData( - // 🟢 中文用 HarmonyOS Sans SC - fontFamily: 'HarmonyOS_Sans_SC', - // 🟢 数字回退到 FlymeFont - fontFamilyFallback: const ['Flyme'], - // 🟢 统一文字颜色为黑色 - colorScheme: ColorScheme.fromSeed( - seedColor: const Color(0xFFFFFFFF), - ).copyWith(onSurface: Colors.black), - textTheme: const TextTheme( - // 大标题/数字(健康度、容量等) - displayMedium: TextStyle( - fontFamily: 'HarmonyOS_Sans_SC', - fontFamilyFallback: ['Flyme'], - color: Colors.black, - ), - // 标题(如“容量百分比变动趋势”) - headlineMedium: TextStyle( - fontFamily: 'HarmonyOS_Sans_SC', - fontFamilyFallback: ['Flyme'], - color: Colors.black, - ), - // 正文(电压、温度、循环次数等) - bodyMedium: TextStyle( - fontFamily: 'HarmonyOS_Sans_SC', - fontFamilyFallback: ['Flyme'], - color: Colors.black, - ), - // 小字(日期、辅助信息) - bodySmall: TextStyle( - fontFamily: 'HarmonyOS_Sans_SC', - fontFamilyFallback: ['Flyme'], - color: Colors.black87, - ), - // 数字专用(大号数字,如健康度百分比) - displayLarge: TextStyle( - fontFamily: 'HarmonyOS_Sans_SC', - fontFamilyFallback: ['Flyme'], - color: Colors.black, - ), + return ValueListenableBuilder( + valueListenable: ThemeService.notifier, + builder: (context, mode, _) { + return MaterialApp( + title: '电池健康度', + themeMode: mode, + theme: _buildTheme(Brightness.light, kLightColors), + darkTheme: _buildTheme(Brightness.dark, kDarkColors), + home: const BatteryHealthPage(), + debugShowCheckedModeBanner: false, + ); + }, + ); + } + + ThemeData _buildTheme(Brightness brightness, AppColors colors) { + return ThemeData( + useMaterial3: true, + brightness: brightness, + scaffoldBackgroundColor: colors.background, + fontFamily: 'HarmonyOS_Sans_SC', + fontFamilyFallback: const ['Flyme'], + colorScheme: ColorScheme.fromSeed( + seedColor: colors.background, + brightness: brightness, + ), + extensions: [colors], // 🆕 注册 ThemeExtension + textTheme: TextTheme( + displayMedium: TextStyle( + fontFamily: 'HarmonyOS_Sans_SC', + fontFamilyFallback: const ['Flyme'], + color: colors.textPrimary, ), - useMaterial3: true, + headlineMedium: TextStyle( + fontFamily: 'HarmonyOS_Sans_SC', + fontFamilyFallback: const ['Flyme'], + color: colors.textPrimary, + ), + bodyMedium: TextStyle( + fontFamily: 'HarmonyOS_Sans_SC', + fontFamilyFallback: const ['Flyme'], + color: colors.textPrimary, + ), + bodySmall: TextStyle( + fontFamily: 'HarmonyOS_Sans_SC', + fontFamilyFallback: const ['Flyme'], + color: colors.textSecondary, + ), + displayLarge: TextStyle( + fontFamily: 'HarmonyOS_Sans_SC', + fontFamilyFallback: const ['Flyme'], + color: colors.textPrimary, + ), ), - home: const BatteryHealthPage(), - debugShowCheckedModeBanner: false, ); } } diff --git a/lib/pages/battery_health_page.dart b/lib/pages/battery_health_page.dart index f6ea1a9..e210164 100644 --- a/lib/pages/battery_health_page.dart +++ b/lib/pages/battery_health_page.dart @@ -3,6 +3,7 @@ import 'dart:developer' as dev; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import '../theme/theme_service.dart'; // --- 本地导入 --- import '../models/device_matching.dart'; @@ -626,6 +627,56 @@ class _BatteryHealthPageState extends State { return ChargeUtils.getChargeTypeLabel(_isWireless, _chargeType); } + // 🆕 夜间模式切换弹窗 ← 加在这里 + void _showThemePicker() { + showModalBottomSheet( + context: context, + builder: (ctx) { + final current = ThemeService.notifier.value; + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.brightness_auto), + title: const Text('跟随系统'), + trailing: current == ThemeMode.system + ? const Icon(Icons.check) + : null, + onTap: () { + ThemeService.setMode(ThemeMode.system); + Navigator.pop(ctx); + }, + ), + ListTile( + leading: const Icon(Icons.light_mode), + title: const Text('浅色'), + trailing: current == ThemeMode.light + ? const Icon(Icons.check) + : null, + onTap: () { + ThemeService.setMode(ThemeMode.light); + Navigator.pop(ctx); + }, + ), + ListTile( + leading: const Icon(Icons.dark_mode), + title: const Text('深色'), + trailing: current == ThemeMode.dark + ? const Icon(Icons.check) + : null, + onTap: () { + ThemeService.setMode(ThemeMode.dark); + Navigator.pop(ctx); + }, + ), + ], + ), + ); + }, + ); + } + // ---------- Build ---------- @override Widget build(BuildContext context) { @@ -695,7 +746,7 @@ class _BatteryHealthPageState extends State { ); break; case 'dark_mode': - // 夜间模式切换逻辑(1.7.0 再实现) + _showThemePicker(); break; } }, @@ -705,11 +756,7 @@ class _BatteryHealthPageState extends State { value: 'about', // 🟢 新增 child: Text('关于'), ), - const PopupMenuItem( - value: 'dark_mode', - enabled: false, // 暂时灰显 - child: Text('夜间模式(开发中)'), - ), + const PopupMenuItem(value: 'dark_mode', child: Text('夜间模式')), ], ), ], diff --git a/lib/theme/app_colors.dart b/lib/theme/app_colors.dart new file mode 100644 index 0000000..7771f92 --- /dev/null +++ b/lib/theme/app_colors.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; + +@immutable +class AppColors extends ThemeExtension { + final Color background; // 页面背景 + final Color surface; // 卡片/面板背景 + final Color textPrimary; // 主文字 + final Color textSecondary; // 次要文字 + final Color textTertiary; // 弱化文字 + final Color divider; // 分割线 + final Color icon; // 图标色 + + final Color healthGreen; // 健康度 ≥80% + final Color healthYellow; // 70~80% + final Color healthOrange; + final Color healthRed; // <70% 或异常 + final Color barEmpty; // 无数据柱 + + final Color voltUp; // 电压↑ + final Color voltDown; // 电压↓ + final Color tempUp; + final Color tempDown; + + final Color progressBar; // 更新日志进度条 + final Color statusBarTint; // 状态栏背景 + + const AppColors({ + required this.background, + required this.surface, + required this.textPrimary, + required this.textSecondary, + required this.textTertiary, + required this.divider, + required this.icon, + required this.healthGreen, + required this.healthYellow, + required this.healthOrange, + required this.healthRed, + required this.barEmpty, + required this.voltUp, + required this.voltDown, + required this.tempUp, + required this.tempDown, + required this.progressBar, + required this.statusBarTint, + }); + + @override + AppColors copyWith({ + Color? background, + Color? surface, + Color? textPrimary, + Color? textSecondary, + Color? textTertiary, + Color? divider, + Color? icon, + Color? healthGreen, + Color? healthYellow, + Color? healthOrange, + Color? healthRed, + Color? barEmpty, + Color? voltUp, + Color? voltDown, + Color? tempUp, + Color? tempDown, + Color? progressBar, + Color? statusBarTint, + }) { + return AppColors( + background: background ?? this.background, + surface: surface ?? this.surface, + textPrimary: textPrimary ?? this.textPrimary, + textSecondary: textSecondary ?? this.textSecondary, + textTertiary: textTertiary ?? this.textTertiary, + divider: divider ?? this.divider, + icon: icon ?? this.icon, + healthGreen: healthGreen ?? this.healthGreen, + healthYellow: healthYellow ?? this.healthYellow, + healthOrange: healthOrange ?? this.healthOrange, + healthRed: healthRed ?? this.healthRed, + barEmpty: barEmpty ?? this.barEmpty, + voltUp: voltUp ?? this.voltUp, + voltDown: voltDown ?? this.voltDown, + tempUp: tempUp ?? this.tempUp, + tempDown: tempDown ?? this.tempDown, + progressBar: progressBar ?? this.progressBar, + statusBarTint: statusBarTint ?? this.statusBarTint, + ); + } + + @override + AppColors lerp(ThemeExtension? other, double t) { + if (other is! AppColors) return this; + return AppColors( + background: Color.lerp(background, other.background, t)!, + surface: Color.lerp(surface, other.surface, t)!, + textPrimary: Color.lerp(textPrimary, other.textPrimary, t)!, + textSecondary: Color.lerp(textSecondary, other.textSecondary, t)!, + textTertiary: Color.lerp(textTertiary, other.textTertiary, t)!, + divider: Color.lerp(divider, other.divider, t)!, + icon: Color.lerp(icon, other.icon, t)!, + healthGreen: Color.lerp(healthGreen, other.healthGreen, t)!, + healthYellow: Color.lerp(healthYellow, other.healthYellow, t)!, + healthOrange: Color.lerp(healthOrange, other.healthOrange, t)!, + healthRed: Color.lerp(healthRed, other.healthRed, t)!, + barEmpty: Color.lerp(barEmpty, other.barEmpty, t)!, + voltUp: Color.lerp(voltUp, other.voltUp, t)!, + voltDown: Color.lerp(voltDown, other.voltDown, t)!, + tempUp: Color.lerp(tempUp, other.tempUp, t)!, + tempDown: Color.lerp(tempDown, other.tempDown, t)!, + progressBar: Color.lerp(progressBar, other.progressBar, t)!, + statusBarTint: Color.lerp(statusBarTint, other.statusBarTint, t)!, + ); + } +} + +/// 浅色主题(保持现有视觉) +const AppColors kLightColors = AppColors( + background: Colors.white, + surface: Color(0xFFF0F4F8), + textPrimary: Colors.black, + textSecondary: Color(0xFF5F5F5F), + textTertiary: Colors.grey, + divider: Color(0xFFE0E0E0), + icon: Colors.black, + healthGreen: Color(0xFF00B140), + healthYellow: Color(0xFFEFDF00), + healthOrange: Color(0xFFD75A16), + healthRed: Colors.red, + barEmpty: Color(0xFFE0E0E0), + voltUp: Colors.red, + voltDown: Colors.blue, + tempUp: Colors.red, + tempDown: Colors.blue, + progressBar: Color.fromRGBO(157, 212, 237, 1.0), + statusBarTint: Color.fromRGBO(157, 212, 237, 0.1), +); + +/// 深色主题(色相保留,明度调整) +const AppColors kDarkColors = AppColors( + background: Color(0xFF121212), + surface: Color(0xFF1E1E1E), + textPrimary: Color(0xFFE8E8E8), + textSecondary: Color(0xFFA0A0A0), + textTertiary: Color(0xFF707070), + divider: Color(0xFF2A2A2A), + icon: Color(0xFFE8E8E8), + healthGreen: Color(0xFF00D14D), + healthYellow: Color(0xFFD4C520), + healthOrange: Color(0xFFFF7838), + healthRed: Color(0xFFFF5252), + barEmpty: Color(0xFF2A2A2A), + voltUp: Color(0xFFFF5252), + voltDown: Color(0xFF5AA9FF), + tempUp: Color(0xFFFF5252), + tempDown: Color(0xFF5AA9FF), + progressBar: Color(0xFF5A9BBD), + statusBarTint: Color(0xFF121212), +); + +/// 便捷扩展:context.colors.textPrimary +extension AppColorsX on BuildContext { + AppColors get colors => Theme.of(this).extension()!; +} diff --git a/lib/theme/theme_service.dart b/lib/theme/theme_service.dart new file mode 100644 index 0000000..0d474fc --- /dev/null +++ b/lib/theme/theme_service.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeService { + static const _prefKey = 'theme_mode'; + static final ValueNotifier notifier = ValueNotifier( + ThemeMode.system, + ); + + /// 启动时调用(main.dart) + static Future init() async { + final prefs = await SharedPreferences.getInstance(); + final saved = prefs.getString(_prefKey); + notifier.value = _parse(saved); + } + + /// 菜单里调用 + static Future setMode(ThemeMode mode) async { + notifier.value = mode; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_prefKey, _name(mode)); + } + + static ThemeMode _parse(String? s) { + switch (s) { + case 'light': + return ThemeMode.light; + case 'dark': + return ThemeMode.dark; + default: + return ThemeMode.system; + } + } + + static String _name(ThemeMode m) { + switch (m) { + case ThemeMode.light: + return 'light'; + case ThemeMode.dark: + return 'dark'; + case ThemeMode.system: + return 'system'; + } + } +}