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 '../../data/models/app_entry.dart'; import '../../data/models/slot_assignment.dart'; import '../../data/models/slot_type.dart'; import '../home/launcher_controller.dart'; Future showAppPicker(BuildContext context, SlotType slot) { return showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: false, backgroundColor: Colors.transparent, barrierColor: const Color(0x8A000000), builder: (_) => AppPickerSheet(slot: slot), ); } class AppPickerSheet extends StatefulWidget { const AppPickerSheet({super.key, required this.slot}); final SlotType slot; @override State createState() => _AppPickerSheetState(); } class _AppPickerSheetState extends State { final _search = TextEditingController(); String _query = ''; @override void initState() { super.initState(); final controller = context.read(); if (!controller.isLoadingApps) { controller.loadApps(force: true); } } @override void dispose() { _search.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final colors = context.colors; final theme = Theme.of(context); final launcher = context.watch(); final current = launcher.assignmentFor(widget.slot); final query = _query.trim().toLowerCase(); final apps = launcher.apps .where( (app) => query.isEmpty || app.label.toLowerCase().contains(query), ) .toList(growable: false); return Padding( padding: EdgeInsets.only(bottom: MediaQuery.viewInsetsOf(context).bottom), child: 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, 12), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Choose ${widget.slot.label.toLowerCase()} app', style: theme.textTheme.headlineSmall, ), const SizedBox(height: 3), Text( current == null ? 'Pick any installed app' : 'Currently ${current.label}', style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ), ), if (current != null) TextButton( onPressed: () async { await launcher.clearSlot(widget.slot); if (context.mounted) Navigator.of(context).pop(); }, child: const Text('Clear'), ), _CircleButton( iconAsset: 'assets/icons/x.svg', semanticLabel: 'Close', onTap: () => Navigator.of(context).pop(), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: TextField( controller: _search, onChanged: (value) => setState(() => _query = value), textInputAction: TextInputAction.search, style: theme.textTheme.bodyLarge, decoration: InputDecoration( hintText: 'Search apps', prefixIcon: Padding( padding: const EdgeInsets.only(left: 4), child: AppIcon( 'assets/icons/search.svg', size: 20, color: colors.inkFaint, ), ), suffixIcon: _query.isEmpty ? null : IconButton( onPressed: () { _search.clear(); setState(() => _query = ''); }, icon: Icon( Icons.close_rounded, size: 20, color: colors.inkFaint, ), ), ), ), ), const SizedBox(height: 12), Expanded(child: _buildBody(context, launcher, apps, current)), ], ), ), ); } Widget _buildBody( BuildContext context, LauncherController launcher, List apps, SlotAssignment? current, ) { final colors = context.colors; final theme = Theme.of(context); if (apps.isEmpty && launcher.isLoadingApps) { return Center( child: SizedBox( width: 28, height: 28, child: CircularProgressIndicator( strokeWidth: 2.4, color: colors.accent, ), ), ); } if (apps.isEmpty && launcher.appsError != null) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Could not read installed apps.', style: theme.textTheme.bodyLarge, textAlign: TextAlign.center, ), const SizedBox(height: 12), TextButton( onPressed: () => launcher.loadApps(force: true), child: const Text('Try again'), ), ], ), ), ); } if (apps.isEmpty) { return Center( child: Text( 'No apps match “${_query.trim()}”.', style: theme.textTheme.bodyMedium?.copyWith(color: colors.inkMuted), ), ); } return ListView.builder( padding: const EdgeInsets.fromLTRB(16, 0, 16, 28), itemCount: apps.length, itemBuilder: (context, index) { final app = apps[index]; final selected = current != null && current.packageName == app.packageName && current.activityName == app.activityName; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: () async { await launcher.assign(widget.slot, app); if (context.mounted) Navigator.of(context).pop(); }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 11, ), child: Row( children: [ AppIconImage(bytes: app.icon, size: 42), const SizedBox(width: 16), Expanded( child: Text( app.label, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, ), ), ), if (selected) Container( width: 26, height: 26, decoration: BoxDecoration( color: colors.accent, shape: BoxShape.circle, ), child: AppIcon( 'assets/icons/check.svg', size: 15, color: colors.accentInk, ), ), ], ), ), ), ), ); }, ); } } class _CircleButton extends StatelessWidget { const _CircleButton({ required this.iconAsset, required this.semanticLabel, required this.onTap, }); final String iconAsset; final String semanticLabel; final VoidCallback onTap; @override Widget build(BuildContext context) { final colors = context.colors; return Pressable( onTap: onTap, semanticLabel: semanticLabel, 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(iconAsset, size: 17, color: colors.inkMuted), ), ); } }