Files
swingmusic-extended/swingmusic_mobile/lib/features/auth/enhanced_auth_screen.dart
T
2026-03-18 19:43:40 +01:00

469 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../../shared/providers/auth_provider.dart';
import '../../core/constants/app_spacing.dart';
import '../../core/enums/auth_state.dart';
class EnhancedAuthScreen extends StatefulWidget {
const EnhancedAuthScreen({super.key});
@override
State<EnhancedAuthScreen> createState() => _EnhancedAuthScreenState();
}
class _EnhancedAuthScreenState extends State<EnhancedAuthScreen> {
final _formKey = GlobalKey<FormState>();
final _baseUrlController = TextEditingController();
final _usernameController = TextEditingController();
final _passwordController = TextEditingController();
final _qrCodeController = TextEditingController();
bool _isPasswordVisible = false;
bool _isLoading = false;
bool _isQrMode = false;
String? _errorMessage;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<AuthProvider>(context, listen: false).initialize();
});
}
@override
void dispose() {
_baseUrlController.dispose();
_usernameController.dispose();
_passwordController.dispose();
_qrCodeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: SafeArea(
child: SingleChildScrollView(
padding: AppSpacing.paddingLG,
child: Column(
children: [
// Logo and Title
_buildHeader(context),
const SizedBox(height: 32),
// Auth Mode Toggle
_buildAuthModeToggle(context),
const SizedBox(height: 32),
// Auth Form
_buildAuthForm(context),
const SizedBox(height: 24),
// Error Message
if (_errorMessage != null)
_buildErrorMessage(context),
],
),
),
),
);
}
Widget _buildHeader(BuildContext context) {
return Column(
children: [
Icon(
Icons.music_note,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'SwingMusic',
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
'Connect to your music library',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
],
);
}
Widget _buildAuthModeToggle(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => setState(() => _isQrMode = false),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: _isQrMode
? Theme.of(context).colorScheme.surface
: Theme.of(context).colorScheme.primary,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.person,
color: _isQrMode
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onPrimary,
),
const SizedBox(width: 8),
Text(
'Username & Password',
style: TextStyle(
color: _isQrMode
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => setState(() => _isQrMode = true),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: _isQrMode
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.only(
topRight: Radius.circular(12),
bottomRight: Radius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.qr_code_scanner,
color: _isQrMode
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Text(
'QR Code',
style: TextStyle(
color: _isQrMode
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
],
),
);
}
Widget _buildAuthForm(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, authProvider, child) {
return Form(
key: _formKey,
child: Column(
children: [
if (_isQrMode) ...[
// QR Code Input
TextFormField(
controller: _qrCodeController,
decoration: InputDecoration(
labelText: 'QR Code',
hintText: 'Enter or scan QR code',
prefixIcon: const Icon(Icons.qr_code_scanner),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
readOnly: true,
onTap: () => _scanQrCode(context),
),
] else ...[
// Server URL Input
TextFormField(
controller: _baseUrlController,
decoration: InputDecoration(
labelText: 'Server URL',
hintText: 'https://your-server.com',
prefixIcon: const Icon(Icons.link),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
validator: (value) {
if (value == null || value!.isEmpty) {
return 'Server URL is required';
}
if (!_isValidUrl(value)) {
return 'Please enter a valid URL';
}
return null;
},
),
const SizedBox(height: 16),
// Username Input
TextFormField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Username',
hintText: 'Enter your username',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
validator: (value) {
if (value == null || value!.isEmpty) {
return 'Username is required';
}
return null;
},
),
const SizedBox(height: 16),
// Password Input
TextFormField(
controller: _passwordController,
obscureText: !_isPasswordVisible,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
onPressed: () => setState(() => _isPasswordVisible = !_isPasswordVisible),
icon: Icon(
_isPasswordVisible ? Icons.visibility : Icons.visibility_off,
),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
),
),
),
validator: (value) {
if (value == null || value!.isEmpty) {
return 'Password is required';
}
return null;
},
),
],
const SizedBox(height: 24),
// Login Button
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : () => _handleLogin(context),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(Colors.white),
),
)
: Text(
_isQrMode ? 'Login with QR' : 'Login',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
);
},
);
}
Widget _buildErrorMessage(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 14,
),
),
),
IconButton(
onPressed: () => setState(() => _errorMessage = null),
icon: const Icon(Icons.close),
iconSize: 16,
color: Theme.of(context).colorScheme.error,
),
],
),
);
}
bool _isValidUrl(String url) {
try {
final uri = Uri.parse(url);
return uri.hasScheme && (uri.scheme == 'http' || uri.scheme == 'https');
} catch (e) {
return false;
}
}
Future<void> _handleLogin(BuildContext context) async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
if (_isQrMode) {
await authProvider.loginWithQrCode(_qrCodeController.text.trim());
} else {
await authProvider.loginWithUsernameAndPassword(
_baseUrlController.text.trim(),
_usernameController.text.trim(),
_passwordController.text,
);
}
if (mounted) {
Navigator.of(context).pushReplacementNamed('/home');
}
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = e.toString();
});
}
}
Future<void> _scanQrCode(BuildContext context) async {
try {
final qrCode = await Navigator.pushNamed<String>('/qr');
if (qrCode != null && mounted) {
_qrCodeController.text = qrCode!;
}
} catch (e) {
setState(() {
_errorMessage = 'Failed to scan QR code: $e';
});
}
}
}