| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442 |
- 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<void> showPinnedAppMenu(
- BuildContext context,
- SlotAssignment assignment,
- ) async {
- final launcher = context.read<LauncherController>();
- final app = launcher.appForAssignment(assignment);
- await showModalBottomSheet<void>(
- context: context,
- useSafeArea: true,
- backgroundColor: Colors.transparent,
- barrierColor: _sheetBarrier,
- builder: (sheetContext) {
- final colors = sheetContext.colors;
- final theme = Theme.of(sheetContext);
- Future<void> closeThen(Future<void> Function() action) async {
- Navigator.of(sheetContext).pop();
- await Future<void>.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<String?> _showRenameDialog(
- BuildContext context,
- SlotAssignment assignment,
- ) async {
- final controller = TextEditingController(text: assignment.label);
- try {
- return await showDialog<String>(
- 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<void> showPinnedIconPicker(
- BuildContext context,
- SlotAssignment assignment,
- ) {
- return showModalBottomSheet<void>(
- 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<LauncherController>();
- 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,
- ),
- ],
- ),
- ),
- );
- }
- }
|