import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:geolocator/geolocator.dart'; import '../../l10n/app_localizations.dart'; import '../places/location_flow.dart'; import '../places/location_service.dart'; import '../places/saved_places.dart'; import '../sources/sources_screen.dart'; /// Settings, currently just location. class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { LocationAvailability? _availability; @override void initState() { super.initState(); unawaited(_refreshAvailability()); } Future _refreshAvailability() async { final availability = await ref.read(locationServiceProvider).availability(); if (!mounted) return; setState(() => _availability = availability); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); final theme = Theme.of(context); final places = ref.watch(savedPlacesProvider).value ?? const []; return Scaffold( appBar: AppBar(title: Text(l10n.settingsTitle)), body: ListView( children: [ _SectionHeader(l10n.settingsLocationSection), // The disclosure text is shown here too, not only before the system // dialog: someone who already granted or refused should still be able // to read what the permission is actually used for. Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Text( l10n.locationDisclosureBody, style: theme.textTheme.bodyMedium, ), ), ListTile( leading: Icon(_iconFor(_availability)), title: Text(_statusLabel(l10n, _availability)), subtitle: _availability == LocationAvailability.granted ? null : Text(l10n.locateMe), trailing: switch (_availability) { LocationAvailability.denied => FilledButton( onPressed: () async { await requestPosition(context, ref); await _refreshAvailability(); }, child: Text(l10n.locationDisclosureContinue), ), LocationAvailability.deniedForever => TextButton( onPressed: () async { await Geolocator.openAppSettings(); await _refreshAvailability(); }, child: Text(l10n.locationOpenSystemSettings), ), LocationAvailability.serviceDisabled => TextButton( onPressed: () async { await Geolocator.openLocationSettings(); await _refreshAvailability(); }, child: Text(l10n.locationOpenSystemSettings), ), _ => null, }, ), const Divider(), _SectionHeader(l10n.placesTitle), ListTile( leading: const Icon(Icons.place_outlined), title: Text(l10n.placesTitle), subtitle: Text('${places.length} / ${SavedPlaces.maxPlaces}'), ), const Divider(), // The map no longer carries a permanent credit line, so this is the // route the attribution guidelines require. It must stay easy to // find: do not bury it deeper or drop the subtitle that says what it // holds. ListTile( leading: const Icon(Icons.info_outline), title: Text(l10n.sourcesTitle), subtitle: Text(l10n.settingsSourcesHint), trailing: const Icon(Icons.chevron_right), onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SourcesScreen()), ), ), ], ), ); } static IconData _iconFor(LocationAvailability? availability) => switch (availability) { LocationAvailability.granted => Icons.my_location, LocationAvailability.serviceDisabled => Icons.location_disabled, null => Icons.location_searching, _ => Icons.location_off, }; static String _statusLabel( AppLocalizations l10n, LocationAvailability? availability, ) => switch (availability) { LocationAvailability.granted => l10n.locationStatusGranted, LocationAvailability.denied => l10n.locationStatusDenied, LocationAvailability.deniedForever => l10n.locationStatusDeniedForever, LocationAvailability.serviceDisabled => l10n.locationStatusServiceDisabled, null => l10n.loading, }; } class _SectionHeader extends StatelessWidget { const _SectionHeader(this.title); final String title; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Text( title, style: theme.textTheme.titleSmall?.copyWith( color: theme.colorScheme.primary, ), ), ); } }