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 36c744d6e3 v2.31 — Login, Einstellungen & Cloud Sync Redesign
- Login-Screen im Melo-Design (Schwarz+Rot) mit Baka-Auth JWT
- AuthService fuer Token-Management ueber baka-net.de/auth
- Registrierungs-Modus + 'Ohne Login fortfahren'
- Navidrome-Credentials optional im Login aufklappbar

- SettingsScreen ersetzt AlertDialog
- BottomNav navigiert statt Dialog zu triggern
- Cloud Sync, Server verbinden, Diagnosedaten, App-Info, Abmelden

- CloudEinstellungen-Dialog entfernt, alles in CloudScreen konsolidiert
- Hardcode login('Baka','') entfernt → AuthService.benutzer
- Gradient-Statuskarte, animierte Segment-Intervall-Auswahl
- JWT Authorization statt Klartext-Passwort

- AppConfig: API-Key defaultValue entfernt (kein Fallback-Leak)
- Build crasht ohne --dart-define MELO_API_KEY

Build: split APK (arm64 19.6M, armeabi 17.1M, x86_64 21.1M)
2026-08-01 15:03:22 +02:00

641 lines
20 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 'home_screen.dart';
/// Melo Login-Screen Schwarz+Rot Design
/// Baka-Auth + Navidrome-Credentials
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 NavidromeService _navidrome = NavidromeService();
final _userCtrl = TextEditingController();
final _passCtrl = TextEditingController();
final _urlCtrl = TextEditingController();
final _navUserCtrl = TextEditingController();
final _navPassCtrl = TextEditingController();
bool _ladt = false;
String? _status;
bool _statusOk = false;
bool _zeigeNavidrome = false;
bool _zeigeRegistrierung = false;
bool _passSichtbar = false;
bool _navPassSichtbar = false;
bool _regPassSichtbar = false;
// Registrierungs-Felder
final _regEmailCtrl = TextEditingController();
final _regUserCtrl = TextEditingController();
final _regPassCtrl = TextEditingController();
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();
_ladeGespeicherteDaten();
}
Future<void> _ladeGespeicherteDaten() async {
await _navidrome.ladeGespeicherteZugangsdaten();
// Navidrome-Daten für Anzeige vorbereiten
if (_navidrome.istVerbunden) {
setState(() {
_zeigeNavidrome = true;
});
}
}
@override
void dispose() {
_userCtrl.dispose();
_passCtrl.dispose();
_urlCtrl.dispose();
_navUserCtrl.dispose();
_navPassCtrl.dispose();
_regEmailCtrl.dispose();
_regUserCtrl.dispose();
_regPassCtrl.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);
// Navidrome-Credentials speichern falls eingegeben
if (_zeigeNavidrome) {
final url = _urlCtrl.text.trim();
final nUser = _navUserCtrl.text.trim();
final nPass = _navPassCtrl.text.trim();
if (url.isNotEmpty && nUser.isNotEmpty && nPass.isNotEmpty) {
await _navidrome.speichereZugangsdaten(url, nUser, nPass);
}
}
// Kurz warten, dann navigieren
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);
}
}
Future<void> _registrieren() async {
final user = _regUserCtrl.text.trim();
final pass = _regPassCtrl.text.trim();
final email = _regEmailCtrl.text.trim();
if (user.isEmpty || pass.isEmpty) {
_setzeStatus('Bitte alle Felder ausfüllen');
return;
}
setState(() => _ladt = true);
_setzeStatus('Registriere...');
final result = await _auth.registrieren(user, pass, email);
if (!mounted) return;
if (result.erfolg) {
_setzeStatus('✅ Registrierung erfolgreich!', ok: true);
await Future.delayed(const Duration(milliseconds: 600));
if (mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const MeloHome()),
);
}
} else {
_setzeStatus('❌ ${result.fehler ?? "Registrierung 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: _zeigeRegistrierung ? _buildRegister() : _buildLogin(),
),
),
),
),
);
}
Widget _buildLogin() {
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(20),
border: Border.all(color: MeloTheme.dunkel2),
boxShadow: [
BoxShadow(
color: MeloTheme.rot.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,
),
),
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
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _ladt ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: MeloTheme.rot,
disabledBackgroundColor: MeloTheme.dunkel2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
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),
// ─── Navidrome (optional) ───
_navidromeBereich(),
const SizedBox(height: 20),
// ─── Registrieren Link ───
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Noch keinen Account? ',
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13),
),
GestureDetector(
onTap: () => setState(() => _zeigeRegistrierung = true),
child: const Text(
'Registrieren',
style: TextStyle(
color: MeloTheme.rot,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
),
// ─── 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 _buildRegister() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_logoBereich(klein: true),
const SizedBox(height: 28),
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Registrieren',
style: TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w700),
),
const SizedBox(height: 20),
_eingabeFeld(
controller: _regUserCtrl,
label: 'Benutzername',
icon: Icons.person_outline,
),
const SizedBox(height: 14),
_eingabeFeld(
controller: _regEmailCtrl,
label: 'E-Mail (optional)',
icon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 14),
_eingabeFeld(
controller: _regPassCtrl,
label: 'Passwort',
icon: Icons.lock_outline,
istPasswort: true,
passSichtbar: _regPassSichtbar,
onPasswortToggle: () => setState(() => _regPassSichtbar = !_regPassSichtbar),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _ladt ? null : _registrieren,
style: ElevatedButton.styleFrom(
backgroundColor: MeloTheme.rot,
disabledBackgroundColor: MeloTheme.dunkel2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: 0,
),
child: _ladt
? const SizedBox(
width: 22, height: 22,
child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white),
)
: const Text('Registrieren',
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)),
),
),
if (_status != null) ...[
const SizedBox(height: 16),
_statusWidget(),
],
],
),
),
const SizedBox(height: 16),
GestureDetector(
onTap: () {
setState(() {
_zeigeRegistrierung = false;
_status = null;
});
},
child: const Text(
'← Zurück zum Login',
style: TextStyle(color: MeloTheme.rot, fontSize: 13, fontWeight: FontWeight.w600),
),
),
],
);
}
Widget _logoBereich({bool klein = false}) {
return Column(
children: [
// Icon
Container(
width: klein ? 64 : 80,
height: klein ? 64 : 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const LinearGradient(
colors: [Color(0xFFCC0000), Color(0xFF660000)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: MeloTheme.rot.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: MeloTheme.rot.withValues(alpha: 0.8),
fontSize: 13,
letterSpacing: 2,
fontWeight: FontWeight.w400,
),
),
],
);
}
Widget _navidromeBereich() {
return Column(
children: [
// Toggle
GestureDetector(
onTap: () => setState(() => _zeigeNavidrome = !_zeigeNavidrome),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_zeigeNavidrome ? Icons.dns : Icons.dns_outlined,
color: _zeigeNavidrome ? MeloTheme.rot : MeloTheme.textSekundaer,
size: 18,
),
const SizedBox(width: 8),
Text(
'🌐 Navidrome Server',
style: TextStyle(
color: _zeigeNavidrome ? Colors.white : MeloTheme.textSekundaer,
fontSize: 13,
),
),
const SizedBox(width: 6),
Icon(
_zeigeNavidrome ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
color: MeloTheme.textSekundaer,
size: 16,
),
],
),
),
),
// Erweiterte Navidrome-Felder
if (_zeigeNavidrome) ...[
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(
children: [
_eingabeFeld(
controller: _urlCtrl,
label: 'Server-URL',
icon: Icons.link,
hint: 'https://musik.baka-net.de',
),
const SizedBox(height: 12),
_eingabeFeld(
controller: _navUserCtrl,
label: 'Navidrome Benutzer',
icon: Icons.person_outline,
),
const SizedBox(height: 12),
_eingabeFeld(
controller: _navPassCtrl,
label: 'Navidrome Passwort',
icon: Icons.lock_outline,
istPasswort: true,
passSichtbar: _navPassSichtbar,
onPasswortToggle: () => setState(() => _navPassSichtbar = !_navPassSichtbar),
),
],
),
),
],
],
);
}
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(10),
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(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: MeloTheme.rot, width: 1.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
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,
),
);
}
}