chore: cleanup unused code and dead imports

This commit is contained in:
zarzet
2026-01-20 02:10:10 +07:00
parent 8e9d0c3e9a
commit 03027813c1
62 changed files with 213 additions and 1326 deletions
+1 -12
View File
@@ -13,7 +13,6 @@ import 'package:spotiflac_android/providers/recent_access_provider.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/widgets/download_service_picker.dart';
/// Simple in-memory cache for album tracks
class _AlbumCache {
static final Map<String, _CacheEntry> _cache = {};
static const Duration _ttl = Duration(minutes: 10);
@@ -39,7 +38,6 @@ class _CacheEntry {
_CacheEntry(this.tracks, this.expiresAt);
}
/// Album detail screen with Material Expressive 3 design
class AlbumScreen extends ConsumerStatefulWidget {
final String albumId;
final String albumName;
@@ -99,7 +97,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
}
void _onScroll() {
// Show title in AppBar when scrolled past the header (320 - kToolbarHeight + info card top)
final shouldShow = _scrollController.offset > 280;
if (shouldShow != _showTitleInAppBar) {
setState(() => _showTitleInAppBar = shouldShow);
@@ -121,7 +118,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
});
}
} catch (_) {
// Ignore palette extraction errors
}
}
@@ -132,12 +128,8 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
if (widget.albumId.startsWith('deezer:')) {
final deezerAlbumId = widget.albumId.replaceFirst('deezer:', '');
// ignore: avoid_print
print('[AlbumScreen] Fetching from Deezer: $deezerAlbumId');
metadata = await PlatformBridge.getDeezerMetadata('album', deezerAlbumId);
} else {
// ignore: avoid_print
print('[AlbumScreen] Fetching from Spotify with fallback: ${widget.albumId}');
final url = 'https://open.spotify.com/album/${widget.albumId}';
metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url);
}
@@ -219,7 +211,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
expandedHeight: 320,
pinned: true,
stretch: true,
backgroundColor: colorScheme.surface, // Use theme color for collapsed state
backgroundColor: colorScheme.surface,
surfaceTintColor: Colors.transparent,
title: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
@@ -261,7 +253,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
),
),
),
// Cover image centered - fade out when collapsing
AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: showContent ? 1.0 : 0.0,
@@ -449,7 +440,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
}
}
/// Build error widget with special handling for rate limit (429)
Widget _buildErrorWidget(String error, ColorScheme colorScheme) {
final isRateLimit = error.contains('429') ||
error.toLowerCase().contains('rate limit') ||
@@ -512,7 +502,6 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
}
}
/// Separate Consumer widget for each track - only rebuilds when this specific track's status changes
class _AlbumTrackItem extends ConsumerWidget {
final Track track;
final VoidCallback onDownload;
-4
View File
@@ -97,7 +97,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
int? _monthlyListeners;
String? _error;
// Sticky title state
bool _showTitleInAppBar = false;
final ScrollController _scrollController = ScrollController();
@@ -310,7 +309,6 @@ return Scaffold(
);
}
/// Build Spotify-style header with full-width image and artist name overlay
Widget _buildHeader(BuildContext context, ColorScheme colorScheme) {
String? imageUrl = _headerImageUrl;
if (imageUrl == null || imageUrl.isEmpty) {
@@ -479,7 +477,6 @@ if (hasValidImage)
);
}
/// Build a single popular track item with dynamic download status
Widget _buildPopularTrackItem(int rank, Track track, ColorScheme colorScheme) {
final queueItem = ref.watch(
downloadQueueLookupProvider.select((lookup) => lookup.byTrackId[track.id]),
@@ -608,7 +605,6 @@ if (hasValidImage)
_downloadTrack(track);
}
/// Build download button with status indicator for popular tracks
Widget _buildPopularDownloadButton({
required Track track,
required ColorScheme colorScheme,
-4
View File
@@ -77,7 +77,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
});
}
} catch (_) {
// Ignore palette extraction errors
}
}
@@ -508,9 +507,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
Widget _buildTrackList(BuildContext context, ColorScheme colorScheme, List<DownloadHistoryItem> tracks) {
final discMap = _groupTracksByDisc(tracks);
// Single disc - use simple list
if (discMap.length <= 1) {
// Single disc - use simple list
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
@@ -525,7 +522,6 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
);
}
// Multiple discs - build list with separators
final discNumbers = discMap.keys.toList()..sort();
final List<Widget> children = [];
+2 -42
View File
@@ -75,7 +75,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 (previous != null &&
!next.hasContent &&
@@ -96,7 +95,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
if (searchProvider == null || searchProvider.isEmpty) return false;
// Check if the extension is enabled and has search capability
final extension = extState.extensions.where((e) => e.id == searchProvider && e.enabled).firstOrNull;
return extension != null;
}
@@ -130,10 +128,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
}
}
/// Execute live search with concurrency protection
/// Prevents race conditions in extensions by ensuring only one search runs at a time
Future<void> _executeLiveSearch(String query) async {
// If a search is already in progress, queue this one
if (_isLiveSearchInProgress) {
_pendingLiveSearchQuery = query;
return;
@@ -151,13 +146,10 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
final pending = _pendingLiveSearchQuery;
_pendingLiveSearchQuery = null;
// Execute pending query if it's different from what we just searched
// and still matches current text field content
if (pending != null &&
pending != query &&
mounted &&
_urlController.text.trim() == pending) {
// Small delay to let extension's state settle
await Future.delayed(const Duration(milliseconds: 100));
if (mounted && _urlController.text.trim() == pending) {
_executeLiveSearch(pending);
@@ -224,7 +216,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
ref.read(settingsProvider.notifier).setHasSearchedBefore();
}
/// Navigate to detail screen based on fetched content type
void _navigateToDetailIfNeeded() {
final trackState = ref.read(trackProvider);
@@ -356,7 +347,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
// ignore: use_build_context_synchronously
final l10n = context.l10n;
// Show quality picker if enabled in settings
if (settings.askQualityBeforeDownload) {
DownloadServicePicker.show(
this.context,
@@ -676,7 +666,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
);
}
/// Build recent access history section (shown when search focused)
Widget _buildRecentAccess(List<RecentAccessItem> items, ColorScheme colorScheme) {
final historyItems = ref.read(downloadHistoryProvider).items;
@@ -690,9 +679,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
albumGroups.putIfAbsent(albumKey, () => []).add(h);
}
// Convert to RecentAccessItem based on track count:
// - 1 track: show as individual Track
// - 2+ tracks: show as Album
final downloadItems = <RecentAccessItem>[];
for (final entry in albumGroups.entries) {
final tracks = entry.value;
@@ -703,7 +689,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
: mostRecent.artistName;
if (tracks.length == 1) {
// Single track - show as Track
downloadItems.add(RecentAccessItem(
id: mostRecent.spotifyId ?? mostRecent.id,
name: mostRecent.trackName,
@@ -714,7 +699,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
providerId: 'download',
));
} else {
// Multiple tracks - show as Album
downloadItems.add(RecentAccessItem(
id: '${mostRecent.albumName}|$artistForKey',
name: mostRecent.albumName,
@@ -727,10 +711,8 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
}
}
// Sort by most recent and take top 10
downloadItems.sort((a, b) => b.accessedAt.compareTo(a.accessedAt));
// Filter out hidden downloads (use ref.watch for reactivity)
final hiddenIds = ref.watch(recentAccessProvider.select((s) => s.hiddenDownloadIds));
final visibleDownloads = downloadItems
.where((item) => !hiddenIds.contains(item.id))
@@ -768,11 +750,9 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
if (uniqueItems.isNotEmpty)
TextButton(
onPressed: () {
// Hide ALL download items (not just visible ones)
for (final item in downloadItems) {
ref.read(recentAccessProvider.notifier).hideDownloadFromRecents(item.id);
}
// Clear non-download recent history
ref.read(recentAccessProvider.notifier).clearHistory();
},
child: Text(
@@ -784,7 +764,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
),
const SizedBox(height: 8),
if (uniqueItems.isEmpty && hasHiddenDownloads)
// Show "Show All" button when recents is empty but there are hidden downloads
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
@@ -897,10 +876,8 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
icon: Icon(Icons.close, size: 20, color: colorScheme.onSurfaceVariant),
onPressed: () {
if (item.providerId == 'download') {
// For download items, hide from recents without deleting the file
ref.read(recentAccessProvider.notifier).hideDownloadFromRecents(item.id);
} else {
// For other items, remove from recent history
ref.read(recentAccessProvider.notifier).removeItem(item);
}
},
@@ -936,7 +913,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
));
}
case RecentAccessType.album:
// Handle downloaded albums - navigate to DownloadedAlbumScreen
if (item.providerId == 'download') {
Navigator.push(context, MaterialPageRoute(
builder: (context) => DownloadedAlbumScreen(
@@ -1000,7 +976,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
);
}
/// Build error widget with special handling for rate limit (429)
Widget _buildErrorWidget(String error, ColorScheme colorScheme) {
final isRateLimit = error.contains('429') ||
error.toLowerCase().contains('rate limit') ||
@@ -1427,7 +1402,6 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
));
}
/// Get search hint based on selected provider
String _getSearchHint() {
final settings = ref.read(settingsProvider);
final searchProvider = settings.searchProvider;
@@ -1474,11 +1448,8 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
),
prefixIcon: _SearchProviderDropdown(
onProviderChanged: () {
// Reset search state when provider changes
_lastSearchQuery = null;
// Force rebuild to update hint text
setState(() {});
// Re-trigger search if there's text
final text = _urlController.text.trim();
if (text.isNotEmpty && text.length >= _minLiveSearchChars) {
_performSearch(text);
@@ -1514,9 +1485,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
);
}
/// Handle Enter key press - search or fetch URL
void _onSearchSubmitted() {
// Cancel any pending live search since user explicitly pressed enter
_liveSearchDebounce?.cancel();
_pendingLiveSearchQuery = null;
@@ -1549,13 +1518,11 @@ class _SearchProviderDropdown extends ConsumerWidget {
final extState = ref.watch(extensionProvider);
final colorScheme = Theme.of(context).colorScheme;
// Get current provider info
final currentProvider = settings.searchProvider;
final searchProviders = extState.extensions
.where((ext) => ext.enabled && ext.hasCustomSearch)
.toList();
// Find current provider extension
Extension? currentExt;
if (currentProvider != null && currentProvider.isNotEmpty) {
currentExt = searchProviders.where((e) => e.id == currentProvider).firstOrNull;
@@ -1567,12 +1534,10 @@ class _SearchProviderDropdown extends ConsumerWidget {
if (currentExt != null) {
iconPath = currentExt.iconPath;
if (currentExt.searchBehavior?.icon != null) {
// Use search behavior icon if available
displayIcon = _getIconFromName(currentExt.searchBehavior!.icon!);
}
}
// Don't show dropdown if no custom search providers available
if (searchProviders.isEmpty) {
return const Icon(Icons.search);
}
@@ -1608,15 +1573,13 @@ class _SearchProviderDropdown extends ConsumerWidget {
offset: const Offset(0, 40),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: (String providerId) {
// Empty string means default (Deezer/Spotify)
final provider = providerId.isEmpty ? null : providerId;
ref.read(settingsProvider.notifier).setSearchProvider(provider);
onProviderChanged?.call();
},
itemBuilder: (context) => [
// Default option (Deezer/Spotify based on metadata source)
PopupMenuItem<String>(
value: '', // Empty string = default provider
value: '',
child: Row(
children: [
Icon(
@@ -1716,7 +1679,6 @@ class _SearchProviderDropdown extends ConsumerWidget {
}
}
/// Separate Consumer widget for each track item - only rebuilds when this specific track's status changes
class _TrackItemWithStatus extends ConsumerWidget {
final Track track;
final int index;
@@ -2028,7 +1990,6 @@ class _CollectionItemWidget extends StatelessWidget {
}
}
/// Screen for viewing extension album with track fetching
class ExtensionAlbumScreen extends ConsumerStatefulWidget {
final String extensionId;
final String albumId;
@@ -2299,7 +2260,6 @@ class _ExtensionPlaylistScreenState extends ConsumerState<ExtensionPlaylistScree
}
}
/// Screen for viewing extension artist with album fetching
class ExtensionArtistScreen extends ConsumerStatefulWidget {
final String extensionId;
final String artistId;
-6
View File
@@ -120,7 +120,6 @@ class _MainShellState extends ConsumerState<MainShell> {
}
}
/// Handle back press with double-tap to exit
void _handleBackPress() {
final trackState = ref.read(trackProvider);
@@ -174,9 +173,6 @@ class _MainShellState extends ConsumerState<MainShell> {
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
// Determine if we can pop (for predictive back animation)
// canPop is true when we're at root with no content - enables predictive back gesture
// IMPORTANT: Never allow pop when keyboard is visible to prevent accidental navigation
final canPop = _currentIndex == 0 &&
!trackState.hasSearchText &&
!trackState.hasContent &&
@@ -250,8 +246,6 @@ class _MainShellState extends ConsumerState<MainShell> {
canPop: canPop,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) {
// System handled the pop - this means predictive back completed
// We need to handle double-tap to exit here
return;
}
-2
View File
@@ -11,7 +11,6 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/widgets/download_service_picker.dart';
/// Playlist detail screen with Material Expressive 3 design
class PlaylistScreen extends ConsumerStatefulWidget {
final String playlistName;
final String? coverUrl;
@@ -69,7 +68,6 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
});
}
} catch (_) {
// Ignore palette extraction errors
}
}
-10
View File
@@ -13,7 +13,6 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
/// Grouped album data for history display
class _GroupedAlbum {
final String albumName;
final String artistName;
@@ -108,7 +107,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
);
}
/// Enter selection mode with initial item
void _enterSelectionMode(String itemId) {
HapticFeedback.mediumImpact();
setState(() {
@@ -125,7 +123,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
});
}
/// Toggle item selection
void _toggleSelection(String itemId) {
setState(() {
if (_selectedIds.contains(itemId)) {
@@ -146,7 +143,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
});
}
/// Delete selected items
Future<void> _deleteSelected() async {
final count = _selectedIds.length;
final confirmed = await showDialog<bool>(
@@ -307,9 +303,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
);
}
/// Filter history items based on current filter mode
/// Album = track yang albumnya punya >1 track di history
/// Single = track yang albumnya cuma 1 track di history
List<DownloadHistoryItem> _filterHistoryItems(
List<DownloadHistoryItem> items,
String filterMode,
@@ -725,7 +718,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
);
}
/// Build content for each filter tab
Widget _buildFilterContent({
required BuildContext context,
required ColorScheme colorScheme,
@@ -931,7 +923,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
);
}
/// Build album grid item for grouped albums view
Widget _buildAlbumGridItem(
BuildContext context,
_GroupedAlbum album,
@@ -1745,7 +1736,6 @@ child: CachedNetworkImage(
}
}
/// Filter chip widget for history filtering
class _FilterChip extends StatelessWidget {
final String label;
final int count;
+3 -3
View File
@@ -15,7 +15,7 @@ class AboutPage extends StatelessWidget {
final topPadding = MediaQuery.of(context).padding.top;
return PopScope(
canPop: true, // Always allow back gesture
canPop: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
@@ -253,9 +253,9 @@ class _AppHeaderCard extends StatelessWidget {
color: colorScheme.primary,
shape: BoxShape.circle,
),
child: Image.asset(
child: Image.asset(
'assets/images/logo-transparant.png',
color: colorScheme.onPrimary, // Tint with onPrimary color
color: colorScheme.onPrimary,
fit: BoxFit.contain,
errorBuilder: (_, _, _) => ClipRRect(
borderRadius: BorderRadius.circular(24),
@@ -17,7 +17,7 @@ class AppearanceSettingsPage extends ConsumerWidget {
final topPadding = MediaQuery.of(context).padding.top;
return PopScope(
canPop: true, // Always allow back gesture
canPop: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
@@ -161,7 +161,7 @@ class _ThemePreviewCard extends StatelessWidget {
width: double.infinity,
decoration: BoxDecoration(
color: colorScheme
.surfaceContainerHighest, // Background similar to reference
.surfaceContainerHighest,
borderRadius: BorderRadius.circular(28),
),
clipBehavior: Clip.antiAlias,
@@ -203,7 +203,7 @@ class _ThemePreviewCard extends StatelessWidget {
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 12, // Reduced from 20 for performance
blurRadius: 12,
offset: const Offset(0, 8),
),
],
@@ -22,7 +22,7 @@ class DownloadSettingsPage extends ConsumerWidget {
final isBuiltInService = _builtInServices.contains(settings.defaultService);
return PopScope(
canPop: true, // Always allow back gesture
canPop: true,
child: Scaffold(
body: CustomScrollView(
slivers: [
+1 -1
View File
@@ -581,7 +581,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
switch (step) {
case 0: return _storagePermissionGranted;
case 1: return _selectedDirectory != null;
case 2: return false; // Spotify step never shows checkmark (optional)
case 2: return false;
}
}
return false;
+1 -1
View File
@@ -122,7 +122,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
),
onChanged: (value) {
ref.read(storeProvider.notifier).setSearchQuery(value);
setState(() {}); // Update suffix icon
setState(() {});
},
),
),
+2 -8
View File
@@ -13,8 +13,6 @@ import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
/// Screen to display detailed metadata for a downloaded track
/// Designed with Material Expressive 3 style
class TrackMetadataScreen extends ConsumerStatefulWidget {
final DownloadHistoryItem item;
@@ -101,7 +99,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
});
}
} catch (_) {
// Ignore palette extraction errors
}
}
@@ -263,7 +260,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
return Stack(
fit: StackFit.expand,
children: [
// Background with dominant color
AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
@@ -280,7 +276,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
),
),
// Cover image centered - fade out when collapsing
AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: showContent ? 1.0 : 0.0,
@@ -683,7 +678,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
),
),
),
// Show 320kbps for MP3, bit depth/sample rate for FLAC
if (fileExtension == 'MP3')
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
@@ -1057,8 +1051,8 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
ref.read(downloadHistoryProvider.notifier).removeFromHistory(item.id);
if (context.mounted) {
Navigator.pop(context); // Close dialog
Navigator.pop(context); // Go back to history
Navigator.pop(context);
Navigator.pop(context);
}
},
child: Text(context.l10n.dialogDelete, style: TextStyle(color: colorScheme.error)),