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/settings_screen.dart
T
Dustin 20ef788178 v2.46 — Jahres-Recap (Spotify-Wrapped-Stil) mit Hörstatistiken
## Neuer Recap-Screen
- lib/screens/recap_screen.dart: Full-Screen mit 7 animierten Seiten (Intro, Top 5 Songs, Top 3 Künstler, Hörzeit, häufigster Tag, Nachtaktive %, Gesamtzahl + Teilen)
- Wrapped-Stil: Verlaufs-Gradienten, große Zahlen mit Count-up-Animation, Fade+Scale-Seitenwechsel, Fortschritts-Punkte, Weiter-Button
- Teilen-Button kopiert Recap-Text in die Zwischenablage (kein neues Package)

## Datenbasis
- lib/services/recap_service.dart: aggregiert lokale Abspiel-Historie zu Top-5-Songs, Top-3-Künstler, Hörzeit, Wochentag, Nacht-%, Gesamtzahl
- Fallback auf Gesamt-Historie wenn im Zieljahr keine Daten; freundlicher Leer-Zustand ohne History

## DB + Tracking
- db_helper.dart: Migration v6→v7 mit neuer Tabelle abspiel_historie (song_id, played_at) + Backfill aus wiedergabe_verlauf
- player_service.dart: registriert jeden Abspielvorgang in der Historie

## Einstieg
- Home-Screen: Karte 'Jahres-Recap 2026' unter der Statistik-Card
- Einstellungen: Kachel 'Jahres-Recap' in Sektion Daten & Privatsphäre

flutter analyze: 0 Issues, Tests 4/4
2026-08-03 02:54:11 +02:00

259 lines
8.4 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 'package:shared_preferences/shared_preferences.dart';
import '../config/app_config.dart';
import '../utils/farb_theme.dart';
import 'cloud_screen.dart';
import 'recap_screen.dart';
import '../services/cloud_service.dart';
/// Vollwertiger Einstellungen-Screen kein Popup mehr
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
bool _diagnose = AppConfig.sendeDiagnosedaten;
@override
void initState() {
super.initState();
_ladeDiagnose();
}
Future<void> _ladeDiagnose() async {
final p = await SharedPreferences.getInstance();
if (mounted) setState(() => _diagnose = p.getBool('diagnose_daten') ?? AppConfig.sendeDiagnosedaten);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MeloTheme.schwarz,
appBar: AppBar(
backgroundColor: MeloTheme.dunkel1,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white, size: 22),
onPressed: () => Navigator.pop(context),
),
title: const Row(
children: [
Icon(Icons.settings, color: MeloTheme.rot, size: 20),
SizedBox(width: 10),
Text(
'Einstellungen',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
),
],
),
),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
// ─── Sektion: Verbindung ───
_sektionHeader('Verbindung'),
_einstellungsKachel(
icon: Icons.cloud_outlined,
titel: 'Cloud Sync',
untertitel: 'Auto-Sync, Upload & Download',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CloudScreen(cloud: CloudService()),
),
);
},
),
_einstellungsKachel(
icon: Icons.dns_outlined,
titel: 'Server verbinden',
untertitel: 'Navidrome Musik-Server',
onTap: () {
// Signal zum Öffnen des Server-Browsers
Navigator.pop(context, 'server');
},
),
const SizedBox(height: 8),
// ─── Sektion: Daten ───
_sektionHeader('Daten & Privatsphäre'),
_einstellungsKachel(
icon: Icons.auto_graph,
titel: 'Jahres-Recap',
untertitel: 'Deine Hörstatistiken wie Spotify Wrapped',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const RecapScreen()),
);
},
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: MeloTheme.dunkel2),
),
child: SwitchListTile(
title: const Text(
'Diagnosedaten senden',
style: TextStyle(color: Colors.white, fontSize: 14),
),
subtitle: const Text(
'Absturz-Logs & anonyme Nutzungsdaten',
style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
),
value: _diagnose,
activeThumbColor: MeloTheme.rot,
onChanged: (v) async {
setState(() => _diagnose = v);
AppConfig.sendeDiagnosedaten = v;
final p = await SharedPreferences.getInstance();
p.setBool('diagnose_daten', v);
},
secondary: const Icon(Icons.bug_report_outlined, color: MeloTheme.rot, size: 22),
),
),
const SizedBox(height: 8),
// ─── Sektion: Info ───
_sektionHeader('Info'),
Container(
decoration: BoxDecoration(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Column(
children: [
_infoZeile('Version', '2.31'),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Theme', 'Schwarz + Rot'),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Auth', AppConfig.authUrl),
const Divider(color: MeloTheme.dunkel2, height: 1),
_infoZeile('Cloud', AppConfig.cloudUrl),
],
),
),
const SizedBox(height: 24),
// ─── Abmelden ───
SizedBox(
width: double.infinity,
height: 48,
child: OutlinedButton.icon(
onPressed: () {
Navigator.pop(context, 'logout');
},
icon: const Icon(Icons.logout, size: 18),
label: const Text('Abmelden', style: TextStyle(fontSize: 14)),
style: OutlinedButton.styleFrom(
foregroundColor: MeloTheme.rot,
side: const BorderSide(color: MeloTheme.rot),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
),
),
const SizedBox(height: 32),
],
),
);
}
Widget _sektionHeader(String titel) {
return Padding(
padding: const EdgeInsets.fromLTRB(4, 16, 4, 8),
child: Text(
titel,
style: const TextStyle(
color: MeloTheme.textSekundaer,
fontSize: 11,
fontWeight: FontWeight.w600,
letterSpacing: 1.2,
),
),
);
}
Widget _einstellungsKachel({
required IconData icon,
required String titel,
required String untertitel,
required VoidCallback onTap,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Material(
color: MeloTheme.dunkel1,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(14),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(color: MeloTheme.dunkel2),
),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: MeloTheme.dunkel2,
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: MeloTheme.rot, size: 20),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
titel,
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 2),
Text(
untertitel,
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12),
),
],
),
),
const Icon(Icons.chevron_right, color: MeloTheme.textSekundaer, size: 20),
],
),
),
),
),
);
}
Widget _infoZeile(String label, String wert) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Text(
label,
style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13),
),
const Spacer(),
Text(
wert,
style: const TextStyle(color: Colors.white70, fontSize: 13),
),
],
),
);
}
}