flutter-update-forge-companion/lib/screens/login_screen.dart

171 lines
6.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import '../config/theme.dart';
import '../services/api_service.dart';
class LoginScreen extends HookWidget {
final ApiService api;
final VoidCallback onLogin;
const LoginScreen({super.key, required this.api, required this.onLogin});
@override
Widget build(BuildContext context) {
final emailController = useTextEditingController();
final passwordController = useTextEditingController();
final serverController = useTextEditingController(text: api.baseUrl);
final formKey = useMemoized(() => GlobalKey<FormState>());
final loading = useState(false);
final error = useState<String?>(null);
Future<void> login() async {
if (!formKey.currentState!.validate()) return;
loading.value = true;
error.value = null;
try {
api.baseUrl = serverController.text.trim();
await api.login(
emailController.text.trim(),
passwordController.text,
);
onLogin();
} on ApiException catch (e) {
error.value = e.message;
} catch (_) {
error.value = 'Connection failed';
} finally {
loading.value = false;
}
}
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32),
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 72,
height: 72,
decoration: BoxDecoration(
color: AppColors.accent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(20),
),
child: Image.asset(
'assets/images/app_icon.png',
width: 36,
height: 36,
fit: BoxFit.contain,
),
),
const SizedBox(height: 24),
const Text(
'UpdateForge',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColors.text,
),
),
const SizedBox(height: 6),
Text(
'Sign in to access your apps',
style: TextStyle(color: AppColors.muted, fontSize: 14),
),
const SizedBox(height: 40),
TextFormField(
controller: serverController,
decoration: const InputDecoration(
labelText: 'Server URL',
hintText: 'https://example.com',
prefixIcon: Icon(Icons.dns_outlined, size: 20),
),
keyboardType: TextInputType.url,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'Required';
final url = Uri.tryParse(v.trim());
if (url == null || !url.hasScheme) return 'Invalid URL';
return null;
},
),
const SizedBox(height: 14),
TextFormField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.mail_outline, size: 20),
),
keyboardType: TextInputType.emailAddress,
validator: (v) =>
v == null || v.trim().isEmpty ? 'Required' : null,
),
const SizedBox(height: 14),
TextFormField(
controller: passwordController,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock_outline, size: 20),
),
obscureText: true,
onFieldSubmitted: (_) => login(),
validator: (v) =>
v == null || v.isEmpty ? 'Required' : null,
),
if (error.value != null) ...[
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.danger.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.danger.withValues(alpha: 0.3),
),
),
child: Row(
children: [
const Icon(Icons.error_outline,
color: AppColors.danger, size: 18),
const SizedBox(width: 10),
Expanded(
child: Text(
error.value!,
style: const TextStyle(
color: AppColors.danger,
fontSize: 13,
),
),
),
],
),
),
],
const SizedBox(height: 28),
ElevatedButton(
onPressed: loading.value ? null : login,
child: loading.value
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('Sign In'),
),
],
),
),
),
),
),
);
}
}