import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/app_clock.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/theme_controller.dart'; import '../../core/widgets/app_icon.dart'; import '../../core/widgets/surface_card.dart'; import '../../data/models/slot_assignment.dart'; import '../../data/models/slot_type.dart'; import '../apps/all_apps_screen.dart'; import '../apps/app_picker_sheet.dart'; import '../settings/settings_screen.dart'; import 'launcher_controller.dart'; import 'system_controller.dart'; import 'widgets/clock_block.dart'; import 'widgets/shortcut_grid.dart'; import 'widgets/status_cluster.dart'; import 'widgets/volume_card.dart'; /// The only home page. No app bar, no dock, no pages: information, the main /// volume control, and up to 16 configurable shortcuts. class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final launcher = context.watch(); final system = context.watch(); final appearance = context.watch(); final screen = MediaQuery.sizeOf(context); final compact = screen.height < 700 || screen.width < 1100; final horizontalPadding = compact ? 30.0 : 44.0; final denseHome = launcher.homeButtonCount > 8; final leftWidth = denseHome ? (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 showHomeBanner = launcher.defaultHomeKnown && !launcher.isDefaultHome; ShortcutSpec slotSpec(SlotType slot) { final assignment = launcher.assignmentFor(slot); final app = assignment == null ? null : launcher.appForAssignment(assignment); final playing = slot == SlotType.music && system.isMusicPlaying && assignment != null; final nonSystemIcon = app != null && !app.isSystemApp ? app.icon : null; return ShortcutSpec( title: slot.label, subtitle: assignment?.label ?? 'Select app', iconAsset: slot.iconAsset, iconBytes: playing ? app?.icon : nonSystemIcon, showEqualizer: playing, assigned: assignment != null, onTap: () => _openSlot(context, slot), onLongPress: () => showAppPicker(context, slot), longPressHint: 'change the app', ); } ShortcutSpec pinnedSpec(SlotAssignment pinned) { final app = launcher.appForAssignment(pinned); final useRealIcon = app != null && !app.isSystemApp; return ShortcutSpec( title: pinned.label.isEmpty ? pinned.packageName : pinned.label, subtitle: null, iconAsset: 'assets/icons/app-window.svg', iconBytes: useRealIcon ? app.icon : null, assigned: true, onTap: () => _openPinned(context, pinned), onLongPress: () => _removePinned(context, pinned), longPressHint: 'remove from Home', ); } final shortcuts = [ for (final slot in SlotType.values) if (launcher.isSlotEnabled(slot)) slotSpec(slot), for (final pinned in launcher.pinnedApps) pinnedSpec(pinned), ShortcutSpec( title: 'Settings', subtitle: 'Theme & PIN', iconAsset: 'assets/icons/settings.svg', assigned: true, onTap: () => _openSettings(context), ), ShortcutSpec( title: 'All apps', subtitle: 'Installed apps', iconAsset: 'assets/icons/layout-grid.svg', assigned: true, onTap: () => _openAllApps(context), ), ]; return PopScope( canPop: false, child: Scaffold( backgroundColor: colors.canvas, body: Stack( children: [ Positioned.fill( child: DecoratedBox( decoration: homeBackground( colors, Theme.of(context).brightness, ), ), ), Positioned( top: -190, right: -120, child: IgnorePointer( child: Container( width: 560, height: 560, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ colors.accent.withValues( alpha: Theme.of(context).brightness == Brightness.dark ? 0.16 : 0.10, ), colors.accent.withValues(alpha: 0), ], ), ), ), ), ), SafeArea( minimum: EdgeInsets.fromLTRB( horizontalPadding, compact ? 18 : 24, horizontalPadding, compact ? 14 : 18, ), child: SizedBox.expand( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClockBlock(compact: compact), const Spacer(), StatusCluster( status: system.status, compact: compact, showBattery: appearance.showBattery, ), ], ), SizedBox(height: compact ? 16 : 24), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( width: leftWidth, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.end, children: [ if (showHomeBanner) ...[ _HomeRoleBanner( launcher: launcher, onSetUp: () => _requestHome(context), ), const SizedBox(height: 14), ], VolumeCard( volume: system.volume, compact: compact, onSetVolume: system.setVolume, onToggleMute: system.toggleMute, ), ], ), ), SizedBox(width: gap), Expanded( child: Column( children: [ Expanded( child: ShortcutGrid( shortcuts: shortcuts, compact: compact, ), ), if (!compact) ...[ const SizedBox(height: 4), _HomeHint( text: launcher.pinnedApps.isEmpty ? 'Hold a shortcut to change the app' : 'Hold a slot to change ยท hold an added app to remove', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkFaint, ), ), ], ], ), ), ], ), ), ], ), ), ), ], ), ), ); } Future _openPinned( BuildContext context, SlotAssignment assignment, ) async { final error = await context.read().launchAssignment( assignment, ); if (error != null && context.mounted) { _showMessage(context, error); } } void _removePinned(BuildContext context, SlotAssignment assignment) { unawaited(context.read().removePinnedApp(assignment)); _showMessage( context, '${assignment.label.isEmpty ? 'App' : assignment.label} removed from Home', ); } Future _openSlot(BuildContext context, SlotType slot) async { final launcher = context.read(); if (launcher.assignmentFor(slot) == null) { await showAppPicker(context, slot); return; } final error = await launcher.launchSlot(slot); if (error != null && context.mounted) { _showMessage(context, error); } } void _openSettings(BuildContext context) { Navigator.of(context) .push(MaterialPageRoute(builder: (_) => const SettingsScreen())); } void _openAllApps(BuildContext context) { Navigator.of(context) .push(MaterialPageRoute(builder: (_) => const AllAppsScreen())); } Future _requestHome(BuildContext context) async { final launcher = context.read(); final launched = await launcher.requestDefaultHome(); if (!launched && context.mounted) { _showMessage(context, 'Could not open the home app settings.'); } } void _showMessage(BuildContext context, String message) { final messenger = ScaffoldMessenger.of(context); messenger.hideCurrentSnackBar(); messenger.showSnackBar(SnackBar(content: Text(message))); } } class _HomeRoleBanner extends StatelessWidget { const _HomeRoleBanner({required this.launcher, required this.onSetUp}); final LauncherController launcher; final VoidCallback onSetUp; @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); return SurfaceCard( padding: const EdgeInsets.fromLTRB(18, 16, 18, 16), radius: 24, color: colors.accentSoft.withValues(alpha: 0.62), borderColor: colors.accent.withValues(alpha: 0.18), showShadow: false, child: Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: colors.accent.withValues(alpha: 0.14), borderRadius: BorderRadius.circular(13), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/smartphone.svg', size: 18, color: colors.accent, ), ), const SizedBox(width: 13), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Make CarDash your home app', style: theme.textTheme.titleSmall, ), const SizedBox(height: 2), Text( 'Open it with the Home button.', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ), ), TextButton( onPressed: onSetUp, style: TextButton.styleFrom( foregroundColor: colors.accent, textStyle: theme.textTheme.labelLarge, ), child: const Text('Set up'), ), ], ), ); } } class _HomeHint extends StatefulWidget { const _HomeHint({required this.text, this.style}); final String text; final TextStyle? style; @override State<_HomeHint> createState() => _HomeHintState(); } class _HomeHintState extends State<_HomeHint> { Timer? _timer; bool _visible = true; @override void initState() { super.initState(); final elapsed = DateTime.now().difference(AppClock.startedAt); final remaining = const Duration(minutes: 2) - elapsed; if (remaining <= Duration.zero) { _visible = false; } else { _timer = Timer(remaining, () { if (mounted) setState(() => _visible = false); }); } } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedSwitcher( duration: const Duration(milliseconds: 350), switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeIn, child: _visible ? Text( widget.text, key: const ValueKey('home-hint'), textAlign: TextAlign.center, style: widget.style, ) : const SizedBox.shrink(key: ValueKey('home-hint-gone')), ); } }