Riverpod + go_router с auth-guard, NavigationRail на широком экране и bottom bar на узком. Токены в flutter_secure_storage, на web access живёт в памяти. Интерцептор подставляет токен и делает ровно один refresh на 401. Деньги приходят строками и форматируются через Decimal: парсить их в double значило бы терять копейки ровно там, где бэкенд их бережёт.
90 lines
3.1 KiB
Dart
90 lines
3.1 KiB
Dart
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(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|