Entwarnung vorweg: es fehlt nichts. Alle vier Bereiche (Meine Musik, Online, Suchen, Favoriten) standen durchgehend in der Bottom-Nav; lib/main.dart wurde von der UI-Politur gar nicht angefasst. Aber die Politur hatte das Menue abgeschwaecht — zwei eigene Fehler: - selectedLabelStyle/unselectedLabelStyle bekamen beide fontSize 12. Damit verlor der aktive Eintrag den Groessenunterschied (Flutter-Vorgabe 14 zu 12), an dem man erkennt, wo man ist. Jetzt keine fontSize mehr. - unselectedItemColor stand auf text3 (Beiwerk-Stufe). Ein Hauptmenue ist kein Beiwerk -> text2. Zusaetzlich deutlicher: Haarlinie ueber der Leiste (auf Schwarz ging sie sonst im Inhalt auf), gefuellte Symbole fuer den aktiven Bereich (zweites Merkmal neben der Farbe) und fetterer aktiver Eintrag. Neu: test/hauptmenue_test.dart — prueft Vorhandensein, Sichtbarkeit auf dem Bildschirm, Position am unteren Rand, Wechsel per Tipp und die Abhebung des aktiven Bereichs. Damit kann keine weitere Politur das Menue unbemerkt abschwaechen. Nebenbei eine Test-Falle geloest: await db.close() nach einem tester.tap laesst den Lauf unbegrenzt haengen (drift plant eine Aufraeum-Aufgabe ein, die der Test-Rahmen nicht mehr abarbeitet). Rezept im Test dokumentiert. Beim bekannten Haenger in song_detail_sheet_test.dart hilft es NICHT — gegengeprueft und als eigene Ursache im BACKLOG vermerkt. 328 Tests gruen (10 neue, 1 uebersprungen), flutter analyze ohne Befund. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
206 lines
6.4 KiB
Dart
206 lines
6.4 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'downloads/downloads_screen.dart';
|
|
import 'library/android_scan.dart';
|
|
import 'library/auto_scan.dart';
|
|
import 'library/category_service.dart';
|
|
import 'library/database.dart';
|
|
import 'library/favorites_screen.dart';
|
|
import 'library/library_service.dart';
|
|
import 'library/my_music_screen.dart';
|
|
import 'library/playlist_service.dart';
|
|
import 'library/search_screen.dart';
|
|
import 'player/audio_effects.dart';
|
|
import 'player/audio_handler.dart';
|
|
import 'player/mini_player.dart';
|
|
import 'services/baka_auth.dart';
|
|
import 'services/logger_service.dart';
|
|
import 'services/melo_cloud_service.dart';
|
|
import 'services/offline_mode.dart';
|
|
import 'services/sync_service.dart';
|
|
import 'services/yt_download_service.dart';
|
|
import 'settings/app_settings.dart';
|
|
import 'shared/theme.dart';
|
|
|
|
late final MeloAudioHandler _handler;
|
|
late final MeloDb _db;
|
|
late final LibraryService _library;
|
|
late final PlaylistService _playlists;
|
|
late final OfflineMode _offlineMode;
|
|
late final CategoryService _categories;
|
|
late final AppSettings _settings;
|
|
late final AudioEffects _effects;
|
|
late final BakaAuth _bakaAuth;
|
|
late final YtDownloadService _ytDownload;
|
|
late final SyncService _sync;
|
|
|
|
Future<void> main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await logger.init();
|
|
_db = MeloDb();
|
|
_library = LibraryService(_db);
|
|
_playlists = PlaylistService(_db);
|
|
_offlineMode = OfflineMode();
|
|
await _offlineMode.init();
|
|
_categories = CategoryService(_db);
|
|
_settings = AppSettings();
|
|
await _settings.init();
|
|
_handler = await AudioService.init(
|
|
builder: () => MeloAudioHandler(db: _db),
|
|
config: const AudioServiceConfig(
|
|
androidNotificationChannelId: 'de.baka.melo.audio',
|
|
androidNotificationChannelName: 'Melo',
|
|
androidNotificationOngoing: true,
|
|
androidShowNotificationBadge: true,
|
|
),
|
|
);
|
|
_effects = AudioEffects();
|
|
await _effects.init();
|
|
_bakaAuth = BakaAuth();
|
|
await _bakaAuth.laden();
|
|
_ytDownload = YtDownloadService(auth: _bakaAuth);
|
|
_sync = SyncService(
|
|
db: _db,
|
|
cloud: MeloCloudService(auth: _bakaAuth),
|
|
);
|
|
await _sync.laden();
|
|
// Die Audio-Session steht erst beim Abspielen fest und kann wechseln —
|
|
// die Klangeffekte müssen dann neu daran gehängt werden.
|
|
_handler.androidAudioSessionIdStream.listen((id) {
|
|
if (id != null) _effects.attach(id);
|
|
});
|
|
void uebernehmeEinstellungen() {
|
|
_handler.autoPlayOnHeadphones = _settings.autoPlayOnHeadphones;
|
|
_handler.normalizeVolume = _settings.normalizeVolume;
|
|
}
|
|
|
|
uebernehmeEinstellungen();
|
|
_settings.addListener(uebernehmeEinstellungen);
|
|
|
|
runApp(const MeloApp());
|
|
}
|
|
|
|
class MeloApp extends StatelessWidget {
|
|
const MeloApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MultiProvider(
|
|
providers: [
|
|
Provider<MeloAudioHandler>.value(value: _handler),
|
|
Provider<MeloDb>.value(value: _db),
|
|
ChangeNotifierProvider<LibraryService>.value(value: _library),
|
|
ChangeNotifierProvider<PlaylistService>.value(value: _playlists),
|
|
ChangeNotifierProvider<OfflineMode>.value(value: _offlineMode),
|
|
ChangeNotifierProvider<CategoryService>.value(value: _categories),
|
|
ChangeNotifierProvider<AppSettings>.value(value: _settings),
|
|
ChangeNotifierProvider<AudioEffects>.value(value: _effects),
|
|
ChangeNotifierProvider<BakaAuth>.value(value: _bakaAuth),
|
|
ChangeNotifierProvider<YtDownloadService>.value(value: _ytDownload),
|
|
ChangeNotifierProvider<SyncService>.value(value: _sync),
|
|
],
|
|
child: MaterialApp(
|
|
title: 'Melo',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: MeloTheme.dark,
|
|
home: const HomeShell(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomeShell extends StatefulWidget {
|
|
const HomeShell({super.key});
|
|
|
|
@override
|
|
State<HomeShell> createState() => _HomeShellState();
|
|
}
|
|
|
|
class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
|
int _index = 0;
|
|
late final AutoScan _autoScan;
|
|
|
|
void _goTo(int index) => setState(() => _index = index);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final db = context.read<MeloDb>();
|
|
final lib = context.read<LibraryService>();
|
|
_autoScan = AutoScan(
|
|
geraeteAnzahl: countAndroidMediaStoreSongs,
|
|
bekannteAnzahl: db.countSongs,
|
|
scanne: lib.rescan,
|
|
);
|
|
WidgetsBinding.instance.addObserver(this);
|
|
// Beim Start einmal mit dem Server abgleichen — neue Titel von anderen
|
|
// Geräten sind dann sofort da.
|
|
context.read<SyncService>().automatisch();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
// Zurück in der App: nachsehen, ob inzwischen Musik dazugekommen ist —
|
|
// auf dem Gerät wie am Server.
|
|
if (state == AppLifecycleState.resumed) {
|
|
if (Platform.isAndroid) _autoScan.pruefe();
|
|
context.read<SyncService>().automatisch();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tabs = <Widget>[
|
|
MyMusicScreen(
|
|
onSearchTap: () => _goTo(2),
|
|
onFavoritesTap: () => _goTo(3),
|
|
),
|
|
const DownloadsScreen(),
|
|
const SearchScreen(),
|
|
const FavoritesScreen(),
|
|
];
|
|
return Scaffold(
|
|
body: Column(
|
|
children: [
|
|
Expanded(child: IndexedStack(index: _index, children: tabs)),
|
|
const MiniPlayer(),
|
|
],
|
|
),
|
|
// Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
|
|
// optisch im Inhalt auf und wirkt nicht wie ein Hauptmenü.
|
|
bottomNavigationBar: DecoratedBox(
|
|
decoration: const BoxDecoration(
|
|
border: Border(top: BorderSide(color: MeloTheme.border)),
|
|
),
|
|
child: BottomNavigationBar(
|
|
currentIndex: _index,
|
|
onTap: _goTo,
|
|
items: const [
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.headphones), label: 'Meine Musik'),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.music_note_outlined),
|
|
activeIcon: Icon(Icons.music_note),
|
|
label: 'Online'),
|
|
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suchen'),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.favorite_border),
|
|
activeIcon: Icon(Icons.favorite),
|
|
label: 'Favoriten'),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|