Feature: Login-Effekte pro Person (Akzentfarbe + Sound + Begruessung)

- UserEffekt: Baka = rot + Fanfare, Tinker = tuerkis + Chime, Standard-Ping
- MeloTheme: dynamische Akzentfarbe via ValueNotifier (Theme wechselt live)
- Ausloeser: App-Start (wenn Token vorhanden) + nach erfolgreichem Login
- Assets: 3 kurze WAV-Sounds (assets/sounds/)
- Web: /whoami-Endpoint (Caddy) + Willkommens-Banner auf baka-net.de
This commit is contained in:
Hermes (Server)
2026-07-31 15:34:04 +02:00
parent 9a880b0f21
commit 19e4aac62e
8 changed files with 113 additions and 14 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+22 -7
View File
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:audio_service/audio_service.dart';
import 'database/db_helper.dart'; import 'database/db_helper.dart';
import 'services/favoriten_service.dart'; import 'services/favoriten_service.dart';
import 'services/melo_logger.dart'; import 'services/melo_logger.dart';
import 'services/audio_handler.dart';
import 'utils/farb_theme.dart'; import 'utils/farb_theme.dart';
import 'screens/home_screen.dart'; import 'screens/home_screen.dart';
@@ -9,12 +11,21 @@ void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Logger startet sofort zeichnet ALLES auf // Logger startet sofort zeichnet ALLES auf
MeloLogger().init('2.7'); MeloLogger().init('2.38');
try { try {
await DbHelper().db; await DbHelper().db;
await FavoritenService().init(); await FavoritenService().init();
MeloLogger().zustand('start_ok', {'db': 'ok'}); // Hintergrund-Audio-Service starten
await AudioService.init(
builder: () => MeloAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'de.baka.melo.audio',
androidNotificationChannelName: 'Melo Wiedergabe',
androidNotificationOngoing: true,
),
);
MeloLogger().zustand('start_ok', {'db': 'ok', 'audio': 'ok'});
} catch (e, stack) { } catch (e, stack) {
MeloLogger().fehler('App-Start', e, stack); MeloLogger().fehler('App-Start', e, stack);
} }
@@ -27,11 +38,15 @@ class MeloApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( // Reagiert live auf Akzent-Änderungen (Login-Effekt pro Person)
title: 'Melo', return ValueListenableBuilder<Color>(
debugShowCheckedModeBanner: false, valueListenable: MeloTheme.akzentNotifier,
theme: MeloTheme.theme, builder: (context, akzent, _) => MaterialApp(
home: const Scaffold(body: MeloHome()), title: 'Melo',
debugShowCheckedModeBanner: false,
theme: MeloTheme.theme,
home: const Scaffold(body: MeloHome()),
),
); );
} }
} }
+18 -1
View File
@@ -6,6 +6,8 @@ import '../viewmodels/melo_home_viewmodel.dart';
import '../models/song.dart'; import '../models/song.dart';
import '../models/playlist.dart'; import '../models/playlist.dart';
import '../utils/farb_theme.dart'; import '../utils/farb_theme.dart';
import '../utils/user_effekte.dart';
import '../services/cloud_service.dart';
import '../widgets/mini_player.dart'; import '../widgets/mini_player.dart';
import '../widgets/melo_header.dart'; import '../widgets/melo_header.dart';
import '../widgets/statistik_card.dart'; import '../widgets/statistik_card.dart';
@@ -18,7 +20,6 @@ import 'cloud_screen.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../widgets/cloud_einstellungen.dart'; import '../widgets/cloud_einstellungen.dart';
import '../services/cloud_service.dart';
import '../config/app_config.dart'; import '../config/app_config.dart';
class MeloHome extends StatefulWidget { class MeloHome extends StatefulWidget {
@@ -50,6 +51,15 @@ class _MeloHomeState extends State<MeloHome> {
setState(() => _nutzer = n); setState(() => _nutzer = n);
_vm.setzeNutzer(n); _vm.setzeNutzer(n);
} }
// Login-Effekt beim App-Start: nur wenn ein Token vorhanden ist (eingeloggt)
final ok = await CloudService().restoreLogin();
if (ok && mounted) {
UserEffekt.anwenden(_nutzer);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(UserEffekt.fuer(_nutzer).begruessung),
duration: const Duration(seconds: 3),
));
}
} }
void _zeigeProfil() { void _zeigeProfil() {
@@ -153,6 +163,13 @@ class _MeloHomeState extends State<MeloHome> {
await p.setString('melo_nutzer', name); await p.setString('melo_nutzer', name);
if (mounted) setState(() => _nutzer = name); if (mounted) setState(() => _nutzer = name);
_vm.setzeNutzer(name); _vm.setzeNutzer(name);
// Login-Effekt: Akzentfarbe + Sound + Begrüßung pro Person
UserEffekt.anwenden(name);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(UserEffekt.fuer(name).begruessung),
duration: const Duration(seconds: 3)));
}
} else { } else {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
+12 -6
View File
@@ -9,12 +9,18 @@ class MeloTheme {
static const Color textPrimaer = Color(0xFFFFFFFF); static const Color textPrimaer = Color(0xFFFFFFFF);
static const Color textSekundaer = Color(0xFF888888); static const Color textSekundaer = Color(0xFF888888);
/// Akzentfarbe wechselt pro angemeldeter Person (Login-Effekt).
/// UI hängt per ValueListenableBuilder an [akzentNotifier].
static final ValueNotifier<Color> akzentNotifier = ValueNotifier<Color>(rot);
static Color get akzent => akzentNotifier.value;
static set akzent(Color farbe) => akzentNotifier.value = farbe;
static ThemeData get theme => ThemeData( static ThemeData get theme => ThemeData(
brightness: Brightness.dark, brightness: Brightness.dark,
scaffoldBackgroundColor: schwarz, scaffoldBackgroundColor: schwarz,
colorScheme: const ColorScheme.dark( colorScheme: ColorScheme.dark(
primary: rot, primary: akzent,
secondary: rot, secondary: akzent,
surface: schwarz, surface: schwarz,
), ),
appBarTheme: const AppBarTheme( appBarTheme: const AppBarTheme(
@@ -22,9 +28,9 @@ class MeloTheme {
foregroundColor: textPrimaer, foregroundColor: textPrimaer,
elevation: 0, elevation: 0,
), ),
bottomNavigationBarTheme: const BottomNavigationBarThemeData( bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: schwarz, backgroundColor: schwarz,
selectedItemColor: rot, selectedItemColor: akzent,
unselectedItemColor: textSekundaer, unselectedItemColor: textSekundaer,
), ),
cardTheme: CardThemeData( cardTheme: CardThemeData(
@@ -35,7 +41,7 @@ class MeloTheme {
), ),
chipTheme: ChipThemeData( chipTheme: ChipThemeData(
backgroundColor: dunkel1, backgroundColor: dunkel1,
selectedColor: rot, selectedColor: akzent,
labelStyle: const TextStyle(color: textPrimaer), labelStyle: const TextStyle(color: textPrimaer),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
+59
View File
@@ -0,0 +1,59 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'farb_theme.dart';
/// Login-Effekte: Pro Person eigener Akzent, Willkommens-Sound und Begrüßung.
/// Wird nach erfolgreichem Login und beim App-Start (wenn Token vorhanden) ausgelöst.
class UserEffekt {
final Color akzent;
final String begruessung;
final String soundAsset;
const UserEffekt(this.akzent, this.begruessung, this.soundAsset);
/// Baka = königlich-rot + Fanfare 👑
static const baka = UserEffekt(
Color(0xFFCC0000),
'Willkommen zurück, Baka! 👑',
'assets/sounds/baka_fanfare.wav',
);
/// Tinker = türkis + heller Chime 🐱
static const tinker = UserEffekt(
Color(0xFF00B8A9),
'Tinker ist da! 🐱✨',
'assets/sounds/tinker_chime.wav',
);
static const standard = UserEffekt(
Color(0xFFCC0000),
'Willkommen! 🎵',
'assets/sounds/willkommen_ping.wav',
);
static UserEffekt fuer(String? user) {
final u = (user ?? '').toLowerCase().trim();
if (u == 'baka' || u == 'dustin') return baka;
if (u == 'tinker') return tinker;
return standard;
}
/// Setzt die Akzentfarbe (Thema wechselt live) und spielt den Sound ab.
static Future<void> anwenden(String? user) async {
final e = fuer(user);
MeloTheme.akzent = e.akzent;
try {
final p = AudioPlayer();
await p.setAsset(e.soundAsset);
unawaited(p.play());
// Player nach dem Abspielen sauber freigeben (kein Memory-Leak)
Future.delayed(const Duration(seconds: 4), () {
if (p.playing) p.stop();
p.dispose();
});
} catch (_) {
// Sound-Fehler ignorieren Akzent bleibt trotzdem aktiv
}
}
}
+2
View File
@@ -37,3 +37,5 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true uses-material-design: true
assets:
- assets/sounds/