Initial commit

This commit is contained in:
Dustin-Mike Jens Hähnel
2026-07-23 14:31:28 +02:00
commit 94a4063070
152 changed files with 8500 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import '../utils/farb_theme.dart';
class MeloHeader extends StatelessWidget {
final VoidCallback onDownload;
final VoidCallback onSearch;
const MeloHeader({super.key, required this.onDownload, required this.onSearch});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
child: 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: [
_btn(Icons.download, onDownload),
const SizedBox(width: 8),
_btn(Icons.search, onSearch),
]),
],
),
);
}
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,
),
);
}
}
+105
View File
@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import '../models/song.dart';
import '../database/db_helper.dart';
import '../utils/farb_theme.dart';
class MetadatenDialog extends StatefulWidget {
final Song song;
const MetadatenDialog({super.key, required this.song});
@override
State<MetadatenDialog> createState() => _MetadatenDialogState();
}
class _MetadatenDialogState extends State<MetadatenDialog> {
late TextEditingController _titelCtrl;
late TextEditingController _kuenstlerCtrl;
late TextEditingController _albumCtrl;
final DbHelper _db = DbHelper();
@override
void initState() {
super.initState();
_titelCtrl = TextEditingController(text: widget.song.titel);
_kuenstlerCtrl = TextEditingController(text: widget.song.kuenstler);
_albumCtrl = TextEditingController(text: widget.song.album);
}
@override
void dispose() {
_titelCtrl.dispose();
_kuenstlerCtrl.dispose();
_albumCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: MeloTheme.dunkel1,
title: const Text('✏️ Metadaten', style: TextStyle(color: Colors.white, fontSize: 18)),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_field('Titel', _titelCtrl),
const SizedBox(height: 12),
_field('Künstler', _kuenstlerCtrl),
const SizedBox(height: 12),
_field('Album', _albumCtrl),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Abbrechen', style: TextStyle(color: Colors.white54)),
),
TextButton(
onPressed: () => _speichern(),
child: const Text('Speichern', style: TextStyle(color: MeloTheme.rot)),
),
],
);
}
Widget _field(String label, TextEditingController ctrl) {
return TextField(
controller: ctrl,
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
labelText: label,
labelStyle: const TextStyle(color: Colors.grey),
border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: MeloTheme.rot),
),
),
);
}
Future<void> _speichern() async {
final id = widget.song.id;
if (id == null) {
if (mounted) Navigator.pop(context, false);
return;
}
final titel = _titelCtrl.text.trim();
final kuenstler = _kuenstlerCtrl.text.trim();
final album = _albumCtrl.text.trim();
if (titel.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Titel darf nicht leer sein')),
);
return;
}
await _db.metadatenAktualisieren(
id,
titel: titel,
kuenstler: kuenstler,
album: album,
);
if (mounted) Navigator.pop(context, true);
}
}
+145
View File
@@ -0,0 +1,145 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import '../services/player_service.dart';
import '../utils/farb_theme.dart';
class MiniPlayer extends StatefulWidget {
const MiniPlayer({super.key});
@override
State<MiniPlayer> createState() => _MiniPlayerState();
}
class _MiniPlayerState extends State<MiniPlayer> {
final PlayerService _player = PlayerService();
Duration _position = Duration.zero;
Duration _dauer = Duration.zero;
bool _spielt = false;
StreamSubscription<Duration>? _posSub;
StreamSubscription<PlayerState>? _stateSub;
@override
void initState() {
super.initState();
_posSub = _player.positionStream.listen((pos) {
if (mounted) {
setState(() => _position = pos);
}
});
_stateSub = _player.stateStream.listen((state) {
if (mounted) {
setState(() {
_spielt = state.playing;
_dauer = state.processingState == ProcessingState.ready
? (_player.dauer) : Duration.zero;
});
}
});
}
@override
void dispose() {
_posSub?.cancel();
_stateSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final song = _player.aktuellerSong;
if (song == null) return const SizedBox.shrink();
final progress = _dauer.inSeconds > 0
? _position.inSeconds / _dauer.inSeconds : 0.0;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: const Color(0xFF1A0A0A),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFF2A1515)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 12, 6),
child: Row(
children: [
// Cover
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Container(
width: 36, height: 36,
color: MeloTheme.rot,
child: const Center(child: Text('', style: TextStyle(fontSize: 16))),
),
),
const SizedBox(width: 10),
// Titel + Künstler
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(song.titel, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white)),
Text(song.kuenstler, style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)),
],
),
),
// Steuerung
_btn(Icons.skip_previous, _player.vorheriges),
_playBtn(),
_btn(Icons.skip_next, _player.naechstes),
],
),
),
// Fortschritt
if (_dauer.inSeconds > 0) Padding(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: progress,
backgroundColor: const Color(0xFF2A2A2A),
valueColor: const AlwaysStoppedAnimation(MeloTheme.rot),
minHeight: 2,
),
),
),
],
),
);
}
Widget _btn(IconData icon, VoidCallback onTap) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(50),
onTap: onTap,
child: Container(
width: 32, height: 32,
alignment: Alignment.center,
child: Icon(icon, size: 18, color: Colors.white),
),
),
);
}
Widget _playBtn() {
return Material(
color: MeloTheme.rot,
borderRadius: BorderRadius.circular(50),
child: InkWell(
borderRadius: BorderRadius.circular(50),
onTap: _player.playPause,
child: Container(
width: 36, height: 36,
alignment: Alignment.center,
child: Icon(_spielt ? Icons.pause : Icons.play_arrow, size: 20, color: Colors.white),
),
),
);
}
}
+71
View File
@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import '../models/song.dart';
import '../utils/farb_theme.dart';
import 'metadaten_dialog.dart';
class SongTile extends StatelessWidget {
final Song song;
final bool istFavorit;
final ValueChanged<Song> onFavoriteToggle;
final ValueChanged<Song> onPlay;
final VoidCallback onMetadataChanged;
const SongTile({
super.key,
required this.song,
required this.istFavorit,
required this.onFavoriteToggle,
required this.onPlay,
required this.onMetadataChanged,
});
@override
Widget build(BuildContext context) {
final hatDatei = song.dateiPfad.isNotEmpty;
return ListTile(
contentPadding: const EdgeInsets.symmetric(vertical: 2),
leading: Container(
width: 44, height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
gradient: const LinearGradient(colors: [Color(0xFF1A0000), Color(0xFF660000)]),
),
child: const Center(child: Text('', style: TextStyle(fontSize: 18, color: Colors.white54))),
),
title: Text(song.titel, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)),
subtitle: Text(song.kuenstler, style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () async {
final geaendert = await showDialog<bool>(
context: context,
builder: (_) => MetadatenDialog(song: song),
);
if (geaendert == true) onMetadataChanged();
},
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(Icons.edit, size: 14, color: MeloTheme.textSekundaer),
),
),
InkWell(
borderRadius: BorderRadius.circular(50),
onTap: () => onFavoriteToggle(song),
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(
istFavorit ? Icons.favorite : Icons.favorite_border,
size: 16, color: MeloTheme.rot,
),
),
),
Text(song.dauerFormatiert, style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)),
],
),
onTap: hatDatei ? () => onPlay(song) : null,
);
}
}
+50
View File
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import '../utils/farb_theme.dart';
class StatistikCard extends StatelessWidget {
final int anzahlSongs;
final String gesamtMB;
final int gesamtMin;
final int anzahlFavoriten;
const StatistikCard({
super.key,
required this.anzahlSongs,
required this.gesamtMB,
required this.gesamtMin,
required this.anzahlFavoriten,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: const Color(0xFF1A0A0A),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: const Color(0xFF2A1515)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_item('$anzahlSongs', 'Lieder'),
_item(gesamtMB, 'MB'),
_item('$gesamtMin', 'Min'),
_item('$anzahlFavoriten', 'Favoriten'),
],
),
),
);
}
Widget _item(String zahl, String label) {
return Column(
children: [
Text(zahl, style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: MeloTheme.rot)),
Text(label, style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)),
],
);
}
}
+57
View File
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import '../utils/farb_theme.dart';
class TagLeiste extends StatelessWidget {
final List<Map<String, String>> tags;
final String aktiverTag;
final ValueChanged<String> onTagSelected;
const TagLeiste({
super.key,
required this.tags,
required this.aktiverTag,
required this.onTagSelected,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('🏷️ Tags', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)),
Text('+ Neu', style: TextStyle(fontSize: 12, color: MeloTheme.rot, fontWeight: FontWeight.w500)),
],
),
),
SizedBox(
height: 36,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
children: tags.map((tag) {
final aktiv = aktiverTag == tag['name'];
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text('${tag['icon']} ${tag['name']}',
style: TextStyle(fontSize: 13, color: aktiv ? Colors.white : MeloTheme.textSekundaer)),
selected: aktiv,
onSelected: (_) => onTagSelected(tag['name']!),
selectedColor: MeloTheme.rot,
backgroundColor: MeloTheme.dunkel1,
side: BorderSide.none,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
);
}).toList(),
),
),
],
);
}
}