home_screen.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import 'package:flutter/material.dart';
  2. import 'package:provider/provider.dart';
  3. import '../../core/theme/app_colors.dart';
  4. import '../../core/widgets/app_icon.dart';
  5. import '../../core/widgets/surface_card.dart';
  6. import '../../data/models/slot_type.dart';
  7. import '../apps/all_apps_screen.dart';
  8. import '../apps/app_picker_sheet.dart';
  9. import '../settings/settings_screen.dart';
  10. import 'launcher_controller.dart';
  11. import 'system_controller.dart';
  12. import 'widgets/clock_block.dart';
  13. import 'widgets/shortcut_grid.dart';
  14. import 'widgets/status_cluster.dart';
  15. import 'widgets/volume_card.dart';
  16. /// The only home page. No app bar, no dock, no pages: information, the main
  17. /// volume control, and five configurable shortcuts.
  18. class HomeScreen extends StatelessWidget {
  19. const HomeScreen({super.key});
  20. @override
  21. Widget build(BuildContext context) {
  22. final colors = context.colors;
  23. final theme = Theme.of(context);
  24. final launcher = context.watch<LauncherController>();
  25. final system = context.watch<SystemController>();
  26. final screen = MediaQuery.sizeOf(context);
  27. final compact = screen.height < 700 || screen.width < 1100;
  28. final horizontalPadding = compact ? 30.0 : 44.0;
  29. final leftWidth = (screen.width * 0.37).clamp(300.0, 440.0);
  30. final gap = compact ? 26.0 : 40.0;
  31. final showHomeBanner = launcher.defaultHomeKnown && !launcher.isDefaultHome;
  32. final navigation = launcher.assignmentFor(SlotType.navigation);
  33. final music = launcher.assignmentFor(SlotType.music);
  34. final radio = launcher.assignmentFor(SlotType.radio);
  35. final shortcuts = <ShortcutSpec>[
  36. ShortcutSpec(
  37. title: 'Navigation',
  38. subtitle: navigation?.label ?? 'Select app',
  39. iconAsset: SlotType.navigation.iconAsset,
  40. assigned: navigation != null,
  41. onTap: () => _openSlot(context, SlotType.navigation),
  42. onLongPress: () => showAppPicker(context, SlotType.navigation),
  43. ),
  44. ShortcutSpec(
  45. title: 'Music',
  46. subtitle: music?.label ?? 'Select app',
  47. iconAsset: SlotType.music.iconAsset,
  48. assigned: music != null,
  49. onTap: () => _openSlot(context, SlotType.music),
  50. onLongPress: () => showAppPicker(context, SlotType.music),
  51. ),
  52. ShortcutSpec(
  53. title: 'Radio',
  54. subtitle: radio?.label ?? 'Select app',
  55. iconAsset: SlotType.radio.iconAsset,
  56. assigned: radio != null,
  57. onTap: () => _openSlot(context, SlotType.radio),
  58. onLongPress: () => showAppPicker(context, SlotType.radio),
  59. ),
  60. ShortcutSpec(
  61. title: 'Settings',
  62. subtitle: 'Theme & PIN',
  63. iconAsset: 'assets/icons/settings.svg',
  64. assigned: true,
  65. onTap: () => _openSettings(context),
  66. ),
  67. ShortcutSpec(
  68. title: 'All apps',
  69. subtitle: 'Installed apps',
  70. iconAsset: 'assets/icons/layout-grid.svg',
  71. assigned: true,
  72. onTap: () => _openAllApps(context),
  73. ),
  74. ];
  75. return PopScope(
  76. canPop: false,
  77. child: Scaffold(
  78. backgroundColor: colors.canvas,
  79. body: Stack(
  80. children: [
  81. Positioned.fill(
  82. child: DecoratedBox(
  83. decoration: homeBackground(
  84. colors,
  85. Theme.of(context).brightness,
  86. ),
  87. ),
  88. ),
  89. Positioned(
  90. top: -190,
  91. right: -120,
  92. child: IgnorePointer(
  93. child: Container(
  94. width: 560,
  95. height: 560,
  96. decoration: BoxDecoration(
  97. shape: BoxShape.circle,
  98. gradient: RadialGradient(
  99. colors: [
  100. colors.accent.withValues(
  101. alpha: Theme.of(context).brightness == Brightness.dark
  102. ? 0.16
  103. : 0.10,
  104. ),
  105. colors.accent.withValues(alpha: 0),
  106. ],
  107. ),
  108. ),
  109. ),
  110. ),
  111. ),
  112. SafeArea(
  113. minimum: EdgeInsets.fromLTRB(
  114. horizontalPadding,
  115. compact ? 18 : 24,
  116. horizontalPadding,
  117. compact ? 14 : 18,
  118. ),
  119. child: SizedBox.expand(
  120. child: Column(
  121. crossAxisAlignment: CrossAxisAlignment.stretch,
  122. children: [
  123. Row(
  124. crossAxisAlignment: CrossAxisAlignment.start,
  125. children: [
  126. ClockBlock(compact: compact),
  127. const Spacer(),
  128. StatusCluster(status: system.status, compact: compact),
  129. ],
  130. ),
  131. SizedBox(height: compact ? 16 : 24),
  132. Expanded(
  133. child: Row(
  134. crossAxisAlignment: CrossAxisAlignment.stretch,
  135. children: [
  136. SizedBox(
  137. width: leftWidth,
  138. child: Column(
  139. crossAxisAlignment: CrossAxisAlignment.stretch,
  140. mainAxisAlignment: MainAxisAlignment.end,
  141. children: [
  142. if (showHomeBanner) ...[
  143. _HomeRoleBanner(
  144. launcher: launcher,
  145. onSetUp: () => _requestHome(context),
  146. ),
  147. const SizedBox(height: 14),
  148. ],
  149. VolumeCard(
  150. volume: system.volume,
  151. compact: compact,
  152. onSetVolume: system.setVolume,
  153. onToggleMute: system.toggleMute,
  154. ),
  155. ],
  156. ),
  157. ),
  158. SizedBox(width: gap),
  159. Expanded(
  160. child: Column(
  161. children: [
  162. Expanded(
  163. child: ShortcutGrid(
  164. shortcuts: shortcuts,
  165. compact: compact,
  166. ),
  167. ),
  168. if (!compact) ...[
  169. const SizedBox(height: 4),
  170. Text(
  171. 'Hold a shortcut to change the app',
  172. style: theme.textTheme.bodySmall?.copyWith(
  173. color: colors.inkFaint,
  174. ),
  175. ),
  176. ],
  177. ],
  178. ),
  179. ),
  180. ],
  181. ),
  182. ),
  183. ],
  184. ),
  185. ),
  186. ),
  187. ],
  188. ),
  189. ),
  190. );
  191. }
  192. Future<void> _openSlot(BuildContext context, SlotType slot) async {
  193. final launcher = context.read<LauncherController>();
  194. if (launcher.assignmentFor(slot) == null) {
  195. await showAppPicker(context, slot);
  196. return;
  197. }
  198. final error = await launcher.launchSlot(slot);
  199. if (error != null && context.mounted) {
  200. _showMessage(context, error);
  201. }
  202. }
  203. void _openSettings(BuildContext context) {
  204. Navigator.of(context)
  205. .push(MaterialPageRoute<void>(builder: (_) => const SettingsScreen()));
  206. }
  207. void _openAllApps(BuildContext context) {
  208. Navigator.of(context)
  209. .push(MaterialPageRoute<void>(builder: (_) => const AllAppsScreen()));
  210. }
  211. Future<void> _requestHome(BuildContext context) async {
  212. final launcher = context.read<LauncherController>();
  213. final launched = await launcher.requestDefaultHome();
  214. if (!launched && context.mounted) {
  215. _showMessage(context, 'Could not open the home app settings.');
  216. }
  217. }
  218. void _showMessage(BuildContext context, String message) {
  219. ScaffoldMessenger.of(context)
  220. .showSnackBar(SnackBar(content: Text(message)));
  221. }
  222. }
  223. class _HomeRoleBanner extends StatelessWidget {
  224. const _HomeRoleBanner({required this.launcher, required this.onSetUp});
  225. final LauncherController launcher;
  226. final VoidCallback onSetUp;
  227. @override
  228. Widget build(BuildContext context) {
  229. final colors = context.colors;
  230. final theme = Theme.of(context);
  231. return SurfaceCard(
  232. padding: const EdgeInsets.fromLTRB(18, 16, 18, 16),
  233. radius: 24,
  234. color: colors.accentSoft.withValues(alpha: 0.62),
  235. borderColor: colors.accent.withValues(alpha: 0.18),
  236. showShadow: false,
  237. child: Row(
  238. children: [
  239. Container(
  240. width: 38,
  241. height: 38,
  242. decoration: BoxDecoration(
  243. color: colors.accent.withValues(alpha: 0.14),
  244. borderRadius: BorderRadius.circular(13),
  245. ),
  246. alignment: Alignment.center,
  247. child: AppIcon(
  248. 'assets/icons/smartphone.svg',
  249. size: 18,
  250. color: colors.accent,
  251. ),
  252. ),
  253. const SizedBox(width: 13),
  254. Expanded(
  255. child: Column(
  256. crossAxisAlignment: CrossAxisAlignment.start,
  257. children: [
  258. Text(
  259. 'Make CarDash your home app',
  260. style: theme.textTheme.titleSmall,
  261. ),
  262. const SizedBox(height: 2),
  263. Text(
  264. 'Open it with the Home button.',
  265. style: theme.textTheme.bodySmall?.copyWith(
  266. color: colors.inkMuted,
  267. ),
  268. ),
  269. ],
  270. ),
  271. ),
  272. TextButton(
  273. onPressed: onSetUp,
  274. style: TextButton.styleFrom(
  275. foregroundColor: colors.accent,
  276. textStyle: theme.textTheme.labelLarge,
  277. ),
  278. child: const Text('Set up'),
  279. ),
  280. ],
  281. ),
  282. );
  283. }
  284. }