m3mo d8164be49a Add user authentication to Flutter frontend
- Create auth feature with Clean Architecture (domain/data/presentation)
- Add login and register pages with form validation
- Implement secure token storage with flutter_secure_storage
- Create AuthenticatedClient for automatic Bearer token headers
- Add AuthViewModel for global auth state management
- Update router with auth guards (redirect to login if not authenticated)
- Add logout option to settings page
- Update TaskRemoteDataSource to use authenticated client
- Add auth-related localization strings (EN/DE)
2026-02-02 22:58:07 +01:00

277 lines
7.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:agenda_tasks/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../../../auth/presentation/viewmodels/auth_viewmodel.dart';
import '../viewmodels/settings_viewmodel.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final settingsVm = context.watch<SettingsViewModel>();
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.pop(),
),
),
body: ListView(
children: [
_SectionHeader(title: l10n.general),
ListTile(
leading: const Icon(Icons.language),
title: Text(l10n.language),
subtitle: Text(settingsVm.getLanguageName(settingsVm.locale!)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showLanguageDialog(context, settingsVm, l10n),
),
const Divider(),
_SectionHeader(title: l10n.appearance),
ListTile(
leading: const Icon(Icons.dark_mode),
title: Text(l10n.darkMode),
subtitle: Text(_getThemeModeLabel(settingsVm.themeMode, l10n)),
trailing: const Icon(Icons.chevron_right),
onTap: () => _showThemeDialog(context, settingsVm, l10n),
),
const Divider(),
_SectionHeader(title: l10n.about),
ListTile(
leading: const Icon(Icons.info_outline),
title: Text(l10n.version),
subtitle: const Text('1.0.0'),
),
const Divider(),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SizedBox(
height: 56,
child: OutlinedButton.icon(
onPressed: () => _showLogoutConfirmation(context, l10n),
icon: const Icon(Icons.logout),
label: Text(l10n.logout),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
side: BorderSide(color: Theme.of(context).colorScheme.error),
),
),
),
),
const SizedBox(height: 32),
],
),
);
}
String _getThemeModeLabel(ThemeMode mode, AppLocalizations l10n) {
switch (mode) {
case ThemeMode.system:
return l10n.systemDefault;
case ThemeMode.light:
return l10n.lightMode;
case ThemeMode.dark:
return l10n.darkModeOption;
}
}
void _showLanguageDialog(
BuildContext context,
SettingsViewModel vm,
AppLocalizations l10n,
) {
showDialog(
context: context,
builder: (dialogContext) => _LanguageDialog(
vm: vm,
l10n: l10n,
),
);
}
void _showThemeDialog(
BuildContext context,
SettingsViewModel vm,
AppLocalizations l10n,
) {
showDialog(
context: context,
builder: (dialogContext) => _ThemeDialog(
vm: vm,
l10n: l10n,
),
);
}
void _showLogoutConfirmation(BuildContext context, AppLocalizations l10n) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(l10n.logout),
content: Text('Are you sure you want to logout?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () {
Navigator.pop(dialogContext);
context.read<AuthViewModel>().logout();
},
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
),
child: Text(l10n.logout),
),
],
),
);
}
}
class _LanguageDialog extends StatefulWidget {
final SettingsViewModel vm;
final AppLocalizations l10n;
const _LanguageDialog({required this.vm, required this.l10n});
@override
State<_LanguageDialog> createState() => _LanguageDialogState();
}
class _LanguageDialogState extends State<_LanguageDialog> {
late String _selectedLanguage;
@override
void initState() {
super.initState();
_selectedLanguage = widget.vm.locale!.languageCode;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.l10n.language),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
...SettingsViewModel.supportedLocales.map((locale) {
return ListTile(
title: Text(widget.vm.getLanguageName(locale)),
leading: Radio<String>(
value: locale.languageCode,
groupValue: _selectedLanguage,
onChanged: (value) {
setState(() => _selectedLanguage = value!);
widget.vm.setLocale(locale);
Navigator.pop(context);
},
),
onTap: () {
setState(() => _selectedLanguage = locale.languageCode);
widget.vm.setLocale(locale);
Navigator.pop(context);
},
);
}),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(widget.l10n.cancel),
),
],
);
}
}
class _ThemeDialog extends StatefulWidget {
final SettingsViewModel vm;
final AppLocalizations l10n;
const _ThemeDialog({required this.vm, required this.l10n});
@override
State<_ThemeDialog> createState() => _ThemeDialogState();
}
class _ThemeDialogState extends State<_ThemeDialog> {
late ThemeMode _selectedMode;
@override
void initState() {
super.initState();
_selectedMode = widget.vm.themeMode;
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.l10n.darkMode),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildThemeOption(ThemeMode.system, widget.l10n.systemDefault),
_buildThemeOption(ThemeMode.light, widget.l10n.lightMode),
_buildThemeOption(ThemeMode.dark, widget.l10n.darkModeOption),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(widget.l10n.cancel),
),
],
);
}
Widget _buildThemeOption(ThemeMode mode, String label) {
return ListTile(
title: Text(label),
leading: Radio<ThemeMode>(
value: mode,
groupValue: _selectedMode,
onChanged: (value) {
setState(() => _selectedMode = value!);
widget.vm.setThemeMode(value!);
Navigator.pop(context);
},
),
onTap: () {
setState(() => _selectedMode = mode);
widget.vm.setThemeMode(mode);
Navigator.pop(context);
},
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title.toUpperCase(),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
}
}