feat(app): Flutter-клиент — логин, дашборд, потоки, категории, транзакции, правила
Riverpod + go_router с auth-guard, NavigationRail на широком экране и bottom bar на узком. Токены в flutter_secure_storage, на web access живёт в памяти. Интерцептор подставляет токен и делает ровно один refresh на 401. Деньги приходят строками и форматируются через Decimal: парсить их в double значило бы терять копейки ровно там, где бэкенд их бережёт.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../core/api/common_providers.dart';
|
||||
import '../../core/app_info.dart';
|
||||
import '../../core/auth/auth_controller.dart';
|
||||
import '../../core/config.dart';
|
||||
import '../../core/theme/theme_controller.dart';
|
||||
import '../../core/widgets/async_value_view.dart';
|
||||
|
||||
/// Настройки: API endpoint, signed-in account, theme and logout.
|
||||
class SettingsPage extends ConsumerWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final me = ref.watch(meProvider);
|
||||
final themeMode = ref.watch(themeModeProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Настройки')),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: [
|
||||
const ListTile(
|
||||
leading: Icon(Icons.dns_outlined),
|
||||
title: Text('Адрес API'),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.only(left: 56, right: 16),
|
||||
title: SelectableText(apiBaseUrl()),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.account_circle_outlined),
|
||||
title: const Text('Аккаунт'),
|
||||
subtitle: AsyncValueView(
|
||||
value: me,
|
||||
data: (u) => Text(u.email),
|
||||
onRetry: () => ref.invalidate(meProvider),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
const ListTile(
|
||||
leading: Icon(Icons.palette_outlined),
|
||||
title: Text('Тема'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: SegmentedButton<ThemeMode>(
|
||||
segments: const [
|
||||
ButtonSegment(
|
||||
value: ThemeMode.system,
|
||||
label: Text('Системная'),
|
||||
icon: Icon(Icons.brightness_auto_outlined),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.light,
|
||||
label: Text('Светлая'),
|
||||
icon: Icon(Icons.light_mode_outlined),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: ThemeMode.dark,
|
||||
label: Text('Тёмная'),
|
||||
icon: Icon(Icons.dark_mode_outlined),
|
||||
),
|
||||
],
|
||||
selected: {themeMode},
|
||||
onSelectionChanged: (selection) =>
|
||||
ref.read(themeModeProvider.notifier).setMode(selection.first),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
const ListTile(
|
||||
leading: Icon(Icons.info_outline),
|
||||
title: Text('Версия приложения'),
|
||||
subtitle: Text(appVersion),
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: Icon(Icons.logout, color: Theme.of(context).colorScheme.error),
|
||||
title: Text('Выйти', style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
onTap: () => ref.read(authControllerProvider.notifier).logout(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user