Tab-Reihenfolge, konfigurierbare Meine-Musik-Sektionen, Berechtigungen beim Start, schärferes Sperrbildschirm-Cover
- Tabs: Meine Musik, Suchen, Download, Favoriten (vorher Download vor Suchen) - "Meine Musik": Schnellzugriffe (Favoriten/Wiedergabelisten/Zuletzt) einzeln über die Einstellungen abschaltbar - Audio- und Benachrichtigungs-Berechtigung werden jetzt beim ersten Start gemeinsam angefragt statt erst bei Bedarf; Hinweisbanner + Einstellungen- Button bei dauerhafter Verweigerung (auch für Benachrichtigungen neu) - Cover-Extraktion beim Android-Scan von 512px auf 1024px erhöht, für ein schärferes Cover im Sperrbildschirm - Fix: notifyListeners() in LibraryService nur noch bei echter Wertänderung (verhinderte einen Test-Hang durch den neuen Start-Berechtigungs-Aufruf) 557 Tests grün, flutter analyze ohne Befund. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VM2JK5mV7AL1g2Rt6H6h9w
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
54cb523cf4
commit
da012a3dca
@@ -56,7 +56,9 @@ Future<int> scanAndroidMediaStore(
|
||||
var coverPath = prev?.coverPath;
|
||||
if (coverPath == null) {
|
||||
try {
|
||||
final art = await audioQuery.queryArtwork(s.id, ArtworkType.AUDIO, size: 512);
|
||||
// 1024px statt 512px: auf dem Sperrbildschirm (MIUI & Co.) erscheint
|
||||
// das Cover großflächig — bei 512px wirkte es dort weich/verpixelt.
|
||||
final art = await audioQuery.queryArtwork(s.id, ArtworkType.AUDIO, size: 1024);
|
||||
if (art != null && art.isNotEmpty) {
|
||||
final f = File(p.join(covers.path, '$id.img'));
|
||||
await f.writeAsBytes(art);
|
||||
|
||||
@@ -25,8 +25,23 @@ class LibraryService extends ChangeNotifier {
|
||||
int scanDone = 0;
|
||||
int scanTotal = 0;
|
||||
bool permissionDenied = false;
|
||||
bool notificationPermissionDenied = false;
|
||||
String? scanError;
|
||||
|
||||
/// Fragt beim ersten Start alle für den Betrieb nötigen Berechtigungen an
|
||||
/// (Musik-Zugriff + Wiedergabe-Benachrichtigung), statt sie erst bei Bedarf
|
||||
/// einzeln einzufordern.
|
||||
///
|
||||
/// Nacheinander, nicht gleichzeitig: `permission_handler` wirft, wenn eine
|
||||
/// zweite Abfrage startet, während die erste noch offen ist.
|
||||
Future<void> ensureStartupPermissions() async {
|
||||
await _ensurePermission();
|
||||
final denied = !await ensureNotificationPermission();
|
||||
if (denied == notificationPermissionDenied) return;
|
||||
notificationPermissionDenied = denied;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Auf Android: automatischer Geräte-Scan. Auf Desktop: Ordner wählen + scannen.
|
||||
Future<void> pickFolderAndScan() async {
|
||||
if (!await _ensurePermission()) return;
|
||||
@@ -52,8 +67,11 @@ class LibraryService extends ChangeNotifier {
|
||||
|
||||
Future<bool> _ensurePermission() async {
|
||||
final granted = await ensureAudioPermission();
|
||||
permissionDenied = !granted;
|
||||
notifyListeners();
|
||||
final denied = !granted;
|
||||
if (denied != permissionDenied) {
|
||||
permissionDenied = denied;
|
||||
notifyListeners();
|
||||
}
|
||||
return granted;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../playlists/playlists_screen.dart';
|
||||
import '../settings/app_settings.dart';
|
||||
import '../settings/settings_screen.dart';
|
||||
import '../shared/cover.dart';
|
||||
import '../shared/sort_store.dart';
|
||||
@@ -57,6 +58,19 @@ class _MyMusicScreenState extends State<MyMusicScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (lib.notificationPermissionDenied)
|
||||
MaterialBanner(
|
||||
backgroundColor: Colors.red.shade900,
|
||||
content: const Text(
|
||||
'Benachrichtigungen für die Wiedergabe sind deaktiviert. '
|
||||
'Bitte in den Einstellungen erlauben.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: openMusicPermissionSettings,
|
||||
child: const Text('Einstellungen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (lib.scanError != null)
|
||||
MaterialBanner(
|
||||
backgroundColor: Colors.red.shade900,
|
||||
@@ -180,54 +194,61 @@ class _QuickAccessRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final db = context.read<MeloDb>();
|
||||
final sektionen = context.watch<AppSettings>().meineMusikSektionen;
|
||||
if (sektionen.isEmpty) return const SizedBox.shrink();
|
||||
return SizedBox(
|
||||
height: 104,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
children: [
|
||||
StreamBuilder<List<Song>>(
|
||||
stream: db.watchFavorites(),
|
||||
builder: (context, snapshot) {
|
||||
final favorites = snapshot.data ?? const <Song>[];
|
||||
return _QuickCard(
|
||||
icon: Icons.favorite,
|
||||
label: 'Favoriten',
|
||||
subtitle: '${favorites.length} Songs',
|
||||
coverPath: favorites.isEmpty ? null : favorites.first.coverPath,
|
||||
onTap: onFavoritesTap,
|
||||
);
|
||||
},
|
||||
),
|
||||
StreamBuilder<List<Playlist>>(
|
||||
stream: db.watchPlaylists(),
|
||||
builder: (context, snapshot) {
|
||||
final playlists = snapshot.data ?? const <Playlist>[];
|
||||
return _QuickCard(
|
||||
icon: Icons.queue_music,
|
||||
label: 'Wiedergabelisten',
|
||||
subtitle: '${playlists.length} Listen',
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const PlaylistsScreen()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
StreamBuilder<List<Song>>(
|
||||
stream: db.watchRecent(limit: 50),
|
||||
builder: (context, snapshot) {
|
||||
final recent = snapshot.data ?? const <Song>[];
|
||||
return _QuickCard(
|
||||
icon: Icons.schedule,
|
||||
label: 'Zuletzt',
|
||||
subtitle: 'Neu hinzugefügt',
|
||||
coverPath: recent.isEmpty ? null : recent.first.coverPath,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const RecentlyAddedScreen()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (sektionen.contains(AppSettings.sektionFavoriten))
|
||||
StreamBuilder<List<Song>>(
|
||||
stream: db.watchFavorites(),
|
||||
builder: (context, snapshot) {
|
||||
final favorites = snapshot.data ?? const <Song>[];
|
||||
return _QuickCard(
|
||||
icon: Icons.favorite,
|
||||
label: 'Favoriten',
|
||||
subtitle: '${favorites.length} Songs',
|
||||
coverPath:
|
||||
favorites.isEmpty ? null : favorites.first.coverPath,
|
||||
onTap: onFavoritesTap,
|
||||
);
|
||||
},
|
||||
),
|
||||
if (sektionen.contains(AppSettings.sektionWiedergabelisten))
|
||||
StreamBuilder<List<Playlist>>(
|
||||
stream: db.watchPlaylists(),
|
||||
builder: (context, snapshot) {
|
||||
final playlists = snapshot.data ?? const <Playlist>[];
|
||||
return _QuickCard(
|
||||
icon: Icons.queue_music,
|
||||
label: 'Wiedergabelisten',
|
||||
subtitle: '${playlists.length} Listen',
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const PlaylistsScreen()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (sektionen.contains(AppSettings.sektionZuletzt))
|
||||
StreamBuilder<List<Song>>(
|
||||
stream: db.watchRecent(limit: 50),
|
||||
builder: (context, snapshot) {
|
||||
final recent = snapshot.data ?? const <Song>[];
|
||||
return _QuickCard(
|
||||
icon: Icons.schedule,
|
||||
label: 'Zuletzt',
|
||||
subtitle: 'Neu hinzugefügt',
|
||||
coverPath: recent.isEmpty ? null : recent.first.coverPath,
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const RecentlyAddedScreen()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user