- Reverted colorScheme.surface back to colorScheme.background in 11 files (user reported the new color was too light/grey) - Fixed search screen podcast Hero structure to match library/detail pattern: - Changed widget order from Container → ClipRRect to ClipRRect → Container - Changed borderRadius from 8 to 16 (matching library/detail) - Changed color from surfaceVariant to surfaceContainerHighest - Removed memCacheWidth constraints to match library behavior - This ensures smooth Hero animation when navigating from search 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
154 lines
4.8 KiB
Dart
154 lines
4.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import '../providers/music_assistant_provider.dart';
|
|
import '../models/media_item.dart';
|
|
import '../widgets/common/empty_state.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
|
|
class LibraryTracksScreen extends StatelessWidget {
|
|
const LibraryTracksScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final provider = context.watch<MusicAssistantProvider>();
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final textTheme = Theme.of(context).textTheme;
|
|
|
|
return Scaffold(
|
|
backgroundColor: colorScheme.background,
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.transparent,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back_rounded),
|
|
onPressed: () => Navigator.pop(context),
|
|
color: colorScheme.onBackground,
|
|
),
|
|
title: Text(
|
|
S.of(context)!.tracks,
|
|
style: textTheme.headlineSmall?.copyWith(
|
|
color: colorScheme.onBackground,
|
|
fontWeight: FontWeight.w300,
|
|
),
|
|
),
|
|
centerTitle: true,
|
|
),
|
|
body: _buildTracksList(context, provider),
|
|
);
|
|
}
|
|
|
|
Widget _buildTracksList(
|
|
BuildContext context, MusicAssistantProvider provider) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
// Show cached data immediately if available, even while loading
|
|
// Only show spinner if we have no data at all AND we're loading
|
|
if (provider.tracks.isEmpty && provider.isLoading) {
|
|
return Center(
|
|
child: CircularProgressIndicator(color: colorScheme.primary),
|
|
);
|
|
}
|
|
|
|
if (provider.tracks.isEmpty) {
|
|
return EmptyState.tracks(context: context, onRefresh: provider.loadLibrary);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: provider.tracks.length,
|
|
padding: const EdgeInsets.all(8),
|
|
cacheExtent: 500, // Prebuild items off-screen for smoother scrolling
|
|
addAutomaticKeepAlives: false, // Tiles don't need individual keep-alive
|
|
addRepaintBoundaries: false, // We add RepaintBoundary manually to tiles
|
|
itemBuilder: (context, index) {
|
|
final track = provider.tracks[index];
|
|
return _buildTrackTile(context, track, provider, index);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildTrackTile(
|
|
BuildContext context,
|
|
Track track,
|
|
MusicAssistantProvider maProvider,
|
|
int index,
|
|
) {
|
|
final imageUrl = track.album != null
|
|
? maProvider.getImageUrl(track.album!, size: 128)
|
|
: null;
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
final textTheme = Theme.of(context).textTheme;
|
|
|
|
return RepaintBoundary(
|
|
child: ListTile(
|
|
key: ValueKey(track.uri ?? track.itemId),
|
|
leading: Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.surfaceVariant,
|
|
borderRadius: BorderRadius.circular(8),
|
|
image: imageUrl != null
|
|
? DecorationImage(
|
|
image: CachedNetworkImageProvider(imageUrl),
|
|
fit: BoxFit.cover,
|
|
)
|
|
: null,
|
|
),
|
|
child: imageUrl == null
|
|
? Icon(Icons.music_note_rounded, color: colorScheme.onSurfaceVariant)
|
|
: null,
|
|
),
|
|
title: Text(
|
|
track.name,
|
|
style: textTheme.bodyLarge?.copyWith(color: colorScheme.onSurface),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
subtitle: Text(
|
|
track.artistsString,
|
|
style: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface.withOpacity(0.7)),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
trailing: track.duration != null
|
|
? Text(
|
|
_formatDuration(track.duration!),
|
|
style: textTheme.bodySmall?.copyWith(color: colorScheme.onSurface.withOpacity(0.54)),
|
|
)
|
|
: null,
|
|
onTap: () => _playTrack(context, maProvider, index),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _playTrack(
|
|
BuildContext context,
|
|
MusicAssistantProvider maProvider,
|
|
int startIndex,
|
|
) async {
|
|
final tracks = maProvider.tracks;
|
|
if (tracks.isEmpty) return;
|
|
|
|
if (maProvider.selectedPlayer == null) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(S.of(context)!.noPlayerSelected)),
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Use Music Assistant to play tracks on the selected player
|
|
await maProvider.playTracks(
|
|
maProvider.selectedPlayer!.playerId,
|
|
tracks,
|
|
startIndex: startIndex,
|
|
);
|
|
}
|
|
|
|
String _formatDuration(Duration duration) {
|
|
final minutes = duration.inMinutes;
|
|
final seconds = duration.inSeconds % 60;
|
|
return '$minutes:${seconds.toString().padLeft(2, '0')}';
|
|
}
|
|
}
|