Initial commit
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
import '../../core/contracts/server.dart';
|
||||
import '../../providers/session_provider.dart';
|
||||
import 'app_loading_ring.dart';
|
||||
import 'app_inline_alert.dart';
|
||||
|
||||
class LoginForm extends ConsumerStatefulWidget {
|
||||
final EmbyServer server;
|
||||
final VoidCallback? onSuccess;
|
||||
final VoidCallback? onBack;
|
||||
|
||||
const LoginForm({
|
||||
super.key,
|
||||
required this.server,
|
||||
this.onSuccess,
|
||||
this.onBack,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<LoginForm> createState() => _LoginFormState();
|
||||
}
|
||||
|
||||
class _LoginFormState extends ConsumerState<LoginForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _username = TextEditingController();
|
||||
final _password = TextEditingController();
|
||||
bool _loading = false;
|
||||
bool _obscure = true;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_username.dispose();
|
||||
_password.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
await ref
|
||||
.read(sessionProvider.notifier)
|
||||
.login(
|
||||
serverId: widget.server.id,
|
||||
username: _username.text.trim(),
|
||||
password: _password.text,
|
||||
);
|
||||
if (!mounted) return;
|
||||
widget.onSuccess?.call();
|
||||
} catch (e) {
|
||||
setState(() => _error = e.toString());
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text('登录到 ${widget.server.name}', style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.server.baseUrl,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: _username,
|
||||
decoration: const InputDecoration(labelText: '用户名'),
|
||||
autofillHints: const [AutofillHints.username],
|
||||
validator: (v) => (v == null || v.trim().isEmpty) ? '请输入用户名' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _password,
|
||||
decoration: InputDecoration(
|
||||
labelText: '密码',
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscure
|
||||
? Icons.visibility_outlined
|
||||
: Icons.visibility_off_outlined,
|
||||
),
|
||||
onPressed: () => setState(() => _obscure = !_obscure),
|
||||
),
|
||||
),
|
||||
obscureText: _obscure,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
onFieldSubmitted: (_) => _submit(),
|
||||
),
|
||||
if (_error != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
AppInlineAlert(message: _error!),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (widget.onBack != null)
|
||||
FButton(
|
||||
variant: FButtonVariant.ghost,
|
||||
onPress: _loading ? null : widget.onBack,
|
||||
child: const Text('返回'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FButton(
|
||||
variant: FButtonVariant.primary,
|
||||
onPress: _loading ? null : _submit,
|
||||
child: _loading
|
||||
? const AppLoadingRing(size: 16, color: Colors.white)
|
||||
: const Text('登录'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user