Melo v2.10 - YouTube Proxy, Download-Screen, Logger, iOS

This commit is contained in:
Hermes (Server)
2026-07-26 14:24:10 +02:00
parent b4baef5875
commit 3e9389ab59
17 changed files with 1065 additions and 347 deletions
+45 -20
View File
@@ -12,31 +12,56 @@ class MeloHeader extends StatelessWidget {
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ShaderMask(
shaderCallback: (bounds) => const LinearGradient(
colors: [Colors.white, MeloTheme.rot],
).createShader(bounds),
child: const Text('Melo', style: TextStyle(
fontSize: 28, fontWeight: FontWeight.w700, color: Colors.white)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShaderMask(
shaderCallback: (bounds) => const LinearGradient(
colors: [Colors.white, MeloTheme.rot],
).createShader(bounds),
child: const Text('Melo', style: TextStyle(
fontSize: 28, fontWeight: FontWeight.w700, color: Colors.white)),
),
const Text('Deine Musik. Deine Art.', style: TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)),
],
),
const Text('Deine Musik. Deine Art.', style: TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)),
Row(children: [
if (onServer != null) ...[
_btn(Icons.cloud, onServer!),
const SizedBox(width: 8),
],
_btn(Icons.download, onDownload),
const SizedBox(width: 8),
_btn(Icons.search, onSearch),
]),
],
),
Row(children: [
if (onServer != null) ...[
_btn(Icons.cloud, onServer!),
const SizedBox(width: 8),
],
_btn(Icons.download, onDownload),
const SizedBox(width: 8),
_btn(Icons.search, onSearch),
]),
const SizedBox(height: 8),
// Suchleiste immer sichtbar
TextField(
onSubmitted: (wert) {
if (wert.isNotEmpty) onSearch();
},
style: const TextStyle(color: Colors.white, fontSize: 14),
decoration: InputDecoration(
hintText: '🔍 Song, Künstler oder Tag suchen...',
hintStyle: TextStyle(color: Colors.grey.shade600, fontSize: 13),
prefixIcon: Icon(Icons.search, size: 18, color: MeloTheme.rot.withValues(alpha: 0.6)),
filled: true,
fillColor: MeloTheme.dunkel1,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
),
),
],
),
);
+51 -27
View File
@@ -52,7 +52,7 @@ class _NavidromeBrowserState extends State<NavidromeBrowser> {
else if (vm.navidromeAlben.isEmpty)
_platzhalter('"Alben laden" um Musik zu sehen')
else
_albumListe(context, vm.navidromeAlben as List<SubsonicAlbum>),
_albumListe(context, vm.navidromeAlben),
],
),
);
@@ -191,6 +191,11 @@ class _NavidromeBrowserState extends State<NavidromeBrowser> {
onTap: () => widget.vm.downloadNavidromeSong(s).then((_) => setState(() {})),
child: const Icon(Icons.download, size: 18, color: MeloTheme.rot),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => widget.vm.starteNavidromeStream(s),
child: const Icon(Icons.play_arrow, size: 18, color: Colors.greenAccent),
),
],
),
);
@@ -214,36 +219,55 @@ class _NavidromeBrowserState extends State<NavidromeBrowser> {
final urlCtrl = TextEditingController();
final userCtrl = TextEditingController();
final passCtrl = TextEditingController();
bool verbindet = false;
String? fehler;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: const Text('🌐 Navidrome', style: TextStyle(color: Colors.white, fontSize: 16)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(controller: urlCtrl, style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(labelText: 'Server-URL', hintText: 'https://musik.baka-net.de',
labelStyle: TextStyle(color: Colors.grey), hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())),
const SizedBox(height: 8),
TextField(controller: userCtrl, style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(labelText: 'Benutzer', labelStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())),
const SizedBox(height: 8),
TextField(controller: passCtrl, style: const TextStyle(color: Colors.white), obscureText: true,
decoration: const InputDecoration(labelText: 'Passwort', labelStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())),
builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) => AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: const Text('🌐 Navidrome', style: TextStyle(color: Colors.white, fontSize: 16)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(controller: urlCtrl, style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(labelText: 'Server-URL', hintText: 'https://musik.baka-net.de',
labelStyle: TextStyle(color: Colors.grey), hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())),
const SizedBox(height: 8),
TextField(controller: userCtrl, style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(labelText: 'Benutzer', labelStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())),
const SizedBox(height: 8),
TextField(controller: passCtrl, style: const TextStyle(color: Colors.white), obscureText: true,
decoration: const InputDecoration(labelText: 'Passwort', labelStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())),
if (fehler != null) ...[
const SizedBox(height: 8),
Text(fehler!, style: const TextStyle(color: Colors.red, fontSize: 12)),
],
if (verbindet)
const Padding(padding: EdgeInsets.only(top: 12), child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: MeloTheme.rot))),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')),
TextButton(
onPressed: verbindet ? null : () async {
verbindet = true;
setDialogState(() {});
widget.vm.verbindeNavidrome(urlCtrl.text, userCtrl.text, passCtrl.text);
final ok = await widget.vm.ladeNavidromeAlben();
verbindet = false;
if (ok && ctx.mounted) {
Navigator.pop(ctx);
if (context.mounted) setState(() {});
} else if (ctx.mounted) {
setDialogState(() => fehler = '❌ Keine Verbindung\nPrüfe URL + Zugangsdaten');
}
},
child: const Text('Verbinden', style: TextStyle(color: MeloTheme.rot)),
),
],
),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')),
TextButton(
onPressed: () {
widget.vm.verbindeNavidrome(urlCtrl.text, userCtrl.text, passCtrl.text);
Navigator.pop(ctx);
widget.vm.ladeNavidromeAlben().then((_) => setState(() {}));
},
child: const Text('Verbinden', style: TextStyle(color: MeloTheme.rot)),
),
],
),
);
}