import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/theme/accent_choice.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/theme_controller.dart'; import '../../core/theme/theme_preference.dart'; import '../../core/widgets/app_icon.dart'; import '../../core/widgets/pressable.dart'; import '../../core/widgets/surface_card.dart'; import '../../data/models/slot_type.dart'; import '../apps/app_picker_sheet.dart'; import '../home/launcher_controller.dart'; import '../lock/lock_controller.dart'; import '../lock/pin_setup_screen.dart'; import 'car_data_section.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final themeController = context.watch(); final launcher = context.watch(); final lock = context.watch(); return Scaffold( backgroundColor: colors.canvas, body: Container( decoration: homeBackground(colors, Theme.of(context).brightness), child: SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(32, 22, 32, 32), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 980), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Pressable( onTap: () => Navigator.of(context).pop(), semanticLabel: 'Back', pressedScale: 0.92, child: Container( width: 46, height: 46, decoration: BoxDecoration( color: colors.surface, shape: BoxShape.circle, border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/arrow-left.svg', size: 20, color: colors.ink, ), ), ), const SizedBox(width: 18), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Settings', style: theme.textTheme.headlineLarge, ), const SizedBox(height: 2), Text( 'Theme, car data and access', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ), ], ), const SizedBox(height: 32), _SectionTitle( title: 'General', colors: colors, theme: theme, ), const SizedBox(height: 12), SurfaceCard( padding: const EdgeInsets.fromLTRB(26, 22, 26, 18), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text('Theme', style: theme.textTheme.titleMedium), const Spacer(), _ThemeSelector( selected: themeController.preference, onSelected: themeController.setPreference, ), ], ), const SizedBox(height: 8), Text( themeController.preference == ThemePreference.automatic ? 'Switches automatically at the times below.' : themeController.preference == ThemePreference.dark ? 'Always dark.' : 'Always light.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), const SizedBox(height: 8), Divider(color: colors.border, height: 24), Row( children: [ Text( 'Accent', style: theme.textTheme.titleMedium, ), const Spacer(), Text( themeController.accent.label, style: theme.textTheme.labelMedium?.copyWith( color: colors.inkMuted, ), ), ], ), const SizedBox(height: 10), _AccentSelector( selected: themeController.accent, onSelected: (choice) { themeController.setAccent(choice); }, ), if (themeController.preference == ThemePreference.automatic) ...[ const SizedBox(height: 8), Divider(color: colors.border, height: 24), _ActionRow( iconAsset: 'assets/icons/moon.svg', title: 'Dark from', value: _formatMinutes( context, themeController.nightStartMinutes, ), onTap: () => _pickTime( context, themeController, start: true, ), ), const SizedBox(height: 2), _ActionRow( iconAsset: 'assets/icons/sun.svg', title: 'Light from', value: _formatMinutes( context, themeController.nightEndMinutes, ), onTap: () => _pickTime( context, themeController, start: false, ), ), ], Divider(color: colors.border, height: 24), _BatteryToggleRow( value: themeController.showBattery, onChanged: (value) { themeController.setShowBattery(value); }, ), Divider(color: colors.border, height: 30), Row( children: [ Text( 'Home buttons', style: theme.textTheme.titleMedium, ), const Spacer(), Text( '${launcher.homeButtonCount} / ' '${LauncherController.maxHomeButtons}', style: theme.textTheme.labelMedium?.copyWith( color: colors.inkMuted, ), ), ], ), const SizedBox(height: 3), Text( 'Turn one off to hide it. All apps is always shown; Settings lives in the top-right status area.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), const SizedBox(height: 4), for (final slot in SlotType.values) _ShortcutToggleRow( slot: slot, launcher: launcher, onPick: () => showAppPicker(context, slot), ), Divider(color: colors.border, height: 30), _HomeRoleRow(launcher: launcher), ], ), ), const SizedBox(height: 30), _SectionTitle( title: 'Car data', colors: colors, theme: theme, ), const SizedBox(height: 12), SurfaceCard( padding: const EdgeInsets.fromLTRB(26, 22, 26, 20), child: const CarDataSection(), ), const SizedBox(height: 30), _SectionTitle( title: 'Security', colors: colors, theme: theme, ), const SizedBox(height: 12), SurfaceCard( padding: const EdgeInsets.fromLTRB(26, 22, 26, 22), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: colors.accentSoft, borderRadius: BorderRadius.circular(14), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/lock.svg', size: 20, color: colors.accent, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'PIN protection', style: theme.textTheme.titleMedium, ), const SizedBox(height: 2), Text( lock.pinSet ? 'On' : 'Off', style: theme.textTheme.bodySmall ?.copyWith( color: lock.pinSet ? colors.accent : colors.inkMuted, fontWeight: FontWeight.w600, ), ), ], ), ), if (!lock.pinSet) _SmallButton( label: 'Set PIN', onTap: () => _openPinSetup(context, PinSetupMode.set), ), ], ), if (lock.pinSet) ...[ const SizedBox(height: 10), Divider(color: colors.border, height: 24), _ActionRow( iconAsset: 'assets/icons/lock.svg', title: 'Change PIN', trailing: const _Chevron(), onTap: () => _openPinSetup(context, PinSetupMode.change), ), _ActionRow( iconAsset: 'assets/icons/x.svg', title: 'Remove PIN', trailing: const _Chevron(), onTap: () => _openPinSetup(context, PinSetupMode.remove), ), ], const SizedBox(height: 16), Text( 'The PIN is requested only when CarDash starts. It does not lock ' 'again when you switch apps or turn the screen off. It protects the ' 'launcher, not the device itself.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, height: 1.5, ), ), ], ), ), const SizedBox(height: 28), Center( child: Text( 'CarDash 0.7.0 · Android 10+', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkFaint, ), ), ), ], ), ), ), ), ), ), ); } Future _openPinSetup(BuildContext context, PinSetupMode mode) async { final result = await Navigator.of( context, ).push(MaterialPageRoute(builder: (_) => PinSetupScreen(mode: mode))); if (result != true || !context.mounted) return; final message = switch (mode) { PinSetupMode.set => 'PIN set. It will be requested the next time CarDash starts.', PinSetupMode.change => 'PIN changed.', PinSetupMode.remove => 'PIN removed.', }; ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } Future _pickTime( BuildContext context, ThemeController controller, { required bool start, }) async { final minutes = start ? controller.nightStartMinutes : controller.nightEndMinutes; final picked = await showTimePicker( context: context, initialTime: TimeOfDay(hour: minutes ~/ 60, minute: minutes % 60), helpText: start ? 'Dark from' : 'Light from', builder: (context, child) { final theme = Theme.of(context); return Theme( data: theme.copyWith( timePickerTheme: TimePickerThemeData( backgroundColor: theme.colorScheme.surface, dialBackgroundColor: theme.colorScheme.surfaceContainerHighest, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), ), ), ), child: child ?? const SizedBox.shrink(), ); }, ); if (picked == null || !context.mounted) return; final newValue = picked.hour * 60 + picked.minute; await controller.setNightWindow( startMinutes: start ? newValue : controller.nightStartMinutes, endMinutes: start ? controller.nightEndMinutes : newValue, ); } String _formatMinutes(BuildContext context, int minutes) { return TimeOfDay(hour: minutes ~/ 60, minute: minutes % 60).format(context); } } class _SectionTitle extends StatelessWidget { const _SectionTitle({ required this.title, required this.colors, required this.theme, }); final String title; final AppColors colors; final ThemeData theme; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 4), child: Text( title, style: theme.textTheme.titleLarge?.copyWith(color: colors.inkMuted), ), ); } } class _ThemeSelector extends StatelessWidget { const _ThemeSelector({required this.selected, required this.onSelected}); final ThemePreference selected; final ValueChanged onSelected; @override Widget build(BuildContext context) { final colors = context.colors; return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: colors.surfaceStrong, borderRadius: BorderRadius.circular(999), border: Border.all(color: colors.border), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ for (final preference in ThemePreference.values) Pressable( onTap: () => onSelected(preference), semanticLabel: 'Theme ${preference.storageValue}', pressedScale: 0.95, child: AnimatedContainer( duration: const Duration(milliseconds: 160), curve: Curves.easeOutCubic, padding: const EdgeInsets.symmetric( horizontal: 18, vertical: 9, ), decoration: BoxDecoration( color: selected == preference ? colors.accent : Colors.transparent, borderRadius: BorderRadius.circular(999), ), child: Text( switch (preference) { ThemePreference.light => 'Light', ThemePreference.dark => 'Dark', ThemePreference.automatic => 'Auto', }, style: Theme.of(context).textTheme.labelMedium?.copyWith( color: selected == preference ? colors.accentInk : colors.inkMuted, fontWeight: FontWeight.w600, ), ), ), ), ], ), ); } } class _AccentSelector extends StatelessWidget { const _AccentSelector({required this.selected, required this.onSelected}); final AccentChoice selected; final ValueChanged onSelected; @override Widget build(BuildContext context) { final colors = context.colors; final brightness = Theme.of(context).brightness; return Wrap( spacing: 10, runSpacing: 10, children: [ for (final choice in AccentChoice.values) Semantics( label: '${choice.label} accent', selected: choice == selected, button: true, child: Material( color: Colors.transparent, shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), onTap: () => onSelected(choice), child: Padding( padding: const EdgeInsets.all(3), child: AnimatedContainer( duration: const Duration(milliseconds: 180), width: 30, height: 30, decoration: BoxDecoration( shape: BoxShape.circle, color: choice.accentFor(brightness), border: Border.all( color: choice == selected ? colors.ink : colors.border, width: choice == selected ? 2 : 1, ), ), alignment: Alignment.center, child: choice == selected ? Icon( Icons.check_rounded, size: 16, color: choice.inkFor(brightness), ) : null, ), ), ), ), ), ], ); } } class _SettingsSwitch extends StatelessWidget { const _SettingsSwitch({ required this.value, required this.onChanged, required this.semanticsLabel, }); final bool value; final ValueChanged onChanged; final String semanticsLabel; @override Widget build(BuildContext context) { final colors = context.colors; return Semantics( label: semanticsLabel, child: Switch( value: value, onChanged: onChanged, thumbColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) ? colors.accentInk : colors.inkFaint, ), trackColor: WidgetStateProperty.resolveWith( (states) => states.contains(WidgetState.selected) ? colors.accent : colors.surfaceStrong, ), trackOutlineColor: WidgetStatePropertyAll(colors.border), ), ); } } class _BatteryToggleRow extends StatelessWidget { const _BatteryToggleRow({required this.value, required this.onChanged}); final bool value; final ValueChanged onChanged; @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); return Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: colors.surfaceStrong, borderRadius: BorderRadius.circular(13), border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/battery-charging.svg', size: 18, color: colors.inkMuted, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Battery & charging', style: theme.textTheme.bodyLarge), const SizedBox(height: 2), Text( 'Show the battery level and charging bolt.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ), ), const SizedBox(width: 8), _SettingsSwitch( value: value, semanticsLabel: value ? 'Hide battery and charging status' : 'Show battery and charging status', onChanged: onChanged, ), ], ); } } class _ActionRow extends StatelessWidget { const _ActionRow({ required this.iconAsset, required this.title, this.value, this.trailing, this.onTap, }); final String iconAsset; final String title; final String? value; final Widget? trailing; final VoidCallback? onTap; @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final content = Padding( padding: const EdgeInsets.symmetric(vertical: 7), child: Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: colors.surfaceStrong, borderRadius: BorderRadius.circular(13), border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon(iconAsset, size: 18, color: colors.inkMuted), ), const SizedBox(width: 14), Expanded(child: Text(title, style: theme.textTheme.bodyLarge)), if (value != null) Text( value!, style: theme.textTheme.labelLarge?.copyWith( color: colors.inkMuted, ), ), if (trailing != null) ...[const SizedBox(width: 8), trailing!], ], ), ); if (onTap == null) return content; return Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(14), onTap: onTap, child: content, ), ); } } class _ShortcutToggleRow extends StatelessWidget { const _ShortcutToggleRow({ required this.slot, required this.launcher, required this.onPick, }); final SlotType slot; final LauncherController launcher; final VoidCallback onPick; @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final enabled = launcher.isSlotEnabled(slot); final assignment = launcher.assignmentFor(slot); return Row( children: [ Expanded( child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(14), onTap: onPick, child: Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ Opacity( opacity: enabled ? 1 : 0.45, child: Container( width: 38, height: 38, decoration: BoxDecoration( color: colors.surfaceStrong, borderRadius: BorderRadius.circular(13), border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( slot.iconAsset, size: 18, color: colors.inkMuted, ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( slot.label, style: theme.textTheme.bodyLarge?.copyWith( color: enabled ? colors.ink : colors.inkFaint, ), ), const SizedBox(height: 2), Text( assignment?.label ?? 'Select app', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: !enabled ? colors.inkFaint : assignment == null ? colors.accent : colors.inkMuted, fontWeight: assignment == null && enabled ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ), ], ), ), ), ), ), const SizedBox(width: 8), _SettingsSwitch( value: enabled, semanticsLabel: enabled ? 'Hide ${slot.label} on Home' : 'Show ${slot.label} on Home', onChanged: (value) async { final error = await launcher.setSlotEnabled(slot, value); if (error != null && context.mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(error))); } }, ), ], ); } } class _Chevron extends StatelessWidget { const _Chevron(); @override Widget build(BuildContext context) { return AppIcon( 'assets/icons/chevron-right.svg', size: 18, color: context.colors.inkFaint, ); } } class _SmallButton extends StatelessWidget { const _SmallButton({required this.label, required this.onTap}); final String label; final VoidCallback onTap; @override Widget build(BuildContext context) { final colors = context.colors; return Pressable( onTap: onTap, semanticLabel: label, pressedScale: 0.95, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: colors.accent, borderRadius: BorderRadius.circular(999), ), child: Text( label, style: Theme.of(context).textTheme.labelMedium ?.copyWith(color: colors.accentInk, fontWeight: FontWeight.w600), ), ), ); } } class _HomeRoleRow extends StatelessWidget { const _HomeRoleRow({required this.launcher}); final LauncherController launcher; @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final isDefault = launcher.isDefaultHome; return Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: colors.surfaceStrong, borderRadius: BorderRadius.circular(13), border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/smartphone.svg', size: 18, color: colors.inkMuted, ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Home app', style: theme.textTheme.bodyLarge), const SizedBox(height: 2), Text( launcher.defaultHomeKnown ? (isDefault ? 'CarDash is your home app' : 'Not set') : 'Checking…', style: theme.textTheme.bodySmall?.copyWith( color: isDefault ? colors.accent : colors.inkMuted, fontWeight: isDefault ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ), if (launcher.defaultHomeKnown && !isDefault) _SmallButton( label: 'Set as default', onTap: () async { final launched = await launcher.requestDefaultHome(); if (!launched && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Could not open the home app settings.'), ), ); } }, ) else if (isDefault) Icon(Icons.check_circle_rounded, color: colors.accent, size: 22), ], ); } }