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/widgets/melo_header.dart
T
Dustin fdfc166807 v2.51.2 — 5-Tab-Navigation (Rechtshänder) + Musikserver-Status
## Navigation (home_screen.dart)
- Neue Tab-Struktur: Start / Bibliothek / Jetzt läuft / Cloud / Mehr (NavigationBar, Icons home, library_music, play_circle_fill, cloud, more_horiz)
- ▶️ Jetzt läuft (Daumen-Zone rechts): öffnet Now-Playing-Fullscreen direkt; bei Pause Hinweis-Screen
- Start: Weiterhören (Zuletzt gehört), Recap-Karte, Statistik, Favoriten-Schnellzugriff, neue Downloads
- Bibliothek: Segmente Songs/Alben/Künstler/Jahre/Genres/Tags/Playlists/Favoriten/Downloads + 'Lied +'- und Such-Buttons oben
- Mehr: Einstellungen, Erweitert, Info, Abmelden
- Keine Funktionalität verloren: YT-Download, Scanner, Recap, Lyrics, Queue erreichbar

## Musikserver (Feature 2)
- 'Navidrome verbinden' → '☁️ Musikserver' + Statuspunkt (grün=verbunden, rot=offline, pulsierend=Sync läuft)
- Detail-Dialog: 'Verbunden mit Navidrome · musik.baka-net.de', Verbinden/Trennen/Browser
- Gespeicherte Zugangsdaten werden beim App-Start geladen
2026-08-05 07:57:01 +02:00

217 lines
6.7 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 '../utils/farb_theme.dart';
class MeloHeader extends StatelessWidget {
final VoidCallback onDownload;
final VoidCallback onSearch;
final VoidCallback? onServer;
final VoidCallback? onSettings;
/// Musikserver-Status (☁️ Musikserver + Statuspunkt):
/// `null` = kein Chip (alter Cloud-Button wird gezeigt).
final bool? serverVerbunden;
/// true = Sync läuft → Statuspunkt pulsiert.
final bool? serverSyncLaeuft;
const MeloHeader({
super.key,
required this.onDownload,
required this.onSearch,
this.onServer,
this.onSettings,
this.serverVerbunden,
this.serverSyncLaeuft,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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)),
],
),
Row(children: [
if (serverVerbunden != null) ...[
_ServerStatusChip(
verbunden: serverVerbunden!,
syncLaeuft: serverSyncLaeuft ?? false,
onTap: onServer,
),
const SizedBox(width: 8),
] else if (onServer != null) ...[
_btn(Icons.cloud, onServer!),
const SizedBox(width: 8),
],
_btn(Icons.download, onDownload),
const SizedBox(width: 8),
_btn(Icons.search, onSearch),
if (onSettings != null) ...[
const SizedBox(width: 8),
_btn(Icons.settings, onSettings!),
],
]),
],
),
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),
),
),
],
),
);
}
Widget _btn(IconData icon, VoidCallback onTap) {
return Container(
width: 40, height: 40,
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(12),
),
child: IconButton(
icon: Icon(icon, size: 18, color: MeloTheme.rot),
onPressed: onTap,
),
);
}
}
/// Kompakter „☁️ Musikserver“-Chip mit Statuspunkt.
/// Grün = verbunden, Rot = offline, pulsierend = Sync läuft.
class _ServerStatusChip extends StatefulWidget {
final bool verbunden;
final bool syncLaeuft;
final VoidCallback? onTap;
const _ServerStatusChip({
required this.verbunden,
required this.syncLaeuft,
this.onTap,
});
@override
State<_ServerStatusChip> createState() => _ServerStatusChipState();
}
class _ServerStatusChipState extends State<_ServerStatusChip>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
);
if (widget.syncLaeuft) _controller.repeat(reverse: true);
}
@override
void didUpdateWidget(covariant _ServerStatusChip oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.syncLaeuft && !oldWidget.syncLaeuft) {
_controller.repeat(reverse: true);
} else if (!widget.syncLaeuft && oldWidget.syncLaeuft) {
_controller.stop();
_controller.value = 0;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final farbe = widget.verbunden
? const Color(0xFF4CAF50)
: const Color(0xFFEF5350);
return GestureDetector(
onTap: widget.onTap,
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('☁️', style: TextStyle(fontSize: 13)),
const SizedBox(width: 6),
const Text(
'Musikserver',
style: TextStyle(fontSize: 11, color: Colors.white70, fontWeight: FontWeight.w500),
),
const SizedBox(width: 6),
AnimatedBuilder(
animation: _controller,
builder: (_, child) {
// Pulsierende Größe während des Syncs
final skala = widget.syncLaeuft
? 0.7 + (0.5 * _controller.value)
: 1.0;
return Transform.scale(
scale: skala,
child: child,
);
},
child: Container(
width: 9,
height: 9,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: farbe,
boxShadow: [
BoxShadow(
color: farbe.withValues(alpha: 0.6),
blurRadius: widget.syncLaeuft ? 10 : 5,
),
],
),
),
),
],
),
),
);
}
}