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
Hermes (Server)andClaude Haiku 4.5 653bc612f5 BUG-FIX: Login auf Navidrome umgestellt (Baka-Auth + Cloud gelöscht)
Fehlerpfad (502):
- App versuchte bei Startup, sich bei altem Baka-Auth (8993/8994) anzumelden → gelöscht
- CloudService versuchte, Cloud-API zu erreichen → gelöscht
- AuthService.initialisieren() crashte bei 502

Lösung:
- LoginScreen nutzt jetzt Navidrome.ping() statt AuthService.login()
- main.dart: AuthService.initialisieren() entfernt (nicht mehr nötig)
- MeloApp: Prüft jetzt Navidrome-Credentials statt Baka-Auth-Token
- MeloApp zeigt Loading-Spinner während Credentials geladen werden

Musikserver-Chip: Nutzt jetzt nur Navidrome (nicht mehr Cloud)
Alle 202 Tests grün

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8HvgtKSsaDZ8bjZrYBBms
2026-08-19 07:11:35 +02:00

398 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 Navidrome...');
final nav = NavidromeService();
nav.setCredentials(AppConfig.navidromeUrl, user, pass);
final ok = await nav.ping();
if (!mounted) return;
if (ok) {
_setzeStatus('✅ Login erfolgreich!', ok: true);
await UserEffekt.anwenden(user);
await nav.speichereZugangsdaten(AppConfig.navidromeUrl, user, pass);
await Future.delayed(const Duration(milliseconds: 600));
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MeloHome()),
);
}
} else {
_setzeStatus('❌ Keine Verbindung zum Musikserver');
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 Navidrome-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,
),
);
}
}