Makes the coarse area human, per feedback that a typed code is jargon. - CoarseLocationProvider interface + geolocator-backed impl (BSD-3): low accuracy only, returns null on any denial/error (degrades quietly). Behind an interface so it's fakeable and the plugin stays at the composition root. - Config sheet: a 'Use my approximate location' button (shown only when a provider is wired) fills the area from the device position, reduced to a 5-char geohash via commons_core's Geohash — a precise fix never leaves the device. 'Couldn't get your location' on failure. - Threaded via TaneApp(location); main wires GeolocatorCoarseLocation. - Platform: ACCESS_COARSE_LOCATION (Android) + NSLocationWhenInUseUsageDescription (iOS), both scoped to the coarse-area use. - i18n en/es/pt. Widget test fills the area from a fake location. 8 tests green.
162 lines
5.1 KiB
Dart
162 lines
5.1 KiB
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/coarse_location.dart';
|
|
import 'services/onboarding_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/home_screen.dart';
|
|
import 'ui/intro_screen.dart';
|
|
import 'ui/inventory_list_screen.dart';
|
|
import 'ui/market_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.showIntro = false,
|
|
super.key,
|
|
}) : _router = _buildRouter(
|
|
repository,
|
|
onboarding,
|
|
showIntro,
|
|
social,
|
|
socialSettings,
|
|
location,
|
|
);
|
|
|
|
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;
|
|
final bool showIntro;
|
|
final GoRouter _router;
|
|
|
|
static GoRouter _buildRouter(
|
|
VarietyRepository repository,
|
|
OnboardingStore onboarding,
|
|
bool showIntro,
|
|
SocialService? social,
|
|
SocialSettings? socialSettings,
|
|
CoarseLocationProvider? location,
|
|
) {
|
|
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,
|
|
),
|
|
),
|
|
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: 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(),
|
|
locale: TranslationProvider.of(context).flutterLocale,
|
|
supportedLocales: AppLocaleUtils.supportedLocales,
|
|
localizationsDelegates: const [
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
routerConfig: _router,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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<PointerDeviceKind> get dragDevices => {
|
|
PointerDeviceKind.touch,
|
|
PointerDeviceKind.mouse,
|
|
PointerDeviceKind.trackpad,
|
|
PointerDeviceKind.stylus,
|
|
};
|
|
}
|