## Critical Fixes - Fix CRITICAL-1: Empty scans no longer delete entire library - Use timestamp-based tombstone logic instead of path-based - Only mark missing if livePaths is not empty - Fix CRITICAL-2: Re-scanned files now return from deleted state - Add deleted=false to SongsCompanion.insert in both scanners ## High Priority Fixes - HIGH-1: Fix SQLite variable limit crash on large libraries - Replace isNotIn() with timestamp comparison (O(1) not O(n)) - HIGH-2: Fix UI freezing during scan - Throttle progress updates (every 50 files or end of scan) - HIGH-3: Add error handling for scan failures - Wrap scanFolders/scanAndroidMediaStore in try/catch - Display scanError banner in LibraryScreen - HIGH-4: Add error handling for playback failures - Wrap loadPlaylist in try/catch ## Medium Priority Fixes - MEDIUM-1: Replace ! with ?? to handle unknown ProcessingState - MEDIUM-2: Escape LIKE wildcards in search queries - MEDIUM-4: Display StreamBuilder errors instead of treating as empty - MEDIUM-8: Add tooltips to all IconButtons for accessibility - Add doc comments to public database APIs ## Testing - Add regression tests for CRITICAL-1 and CRITICAL-2 - All 9 existing tests pass Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
74 lines
2.6 KiB
Dart
74 lines
2.6 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../shared/cover.dart';
|
|
import '../shared/theme.dart';
|
|
import 'audio_handler.dart';
|
|
import 'now_playing_screen.dart';
|
|
|
|
/// Kompakte Wiedergabe-Leiste über der Bottom-Nav.
|
|
/// Blendet sich aus, wenn nichts läuft.
|
|
class MiniPlayer extends StatelessWidget {
|
|
const MiniPlayer({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final handler = context.read<MeloAudioHandler>();
|
|
return StreamBuilder<MediaItem?>(
|
|
stream: handler.mediaItem,
|
|
builder: (context, snapshot) {
|
|
final item = snapshot.data;
|
|
if (item == null) return const SizedBox.shrink();
|
|
return Material(
|
|
color: MeloTheme.surface,
|
|
child: InkWell(
|
|
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (_) => const NowPlayingScreen(),
|
|
)),
|
|
child: SizedBox(
|
|
height: 60,
|
|
child: Row(
|
|
children: [
|
|
const SizedBox(width: 8),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8),
|
|
child: CoverImage(artUri: item.artUri, size: 44, radius: 6),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(item.title,
|
|
maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
Text(item.artist ?? 'Unbekannt',
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: Colors.white54, fontSize: 12)),
|
|
],
|
|
),
|
|
),
|
|
StreamBuilder<PlaybackState>(
|
|
stream: handler.playbackState,
|
|
builder: (context, snap) {
|
|
final playing = snap.data?.playing ?? false;
|
|
return IconButton(
|
|
tooltip: playing ? 'Pause' : 'Abspielen',
|
|
icon: Icon(playing ? Icons.pause : Icons.play_arrow),
|
|
onPressed: playing ? handler.pause : handler.play,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|