mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-06 20:57:57 +02:00
refactor: more code cleanup
This commit is contained in:
+1
-2
@@ -50,8 +50,7 @@ class SpotiFLACApp extends ConsumerWidget {
|
||||
themeAnimationDuration: const Duration(milliseconds: 300),
|
||||
themeAnimationCurve: Curves.easeInOut,
|
||||
routerConfig: router,
|
||||
// Localization
|
||||
locale: locale, // null = follow system
|
||||
locale: locale,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
|
||||
@@ -11,10 +11,8 @@ import 'package:spotiflac_android/services/share_intent_service.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize notification service
|
||||
await NotificationService().initialize();
|
||||
|
||||
// Initialize share intent service
|
||||
await ShareIntentService().initialize();
|
||||
|
||||
runApp(
|
||||
@@ -51,7 +49,6 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> {
|
||||
await Directory(extensionsDir).create(recursive: true);
|
||||
await Directory(dataDir).create(recursive: true);
|
||||
|
||||
// Initialize extension system
|
||||
await ref.read(extensionProvider.notifier).initialize(extensionsDir, dataDir);
|
||||
} catch (e) {
|
||||
debugPrint('Failed to initialize extensions: $e');
|
||||
@@ -60,7 +57,6 @@ class _EagerInitializationState extends ConsumerState<_EagerInitialization> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Eagerly initialize download history provider to load from storage
|
||||
ref.watch(downloadHistoryProvider);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
@@ -280,7 +280,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
Future<void> search(String query, {String? metadataSource}) async {
|
||||
final requestId = ++_currentRequestId;
|
||||
|
||||
// Preserve hasSearchText during search
|
||||
state = TrackState(isLoading: true, hasSearchText: state.hasSearchText);
|
||||
|
||||
try {
|
||||
@@ -402,7 +401,6 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
Future<void> customSearch(String extensionId, String query, {Map<String, dynamic>? options}) async {
|
||||
final requestId = ++_currentRequestId;
|
||||
|
||||
// Preserve hasSearchText during search
|
||||
state = TrackState(isLoading: true, hasSearchText: state.hasSearchText);
|
||||
|
||||
try {
|
||||
|
||||
@@ -65,7 +65,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Record access for recent history
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final providerId = widget.albumId.startsWith('deezer:') ? 'deezer' : 'spotify';
|
||||
ref.read(recentAccessProvider.notifier).recordAlbumAccess(
|
||||
@@ -77,7 +76,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
);
|
||||
});
|
||||
|
||||
// Priority: widget.tracks > cache > fetch
|
||||
_tracks = widget.tracks ?? _AlbumCache.get(widget.albumId);
|
||||
if (_tracks == null) {
|
||||
_fetchTracks();
|
||||
@@ -104,7 +102,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList.map((t) => _parseTrack(t as Map<String, dynamic>)).toList();
|
||||
|
||||
// Store in cache
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
if (mounted) {
|
||||
@@ -411,7 +408,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// Default error display
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: colorScheme.errorContainer.withValues(alpha: 0.5),
|
||||
@@ -441,7 +437,6 @@ class _AlbumTrackItem extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Only watch the specific item for this track
|
||||
final queueItem = ref.watch(downloadQueueProvider.select((state) {
|
||||
return state.items.where((item) => item.track.id == track.id).firstOrNull;
|
||||
}));
|
||||
@@ -456,7 +451,6 @@ class _AlbumTrackItem extends ConsumerWidget {
|
||||
final isCompleted = queueItem?.status == DownloadStatus.completed;
|
||||
final progress = queueItem?.progress ?? 0.0;
|
||||
|
||||
// Show as downloaded if in queue completed OR in history
|
||||
final showAsDownloaded = isCompleted || (!isQueued && isInHistory);
|
||||
|
||||
return Padding(
|
||||
|
||||
@@ -100,7 +100,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Record access for recent history
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final providerId = widget.extensionId ??
|
||||
(widget.artistId.startsWith('deezer:') ? 'deezer' : 'spotify');
|
||||
@@ -117,7 +116,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
_topTracks = widget.topTracks;
|
||||
_headerImageUrl = widget.headerImageUrl;
|
||||
_monthlyListeners = widget.monthlyListeners;
|
||||
// Extension artists don't need additional fetching
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,7 +136,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
_headerImageUrl = cached.headerImageUrl;
|
||||
_monthlyListeners = cached.monthlyListeners;
|
||||
|
||||
// If cache has no top tracks, fetch
|
||||
if (_topTracks == null || _topTracks!.isEmpty) {
|
||||
_fetchDiscography();
|
||||
}
|
||||
@@ -169,7 +166,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
final albumsList = artistData['albums'] as List<dynamic>? ?? [];
|
||||
albums = albumsList.map((a) => _parseArtistAlbum(a as Map<String, dynamic>)).toList();
|
||||
|
||||
// Parse top tracks if available
|
||||
final topTracksList = artistData['top_tracks'] as List<dynamic>? ?? [];
|
||||
if (topTracksList.isNotEmpty) {
|
||||
topTracks = topTracksList.map((t) => _parseTrack(t as Map<String, dynamic>)).toList();
|
||||
@@ -178,14 +174,12 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
headerImage = artistData['header_image'] as String?;
|
||||
listeners = artistData['listeners'] as int?;
|
||||
} else {
|
||||
// Fallback to Spotify API metadata
|
||||
final metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url);
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
albums = albumsList.map((a) => _parseArtistAlbum(a as Map<String, dynamic>)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Store in cache (preserve existing values if new ones are null)
|
||||
final finalHeaderImage = headerImage ?? _headerImageUrl ?? widget.headerImageUrl;
|
||||
final finalListeners = listeners ?? _monthlyListeners ?? widget.monthlyListeners;
|
||||
|
||||
@@ -277,10 +271,8 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
child: _buildErrorWidget(_error!, colorScheme),
|
||||
)),
|
||||
if (!_isLoadingDiscography && _error == null) ...[
|
||||
// Popular tracks section
|
||||
if (_topTracks != null && _topTracks!.isNotEmpty)
|
||||
SliverToBoxAdapter(child: _buildPopularSection(colorScheme)),
|
||||
// Discography sections
|
||||
if (albumsOnly.isNotEmpty)
|
||||
SliverToBoxAdapter(child: _buildAlbumSection(context.l10n.artistAlbums, albumsOnly, colorScheme)),
|
||||
if (singles.isNotEmpty)
|
||||
@@ -308,7 +300,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
imageUrl.isNotEmpty &&
|
||||
Uri.tryParse(imageUrl)?.hasAuthority == true;
|
||||
|
||||
// Format monthly listeners
|
||||
String? listenersText;
|
||||
final listeners = _monthlyListeners ?? widget.monthlyListeners;
|
||||
if (listeners != null && listeners > 0) {
|
||||
@@ -326,7 +317,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
background: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Background image - full width, no circular crop
|
||||
if (hasValidImage)
|
||||
CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
@@ -346,7 +336,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(Icons.person, size: 80, color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
// Gradient overlay for text readability
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -362,7 +351,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Artist name and listeners at bottom
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
@@ -428,7 +416,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
Widget _buildPopularSection(ColorScheme colorScheme) {
|
||||
if (_topTracks == null || _topTracks!.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
// Show max 5 tracks
|
||||
final tracks = _topTracks!.take(5).toList();
|
||||
|
||||
return Column(
|
||||
@@ -454,7 +441,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
|
||||
/// Build a single popular track item with dynamic download status
|
||||
Widget _buildPopularTrackItem(int rank, Track track, ColorScheme colorScheme) {
|
||||
// Watch download queue for this track's status
|
||||
final queueItem = ref.watch(downloadQueueProvider.select((state) {
|
||||
return state.items.where((item) => item.track.id == track.id).firstOrNull;
|
||||
}));
|
||||
@@ -469,7 +455,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
final isCompleted = queueItem?.status == DownloadStatus.completed;
|
||||
final progress = queueItem?.progress ?? 0.0;
|
||||
|
||||
// Show as downloaded if in queue completed OR in history
|
||||
final showAsDownloaded = isCompleted || (!isQueued && isInHistory);
|
||||
|
||||
return InkWell(
|
||||
@@ -478,7 +463,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
// Rank number
|
||||
SizedBox(
|
||||
width: 24,
|
||||
child: Text(
|
||||
@@ -490,7 +474,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Album art
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: track.coverUrl != null
|
||||
@@ -520,7 +503,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Track info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -545,7 +527,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Download button with status
|
||||
_buildPopularDownloadButton(
|
||||
track: track,
|
||||
colorScheme: colorScheme,
|
||||
@@ -729,7 +710,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Album cover
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: album.coverUrl != null
|
||||
@@ -759,7 +739,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Album name
|
||||
Text(
|
||||
album.name,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
@@ -769,7 +748,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Year and track count
|
||||
Text(
|
||||
album.totalTracks > 0
|
||||
? '${album.releaseDate.length >= 4 ? album.releaseDate.substring(0, 4) : album.releaseDate} ${context.l10n.tracksCount(album.totalTracks)}'
|
||||
|
||||
@@ -27,7 +27,6 @@ class DownloadedAlbumScreen extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
// Multi-select state
|
||||
bool _isSelectionMode = false;
|
||||
final Set<String> _selectedIds = {};
|
||||
|
||||
@@ -162,7 +161,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
final allHistoryItems = ref.watch(downloadHistoryProvider.select((s) => s.items));
|
||||
final tracks = _getAlbumTracks(allHistoryItems);
|
||||
|
||||
// Auto-pop if album has less than 2 tracks (no longer an "album")
|
||||
if (tracks.length < 2) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) Navigator.pop(context);
|
||||
@@ -170,7 +168,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// Clean up selected IDs that no longer exist
|
||||
final validIds = tracks.map((t) => t.id).toSet();
|
||||
_selectedIds.removeWhere((id) => !validIds.contains(id));
|
||||
if (_selectedIds.isEmpty && _isSelectionMode) {
|
||||
@@ -199,7 +196,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
|
||||
],
|
||||
),
|
||||
|
||||
// Bottom Selection Action Bar
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOutCubic,
|
||||
|
||||
@@ -75,7 +75,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
setState(() => _currentIndex = index);
|
||||
switch (index) {
|
||||
case 0:
|
||||
// Already on home
|
||||
break;
|
||||
case 1:
|
||||
context.push('/queue');
|
||||
@@ -112,7 +111,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// URL Input
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: TextField(
|
||||
@@ -132,7 +130,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Error message
|
||||
if (trackState.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
@@ -142,15 +139,12 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Loading indicator
|
||||
if (trackState.isLoading)
|
||||
LinearProgressIndicator(color: colorScheme.primary),
|
||||
|
||||
// Album/Playlist header
|
||||
if (trackState.albumName != null || trackState.playlistName != null)
|
||||
_buildHeader(trackState, colorScheme),
|
||||
|
||||
// Download All button
|
||||
if (trackState.tracks.length > 1)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
@@ -164,7 +158,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Track list
|
||||
Expanded(
|
||||
child: trackState.tracks.isEmpty
|
||||
? _buildEmptyState(colorScheme)
|
||||
@@ -252,7 +245,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Play all button
|
||||
FilledButton.tonal(
|
||||
onPressed: _downloadAll,
|
||||
style: FilledButton.styleFrom(
|
||||
@@ -271,7 +263,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
final track = ref.watch(trackProvider).tracks[index];
|
||||
final isCollection = track.isCollection;
|
||||
|
||||
// Determine subtitle text based on item type
|
||||
String subtitleText;
|
||||
if (isCollection) {
|
||||
final typeLabel = track.albumType ?? (track.isPlaylistItem ? 'Playlist' : 'Album');
|
||||
@@ -332,7 +323,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
final extensionId = track.source;
|
||||
if (extensionId == null) return;
|
||||
|
||||
// Fetch album/playlist tracks using the extension
|
||||
try {
|
||||
if (track.isAlbumItem) {
|
||||
final albumData = await PlatformBridge.getAlbumWithExtension(extensionId, track.id);
|
||||
|
||||
+3
-100
@@ -30,7 +30,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
final _urlController = TextEditingController();
|
||||
bool _isTyping = false;
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
String? _lastSearchQuery; // Track last searched query to avoid duplicate searches
|
||||
String? _lastSearchQuery;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@@ -52,9 +52,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
}
|
||||
|
||||
void _onSearchFocusChanged() {
|
||||
// When focused, enter recent access mode
|
||||
// When unfocused (keyboard dismissed), keep recent access mode visible
|
||||
// User must press back button to exit recent access mode
|
||||
if (_searchFocusNode.hasFocus) {
|
||||
ref.read(trackProvider.notifier).setShowingRecentAccess(true);
|
||||
}
|
||||
@@ -62,8 +59,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
/// Called when trackState changes - used to sync search bar with state
|
||||
void _onTrackStateChanged(TrackState? previous, TrackState next) {
|
||||
// If state was cleared (no content, no search text, not loading), clear the search bar
|
||||
// BUT only if search field is not focused (to prevent clearing while user is typing)
|
||||
if (previous != null &&
|
||||
!next.hasContent &&
|
||||
!next.hasSearchText &&
|
||||
@@ -86,9 +81,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
// Provider will be cleared when user explicitly clears or navigates away
|
||||
return;
|
||||
}
|
||||
|
||||
// No auto-search - user must press Enter to search
|
||||
// This saves API calls and avoids rate limiting
|
||||
}
|
||||
|
||||
Future<void> _performSearch(String query) async {
|
||||
@@ -96,7 +88,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
final extState = ref.read(extensionProvider);
|
||||
final searchProvider = settings.searchProvider;
|
||||
|
||||
// Skip if same query already searched with same provider
|
||||
final searchKey = '${searchProvider ?? 'default'}:$query';
|
||||
if (_lastSearchQuery == searchKey) return;
|
||||
_lastSearchQuery = searchKey;
|
||||
@@ -120,7 +111,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data?.text != null) {
|
||||
_urlController.text = data!.text!;
|
||||
// For URLs, trigger fetch immediately after paste
|
||||
final text = data.text!.trim();
|
||||
if (text.startsWith('http') || text.startsWith('spotify:')) {
|
||||
_fetchMetadata();
|
||||
@@ -131,7 +121,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
Future<void> _clearAndRefresh() async {
|
||||
_urlController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
_lastSearchQuery = null; // Reset last query
|
||||
_lastSearchQuery = null;
|
||||
setState(() => _isTyping = false);
|
||||
ref.read(trackProvider.notifier).clear();
|
||||
}
|
||||
@@ -153,7 +143,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
void _navigateToDetailIfNeeded() {
|
||||
final trackState = ref.read(trackProvider);
|
||||
|
||||
// Navigate to Album screen (recording is done in AlbumScreen.initState)
|
||||
if (trackState.albumId != null && trackState.albumName != null && trackState.tracks.isNotEmpty) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => AlbumScreen(
|
||||
albumId: trackState.albumId!,
|
||||
@@ -167,9 +156,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to Playlist screen
|
||||
if (trackState.playlistName != null && trackState.tracks.isNotEmpty) {
|
||||
// Record access for playlist (no separate screen to record in)
|
||||
ref.read(recentAccessProvider.notifier).recordPlaylistAccess(
|
||||
id: trackState.playlistName!,
|
||||
name: trackState.playlistName!,
|
||||
@@ -188,7 +175,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigate to Artist screen (recording is done in ArtistScreen.initState)
|
||||
if (trackState.artistId != null && trackState.artistName != null && trackState.artistAlbums != null) {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (context) => ArtistScreen(
|
||||
artistId: trackState.artistId!,
|
||||
@@ -228,7 +214,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
}
|
||||
|
||||
Future<void> _importCsv(BuildContext context, WidgetRef ref) async {
|
||||
// Show loading dialog with progress
|
||||
int currentProgress = 0;
|
||||
int totalTracks = 0;
|
||||
|
||||
@@ -274,7 +259,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
},
|
||||
);
|
||||
|
||||
// Close progress dialog
|
||||
if (dialogShown && mounted) {
|
||||
Navigator.of(this.context).pop();
|
||||
}
|
||||
@@ -287,7 +271,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
// ignore: use_build_context_synchronously
|
||||
final l10n = context.l10n;
|
||||
|
||||
// Optionally show confirmation dialog
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: this.context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
@@ -321,8 +304,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Only show error if pick was not cancelled (handled inside service logging usually, but maybe show snackbar if file empty)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,10 +311,8 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
// Listen for state changes to sync search bar and auto-navigate
|
||||
ref.listen<TrackState>(trackProvider, (previous, next) {
|
||||
_onTrackStateChanged(previous, next);
|
||||
// Auto-navigate when URL fetch completes
|
||||
if (previous != null && previous.isLoading && !next.isLoading && next.error == null) {
|
||||
_navigateToDetailIfNeeded();
|
||||
}
|
||||
@@ -351,18 +330,15 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final hasActualResults = tracks.isNotEmpty || (searchArtists != null && searchArtists.isNotEmpty);
|
||||
final isShowingRecentAccess = ref.watch(trackProvider.select((s) => s.isShowingRecentAccess));
|
||||
// Move search bar up when in recent access mode or has results
|
||||
final hasResults = isShowingRecentAccess || hasActualResults || isLoading;
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
final historyItems = ref.watch(downloadHistoryProvider.select((s) => s.items));
|
||||
final recentAccessItems = ref.watch(recentAccessProvider.select((s) => s.items));
|
||||
|
||||
// Show recent access when in mode but no actual results yet (includes download history)
|
||||
final hasRecentItems = recentAccessItems.isNotEmpty || historyItems.isNotEmpty;
|
||||
final showRecentAccess = isShowingRecentAccess && hasRecentItems && !hasActualResults && !isLoading;
|
||||
|
||||
// Exit recent access mode when results appear
|
||||
if (hasActualResults && isShowingRecentAccess) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) ref.read(trackProvider.notifier).setShowingRecentAccess(false);
|
||||
@@ -371,7 +347,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// Unfocus search bar when tapping outside
|
||||
if (_searchFocusNode.hasFocus) {
|
||||
_searchFocusNode.unfocus();
|
||||
}
|
||||
@@ -381,7 +356,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
body: CustomScrollView(
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
slivers: [
|
||||
// App Bar - always present
|
||||
SliverAppBar(
|
||||
expandedHeight: 120 + topPadding,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
@@ -412,7 +386,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Idle content (logo, title) - always in tree, animated size
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
@@ -431,10 +404,9 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
child: Image.asset(
|
||||
'assets/images/logo-transparant.png',
|
||||
color: colorScheme.onPrimary, // Tint with onPrimary color
|
||||
color: colorScheme.onPrimary,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (_, _, _) => ClipRRect(
|
||||
// Fallback to original logo if transparent one is missing
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
@@ -465,7 +437,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Search bar - always present at same position in tree
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, hasResults ? 8 : 32, 16, hasResults ? 8 : 16),
|
||||
@@ -473,14 +444,11 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Recent access history - shown when in recent access mode (persists after keyboard dismissed)
|
||||
// User can exit by pressing back button
|
||||
if (showRecentAccess)
|
||||
SliverToBoxAdapter(
|
||||
child: _buildRecentAccess(recentAccessItems, colorScheme),
|
||||
),
|
||||
|
||||
// Idle content below search bar - always in tree
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
@@ -510,7 +478,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Results content - search results only (albums/artists/playlists navigate to separate screens)
|
||||
..._buildSearchResults(
|
||||
tracks: tracks,
|
||||
searchArtists: searchArtists,
|
||||
@@ -598,7 +565,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
/// Build recent access history section (shown when search focused)
|
||||
Widget _buildRecentAccess(List<RecentAccessItem> items, ColorScheme colorScheme) {
|
||||
// Merge with recent downloads to make the list more populated
|
||||
final historyItems = ref.read(downloadHistoryProvider).items;
|
||||
|
||||
final downloadItems = historyItems.take(10).where((h) => h.spotifyId != null && h.spotifyId!.isNotEmpty).map((h) => RecentAccessItem(
|
||||
@@ -611,11 +577,9 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
providerId: 'download',
|
||||
)).toList();
|
||||
|
||||
// Merge and sort by accessedAt (most recent first)
|
||||
final allItems = [...items, ...downloadItems];
|
||||
allItems.sort((a, b) => b.accessedAt.compareTo(a.accessedAt));
|
||||
|
||||
// Remove duplicates (keep the most recent one)
|
||||
final seen = <String>{};
|
||||
final uniqueItems = allItems.where((item) {
|
||||
final key = '${item.type.name}:${item.id}';
|
||||
@@ -629,7 +593,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with clear button
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -651,7 +614,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// List of recent items
|
||||
...uniqueItems.map((item) => _buildRecentAccessItem(item, colorScheme)),
|
||||
],
|
||||
),
|
||||
@@ -659,7 +621,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
}
|
||||
|
||||
Widget _buildRecentAccessItem(RecentAccessItem item, ColorScheme colorScheme) {
|
||||
// Icon and label based on type
|
||||
IconData typeIcon;
|
||||
String typeLabel;
|
||||
switch (item.type) {
|
||||
@@ -686,7 +647,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
// Image
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(item.type == RecentAccessType.artist ? 28 : 4),
|
||||
child: item.imageUrl != null && item.imageUrl!.isNotEmpty
|
||||
@@ -711,7 +671,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Text content
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -792,19 +751,15 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
));
|
||||
}
|
||||
case RecentAccessType.track:
|
||||
// For tracks from download history, navigate to metadata screen
|
||||
final historyItem = ref.read(downloadHistoryProvider.notifier).getBySpotifyId(item.id);
|
||||
if (historyItem != null) {
|
||||
_navigateToMetadataScreen(historyItem);
|
||||
} else {
|
||||
// Track not in history anymore
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(item.name)),
|
||||
);
|
||||
}
|
||||
case RecentAccessType.playlist:
|
||||
// Playlist needs tracks, so we just show info
|
||||
// Could potentially re-fetch using URL handler if we stored URL
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.recentPlaylistInfo(item.name))),
|
||||
);
|
||||
@@ -865,7 +820,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
);
|
||||
}
|
||||
|
||||
// Default error display
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: colorScheme.errorContainer.withValues(alpha: 0.5),
|
||||
@@ -883,7 +837,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
);
|
||||
}
|
||||
|
||||
// Search results slivers - only shows search results (track list)
|
||||
List<Widget> _buildSearchResults({
|
||||
required List<Track> tracks,
|
||||
required List<SearchArtist>? searchArtists,
|
||||
@@ -896,29 +849,24 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
return [const SliverToBoxAdapter(child: SizedBox.shrink())];
|
||||
}
|
||||
|
||||
// Separate tracks from albums/playlists/artists
|
||||
final realTracks = tracks.where((t) => !t.isCollection).toList();
|
||||
final albumItems = tracks.where((t) => t.isAlbumItem).toList();
|
||||
final playlistItems = tracks.where((t) => t.isPlaylistItem).toList();
|
||||
final artistItems = tracks.where((t) => t.isArtistItem).toList();
|
||||
|
||||
return [
|
||||
// Error message - with special handling for rate limit (429)
|
||||
if (error != null)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _buildErrorWidget(error, colorScheme),
|
||||
)),
|
||||
|
||||
// Loading indicator
|
||||
if (isLoading)
|
||||
const SliverToBoxAdapter(child: Padding(padding: EdgeInsets.symmetric(horizontal: 16), child: LinearProgressIndicator())),
|
||||
|
||||
// Artist search results (horizontal scroll) - from built-in providers
|
||||
if (searchArtists != null && searchArtists.isNotEmpty)
|
||||
SliverToBoxAdapter(child: _buildArtistSearchResults(searchArtists, colorScheme)),
|
||||
|
||||
// Artists section - from extension search
|
||||
if (artistItems.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
@@ -953,7 +901,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Albums section
|
||||
if (albumItems.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
@@ -988,7 +935,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Playlists section
|
||||
if (playlistItems.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
@@ -1023,14 +969,12 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Songs section header
|
||||
if (realTracks.isNotEmpty)
|
||||
SliverToBoxAdapter(child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Text(context.l10n.searchSongs, style: Theme.of(context).textTheme.titleSmall?.copyWith(color: colorScheme.onSurfaceVariant)),
|
||||
)),
|
||||
|
||||
// Track list in grouped card
|
||||
if (realTracks.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
@@ -1061,7 +1005,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom padding
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
];
|
||||
}
|
||||
@@ -1094,7 +1037,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
}
|
||||
|
||||
Widget _buildArtistCard(SearchArtist artist, ColorScheme colorScheme) {
|
||||
// Validate image URL - must be non-null, non-empty, and have a valid host
|
||||
final hasValidImage = artist.imageUrl != null &&
|
||||
artist.imageUrl!.isNotEmpty &&
|
||||
Uri.tryParse(artist.imageUrl!)?.hasAuthority == true;
|
||||
@@ -1144,17 +1086,13 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
}
|
||||
|
||||
void _navigateToArtist(String artistId, String artistName, String? imageUrl) {
|
||||
// Navigate immediately with data from search, fetch albums in ArtistScreen
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
|
||||
// Recording is done in ArtistScreen.initState to avoid duplicates
|
||||
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => ArtistScreen(
|
||||
artistId: artistId,
|
||||
artistName: artistName,
|
||||
coverUrl: imageUrl,
|
||||
// albums: null - will be fetched in ArtistScreen
|
||||
),
|
||||
));
|
||||
}
|
||||
@@ -1170,7 +1108,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
|
||||
// Record access for recent history
|
||||
ref.read(recentAccessProvider.notifier).recordAlbumAccess(
|
||||
id: albumItem.id,
|
||||
name: albumItem.name,
|
||||
@@ -1179,7 +1116,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
providerId: extensionId,
|
||||
);
|
||||
|
||||
// Navigate to AlbumScreen - it will fetch tracks via extension
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => ExtensionAlbumScreen(
|
||||
extensionId: extensionId,
|
||||
@@ -1201,7 +1137,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
|
||||
// Record access for recent history
|
||||
ref.read(recentAccessProvider.notifier).recordPlaylistAccess(
|
||||
id: playlistItem.id,
|
||||
name: playlistItem.name,
|
||||
@@ -1210,7 +1145,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
providerId: extensionId,
|
||||
);
|
||||
|
||||
// Navigate to ExtensionPlaylistScreen - it will fetch tracks via extension
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => ExtensionPlaylistScreen(
|
||||
extensionId: extensionId,
|
||||
@@ -1232,7 +1166,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
|
||||
// Record access for recent history
|
||||
ref.read(recentAccessProvider.notifier).recordArtistAccess(
|
||||
id: artistItem.id,
|
||||
name: artistItem.name,
|
||||
@@ -1240,7 +1173,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
providerId: extensionId,
|
||||
);
|
||||
|
||||
// Navigate to ExtensionArtistScreen - it will fetch albums via extension
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (context) => ExtensionArtistScreen(
|
||||
extensionId: extensionId,
|
||||
@@ -1257,22 +1189,18 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
final searchProvider = settings.searchProvider;
|
||||
final extState = ref.read(extensionProvider);
|
||||
|
||||
// If extension system not initialized yet, show default hint
|
||||
if (!extState.isInitialized) {
|
||||
return 'Paste Spotify URL or search...';
|
||||
}
|
||||
|
||||
if (searchProvider != null && searchProvider.isNotEmpty) {
|
||||
final ext = extState.extensions.where((e) => e.id == searchProvider).firstOrNull;
|
||||
// Only show extension placeholder if extension exists AND is enabled
|
||||
if (ext != null && ext.enabled) {
|
||||
if (ext.searchBehavior?.placeholder != null) {
|
||||
return ext.searchBehavior!.placeholder!;
|
||||
}
|
||||
return 'Search with ${ext.displayName}...';
|
||||
}
|
||||
// Extension not found or disabled - clear the search provider setting
|
||||
// and return default hint
|
||||
}
|
||||
return 'Paste Spotify URL or search...';
|
||||
}
|
||||
@@ -1335,14 +1263,12 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
final text = _urlController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
// If it's a URL, fetch metadata
|
||||
if (text.startsWith('http') || text.startsWith('spotify:')) {
|
||||
_fetchMetadata();
|
||||
_searchFocusNode.unfocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// For search queries, always search (minimum 2 chars)
|
||||
if (text.length >= 2) {
|
||||
_performSearch(text);
|
||||
}
|
||||
@@ -1370,7 +1296,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Only watch the specific item for this track using select()
|
||||
final queueItem = ref.watch(downloadQueueProvider.select((state) {
|
||||
return state.items.where((item) => item.track.id == track.id).firstOrNull;
|
||||
}));
|
||||
@@ -1392,7 +1317,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
final size = extension!.searchBehavior!.getThumbnailSize(defaultSize: 56);
|
||||
thumbWidth = size.$1;
|
||||
thumbHeight = size.$2;
|
||||
// Debug: log only when using custom size
|
||||
if (thumbWidth != 56 || thumbHeight != 56) {
|
||||
debugPrint('[Thumbnail] ${track.name}: using ${thumbWidth.toInt()}x${thumbHeight.toInt()} from ${extension.id}');
|
||||
}
|
||||
@@ -1405,7 +1329,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
final isCompleted = queueItem?.status == DownloadStatus.completed;
|
||||
final progress = queueItem?.progress ?? 0.0;
|
||||
|
||||
// Show as downloaded if in queue completed OR in history
|
||||
final showAsDownloaded = isCompleted || (!isQueued && isInHistory);
|
||||
|
||||
return Column(
|
||||
@@ -1419,7 +1342,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
// Album art with dynamic size based on extension config
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: track.coverUrl != null
|
||||
@@ -1439,7 +1361,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Track info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1460,7 +1381,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Download button / status indicator
|
||||
_buildDownloadButton(context, ref, colorScheme, isQueued: isQueued, isDownloading: isDownloading, isFinalizing: isFinalizing, showAsDownloaded: showAsDownloaded, isInHistory: isInHistory, progress: progress),
|
||||
],
|
||||
),
|
||||
@@ -1479,7 +1399,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
}
|
||||
|
||||
void _handleTap(BuildContext context, WidgetRef ref, {required bool isQueued, required bool isInHistory}) async {
|
||||
// If already in queue, do nothing
|
||||
if (isQueued) return;
|
||||
|
||||
if (isInHistory) {
|
||||
@@ -1487,7 +1406,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
if (historyItem != null) {
|
||||
final fileExists = await File(historyItem.filePath).exists();
|
||||
if (fileExists) {
|
||||
// File exists, show snackbar
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.snackbarAlreadyDownloaded(track.name))),
|
||||
@@ -1495,13 +1413,11 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
// File doesn't exist, remove from history and allow download
|
||||
ref.read(downloadHistoryProvider.notifier).removeBySpotifyId(track.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Proceed with download
|
||||
onDownload();
|
||||
}
|
||||
|
||||
@@ -1527,7 +1443,6 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
} else if (isFinalizing) {
|
||||
// Show finalizing status (embedding metadata)
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
@@ -1591,7 +1506,6 @@ class _CollectionItemWidget extends StatelessWidget {
|
||||
final isPlaylist = item.isPlaylistItem;
|
||||
final isArtist = item.isArtistItem;
|
||||
|
||||
// Determine icon for placeholder
|
||||
IconData placeholderIcon = Icons.album;
|
||||
if (isPlaylist) placeholderIcon = Icons.playlist_play;
|
||||
if (isArtist) placeholderIcon = Icons.person;
|
||||
@@ -1607,7 +1521,6 @@ class _CollectionItemWidget extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
// Cover art (circular for artists)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(isArtist ? 28 : 10),
|
||||
child: item.coverUrl != null && item.coverUrl!.isNotEmpty
|
||||
@@ -1630,7 +1543,6 @@ class _CollectionItemWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1651,7 +1563,6 @@ class _CollectionItemWidget extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Arrow indicator
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
@@ -1725,7 +1636,6 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse tracks from result
|
||||
final trackList = result['tracks'] as List<dynamic>?;
|
||||
if (trackList == null) {
|
||||
setState(() {
|
||||
@@ -1802,7 +1712,6 @@ class _ExtensionAlbumScreenState extends ConsumerState<ExtensionAlbumScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate to AlbumScreen with fetched tracks
|
||||
return AlbumScreen(
|
||||
albumId: widget.albumId,
|
||||
albumName: widget.albumName,
|
||||
@@ -1863,7 +1772,6 @@ class _ExtensionPlaylistScreenState extends ConsumerState<ExtensionPlaylistScree
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse tracks from result
|
||||
final trackList = result['tracks'] as List<dynamic>?;
|
||||
if (trackList == null) {
|
||||
setState(() {
|
||||
@@ -1940,7 +1848,6 @@ class _ExtensionPlaylistScreenState extends ConsumerState<ExtensionPlaylistScree
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate to PlaylistScreen with fetched tracks
|
||||
return PlaylistScreen(
|
||||
playlistName: widget.playlistName,
|
||||
coverUrl: widget.coverUrl,
|
||||
@@ -2003,18 +1910,15 @@ class _ExtensionArtistScreenState extends ConsumerState<ExtensionArtistScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse albums from result
|
||||
final albumList = result['albums'] as List<dynamic>?;
|
||||
final albums = albumList?.map((a) => _parseAlbum(a as Map<String, dynamic>)).toList() ?? [];
|
||||
|
||||
// Parse top tracks from result
|
||||
final topTracksList = result['top_tracks'] as List<dynamic>?;
|
||||
List<Track>? topTracks;
|
||||
if (topTracksList != null && topTracksList.isNotEmpty) {
|
||||
topTracks = topTracksList.map((t) => _parseTrack(t as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
// Parse additional artist info
|
||||
final headerImage = result['header_image'] as String?;
|
||||
final listeners = result['listeners'] as int?;
|
||||
|
||||
@@ -2097,7 +2001,6 @@ class _ExtensionArtistScreenState extends ConsumerState<ExtensionArtistScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate to ArtistScreen with fetched albums and top tracks
|
||||
return ArtistScreen(
|
||||
artistId: widget.artistId,
|
||||
artistName: widget.artistName,
|
||||
|
||||
@@ -30,7 +30,7 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
late PageController _pageController;
|
||||
bool _hasCheckedUpdate = false;
|
||||
StreamSubscription<String>? _shareSubscription;
|
||||
DateTime? _lastBackPress; // For double-tap to exit
|
||||
DateTime? _lastBackPress;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -49,7 +49,6 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
_handleSharedUrl(pendingUrl);
|
||||
}
|
||||
|
||||
// Listen for future shared URLs with error handling
|
||||
_shareSubscription = ShareIntentService().sharedUrlStream.listen(
|
||||
(url) {
|
||||
_log.d('Received shared URL from stream: $url');
|
||||
@@ -63,18 +62,13 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
}
|
||||
|
||||
void _handleSharedUrl(String url) {
|
||||
// Pop any existing screens (Album, Artist, Settings sub-pages) to return to root
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
|
||||
// Navigate to Home tab
|
||||
if (_currentIndex != 0) {
|
||||
_onNavTap(0);
|
||||
}
|
||||
// Fetch metadata for shared URL
|
||||
ref.read(trackProvider.notifier).fetchFromUrl(url);
|
||||
// Mark that user has searched (hide helper text)
|
||||
ref.read(settingsProvider.notifier).setHasSearchedBefore();
|
||||
// Show snackbar
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.loadingSharedLink)),
|
||||
@@ -136,31 +130,26 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
return;
|
||||
}
|
||||
|
||||
// If on Home tab and showing recent access mode, exit it
|
||||
if (_currentIndex == 0 && trackState.isShowingRecentAccess) {
|
||||
ref.read(trackProvider.notifier).setShowingRecentAccess(false);
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
return;
|
||||
}
|
||||
|
||||
// If on Home tab and has text in search bar or has content (but not loading), clear it
|
||||
if (_currentIndex == 0 && !trackState.isLoading && (trackState.hasSearchText || trackState.hasContent)) {
|
||||
ref.read(trackProvider.notifier).clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not on Home tab, go to Home tab first
|
||||
if (_currentIndex != 0) {
|
||||
_onNavTap(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// If loading, ignore back press
|
||||
if (trackState.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Double-tap to exit
|
||||
final now = DateTime.now();
|
||||
if (_lastBackPress != null && now.difference(_lastBackPress!) < const Duration(seconds: 2)) {
|
||||
SystemNavigator.pop();
|
||||
@@ -247,7 +236,6 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
),
|
||||
];
|
||||
|
||||
// Clamp current index if tabs changed
|
||||
final maxIndex = tabs.length - 1;
|
||||
if (_currentIndex > maxIndex) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -267,7 +255,6 @@ class _MainShellState extends ConsumerState<MainShell> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle back press manually when canPop is false
|
||||
_handleBackPress();
|
||||
},
|
||||
child: Scaffold(
|
||||
|
||||
@@ -217,7 +217,6 @@ class _PlaylistTrackItem extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Only watch the specific item for this track
|
||||
final queueItem = ref.watch(downloadQueueProvider.select((state) {
|
||||
return state.items.where((item) => item.track.id == track.id).firstOrNull;
|
||||
}));
|
||||
@@ -232,7 +231,6 @@ class _PlaylistTrackItem extends ConsumerWidget {
|
||||
final isCompleted = queueItem?.status == DownloadStatus.completed;
|
||||
final progress = queueItem?.progress ?? 0.0;
|
||||
|
||||
// Show as downloaded if in queue completed OR in history
|
||||
final showAsDownloaded = isCompleted || (!isQueued && isInHistory);
|
||||
|
||||
return Padding(
|
||||
|
||||
@@ -559,7 +559,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
}, childCount: queueItems.length),
|
||||
),
|
||||
|
||||
// Filter chips (only show when history has items)
|
||||
if (allHistoryItems.isNotEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
@@ -788,7 +787,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
),
|
||||
|
||||
// Albums Grid (when Albums filter is selected)
|
||||
if (filterMode == 'albums' && groupedAlbums.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -1045,7 +1043,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle bar
|
||||
Container(
|
||||
width: 32,
|
||||
height: 4,
|
||||
@@ -1067,7 +1064,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Selection count
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1088,7 +1084,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
),
|
||||
|
||||
// Select all toggle
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
if (allSelected) {
|
||||
|
||||
@@ -694,7 +694,6 @@ class _LanguageSelector extends StatelessWidget {
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
// All available languages (code, displayName, icon)
|
||||
static const _allLanguages = [
|
||||
('system', 'System Default', Icons.phone_android),
|
||||
('en', 'English', Icons.language),
|
||||
|
||||
@@ -65,7 +65,6 @@ class DownloadSettingsPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Service section
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.sectionService),
|
||||
),
|
||||
@@ -470,7 +469,6 @@ class DownloadSettingsPage extends ConsumerWidget {
|
||||
|
||||
Future<void> _pickDirectory(BuildContext context, WidgetRef ref) async {
|
||||
if (Platform.isIOS) {
|
||||
// iOS: Show options dialog
|
||||
_showIOSDirectoryOptions(context, ref);
|
||||
} else {
|
||||
final result = await FilePicker.platform.getDirectoryPath();
|
||||
@@ -697,7 +695,6 @@ class _ServiceSelector extends ConsumerWidget {
|
||||
? extensionProviders.any((e) => e.id == currentService)
|
||||
: true;
|
||||
|
||||
// If current extension is disabled, show it as not selected
|
||||
final effectiveService = isCurrentExtensionEnabled ? currentService : '';
|
||||
|
||||
return Padding(
|
||||
|
||||
@@ -50,7 +50,6 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
||||
child: Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
// App Bar
|
||||
SliverAppBar(
|
||||
expandedHeight: 120 + topPadding,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
@@ -120,7 +119,6 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Provider Priority
|
||||
SliverToBoxAdapter(
|
||||
child: SettingsSectionHeader(title: context.l10n.extensionsProviderPrioritySection),
|
||||
),
|
||||
@@ -216,7 +214,6 @@ class _ExtensionsPageState extends ConsumerState<ExtensionsPage> {
|
||||
),
|
||||
),
|
||||
|
||||
// Info section
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
|
||||
@@ -344,7 +341,6 @@ class _ExtensionItem extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// Extension icon
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
@@ -402,7 +398,6 @@ class _ExtensionItem extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Toggle switch
|
||||
Switch(
|
||||
value: extension.enabled,
|
||||
onChanged: hasError ? null : onToggle,
|
||||
|
||||
@@ -835,7 +835,6 @@ class _MetadataSourceSelector extends ConsumerWidget {
|
||||
_SourceChip(
|
||||
icon: Icons.graphic_eq,
|
||||
label: 'Deezer',
|
||||
// Not selected if extension is active
|
||||
isSelected: currentSource == 'deezer' && !hasExtensionSearch,
|
||||
onTap: () {
|
||||
if (hasExtensionSearch) {
|
||||
|
||||
@@ -267,7 +267,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
|
||||
try {
|
||||
if (Platform.isIOS) {
|
||||
// iOS: Show options dialog
|
||||
await _showIOSDirectoryOptions();
|
||||
} else {
|
||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(
|
||||
@@ -418,7 +417,6 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
|
||||
ref.read(settingsProvider.notifier).setDownloadDirectory(_selectedDirectory!);
|
||||
|
||||
// Save Spotify credentials if provided
|
||||
if (_useSpotifyApi &&
|
||||
_clientIdController.text.trim().isNotEmpty &&
|
||||
_clientSecretController.text.trim().isNotEmpty) {
|
||||
@@ -573,15 +571,13 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
|
||||
|
||||
bool _isStepCompleted(int step) {
|
||||
if (_androidSdkVersion >= 33) {
|
||||
// 4 steps: Storage, Notification, Folder, Spotify
|
||||
switch (step) {
|
||||
case 0: return _storagePermissionGranted;
|
||||
case 1: return _notificationPermissionGranted;
|
||||
case 2: return _selectedDirectory != null;
|
||||
case 3: return false; // Spotify step never shows checkmark (optional)
|
||||
case 3: return false;
|
||||
}
|
||||
} else {
|
||||
// 3 steps: Permission, Folder, Spotify
|
||||
switch (step) {
|
||||
case 0: return _storagePermissionGranted;
|
||||
case 1: return _selectedDirectory != null;
|
||||
|
||||
@@ -19,7 +19,6 @@ class DynamicColorWrapper extends ConsumerWidget {
|
||||
|
||||
return DynamicColorBuilder(
|
||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||
// Determine which color scheme to use
|
||||
ColorScheme lightScheme;
|
||||
ColorScheme darkScheme;
|
||||
|
||||
@@ -28,7 +27,6 @@ class DynamicColorWrapper extends ConsumerWidget {
|
||||
lightScheme = lightDynamic;
|
||||
darkScheme = darkDynamic;
|
||||
} else {
|
||||
// Fallback to seed color
|
||||
final seedColor = themeSettings.seedColor;
|
||||
lightScheme = ColorScheme.fromSeed(
|
||||
seedColor: seedColor,
|
||||
|
||||
@@ -188,14 +188,12 @@ class BufferedOutput extends LogOutput {
|
||||
|
||||
@override
|
||||
void output(OutputEvent event) {
|
||||
// Print to console in debug mode
|
||||
if (kDebugMode) {
|
||||
for (final line in event.lines) {
|
||||
debugPrint(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to buffer
|
||||
final level = _levelToString(event.level);
|
||||
final message = event.lines.join('\n');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user