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 '../home/launcher_controller.dart'; /// Every launchable app, with real icons and a search field. The header is /// page content; there is no persistent app bar. class AllAppsScreen extends StatefulWidget { const AllAppsScreen({super.key}); @override State createState() => _AllAppsScreenState(); } class _AllAppsScreenState extends State { final _search = TextEditingController(); String _query = ''; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) context.read().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 query = _query.trim().toLowerCase(); final apps = launcher.apps .where( (app) => query.isEmpty || app.label.toLowerCase().contains(query), ) .toList(growable: false); return Scaffold( backgroundColor: colors.canvas, body: Container( decoration: homeBackground(colors, Theme.of(context).brightness), child: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(36, 24, 36, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _BackButton(onTap: () => Navigator.of(context).pop()), const SizedBox(width: 18), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'All apps', style: theme.textTheme.headlineLarge, ), const SizedBox(height: 2), Text( '${apps.length} installed ${apps.length == 1 ? 'app' : 'apps'}' ' · hold an app to add it to Home', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: colors.inkMuted, ), ), ], ), ), SizedBox( width: 300, child: TextField( controller: _search, onChanged: (value) => setState(() => _query = value), style: theme.textTheme.bodyMedium, decoration: InputDecoration( hintText: 'Search apps', isDense: true, prefixIcon: Padding( padding: const EdgeInsets.only(left: 2), child: AppIcon( 'assets/icons/search.svg', size: 18, color: colors.inkFaint, ), ), ), ), ), ], ), const SizedBox(height: 24), Expanded(child: _buildBody(context, launcher, apps)), ], ), ), ), ), ); } Widget _buildBody( BuildContext context, LauncherController launcher, List apps, ) { final colors = context.colors; final theme = Theme.of(context); if (apps.isEmpty && launcher.isLoadingApps) { return Center( child: SizedBox( width: 30, height: 30, child: CircularProgressIndicator( strokeWidth: 2.4, color: colors.accent, ), ), ); } if (apps.isEmpty && launcher.appsError != null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Could not read installed apps.', style: theme.textTheme.bodyLarge, ), 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 GridView.builder( padding: const EdgeInsets.only(bottom: 20), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 162, mainAxisExtent: 150, crossAxisSpacing: 12, mainAxisSpacing: 12, ), itemCount: apps.length, itemBuilder: (context, index) { final app = apps[index]; return Pressable( semanticLabel: '${app.label}. Tap to open. Long press to add to Home.', onTap: () => _launch(app), onLongPress: () => _addToHome(app), child: Container( decoration: BoxDecoration( color: colors.surface, borderRadius: BorderRadius.circular(24), border: Border.all(color: colors.border), ), padding: const EdgeInsets.fromLTRB(12, 16, 12, 12), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ AppIconImage(bytes: app.icon, size: 46), const SizedBox(height: 10), Text( app.label, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.textTheme.bodySmall?.copyWith( color: colors.ink, fontWeight: FontWeight.w500, ), ), ], ), ), ); }, ); } Future _addToHome(AppEntry app) async { final launcher = context.read(); final error = await launcher.addPinnedApp(app); if (!mounted) return; final message = error ?? '${app.label} added to Home'; ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } Future _launch(AppEntry app) async { final launcher = context.read(); final error = await launcher.launchAppEntry(app); if (!mounted) return; if (error != null) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(error))); return; } // Pressing Home should land on the dashboard, not on All apps. Navigator.of(context).popUntil((route) => route.isFirst); } } class _BackButton extends StatelessWidget { const _BackButton({required this.onTap}); final VoidCallback onTap; @override Widget build(BuildContext context) { final colors = context.colors; return Pressable( onTap: onTap, semanticLabel: 'Back', pressedScale: 0.92, child: Container( width: 46, height: 46, decoration: BoxDecoration( color: colors.surface, shape: BoxShape.circle, border: Border.all(color: colors.border), ), alignment: Alignment.center, child: AppIcon( 'assets/icons/arrow-left.svg', size: 20, color: colors.ink, ), ), ); } }