import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/theme/app_colors.dart'; import '../../core/widgets/app_icon.dart'; import '../../core/widgets/app_icon_image.dart'; import '../../core/widgets/pressable.dart'; import '../../core/widgets/surface_card.dart'; import '../../data/models/slot_assignment.dart'; import 'launcher_controller.dart'; import 'style_icon_choices.dart'; const _sheetBarrier = Color(0x8A000000); /// The long-press menu for an app pinned to Home. Future showPinnedAppMenu( BuildContext context, SlotAssignment assignment, ) async { final launcher = context.read(); final app = launcher.appForAssignment(assignment); await showModalBottomSheet( context: context, useSafeArea: true, backgroundColor: Colors.transparent, barrierColor: _sheetBarrier, builder: (sheetContext) { final colors = sheetContext.colors; final theme = Theme.of(sheetContext); Future closeThen(Future Function() action) async { Navigator.of(sheetContext).pop(); await Future.delayed(const Duration(milliseconds: 190)); if (!context.mounted) return; await action(); } return Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 20), child: Center( heightFactor: 1, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480), child: SurfaceCard( padding: const EdgeInsets.fromLTRB(22, 18, 22, 16), radius: 28, child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ if (assignment.iconAsset != null) Container( width: 44, height: 44, decoration: BoxDecoration( color: colors.surfaceStrong, borderRadius: BorderRadius.circular(15), border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( assignment.iconAsset!, size: 22, color: colors.ink, ), ) else AppIconImage(bytes: app?.icon, size: 44), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( assignment.label.isEmpty ? assignment.packageName : assignment.label, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium, ), const SizedBox(height: 2), Text( 'Home shortcut', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ), ), Pressable( onTap: () => Navigator.of(sheetContext).pop(), semanticLabel: 'Close', pressedScale: 0.92, child: Container( width: 38, height: 38, decoration: BoxDecoration( color: colors.surfaceStrong, shape: BoxShape.circle, border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/x.svg', size: 16, color: colors.inkMuted, ), ), ), ], ), const SizedBox(height: 12), Divider(color: colors.border, height: 1), const SizedBox(height: 4), _MenuAction( iconAsset: 'assets/icons/pencil.svg', title: 'Rename', onTap: () => closeThen(() async { final name = await _showRenameDialog(context, assignment); if (name != null) { await launcher.renamePinnedApp(assignment, name); } }), ), _MenuAction( iconAsset: 'assets/icons/image.svg', title: 'Change icon', onTap: () => closeThen( () => showPinnedIconPicker(context, assignment), ), ), _MenuAction( iconAsset: 'assets/icons/delete.svg', title: 'Remove', danger: true, onTap: () => closeThen(() async { await launcher.removePinnedApp(assignment); if (context.mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${assignment.label.isEmpty ? 'App' : assignment.label} ' 'removed from Home', ), ), ); } }), ), ], ), ), ), ), ); }, ); } Future _showRenameDialog( BuildContext context, SlotAssignment assignment, ) async { final controller = TextEditingController(text: assignment.label); try { return await showDialog( context: context, builder: (dialogContext) { void submit() { final name = controller.text.trim(); if (name.isEmpty) return; Navigator.of(dialogContext).pop(name); } return AlertDialog( title: const Text('Rename shortcut'), content: TextField( controller: controller, autofocus: true, maxLength: 32, textInputAction: TextInputAction.done, decoration: const InputDecoration( labelText: 'Home label', helperText: 'Only the Home label changes.', ), onSubmitted: (_) => submit(), ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancel'), ), FilledButton(onPressed: submit, child: const Text('Save')), ], ); }, ); } finally { controller.dispose(); } } Future showPinnedIconPicker( BuildContext context, SlotAssignment assignment, ) { return showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, barrierColor: _sheetBarrier, builder: (_) => _PinnedIconSheet(assignment: assignment), ); } class _PinnedIconSheet extends StatelessWidget { const _PinnedIconSheet({required this.assignment}); final SlotAssignment assignment; @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final selectedAsset = assignment.iconAsset; return Container( height: MediaQuery.sizeOf(context).height * 0.86, decoration: BoxDecoration( color: colors.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(32)), ), child: Column( children: [ const SizedBox(height: 12), Container( width: 44, height: 4, decoration: BoxDecoration( color: colors.inkFaint.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(99), ), ), Padding( padding: const EdgeInsets.fromLTRB(28, 20, 18, 8), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Change icon', style: theme.textTheme.headlineSmall), const SizedBox(height: 3), Text( 'Choose a stylized icon for ${assignment.label}.', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ), ), Pressable( onTap: () => Navigator.of(context).pop(), semanticLabel: 'Close', pressedScale: 0.92, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: colors.surfaceStrong, shape: BoxShape.circle, border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/x.svg', size: 16, color: colors.inkMuted, ), ), ), ], ), ), const SizedBox(height: 8), Expanded( child: GridView.builder( padding: const EdgeInsets.fromLTRB(24, 4, 24, 28), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 116, mainAxisExtent: 108, crossAxisSpacing: 10, mainAxisSpacing: 10, ), itemCount: styleIconChoices.length + 1, itemBuilder: (context, index) { final isDefault = index == 0; final choice = isDefault ? null : styleIconChoices[index - 1]; final asset = isDefault ? 'assets/icons/app-window.svg' : choice!.asset; final label = isDefault ? 'Default' : choice!.label; final selected = isDefault ? !assignment.hasCustomIcon : selectedAsset == asset; return Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(24), onTap: () async { final launcher = context.read(); await launcher.setPinnedIcon( assignment, isDefault ? null : asset, ); if (context.mounted) Navigator.of(context).pop(); }, child: Container( padding: const EdgeInsets.fromLTRB(8, 10, 8, 8), decoration: BoxDecoration( color: colors.surfaceStrong, borderRadius: BorderRadius.circular(24), border: Border.all( color: selected ? colors.accent : colors.border, width: selected ? 1.8 : 1, ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AppIcon( asset, size: 30, color: selected ? colors.accent : colors.ink, ), const SizedBox(height: 7), Text( label, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: selected ? colors.accent : colors.ink, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ), ), ); }, ), ), ], ), ); } } class _MenuAction extends StatelessWidget { const _MenuAction({ required this.iconAsset, required this.title, required this.onTap, this.danger = false, }); final String iconAsset; final String title; final VoidCallback onTap; final bool danger; @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final color = danger ? colors.danger : colors.ink; return Pressable( onTap: onTap, semanticLabel: title, pressedScale: 0.98, child: Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: danger ? colors.danger.withValues(alpha: 0.1) : colors.surfaceStrong, borderRadius: BorderRadius.circular(13), border: Border.all( color: danger ? colors.danger.withValues(alpha: 0.22) : colors.border, ), ), alignment: Alignment.center, child: AppIcon( iconAsset, size: 17, color: danger ? colors.danger : colors.inkMuted, ), ), const SizedBox(width: 14), Expanded( child: Text( title, style: theme.textTheme.bodyLarge?.copyWith( color: color, fontWeight: FontWeight.w500, ), ), ), AppIcon( 'assets/icons/chevron-right.svg', size: 16, color: danger ? colors.danger.withValues(alpha: 0.7) : colors.inkFaint, ), ], ), ), ); } }