maps/mobile/lib/features/settings/presentation/screens/settings_screen.dart
2026-03-31 21:01:08 +02:00

252 lines
7.2 KiB
Dart

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 =
NotifierProvider<ThemeModeNotifier, ThemeMode>(ThemeModeNotifier.new);
class ThemeModeNotifier extends Notifier<ThemeMode> {
late AppDatabase _db;
@override
ThemeMode build() {
_db = ref.watch(appDatabaseProvider);
Future.microtask(_load);
return ThemeMode.system;
}
Future<void> _load() async {
final value = await _db.getSetting(AppConstants.settingThemeMode);
if (value != null) {
state = _parse(value);
}
}
Future<void> 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<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
late final TextEditingController _backendUrlController;
bool _urlSaved = false;
@override
void initState() {
super.initState();
_backendUrlController = TextEditingController();
_loadBackendUrl();
}
Future<void> _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<ThemeMode>(
title: const Text('System default'),
value: ThemeMode.system,
groupValue: themeMode,
onChanged: (v) =>
ref.read(themeModeProvider.notifier).setThemeMode(v!),
),
RadioListTile<ThemeMode>(
title: const Text('Day (light)'),
value: ThemeMode.light,
groupValue: themeMode,
onChanged: (v) =>
ref.read(themeModeProvider.notifier).setThemeMode(v!),
),
RadioListTile<ThemeMode>(
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<void> _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,
),
),
);
}
}