import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/api/api_client.dart'; import '../../../../core/constants.dart'; import '../../../../core/database/app_database.dart'; /// ThemeMode provider, watched by PrivacyMapsApp. final themeModeProvider = StateNotifierProvider((ref) { return ThemeModeNotifier(ref.watch(appDatabaseProvider)); }); class ThemeModeNotifier extends StateNotifier { final AppDatabase _db; ThemeModeNotifier(this._db) : super(ThemeMode.system) { _load(); } Future _load() async { final value = await _db.getSetting(AppConstants.settingThemeMode); if (value != null && mounted) { state = _parse(value); } } Future setThemeMode(ThemeMode mode) async { state = mode; await _db.setSetting(AppConstants.settingThemeMode, mode.name); } ThemeMode _parse(String value) { switch (value) { case 'light': return ThemeMode.light; case 'dark': return ThemeMode.dark; default: return ThemeMode.system; } } } class SettingsScreen extends ConsumerStatefulWidget { const SettingsScreen({super.key}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState { late final TextEditingController _backendUrlController; bool _urlSaved = false; @override void initState() { super.initState(); _backendUrlController = TextEditingController(); _loadBackendUrl(); } Future _loadBackendUrl() async { final db = ref.read(appDatabaseProvider); final url = await db.getSetting(AppConstants.settingBackendUrl); _backendUrlController.text = url ?? AppConstants.defaultBackendUrl; } @override void dispose() { _backendUrlController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final themeMode = ref.watch(themeModeProvider); return Scaffold( appBar: AppBar( title: const Text('Settings'), ), body: ListView( children: [ // Backend URL Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Backend Server', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), TextField( controller: _backendUrlController, decoration: InputDecoration( labelText: 'Server URL', hintText: AppConstants.defaultBackendUrl, suffixIcon: _urlSaved ? const Icon(Icons.check, color: Colors.green) : null, ), keyboardType: TextInputType.url, onChanged: (_) { if (_urlSaved) setState(() => _urlSaved = false); }, ), const SizedBox(height: 8), FilledButton( onPressed: _saveBackendUrl, child: const Text('Save'), ), ], ), ), const Divider(), // Theme Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text( 'Appearance', style: Theme.of(context).textTheme.titleSmall, ), ), RadioListTile( title: const Text('System default'), value: ThemeMode.system, groupValue: themeMode, onChanged: (v) => ref.read(themeModeProvider.notifier).setThemeMode(v!), ), RadioListTile( title: const Text('Day (light)'), value: ThemeMode.light, groupValue: themeMode, onChanged: (v) => ref.read(themeModeProvider.notifier).setThemeMode(v!), ), RadioListTile( title: const Text('Night (dark)'), value: ThemeMode.dark, groupValue: themeMode, onChanged: (v) => ref.read(themeModeProvider.notifier).setThemeMode(v!), ), const Divider(), // About Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text( 'About', style: Theme.of(context).textTheme.titleSmall, ), ), const ListTile( leading: Icon(Icons.info_outline), title: Text('Privacy Maps'), subtitle: Text('Version 1.0.0'), ), const Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text( 'A privacy-first maps application. No tracking, no analytics, ' 'no third-party SDKs. All data stays on your device or your ' 'own server.', ), ), const SizedBox(height: 16), // Attributions Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text( 'Open Source Attributions', style: Theme.of(context).textTheme.titleSmall, ), ), const _AttributionTile( name: 'OpenStreetMap', description: 'Map data', license: 'ODbL 1.0', ), const _AttributionTile( name: 'OSRM', description: 'Open Source Routing Machine', license: 'BSD 2-Clause', ), const _AttributionTile( name: 'Photon', description: 'Geocoding / search powered by Komoot', license: 'Apache License 2.0', ), const _AttributionTile( name: 'Martin', description: 'Vector tile server', license: 'Apache License 2.0 / MIT', ), const SizedBox(height: 32), ], ), ); } Future _saveBackendUrl() async { final url = _backendUrlController.text.trim(); if (url.isEmpty) return; final db = ref.read(appDatabaseProvider); await db.setSetting(AppConstants.settingBackendUrl, url); ref.read(apiClientProvider).updateBaseUrl(url); setState(() => _urlSaved = true); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Backend URL saved.')), ); } } } class _AttributionTile extends StatelessWidget { final String name; final String description; final String license; const _AttributionTile({ required this.name, required this.description, required this.license, }); @override Widget build(BuildContext context) { return ListTile( dense: true, title: Text(name), subtitle: Text(description), trailing: Text( license, style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Theme.of(context).colorScheme.outline, ), ), ); } }