Адрес бэкенда хранится в shared_preferences и читается до runApp; switchApiBaseUrl сбрасывает токены и кэш старого сервера. Выход стирает refresh-токен и sqlite-кэш ответов. Экран настроек переделан под разделы.
128 lines
4.5 KiB
Dart
128 lines
4.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../core/auth/auth_controller.dart';
|
|
import '../../core/config.dart';
|
|
import '../settings/api_base_url_dialog.dart';
|
|
|
|
class LoginPage extends ConsumerStatefulWidget {
|
|
const LoginPage({super.key});
|
|
|
|
@override
|
|
ConsumerState<LoginPage> createState() => _LoginPageState();
|
|
}
|
|
|
|
class _LoginPageState extends ConsumerState<LoginPage> {
|
|
final _email = TextEditingController();
|
|
final _password = TextEditingController();
|
|
String? _error;
|
|
bool _busy = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_email.dispose();
|
|
_password.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
setState(() {
|
|
_busy = true;
|
|
_error = null;
|
|
});
|
|
final error = await ref
|
|
.read(authControllerProvider.notifier)
|
|
.login(_email.text.trim(), _password.text);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_busy = false;
|
|
_error = error;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final status = ref.watch(authControllerProvider).status;
|
|
return Scaffold(
|
|
body: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 360),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: status == AuthStatus.unknown
|
|
? const CircularProgressIndicator()
|
|
: AutofillGroup(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
'fin-tracker',
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Flexible(
|
|
child: Text(
|
|
ref.watch(apiBaseUrlProvider),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.edit_outlined, size: 16),
|
|
tooltip: 'Изменить адрес',
|
|
onPressed: () => showApiBaseUrlDialog(context),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
TextField(
|
|
controller: _email,
|
|
autofillHints: const [AutofillHints.username],
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: const InputDecoration(labelText: 'Email'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _password,
|
|
autofillHints: const [AutofillHints.password],
|
|
obscureText: true,
|
|
onSubmitted: (_) => _busy ? null : _submit(),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Пароль',
|
|
),
|
|
),
|
|
if (_error != null) ...[
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
_error!,
|
|
style: TextStyle(
|
|
color: Theme.of(context).colorScheme.error,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 24),
|
|
FilledButton(
|
|
onPressed: _busy ? null : _submit,
|
|
child: _busy
|
|
? const SizedBox.square(
|
|
dimension: 18,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Text('Войти'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|