This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/screens/login_screen.dart
T
Dustin 79b15a3cb8 v2.56 — UI/Design-Rework: Melo Dark Fusion (neue Startseite, 2-stufige Einstellungen, Design-System)
- Design-System: MeloDesignTokens ThemeExtension, Outfit-Font, Farben #0B0B10/#14141C/#1C1C26
- Akzentfarbe-User-Einstellung behalten (MeloTheme.akzentNotifier), Gradient aus Akzent
- Startseite: Hero-Weiterhören-Karte mit Cover, Titel, Glow-Play-Button
- 2-stufige Einstellungen: Standard + Expertenmodus (SharedPreferences Key 'experten_modus')
- Komponenten: Pill-Buttons (radius 999), Card-Radius 20, Glow-Shadow, PressScale-Widget
- Login: Gradient-Button mit Akzent-Glow
- 199 Tests grün (194 bestehend + 5 neu): ThemeExtension, Expertenmodus, Akzent, PressScale, Hero
- flutter analyze lib/ + test/ = 0 Issues
- KEIN APK
2026-08-06 19:56:02 +02:00

397 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import '../services/navidrome_service.dart';
import '../utils/farb_theme.dart';
import '../utils/user_effekte.dart';
import '../config/app_config.dart';
import 'home_screen.dart';
/// Melo Login-Screen Melo Dark Fusion Design
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen>
with SingleTickerProviderStateMixin {
final AuthService _auth = AuthService();
final _userCtrl = TextEditingController();
final _passCtrl = TextEditingController();
bool _ladt = false;
String? _status;
bool _statusOk = false;
bool _passSichtbar = false;
late final AnimationController _animCtrl;
late final Animation<double> _fadeAnim;
@override
void initState() {
super.initState();
_animCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_fadeAnim = CurvedAnimation(parent: _animCtrl, curve: Curves.easeOut);
_animCtrl.forward();
}
@override
void dispose() {
_userCtrl.dispose();
_passCtrl.dispose();
_animCtrl.dispose();
super.dispose();
}
void _setzeStatus(String msg, {bool ok = false}) {
setState(() {
_status = msg;
_statusOk = ok;
});
}
Future<void> _login() async {
final user = _userCtrl.text.trim();
final pass = _passCtrl.text.trim();
if (user.isEmpty || pass.isEmpty) {
_setzeStatus('Bitte Benutzername und Passwort eingeben');
return;
}
setState(() => _ladt = true);
_setzeStatus('Verbinde mit Baka-Auth...');
final result = await _auth.login(user, pass);
if (!mounted) return;
if (result.erfolg) {
_setzeStatus('✅ Login erfolgreich!', ok: true);
await UserEffekt.anwenden(user);
await NavidromeService().speichereZugangsdaten(
AppConfig.navidromeUrl, user, pass);
await Future.delayed(const Duration(milliseconds: 600));
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MeloHome()),
);
}
} else {
_setzeStatus('❌ ${result.fehler ?? "Login fehlgeschlagen"}');
setState(() => _ladt = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MeloTheme.schwarz,
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24),
child: FadeTransition(
opacity: _fadeAnim,
child: _buildLogin(),
),
),
),
),
);
}
Widget _buildLogin() {
final akzent = MeloTheme.akzent;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// ─── Logo ───
_logoBereich(),
const SizedBox(height: 36),
// ─── Login-Karte ───
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(24),
border: Border.all(color: MeloTheme.dunkel2),
boxShadow: [
BoxShadow(
color: akzent.withValues(alpha: 0.15),
blurRadius: 30,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Anmelden',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
),
),
const SizedBox(height: 4),
const Text(
'Mit deinem Baka-Account',
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13),
),
const SizedBox(height: 24),
// Benutzername
_eingabeFeld(
controller: _userCtrl,
label: 'Benutzername',
icon: Icons.person_outline,
onSubmitted: (_) => _login(),
),
const SizedBox(height: 16),
// Passwort
_eingabeFeld(
controller: _passCtrl,
label: 'Passwort',
icon: Icons.lock_outline,
istPasswort: true,
passSichtbar: _passSichtbar,
onPasswortToggle: () => setState(() => _passSichtbar = !_passSichtbar),
onSubmitted: (_) => _login(),
),
const SizedBox(height: 24),
// Login Button Gradient Pill
SizedBox(
width: double.infinity,
height: 50,
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
gradient: LinearGradient(
colors: [akzent, Color.lerp(akzent, Colors.black, 0.4)!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: akzent.withValues(alpha: 0.3),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton(
onPressed: _ladt ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
disabledBackgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(999),
),
elevation: 0,
),
child: _ladt
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: Colors.white,
),
)
: const Text(
'Anmelden',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
// Status
if (_status != null) ...[
const SizedBox(height: 16),
_statusWidget(),
],
],
),
),
const SizedBox(height: 20),
// ─── Offline-Modus ───
const SizedBox(height: 16),
GestureDetector(
onTap: () {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MeloHome()),
);
},
child: const Text(
'Ohne Login fortfahren',
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
),
),
const SizedBox(height: 24),
],
);
}
Widget _logoBereich({bool klein = false}) {
final akzent = MeloTheme.akzent;
return Column(
children: [
// Icon
Container(
width: klein ? 64 : 80,
height: klein ? 64 : 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [akzent, Color.lerp(akzent, Colors.black, 0.5)!],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: akzent.withValues(alpha: 0.4),
blurRadius: 24,
offset: const Offset(0, 6),
),
],
),
child: const Center(
child: Text(
'♪',
style: TextStyle(fontSize: 36, color: Colors.white, fontWeight: FontWeight.w300),
),
),
),
const SizedBox(height: 16),
const Text(
'MELO',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w800,
letterSpacing: 6,
),
),
const SizedBox(height: 4),
Text(
'Musik. Für immer.',
style: TextStyle(
color: akzent.withValues(alpha: 0.8),
fontSize: 13,
letterSpacing: 2,
fontWeight: FontWeight.w400,
),
),
],
);
}
Widget _statusWidget() {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: _statusOk
? const Color(0xFF0D3B1E)
: const Color(0xFF3B0D0D),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: _statusOk ? const Color(0xFF2E7D32) : const Color(0xFF7D2E2E),
),
),
child: Row(
children: [
Icon(
_statusOk ? Icons.check_circle : Icons.error_outline,
size: 16,
color: _statusOk ? const Color(0xFF4CAF50) : const Color(0xFFEF5350),
),
const SizedBox(width: 8),
Expanded(
child: Text(
_status!,
style: TextStyle(
color: _statusOk ? const Color(0xFFA5D6A7) : const Color(0xFFEF9A9A),
fontSize: 12,
),
),
),
],
),
);
}
Widget _eingabeFeld({
required TextEditingController controller,
required String label,
required IconData icon,
bool istPasswort = false,
bool passSichtbar = false,
VoidCallback? onPasswortToggle,
String? hint,
TextInputType keyboardType = TextInputType.text,
ValueChanged<String>? onSubmitted,
}) {
return TextField(
controller: controller,
obscureText: istPasswort && !passSichtbar,
keyboardType: keyboardType,
style: const TextStyle(color: Colors.white, fontSize: 15),
onSubmitted: onSubmitted,
decoration: InputDecoration(
labelText: label,
hintText: hint,
hintStyle: const TextStyle(color: Color(0xFF444444), fontSize: 13),
labelStyle: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13),
prefixIcon: Icon(icon, color: MeloTheme.textSekundaer, size: 20),
filled: true,
fillColor: MeloTheme.dunkel2,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: MeloTheme.akzent, width: 1.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: const BorderSide(color: MeloTheme.dunkel2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
suffixIcon: istPasswort
? IconButton(
icon: Icon(
passSichtbar ? Icons.visibility : Icons.visibility_off,
size: 18,
color: MeloTheme.textSekundaer,
),
onPressed: onPasswortToggle,
)
: null,
),
);
}
}