settings_screen.dart 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. import 'package:flutter/material.dart';
  2. import 'package:provider/provider.dart';
  3. import '../../core/theme/accent_choice.dart';
  4. import '../../core/theme/app_colors.dart';
  5. import '../../core/theme/theme_controller.dart';
  6. import '../../core/theme/theme_preference.dart';
  7. import '../../core/widgets/app_icon.dart';
  8. import '../../core/widgets/pressable.dart';
  9. import '../../core/widgets/surface_card.dart';
  10. import '../../data/models/slot_type.dart';
  11. import '../apps/app_picker_sheet.dart';
  12. import '../home/launcher_controller.dart';
  13. import '../lock/lock_controller.dart';
  14. import '../lock/pin_setup_screen.dart';
  15. class SettingsScreen extends StatelessWidget {
  16. const SettingsScreen({super.key});
  17. @override
  18. Widget build(BuildContext context) {
  19. final colors = context.colors;
  20. final theme = Theme.of(context);
  21. final themeController = context.watch<ThemeController>();
  22. final launcher = context.watch<LauncherController>();
  23. final lock = context.watch<LockController>();
  24. return Scaffold(
  25. backgroundColor: colors.canvas,
  26. body: Container(
  27. decoration: homeBackground(colors, Theme.of(context).brightness),
  28. child: SafeArea(
  29. child: SingleChildScrollView(
  30. padding: const EdgeInsets.fromLTRB(32, 22, 32, 32),
  31. child: Center(
  32. child: ConstrainedBox(
  33. constraints: const BoxConstraints(maxWidth: 980),
  34. child: Column(
  35. crossAxisAlignment: CrossAxisAlignment.start,
  36. children: [
  37. Row(
  38. children: [
  39. Pressable(
  40. onTap: () => Navigator.of(context).pop(),
  41. semanticLabel: 'Back',
  42. pressedScale: 0.92,
  43. child: Container(
  44. width: 46,
  45. height: 46,
  46. decoration: BoxDecoration(
  47. color: colors.surface,
  48. shape: BoxShape.circle,
  49. border: Border.all(color: colors.border),
  50. ),
  51. alignment: Alignment.center,
  52. child: AppIcon(
  53. 'assets/icons/arrow-left.svg',
  54. size: 20,
  55. color: colors.ink,
  56. ),
  57. ),
  58. ),
  59. const SizedBox(width: 18),
  60. Column(
  61. crossAxisAlignment: CrossAxisAlignment.start,
  62. children: [
  63. Text(
  64. 'Settings',
  65. style: theme.textTheme.headlineLarge,
  66. ),
  67. const SizedBox(height: 2),
  68. Text(
  69. 'Theme, shortcuts and access',
  70. style: theme.textTheme.bodySmall?.copyWith(
  71. color: colors.inkMuted,
  72. ),
  73. ),
  74. ],
  75. ),
  76. ],
  77. ),
  78. const SizedBox(height: 32),
  79. _SectionTitle(
  80. title: 'General',
  81. colors: colors,
  82. theme: theme,
  83. ),
  84. const SizedBox(height: 12),
  85. SurfaceCard(
  86. padding: const EdgeInsets.fromLTRB(26, 22, 26, 18),
  87. child: Column(
  88. crossAxisAlignment: CrossAxisAlignment.start,
  89. children: [
  90. Row(
  91. children: [
  92. Text('Theme', style: theme.textTheme.titleMedium),
  93. const Spacer(),
  94. _ThemeSelector(
  95. selected: themeController.preference,
  96. onSelected: themeController.setPreference,
  97. ),
  98. ],
  99. ),
  100. const SizedBox(height: 8),
  101. Text(
  102. themeController.preference ==
  103. ThemePreference.automatic
  104. ? 'Switches automatically at the times below.'
  105. : themeController.preference ==
  106. ThemePreference.dark
  107. ? 'Always dark.'
  108. : 'Always light.',
  109. style: theme.textTheme.bodySmall?.copyWith(
  110. color: colors.inkMuted,
  111. ),
  112. ),
  113. const SizedBox(height: 8),
  114. Divider(color: colors.border, height: 24),
  115. Row(
  116. children: [
  117. Text(
  118. 'Accent',
  119. style: theme.textTheme.titleMedium,
  120. ),
  121. const Spacer(),
  122. Text(
  123. themeController.accent.label,
  124. style: theme.textTheme.labelMedium?.copyWith(
  125. color: colors.inkMuted,
  126. ),
  127. ),
  128. ],
  129. ),
  130. const SizedBox(height: 10),
  131. _AccentSelector(
  132. selected: themeController.accent,
  133. onSelected: (choice) {
  134. themeController.setAccent(choice);
  135. },
  136. ),
  137. if (themeController.preference ==
  138. ThemePreference.automatic) ...[
  139. const SizedBox(height: 8),
  140. Divider(color: colors.border, height: 24),
  141. _ActionRow(
  142. iconAsset: 'assets/icons/moon.svg',
  143. title: 'Dark from',
  144. value: _formatMinutes(
  145. context,
  146. themeController.nightStartMinutes,
  147. ),
  148. onTap: () => _pickTime(
  149. context,
  150. themeController,
  151. start: true,
  152. ),
  153. ),
  154. const SizedBox(height: 2),
  155. _ActionRow(
  156. iconAsset: 'assets/icons/sun.svg',
  157. title: 'Light from',
  158. value: _formatMinutes(
  159. context,
  160. themeController.nightEndMinutes,
  161. ),
  162. onTap: () => _pickTime(
  163. context,
  164. themeController,
  165. start: false,
  166. ),
  167. ),
  168. ],
  169. Divider(color: colors.border, height: 24),
  170. _BatteryToggleRow(
  171. value: themeController.showBattery,
  172. onChanged: (value) {
  173. themeController.setShowBattery(value);
  174. },
  175. ),
  176. Divider(color: colors.border, height: 30),
  177. Row(
  178. children: [
  179. Text(
  180. 'Home buttons',
  181. style: theme.textTheme.titleMedium,
  182. ),
  183. const Spacer(),
  184. Text(
  185. '${launcher.homeButtonCount} / '
  186. '${LauncherController.maxHomeButtons}',
  187. style: theme.textTheme.labelMedium?.copyWith(
  188. color: colors.inkMuted,
  189. ),
  190. ),
  191. ],
  192. ),
  193. const SizedBox(height: 3),
  194. Text(
  195. 'Turn one off to hide it. Settings and All apps are always shown.',
  196. style: theme.textTheme.bodySmall?.copyWith(
  197. color: colors.inkMuted,
  198. ),
  199. ),
  200. const SizedBox(height: 4),
  201. for (final slot in SlotType.values)
  202. _ShortcutToggleRow(
  203. slot: slot,
  204. launcher: launcher,
  205. onPick: () => showAppPicker(context, slot),
  206. ),
  207. Divider(color: colors.border, height: 30),
  208. _HomeRoleRow(launcher: launcher),
  209. ],
  210. ),
  211. ),
  212. const SizedBox(height: 30),
  213. _SectionTitle(
  214. title: 'Security',
  215. colors: colors,
  216. theme: theme,
  217. ),
  218. const SizedBox(height: 12),
  219. SurfaceCard(
  220. padding: const EdgeInsets.fromLTRB(26, 22, 26, 22),
  221. child: Column(
  222. crossAxisAlignment: CrossAxisAlignment.start,
  223. children: [
  224. Row(
  225. children: [
  226. Container(
  227. width: 42,
  228. height: 42,
  229. decoration: BoxDecoration(
  230. color: colors.accentSoft,
  231. borderRadius: BorderRadius.circular(14),
  232. ),
  233. alignment: Alignment.center,
  234. child: AppIcon(
  235. 'assets/icons/lock.svg',
  236. size: 20,
  237. color: colors.accent,
  238. ),
  239. ),
  240. const SizedBox(width: 14),
  241. Expanded(
  242. child: Column(
  243. crossAxisAlignment: CrossAxisAlignment.start,
  244. children: [
  245. Text(
  246. 'PIN protection',
  247. style: theme.textTheme.titleMedium,
  248. ),
  249. const SizedBox(height: 2),
  250. Text(
  251. lock.pinSet ? 'On' : 'Off',
  252. style: theme.textTheme.bodySmall
  253. ?.copyWith(
  254. color: lock.pinSet
  255. ? colors.accent
  256. : colors.inkMuted,
  257. fontWeight: FontWeight.w600,
  258. ),
  259. ),
  260. ],
  261. ),
  262. ),
  263. if (!lock.pinSet)
  264. _SmallButton(
  265. label: 'Set PIN',
  266. onTap: () =>
  267. _openPinSetup(context, PinSetupMode.set),
  268. ),
  269. ],
  270. ),
  271. if (lock.pinSet) ...[
  272. const SizedBox(height: 10),
  273. Divider(color: colors.border, height: 24),
  274. _ActionRow(
  275. iconAsset: 'assets/icons/lock.svg',
  276. title: 'Change PIN',
  277. trailing: const _Chevron(),
  278. onTap: () =>
  279. _openPinSetup(context, PinSetupMode.change),
  280. ),
  281. _ActionRow(
  282. iconAsset: 'assets/icons/x.svg',
  283. title: 'Remove PIN',
  284. trailing: const _Chevron(),
  285. onTap: () =>
  286. _openPinSetup(context, PinSetupMode.remove),
  287. ),
  288. ],
  289. const SizedBox(height: 16),
  290. Text(
  291. 'The PIN is requested only when CarDash starts. It does not lock '
  292. 'again when you switch apps or turn the screen off. It protects the '
  293. 'launcher, not the device itself.',
  294. style: theme.textTheme.bodySmall?.copyWith(
  295. color: colors.inkMuted,
  296. height: 1.5,
  297. ),
  298. ),
  299. ],
  300. ),
  301. ),
  302. const SizedBox(height: 28),
  303. Center(
  304. child: Text(
  305. 'CarDash 0.4.0 · Android 10+',
  306. style: theme.textTheme.bodySmall?.copyWith(
  307. color: colors.inkFaint,
  308. ),
  309. ),
  310. ),
  311. ],
  312. ),
  313. ),
  314. ),
  315. ),
  316. ),
  317. ),
  318. );
  319. }
  320. Future<void> _openPinSetup(BuildContext context, PinSetupMode mode) async {
  321. final result = await Navigator.of(
  322. context,
  323. ).push<bool>(MaterialPageRoute(builder: (_) => PinSetupScreen(mode: mode)));
  324. if (result != true || !context.mounted) return;
  325. final message = switch (mode) {
  326. PinSetupMode.set =>
  327. 'PIN set. It will be requested the next time CarDash starts.',
  328. PinSetupMode.change => 'PIN changed.',
  329. PinSetupMode.remove => 'PIN removed.',
  330. };
  331. ScaffoldMessenger.of(context)
  332. .showSnackBar(SnackBar(content: Text(message)));
  333. }
  334. Future<void> _pickTime(
  335. BuildContext context,
  336. ThemeController controller, {
  337. required bool start,
  338. }) async {
  339. final minutes = start
  340. ? controller.nightStartMinutes
  341. : controller.nightEndMinutes;
  342. final picked = await showTimePicker(
  343. context: context,
  344. initialTime: TimeOfDay(hour: minutes ~/ 60, minute: minutes % 60),
  345. helpText: start ? 'Dark from' : 'Light from',
  346. builder: (context, child) {
  347. final theme = Theme.of(context);
  348. return Theme(
  349. data: theme.copyWith(
  350. timePickerTheme: TimePickerThemeData(
  351. backgroundColor: theme.colorScheme.surface,
  352. dialBackgroundColor: theme.colorScheme.surfaceContainerHighest,
  353. shape: RoundedRectangleBorder(
  354. borderRadius: BorderRadius.circular(28),
  355. ),
  356. ),
  357. ),
  358. child: child ?? const SizedBox.shrink(),
  359. );
  360. },
  361. );
  362. if (picked == null || !context.mounted) return;
  363. final newValue = picked.hour * 60 + picked.minute;
  364. await controller.setNightWindow(
  365. startMinutes: start ? newValue : controller.nightStartMinutes,
  366. endMinutes: start ? controller.nightEndMinutes : newValue,
  367. );
  368. }
  369. String _formatMinutes(BuildContext context, int minutes) {
  370. return TimeOfDay(hour: minutes ~/ 60, minute: minutes % 60).format(context);
  371. }
  372. }
  373. class _SectionTitle extends StatelessWidget {
  374. const _SectionTitle({
  375. required this.title,
  376. required this.colors,
  377. required this.theme,
  378. });
  379. final String title;
  380. final AppColors colors;
  381. final ThemeData theme;
  382. @override
  383. Widget build(BuildContext context) {
  384. return Padding(
  385. padding: const EdgeInsets.only(left: 4),
  386. child: Text(
  387. title,
  388. style: theme.textTheme.titleLarge?.copyWith(color: colors.inkMuted),
  389. ),
  390. );
  391. }
  392. }
  393. class _ThemeSelector extends StatelessWidget {
  394. const _ThemeSelector({required this.selected, required this.onSelected});
  395. final ThemePreference selected;
  396. final ValueChanged<ThemePreference> onSelected;
  397. @override
  398. Widget build(BuildContext context) {
  399. final colors = context.colors;
  400. return Container(
  401. padding: const EdgeInsets.all(4),
  402. decoration: BoxDecoration(
  403. color: colors.surfaceStrong,
  404. borderRadius: BorderRadius.circular(999),
  405. border: Border.all(color: colors.border),
  406. ),
  407. child: Row(
  408. mainAxisSize: MainAxisSize.min,
  409. children: [
  410. for (final preference in ThemePreference.values)
  411. Pressable(
  412. onTap: () => onSelected(preference),
  413. semanticLabel: 'Theme ${preference.storageValue}',
  414. pressedScale: 0.95,
  415. child: AnimatedContainer(
  416. duration: const Duration(milliseconds: 160),
  417. curve: Curves.easeOutCubic,
  418. padding: const EdgeInsets.symmetric(
  419. horizontal: 18,
  420. vertical: 9,
  421. ),
  422. decoration: BoxDecoration(
  423. color: selected == preference
  424. ? colors.accent
  425. : Colors.transparent,
  426. borderRadius: BorderRadius.circular(999),
  427. ),
  428. child: Text(
  429. switch (preference) {
  430. ThemePreference.light => 'Light',
  431. ThemePreference.dark => 'Dark',
  432. ThemePreference.automatic => 'Auto',
  433. },
  434. style: Theme.of(context).textTheme.labelMedium?.copyWith(
  435. color: selected == preference
  436. ? colors.accentInk
  437. : colors.inkMuted,
  438. fontWeight: FontWeight.w600,
  439. ),
  440. ),
  441. ),
  442. ),
  443. ],
  444. ),
  445. );
  446. }
  447. }
  448. class _AccentSelector extends StatelessWidget {
  449. const _AccentSelector({required this.selected, required this.onSelected});
  450. final AccentChoice selected;
  451. final ValueChanged<AccentChoice> onSelected;
  452. @override
  453. Widget build(BuildContext context) {
  454. final colors = context.colors;
  455. final brightness = Theme.of(context).brightness;
  456. return Wrap(
  457. spacing: 10,
  458. runSpacing: 10,
  459. children: [
  460. for (final choice in AccentChoice.values)
  461. Semantics(
  462. label: '${choice.label} accent',
  463. selected: choice == selected,
  464. button: true,
  465. child: Material(
  466. color: Colors.transparent,
  467. shape: const CircleBorder(),
  468. child: InkWell(
  469. customBorder: const CircleBorder(),
  470. onTap: () => onSelected(choice),
  471. child: Padding(
  472. padding: const EdgeInsets.all(3),
  473. child: AnimatedContainer(
  474. duration: const Duration(milliseconds: 180),
  475. width: 30,
  476. height: 30,
  477. decoration: BoxDecoration(
  478. shape: BoxShape.circle,
  479. color: choice.accentFor(brightness),
  480. border: Border.all(
  481. color: choice == selected ? colors.ink : colors.border,
  482. width: choice == selected ? 2 : 1,
  483. ),
  484. ),
  485. alignment: Alignment.center,
  486. child: choice == selected
  487. ? Icon(
  488. Icons.check_rounded,
  489. size: 16,
  490. color: choice.inkFor(brightness),
  491. )
  492. : null,
  493. ),
  494. ),
  495. ),
  496. ),
  497. ),
  498. ],
  499. );
  500. }
  501. }
  502. class _SettingsSwitch extends StatelessWidget {
  503. const _SettingsSwitch({
  504. required this.value,
  505. required this.onChanged,
  506. required this.semanticsLabel,
  507. });
  508. final bool value;
  509. final ValueChanged<bool> onChanged;
  510. final String semanticsLabel;
  511. @override
  512. Widget build(BuildContext context) {
  513. final colors = context.colors;
  514. return Semantics(
  515. label: semanticsLabel,
  516. child: Switch(
  517. value: value,
  518. onChanged: onChanged,
  519. thumbColor: WidgetStateProperty.resolveWith<Color?>(
  520. (states) => states.contains(WidgetState.selected)
  521. ? colors.accentInk
  522. : colors.inkFaint,
  523. ),
  524. trackColor: WidgetStateProperty.resolveWith<Color?>(
  525. (states) => states.contains(WidgetState.selected)
  526. ? colors.accent
  527. : colors.surfaceStrong,
  528. ),
  529. trackOutlineColor: WidgetStatePropertyAll(colors.border),
  530. ),
  531. );
  532. }
  533. }
  534. class _BatteryToggleRow extends StatelessWidget {
  535. const _BatteryToggleRow({required this.value, required this.onChanged});
  536. final bool value;
  537. final ValueChanged<bool> onChanged;
  538. @override
  539. Widget build(BuildContext context) {
  540. final colors = context.colors;
  541. final theme = Theme.of(context);
  542. return Row(
  543. children: [
  544. Container(
  545. width: 38,
  546. height: 38,
  547. decoration: BoxDecoration(
  548. color: colors.surfaceStrong,
  549. borderRadius: BorderRadius.circular(13),
  550. border: Border.all(color: colors.border),
  551. ),
  552. alignment: Alignment.center,
  553. child: AppIcon(
  554. 'assets/icons/battery-charging.svg',
  555. size: 18,
  556. color: colors.inkMuted,
  557. ),
  558. ),
  559. const SizedBox(width: 14),
  560. Expanded(
  561. child: Column(
  562. crossAxisAlignment: CrossAxisAlignment.start,
  563. children: [
  564. Text('Battery & charging', style: theme.textTheme.bodyLarge),
  565. const SizedBox(height: 2),
  566. Text(
  567. 'Show the battery level and charging bolt.',
  568. style: theme.textTheme.bodySmall?.copyWith(
  569. color: colors.inkMuted,
  570. ),
  571. ),
  572. ],
  573. ),
  574. ),
  575. const SizedBox(width: 8),
  576. _SettingsSwitch(
  577. value: value,
  578. semanticsLabel: value
  579. ? 'Hide battery and charging status'
  580. : 'Show battery and charging status',
  581. onChanged: onChanged,
  582. ),
  583. ],
  584. );
  585. }
  586. }
  587. class _ActionRow extends StatelessWidget {
  588. const _ActionRow({
  589. required this.iconAsset,
  590. required this.title,
  591. this.value,
  592. this.trailing,
  593. this.onTap,
  594. });
  595. final String iconAsset;
  596. final String title;
  597. final String? value;
  598. final Widget? trailing;
  599. final VoidCallback? onTap;
  600. @override
  601. Widget build(BuildContext context) {
  602. final colors = context.colors;
  603. final theme = Theme.of(context);
  604. final content = Padding(
  605. padding: const EdgeInsets.symmetric(vertical: 7),
  606. child: Row(
  607. children: [
  608. Container(
  609. width: 38,
  610. height: 38,
  611. decoration: BoxDecoration(
  612. color: colors.surfaceStrong,
  613. borderRadius: BorderRadius.circular(13),
  614. border: Border.all(color: colors.border),
  615. ),
  616. alignment: Alignment.center,
  617. child: AppIcon(iconAsset, size: 18, color: colors.inkMuted),
  618. ),
  619. const SizedBox(width: 14),
  620. Expanded(child: Text(title, style: theme.textTheme.bodyLarge)),
  621. if (value != null)
  622. Text(
  623. value!,
  624. style: theme.textTheme.labelLarge?.copyWith(
  625. color: colors.inkMuted,
  626. ),
  627. ),
  628. if (trailing != null) ...[const SizedBox(width: 8), trailing!],
  629. ],
  630. ),
  631. );
  632. if (onTap == null) return content;
  633. return Material(
  634. color: Colors.transparent,
  635. child: InkWell(
  636. borderRadius: BorderRadius.circular(14),
  637. onTap: onTap,
  638. child: content,
  639. ),
  640. );
  641. }
  642. }
  643. class _ShortcutToggleRow extends StatelessWidget {
  644. const _ShortcutToggleRow({
  645. required this.slot,
  646. required this.launcher,
  647. required this.onPick,
  648. });
  649. final SlotType slot;
  650. final LauncherController launcher;
  651. final VoidCallback onPick;
  652. @override
  653. Widget build(BuildContext context) {
  654. final colors = context.colors;
  655. final theme = Theme.of(context);
  656. final enabled = launcher.isSlotEnabled(slot);
  657. final assignment = launcher.assignmentFor(slot);
  658. return Row(
  659. children: [
  660. Expanded(
  661. child: Material(
  662. color: Colors.transparent,
  663. child: InkWell(
  664. borderRadius: BorderRadius.circular(14),
  665. onTap: onPick,
  666. child: Padding(
  667. padding: const EdgeInsets.symmetric(vertical: 6),
  668. child: Row(
  669. children: [
  670. Opacity(
  671. opacity: enabled ? 1 : 0.45,
  672. child: Container(
  673. width: 38,
  674. height: 38,
  675. decoration: BoxDecoration(
  676. color: colors.surfaceStrong,
  677. borderRadius: BorderRadius.circular(13),
  678. border: Border.all(color: colors.border),
  679. ),
  680. alignment: Alignment.center,
  681. child: AppIcon(
  682. slot.iconAsset,
  683. size: 18,
  684. color: colors.inkMuted,
  685. ),
  686. ),
  687. ),
  688. const SizedBox(width: 14),
  689. Expanded(
  690. child: Column(
  691. crossAxisAlignment: CrossAxisAlignment.start,
  692. children: [
  693. Text(
  694. slot.label,
  695. style: theme.textTheme.bodyLarge?.copyWith(
  696. color: enabled ? colors.ink : colors.inkFaint,
  697. ),
  698. ),
  699. const SizedBox(height: 2),
  700. Text(
  701. assignment?.label ?? 'Select app',
  702. maxLines: 1,
  703. overflow: TextOverflow.ellipsis,
  704. style: theme.textTheme.bodySmall?.copyWith(
  705. color: !enabled
  706. ? colors.inkFaint
  707. : assignment == null
  708. ? colors.accent
  709. : colors.inkMuted,
  710. fontWeight: assignment == null && enabled
  711. ? FontWeight.w600
  712. : FontWeight.w400,
  713. ),
  714. ),
  715. ],
  716. ),
  717. ),
  718. ],
  719. ),
  720. ),
  721. ),
  722. ),
  723. ),
  724. const SizedBox(width: 8),
  725. _SettingsSwitch(
  726. value: enabled,
  727. semanticsLabel: enabled
  728. ? 'Hide ${slot.label} on Home'
  729. : 'Show ${slot.label} on Home',
  730. onChanged: (value) async {
  731. final error = await launcher.setSlotEnabled(slot, value);
  732. if (error != null && context.mounted) {
  733. ScaffoldMessenger.of(context)
  734. .showSnackBar(SnackBar(content: Text(error)));
  735. }
  736. },
  737. ),
  738. ],
  739. );
  740. }
  741. }
  742. class _Chevron extends StatelessWidget {
  743. const _Chevron();
  744. @override
  745. Widget build(BuildContext context) {
  746. return AppIcon(
  747. 'assets/icons/chevron-right.svg',
  748. size: 18,
  749. color: context.colors.inkFaint,
  750. );
  751. }
  752. }
  753. class _SmallButton extends StatelessWidget {
  754. const _SmallButton({required this.label, required this.onTap});
  755. final String label;
  756. final VoidCallback onTap;
  757. @override
  758. Widget build(BuildContext context) {
  759. final colors = context.colors;
  760. return Pressable(
  761. onTap: onTap,
  762. semanticLabel: label,
  763. pressedScale: 0.95,
  764. child: Container(
  765. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
  766. decoration: BoxDecoration(
  767. color: colors.accent,
  768. borderRadius: BorderRadius.circular(999),
  769. ),
  770. child: Text(
  771. label,
  772. style: Theme.of(context).textTheme.labelMedium
  773. ?.copyWith(color: colors.accentInk, fontWeight: FontWeight.w600),
  774. ),
  775. ),
  776. );
  777. }
  778. }
  779. class _HomeRoleRow extends StatelessWidget {
  780. const _HomeRoleRow({required this.launcher});
  781. final LauncherController launcher;
  782. @override
  783. Widget build(BuildContext context) {
  784. final colors = context.colors;
  785. final theme = Theme.of(context);
  786. final isDefault = launcher.isDefaultHome;
  787. return Row(
  788. children: [
  789. Container(
  790. width: 38,
  791. height: 38,
  792. decoration: BoxDecoration(
  793. color: colors.surfaceStrong,
  794. borderRadius: BorderRadius.circular(13),
  795. border: Border.all(color: colors.border),
  796. ),
  797. alignment: Alignment.center,
  798. child: AppIcon(
  799. 'assets/icons/smartphone.svg',
  800. size: 18,
  801. color: colors.inkMuted,
  802. ),
  803. ),
  804. const SizedBox(width: 14),
  805. Expanded(
  806. child: Column(
  807. crossAxisAlignment: CrossAxisAlignment.start,
  808. children: [
  809. Text('Home app', style: theme.textTheme.bodyLarge),
  810. const SizedBox(height: 2),
  811. Text(
  812. launcher.defaultHomeKnown
  813. ? (isDefault ? 'CarDash is your home app' : 'Not set')
  814. : 'Checking…',
  815. style: theme.textTheme.bodySmall?.copyWith(
  816. color: isDefault ? colors.accent : colors.inkMuted,
  817. fontWeight: isDefault ? FontWeight.w600 : FontWeight.w400,
  818. ),
  819. ),
  820. ],
  821. ),
  822. ),
  823. if (launcher.defaultHomeKnown && !isDefault)
  824. _SmallButton(
  825. label: 'Set as default',
  826. onTap: () async {
  827. final launched = await launcher.requestDefaultHome();
  828. if (!launched && context.mounted) {
  829. ScaffoldMessenger.of(context).showSnackBar(
  830. const SnackBar(
  831. content: Text('Could not open the home app settings.'),
  832. ),
  833. );
  834. }
  835. },
  836. )
  837. else if (isDefault)
  838. Icon(Icons.check_circle_rounded, color: colors.accent, size: 22),
  839. ],
  840. );
  841. }
  842. }