Melo v2.10 - YouTube Proxy, Download-Screen, Logger, iOS
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user