- 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)
277 lines
7.6 KiB
Dart
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,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|