import 'package:commons_core/commons_core.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:go_router/go_router.dart'; import 'data/species_repository.dart'; import 'data/variety_repository.dart'; import 'i18n/strings.g.dart'; import 'services/auto_backup_service.dart'; import 'services/coarse_location.dart'; import 'services/inbox_service.dart'; import 'services/message_store.dart'; import 'services/notification_service.dart'; import 'services/offer_outbox.dart'; import 'services/onboarding_store.dart'; import 'services/profile_cache.dart'; import 'services/profile_store.dart'; import 'services/social_service.dart'; import 'services/social_settings.dart'; import 'state/inventory_cubit.dart'; import 'state/variety_detail_cubit.dart'; import 'ui/about_screen.dart'; import 'ui/auto_backup_gate.dart'; import 'ui/chat_list_screen.dart'; import 'ui/chat_screen.dart'; import 'ui/home_screen.dart'; import 'ui/intro_screen.dart'; import 'ui/inventory_list_screen.dart'; import 'ui/market_offer_detail_screen.dart'; import 'ui/market_screen.dart'; import 'ui/offline_banner.dart'; import 'ui/profile_screen.dart'; import 'ui/settings_screen.dart'; import 'ui/theme.dart'; import 'ui/variety_detail_screen.dart'; /// Root widget. Provides the repositories to the tree and wires go_router: /// `/` is the home menu, `/inventory` the list, `/variety/:id` the item detail. /// When [showIntro] is set (first launch), the app opens on `/intro`. class TaneApp extends StatelessWidget { TaneApp({ required this.repository, required this.species, required this.onboarding, this.social, this.socialSettings, this.location, this.outbox, this.messageStore, this.profileStore, this.profileCache, this.inbox, this.notifications, this.showIntro = false, this.autoBackup, super.key, }) : _router = _buildRouter( repository, onboarding, showIntro, social, socialSettings, location, outbox, messageStore, profileStore, profileCache, inbox, ) { // A tapped message notification opens that peer's chat. Wired here because // the router only exists now; taps only happen while the app is foreground, // so a live router reference is enough. notifications?.onTapChat = (pubkey) => _router.push('/chat/$pubkey'); } final VarietyRepository repository; final SpeciesRepository species; final OnboardingStore onboarding; /// Block 2 social layer. Null until the social round is wired in `main`; when /// null the market stays a "coming soon" card and `/market` is not routed. final SocialService? social; final SocialSettings? socialSettings; /// Optional device-location source for the market's "use my location". final CoarseLocationProvider? location; /// Optional offline outbox for the market's "share my seeds". final OfferOutbox? outbox; /// Optional persistence for chat history. final MessageStore? messageStore; /// Optional local store for your own profile (name/about). final ProfileStore? profileStore; /// Optional cache of peers' published display names. final ProfileCache? profileCache; /// App-wide inbox listener; drives the messages list's live refresh. final InboxService? inbox; /// OS notifications for foreground messages; its tap handler is wired to the /// router here. Null in tests / on platforms without notification support. final NotificationService? notifications; final bool showIntro; /// Drives silent periodic backups off the app lifecycle. Null in widget tests /// and on platforms without file storage (web) — the gate then does nothing. final AutoBackupService? autoBackup; final GoRouter _router; static GoRouter _buildRouter( VarietyRepository repository, OnboardingStore onboarding, bool showIntro, SocialService? social, SocialSettings? socialSettings, CoarseLocationProvider? location, OfferOutbox? outbox, MessageStore? messageStore, ProfileStore? profileStore, ProfileCache? profileCache, InboxService? inbox, ) { return GoRouter( initialLocation: showIntro ? '/intro' : '/', routes: [ GoRoute( path: '/', builder: (context, state) => HomeScreen(marketEnabled: social != null), ), if (social != null && socialSettings != null) GoRoute( path: '/market', builder: (context, state) => MarketScreen( social: social, settings: socialSettings, location: location, outbox: outbox, ), ), if (social != null && socialSettings != null) GoRoute( path: '/market/offer', builder: (context, state) { final offer = state.extra as Offer; return MarketOfferDetailScreen( offer: offer, social: social, settings: socialSettings, mine: offer.authorPubkeyHex == social.publicKeyHex, profileCache: profileCache, ); }, ), if (social != null && socialSettings != null && messageStore != null) GoRoute( path: '/messages', builder: (context, state) => ChatListScreen( store: messageStore, social: social, settings: socialSettings, profileCache: profileCache, inbox: inbox, ), ), if (social != null && socialSettings != null && profileStore != null) GoRoute( path: '/profile', builder: (context, state) => ProfileScreen( social: social, settings: socialSettings, profileStore: profileStore, ), ), if (social != null && socialSettings != null) GoRoute( path: '/chat/:pubkey', builder: (context, state) => ChatScreen( social: social, settings: socialSettings, peerPubkey: state.pathParameters['pubkey']!, messageStore: messageStore, profileCache: profileCache, ), ), GoRoute( path: '/intro', builder: (context, state) => IntroScreen( onDone: () async { await onboarding.markIntroSeen(); if (context.mounted) context.go('/'); }, ), ), GoRoute( path: '/settings', builder: (context, state) => const SettingsScreen(), ), GoRoute( path: '/about', builder: (context, state) => const AboutScreen(), ), GoRoute( path: '/inventory', builder: (context, state) => BlocProvider( create: (_) => InventoryCubit(repository), child: const InventoryListScreen(), ), ), GoRoute( path: '/variety/:id', builder: (context, state) => BlocProvider( create: (_) => VarietyDetailCubit(repository, state.pathParameters['id']!), child: const VarietyDetailScreen(), ), ), ], ); } @override Widget build(BuildContext context) { return MultiRepositoryProvider( providers: [ RepositoryProvider.value(value: repository), RepositoryProvider.value(value: species), ], child: AutoBackupGate( service: autoBackup, child: MaterialApp.router( onGenerateTitle: (context) => context.t.app.title, debugShowCheckedModeBanner: false, theme: buildTaneTheme(), // Let scrollables & PageViews be dragged with a mouse/trackpad on // desktop (Flutter disables that by default), so the photo carousel and // lists swipe there too. scrollBehavior: const _DragScrollBehavior(), // Flutter ships no Material/Cupertino localizations for Asturian // (`ast`), so render the framework chrome (date pickers, native dialog // buttons) in Spanish — the closest shipped language — while the app's // own text stays Asturian via slang. Every other locale maps 1:1. locale: materialLocaleFor(TranslationProvider.of(context).flutterLocale), supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], // A global "you're offline" strip above every screen — makes it clear // why the social layer is paused (the inventory still works). builder: (context, child) => OfflineBanner(child: child ?? const SizedBox.shrink()), routerConfig: _router, ), ), ); } } /// Maps the app's active locale to one Flutter's bundled Material/Cupertino /// localizations can serve. Asturian (`ast`) has none, so it borrows Spanish /// (`es`) for the framework chrome; the app's own strings stay Asturian (they /// come from slang, not from `Localizations`). All other locales pass through. @visibleForTesting Locale materialLocaleFor(Locale locale) => locale.languageCode == 'ast' ? const Locale('es') : locale; /// Enables mouse/trackpad drag scrolling (off by default on desktop) so the /// photo carousel and lists can be swiped with a pointer. class _DragScrollBehavior extends MaterialScrollBehavior { const _DragScrollBehavior(); @override Set get dragDevices => { PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.trackpad, PointerDeviceKind.stylus, }; }