From f29177216dd3fd34522746f9b699d349af75715b Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 27 Mar 2026 19:28:42 +0700 Subject: [PATCH] refactor: enable strict analysis options and fix type safety across codebase Enable strict-casts, strict-inference, and strict-raw-types in analysis_options.yaml. Add custom_lint with riverpod_lint. Fix all resulting type warnings with explicit type parameters and safer casts. Also improves APK update checker to detect device ABIs for correct variant selection and fixes Deezer artist name parsing edge case. --- analysis_options.yaml | 20 +++ .../kotlin/com/zarz/spotiflac/MainActivity.kt | 1 + lib/providers/download_queue_provider.dart | 16 +- .../library_collections_provider.dart | 8 +- lib/providers/settings_provider.dart | 12 +- lib/providers/track_provider.dart | 14 +- lib/screens/artist_screen.dart | 16 +- lib/screens/downloaded_album_screen.dart | 4 +- lib/screens/home_tab.dart | 48 ++--- lib/screens/library_playlists_screen.dart | 4 +- lib/screens/library_tracks_folder_screen.dart | 18 +- lib/screens/local_album_screen.dart | 2 +- lib/screens/main_shell.dart | 6 +- lib/screens/playlist_screen.dart | 2 +- lib/screens/queue_tab.dart | 14 +- .../settings/appearance_settings_page.dart | 2 +- .../settings/download_settings_page.dart | 22 +-- .../settings/extension_detail_page.dart | 8 +- lib/screens/settings/extensions_page.dart | 17 +- .../settings/library_settings_page.dart | 2 +- lib/screens/settings/log_screen.dart | 2 +- .../settings/options_settings_page.dart | 4 +- lib/screens/settings/settings_tab.dart | 2 +- lib/screens/setup_screen.dart | 6 +- lib/screens/store_tab.dart | 4 +- lib/screens/track_metadata_screen.dart | 14 +- lib/screens/tutorial_screen.dart | 4 +- lib/services/app_state_database.dart | 4 +- lib/services/csv_import_service.dart | 2 +- lib/services/history_database.dart | 4 +- .../library_collections_database.dart | 16 +- lib/services/platform_bridge.dart | 4 +- lib/services/share_intent_service.dart | 2 +- lib/services/update_checker.dart | 170 +++++++++++++++--- lib/utils/clickable_metadata.dart | 6 +- lib/utils/logger.dart | 11 +- lib/widgets/audio_analysis_widget.dart | 4 +- lib/widgets/download_service_picker.dart | 2 +- .../track_collection_quick_actions.dart | 2 +- pubspec.lock | 68 ++++++- pubspec.yaml | 4 +- 41 files changed, 397 insertions(+), 174 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d290213..577a9667 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -9,6 +9,19 @@ # packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - build/** + - .dart_tool/** + - lib/**/*.g.dart + - lib/l10n/*.dart + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + plugins: + - custom_lint + linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` @@ -23,6 +36,13 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + avoid_dynamic_calls: true + cancel_subscriptions: true + close_sinks: true + +custom_lint: + rules: + - avoid_public_notifier_properties # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt index dbdd74ff..497da4cf 100644 --- a/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt +++ b/android/app/src/main/kotlin/com/zarz/spotiflac/MainActivity.kt @@ -304,6 +304,7 @@ class MainActivity: FlutterFragmentActivity() { ".mp3" -> "audio/mpeg" ".opus" -> "audio/ogg" ".flac" -> "audio/flac" + ".lrc" -> "application/octet-stream" else -> "application/octet-stream" } } diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 7ac7453a..e157bec6 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -510,7 +510,7 @@ class DownloadHistoryNotifier extends Notifier { } if ((c + 1) % _safRepairBatchSize == 0) { - await Future.delayed(const Duration(milliseconds: 16)); + await Future.delayed(const Duration(milliseconds: 16)); } } @@ -762,7 +762,7 @@ class DownloadHistoryNotifier extends Notifier { _historyLog.d('Added new history entry: ${mergedItem.trackName}'); } - _db.upsert(mergedItem.toJson()).catchError((e) { + _db.upsert(mergedItem.toJson()).catchError((Object e) { _historyLog.e('Failed to save to database: $e'); }); } @@ -771,7 +771,7 @@ class DownloadHistoryNotifier extends Notifier { state = state.copyWith( items: state.items.where((item) => item.id != id).toList(), ); - _db.deleteById(id).catchError((e) { + _db.deleteById(id).catchError((Object e) { _historyLog.e('Failed to delete from database: $e'); }); } @@ -780,7 +780,7 @@ class DownloadHistoryNotifier extends Notifier { state = state.copyWith( items: state.items.where((item) => item.spotifyId != spotifyId).toList(), ); - _db.deleteBySpotifyId(spotifyId).catchError((e) { + _db.deleteBySpotifyId(spotifyId).catchError((Object e) { _historyLog.e('Failed to delete from database: $e'); }); _historyLog.d('Removed item with spotifyId: $spotifyId'); @@ -1081,7 +1081,7 @@ class DownloadHistoryNotifier extends Notifier { void clearHistory() { state = DownloadHistoryState(); - _db.clearAll().catchError((e) { + _db.clearAll().catchError((Object e) { _historyLog.e('Failed to clear database: $e'); }); } @@ -3602,7 +3602,7 @@ class DownloadQueueNotifier extends Notifier { _log.d('Queue is paused, waiting for active downloads...'); await Future.any([ Future.wait(activeDownloads.values), - Future.delayed(_queueSchedulingInterval), + Future.delayed(_queueSchedulingInterval), ]); continue; } @@ -3647,10 +3647,10 @@ class DownloadQueueNotifier extends Notifier { if (activeDownloads.isNotEmpty) { await Future.any([ Future.any(activeDownloads.values), - Future.delayed(_queueSchedulingInterval), + Future.delayed(_queueSchedulingInterval), ]); } else { - await Future.delayed(_queueSchedulingInterval); + await Future.delayed(_queueSchedulingInterval); } } diff --git a/lib/providers/library_collections_provider.dart b/lib/providers/library_collections_provider.dart index 0fa89735..d6fece8e 100644 --- a/lib/providers/library_collections_provider.dart +++ b/lib/providers/library_collections_provider.dart @@ -118,7 +118,7 @@ class UserPlaylistCollection { createdAt: createdAt, updatedAt: updatedAt, tracks: tracksRaw - .whereType() + .whereType>() .map( (e) => CollectionTrackEntry.fromJson(Map.from(e)), ) @@ -233,19 +233,19 @@ class LibraryCollectionsState { return LibraryCollectionsState( wishlist: wishlistRaw - .whereType() + .whereType>() .map( (e) => CollectionTrackEntry.fromJson(Map.from(e)), ) .toList(growable: false), loved: lovedRaw - .whereType() + .whereType>() .map( (e) => CollectionTrackEntry.fromJson(Map.from(e)), ) .toList(growable: false), playlists: playlistsRaw - .whereType() + .whereType>() .map( (e) => UserPlaylistCollection.fromJson(Map.from(e)), diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 77feff76..832ecff5 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -34,7 +34,9 @@ class SettingsNotifier extends Notifier { final prefs = await _prefs; final json = prefs.getString(_settingsKey); if (json != null) { - state = AppSettings.fromJson(jsonDecode(json)); + state = AppSettings.fromJson( + Map.from(jsonDecode(json) as Map), + ); await _runMigrations(prefs); await _normalizeIosDownloadDirectoryIfNeeded(); @@ -52,7 +54,9 @@ class SettingsNotifier extends Notifier { void _syncLyricsSettingsToBackend() { if (!PlatformBridge.supportsCoreBackend) return; - PlatformBridge.setLyricsProviders(state.lyricsProviders).catchError((e) { + PlatformBridge.setLyricsProviders(state.lyricsProviders).catchError(( + Object e, + ) { _log.w('Failed to sync lyrics providers to backend: $e'); }); @@ -61,7 +65,7 @@ class SettingsNotifier extends Notifier { 'include_romanization_netease': state.lyricsIncludeRomanizationNetease, 'multi_person_word_by_word': state.lyricsMultiPersonWordByWord, 'musixmatch_language': state.musixmatchLanguage, - }).catchError((e) { + }).catchError((Object e) { _log.w('Failed to sync lyrics fetch options to backend: $e'); }); } @@ -73,7 +77,7 @@ class SettingsNotifier extends Notifier { PlatformBridge.setNetworkCompatibilityOptions( allowHttp: compatibilityMode, insecureTls: compatibilityMode, - ).catchError((e) { + ).catchError((Object e) { _log.w('Failed to sync network compatibility options to backend: $e'); }); } diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index e3cfaf09..20780c49 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -234,7 +234,7 @@ class TrackNotifier extends Notifier { } if (attempt < 3) { - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); } } @@ -275,10 +275,12 @@ class TrackNotifier extends Notifier { state = TrackState( tracks: tracks, isLoading: false, - albumId: result['album']?['id'] as String?, + albumId: + (result['album'] as Map?)?['id'] as String?, albumName: result['name'] as String? ?? - result['album']?['name'] as String?, + (result['album'] as Map?)?['name'] + as String?, playlistName: type == 'playlist' ? result['name'] as String? : null, @@ -825,8 +827,7 @@ class TrackNotifier extends Notifier { isLoading: true, hasSearchText: state.hasSearchText, isShowingRecentAccess: state.isShowingRecentAccess, - selectedSearchFilter: - state.selectedSearchFilter, + selectedSearchFilter: state.selectedSearchFilter, ); try { @@ -921,8 +922,7 @@ class TrackNotifier extends Notifier { final tracks = List.from(state.tracks); tracks[index] = updatedTrack; state = state.copyWith(tracks: tracks); - } catch (_) { - } + } catch (_) {} } void clear() { diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index 78549d98..1267b694 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -805,7 +805,7 @@ class _ArtistScreenState extends ConsumerState { ); final singleTracks = singles.fold(0, (sum, a) => sum + a.totalTracks); - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -939,7 +939,7 @@ class _ArtistScreenState extends ConsumerState { return; } - showDialog( + showDialog( context: context, barrierDismissible: false, builder: (ctx) => _FetchingProgressDialog( @@ -1121,6 +1121,10 @@ class _ArtistScreenState extends ConsumerState { Track _parseTrackFromDeezer(Map data, ArtistAlbum album) { int durationMs = 0; final durationValue = data['duration']; + final artistData = data['artist']; + final artistName = artistData is Map + ? (artistData['name'] as String? ?? widget.artistName) + : (artistData?.toString() ?? widget.artistName); if (durationValue is int) { durationMs = durationValue * 1000; // Deezer returns seconds } else if (durationValue is double) { @@ -1130,9 +1134,7 @@ class _ArtistScreenState extends ConsumerState { return Track( id: 'deezer:${data['id']}', name: (data['title'] ?? data['name'] ?? '').toString(), - artistName: - (data['artist']?['name'] ?? data['artist'] ?? widget.artistName) - .toString(), + artistName: artistName, albumName: album.name, albumArtist: widget.artistName, artistId: widget.artistId, @@ -1938,7 +1940,7 @@ class _ArtistScreenState extends ConsumerState { if (album.providerId != null && album.providerId!.isNotEmpty) { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionAlbumScreen( extensionId: album.providerId!, albumId: album.id, @@ -1950,7 +1952,7 @@ class _ArtistScreenState extends ConsumerState { } else { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => AlbumScreen( albumId: album.id, albumName: album.name, diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index ca41f0f1..3a35dee0 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -309,7 +309,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { if (!mounted) return; final result = await navigator.push( - slidePageRoute(page: TrackMetadataScreen(item: item)), + slidePageRoute(page: TrackMetadataScreen(item: item)), ); await DownloadedEmbeddedCoverResolver.scheduleRefreshForPath( item.filePath, @@ -932,7 +932,7 @@ class _DownloadedAlbumScreenState extends ConsumerState { ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, shape: const RoundedRectangleBorder( diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 504e7edc..5483bc7d 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -556,7 +556,7 @@ class _HomeTabState extends ConsumerState pending != query && mounted && _urlController.text.trim() == pending) { - await Future.delayed(const Duration(milliseconds: 100)); + await Future.delayed(const Duration(milliseconds: 100)); if (mounted && _urlController.text.trim() == pending) { _executeLiveSearch(pending); } @@ -681,7 +681,7 @@ class _HomeTabState extends ConsumerState final extensionId = trackState.searchExtensionId; Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => AlbumScreen( albumId: trackState.albumId!, albumName: trackState.albumName!, @@ -708,7 +708,7 @@ class _HomeTabState extends ConsumerState Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => PlaylistScreen( playlistName: trackState.playlistName!, coverUrl: trackState.coverUrl, @@ -729,7 +729,7 @@ class _HomeTabState extends ConsumerState final extensionId = trackState.searchExtensionId; Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ArtistScreen( artistId: trackState.artistId!, artistName: trackState.artistName!, @@ -798,7 +798,7 @@ class _HomeTabState extends ConsumerState if (progressDialogInitialized || !mounted) return; progressDialogInitialized = true; progressDialogVisible = true; - showDialog( + showDialog( context: this.context, useRootNavigator: false, barrierDismissible: false, @@ -1691,7 +1691,7 @@ class _HomeTabState extends ConsumerState case 'album': Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionAlbumScreen( extensionId: extensionId, albumId: item.id, @@ -1704,7 +1704,7 @@ class _HomeTabState extends ConsumerState case 'playlist': Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionPlaylistScreen( extensionId: extensionId, playlistId: item.id, @@ -1717,7 +1717,7 @@ class _HomeTabState extends ConsumerState case 'artist': Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionArtistScreen( extensionId: extensionId, artistId: item.id, @@ -1738,7 +1738,7 @@ class _HomeTabState extends ConsumerState void _showTrackBottomSheet(ExploreItem item) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surface, @@ -1884,7 +1884,7 @@ class _HomeTabState extends ConsumerState if (item.albumId != null && item.albumId!.isNotEmpty) { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionAlbumScreen( extensionId: item.providerId ?? 'spotify-web', albumId: item.albumId!, @@ -2148,7 +2148,7 @@ class _HomeTabState extends ConsumerState item.providerId != 'qobuz') { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionArtistScreen( extensionId: item.providerId!, artistId: item.id, @@ -2160,7 +2160,7 @@ class _HomeTabState extends ConsumerState } else { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ArtistScreen( artistId: item.id, artistName: item.name, @@ -2174,7 +2174,7 @@ class _HomeTabState extends ConsumerState if (item.providerId == 'download') { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => DownloadedAlbumScreen( albumName: item.name, artistName: item.subtitle ?? '', @@ -2190,7 +2190,7 @@ class _HomeTabState extends ConsumerState item.providerId != 'qobuz') { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionAlbumScreen( extensionId: item.providerId!, albumId: item.id, @@ -2202,7 +2202,7 @@ class _HomeTabState extends ConsumerState } else { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => AlbumScreen( albumId: item.id, albumName: item.name, @@ -2240,7 +2240,7 @@ class _HomeTabState extends ConsumerState item.providerId != 'qobuz') { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionPlaylistScreen( extensionId: item.providerId!, playlistId: item.id, @@ -2252,7 +2252,7 @@ class _HomeTabState extends ConsumerState } else { Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => PlaylistScreen( playlistName: item.name, coverUrl: item.imageUrl, @@ -2275,7 +2275,7 @@ class _HomeTabState extends ConsumerState ); if (!mounted) return; final result = await navigator.push( - slidePageRoute(page: TrackMetadataScreen(item: item)), + slidePageRoute(page: TrackMetadataScreen(item: item)), ); await DownloadedEmbeddedCoverResolver.scheduleRefreshForPath( item.filePath, @@ -2910,7 +2910,7 @@ class _HomeTabState extends ConsumerState Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ArtistScreen( artistId: artistId, artistName: artistName, @@ -2936,7 +2936,7 @@ class _HomeTabState extends ConsumerState // Keep the full ID with prefix (e.g., "deezer:123") for AlbumScreen to detect source Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => AlbumScreen( albumId: album.id, albumName: album.name, @@ -2963,7 +2963,7 @@ class _HomeTabState extends ConsumerState // Keep the full ID with prefix (e.g., "deezer:123") for PlaylistScreen to detect source Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => PlaylistScreen( playlistName: playlist.name, coverUrl: playlist.imageUrl, @@ -2999,7 +2999,7 @@ class _HomeTabState extends ConsumerState Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionAlbumScreen( extensionId: extensionId, albumId: albumItem.id, @@ -3035,7 +3035,7 @@ class _HomeTabState extends ConsumerState Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionPlaylistScreen( extensionId: extensionId, playlistId: playlistItem.id, @@ -3070,7 +3070,7 @@ class _HomeTabState extends ConsumerState Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionArtistScreen( extensionId: extensionId, artistId: artistItem.id, diff --git a/lib/screens/library_playlists_screen.dart b/lib/screens/library_playlists_screen.dart index d3258424..388a8327 100644 --- a/lib/screens/library_playlists_screen.dart +++ b/lib/screens/library_playlists_screen.dart @@ -119,7 +119,7 @@ class LibraryPlaylistsScreen extends ConsumerWidget { ), onTap: () { Navigator.of(context).push( - MaterialPageRoute( + MaterialPageRoute( builder: (_) => LibraryTracksFolderScreen( mode: LibraryTracksFolderMode.playlist, playlistId: playlist.id, @@ -149,7 +149,7 @@ class LibraryPlaylistsScreen extends ConsumerWidget { ) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, diff --git a/lib/screens/library_tracks_folder_screen.dart b/lib/screens/library_tracks_folder_screen.dart index 17444c7b..a4acc687 100644 --- a/lib/screens/library_tracks_folder_screen.dart +++ b/lib/screens/library_tracks_folder_screen.dart @@ -847,7 +847,7 @@ class _LibraryTracksFolderScreenState void _confirmDownloadAll(List tracks) { if (tracks.isEmpty) return; - showDialog( + showDialog( context: context, builder: (dialogContext) { final colorScheme = Theme.of(dialogContext).colorScheme; @@ -980,7 +980,7 @@ class _LibraryTracksFolderScreenState void _showCoverOptionsSheet(BuildContext context, bool hasCustomCover) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1338,7 +1338,7 @@ class _CollectionTrackTile extends ConsumerWidget { final showAddToPlaylist = mode != LibraryTracksFolderMode.wishlist || isDownloaded; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1523,9 +1523,9 @@ class _CollectionTrackTile extends ConsumerWidget { ); if (historyItem != null) { - await Navigator.of( - context, - ).push(slidePageRoute(page: TrackMetadataScreen(item: historyItem))); + await Navigator.of(context).push( + slidePageRoute(page: TrackMetadataScreen(item: historyItem)), + ); return; } @@ -1540,9 +1540,9 @@ class _CollectionTrackTile extends ConsumerWidget { localItem ??= localState.findByTrackAndArtist(track.name, track.artistName); if (localItem != null) { - await Navigator.of( - context, - ).push(slidePageRoute(page: TrackMetadataScreen(localItem: localItem))); + await Navigator.of(context).push( + slidePageRoute(page: TrackMetadataScreen(localItem: localItem)), + ); return; } diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index ed0c6e36..09ff33df 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -1180,7 +1180,7 @@ class _LocalAlbumScreenState extends ConsumerState { ? '320k' : (selectedFormat == 'Opus' ? '128k' : '320k'); - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, shape: const RoundedRectangleBorder( diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 49bbe89c..c6a8b768 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -79,7 +79,7 @@ class _MainShellState extends ConsumerState _log.d('Received shared URL from stream: $url'); _handleSharedUrl(url); }, - onError: (error) { + onError: (Object error) { _log.e('Share stream error: $error'); }, cancelOnError: false, @@ -92,7 +92,7 @@ class _MainShellState extends ConsumerState if (!extState.isInitialized) { _log.d('Waiting for extensions to initialize before handling URL...'); for (int i = 0; i < 50; i++) { - await Future.delayed(const Duration(milliseconds: 100)); + await Future.delayed(const Duration(milliseconds: 100)); if (!mounted) return; if (ref.read(extensionProvider).isInitialized) { _log.d('Extensions initialized, proceeding with URL handling'); @@ -177,7 +177,7 @@ class _MainShellState extends ConsumerState final colorScheme = Theme.of(context).colorScheme; - showDialog( + showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( diff --git a/lib/screens/playlist_screen.dart b/lib/screens/playlist_screen.dart index 1ac74e77..364324a0 100644 --- a/lib/screens/playlist_screen.dart +++ b/lib/screens/playlist_screen.dart @@ -578,7 +578,7 @@ class _PlaylistScreenState extends ConsumerState { void _confirmDownloadAll(BuildContext context) { if (_tracks.isEmpty) return; - showDialog( + showDialog( context: context, builder: (dialogContext) { final colorScheme = Theme.of(dialogContext).colorScheme; diff --git a/lib/screens/queue_tab.dart b/lib/screens/queue_tab.dart index 4b2a5cb0..5e3f9fcb 100644 --- a/lib/screens/queue_tab.dart +++ b/lib/screens/queue_tab.dart @@ -656,8 +656,8 @@ final _queueFilteredAlbumsProvider = }); Map> _filterHistoryInIsolate(Map payload) { - final entries = (payload['entries'] as List).cast(); - final albumCounts = (payload['albumCounts'] as Map).cast(); + final entries = (payload['entries'] as List).cast>(); + final albumCounts = Map.from(payload['albumCounts'] as Map); final query = (payload['query'] as String?) ?? ''; final hasQuery = query.isNotEmpty; @@ -1968,7 +1968,7 @@ class _QueueTabState extends ConsumerState { String? tempFormat = _filterFormat; String tempSortMode = _sortMode; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, isScrollControlled: true, @@ -2280,7 +2280,7 @@ class _QueueTabState extends ConsumerState { final beforeModTime = await _readFileModTimeMillis(historyItem.filePath); if (!mounted) return; final result = await navigator.push( - slidePageRoute(page: TrackMetadataScreen(item: historyItem)), + slidePageRoute(page: TrackMetadataScreen(item: historyItem)), ); _searchFocusNode.unfocus(); if (result == true) { @@ -2306,7 +2306,7 @@ class _QueueTabState extends ConsumerState { final beforeModTime = await _readFileModTimeMillis(item.filePath); if (!mounted) return; final result = await navigator.push( - slidePageRoute(page: TrackMetadataScreen(item: item)), + slidePageRoute(page: TrackMetadataScreen(item: item)), ); _searchFocusNode.unfocus(); if (result == true) { @@ -2327,7 +2327,7 @@ class _QueueTabState extends ConsumerState { _searchFocusNode.unfocus(); Navigator.push( context, - slidePageRoute(page: TrackMetadataScreen(localItem: item)), + slidePageRoute(page: TrackMetadataScreen(localItem: item)), ).then((_) => _searchFocusNode.unfocus()); } @@ -4711,7 +4711,7 @@ class _QueueTabState extends ConsumerState { _hideSelectionOverlay(); _hidePlaylistSelectionOverlay(); - await showModalBottomSheet( + await showModalBottomSheet( context: context, useRootNavigator: true, shape: const RoundedRectangleBorder( diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 4884328a..75d93e6e 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -770,7 +770,7 @@ class _LanguageSelector extends StatelessWidget { void _showLanguagePicker(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surface, diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 6b60e332..39b116e6 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -510,7 +510,7 @@ class _DownloadSettingsPageState extends ConsumerState { ), onTap: () => Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (_) => const LyricsProviderPriorityPage(), ), ), @@ -853,7 +853,7 @@ class _DownloadSettingsPageState extends ConsumerState { WidgetRef ref, String current, ) { - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, builder: (context) => SafeArea( @@ -1002,7 +1002,7 @@ class _DownloadSettingsPageState extends ConsumerState { ); } - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, isScrollControlled: true, @@ -1220,7 +1220,7 @@ class _DownloadSettingsPageState extends ConsumerState { final settings = ref.read(settingsProvider); final isSafMode = settings.storageMode == 'saf' && settings.downloadTreeUri.isNotEmpty; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1298,7 +1298,7 @@ class _DownloadSettingsPageState extends ConsumerState { void _showIOSDirectoryOptions(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1493,7 +1493,7 @@ class _DownloadSettingsPageState extends ConsumerState { String current, ) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1598,7 +1598,7 @@ class _DownloadSettingsPageState extends ConsumerState { String current, ) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1685,7 +1685,7 @@ class _DownloadSettingsPageState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final controller = TextEditingController(text: currentLanguage); - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1771,7 +1771,7 @@ class _DownloadSettingsPageState extends ConsumerState { String current, ) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1843,7 +1843,7 @@ class _DownloadSettingsPageState extends ConsumerState { ) { final colorScheme = Theme.of(context).colorScheme; final normalizedCurrent = current.trim().toUpperCase(); - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -1911,7 +1911,7 @@ class _DownloadSettingsPageState extends ConsumerState { String current, ) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index f2f0b950..48d23d0d 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -832,9 +832,9 @@ class _SettingItemState extends State<_SettingItem> { } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(context.l10n.snackbarError(e.toString())))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.snackbarError(e.toString()))), + ); } } finally { if (mounted) { @@ -849,7 +849,7 @@ class _SettingItemState extends State<_SettingItem> { ); final colorScheme = Theme.of(context).colorScheme; - showDialog( + showDialog( context: context, builder: (context) => AlertDialog( title: Text(widget.setting.label), diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 7e5aa26e..9fec23ee 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; import 'package:spotiflac_android/providers/explore_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; @@ -212,7 +213,7 @@ class _ExtensionsPageState extends ConsumerState { showDivider: index < extState.extensions.length - 1, onTap: () => Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (_) => ExtensionDetailPage(extensionId: ext.id), ), @@ -469,7 +470,9 @@ class _DownloadPriorityItem extends ConsumerWidget { onTap: hasDownloadExtensions ? () => Navigator.push( context, - MaterialPageRoute(builder: (_) => const ProviderPriorityPage()), + MaterialPageRoute( + builder: (_) => const ProviderPriorityPage(), + ), ) : null, child: Padding( @@ -534,7 +537,7 @@ class _MetadataPriorityItem extends ConsumerWidget { onTap: hasMetadataExtensions ? () => Navigator.push( context, - MaterialPageRoute( + MaterialPageRoute( builder: (_) => const MetadataProviderPriorityPage(), ), ) @@ -678,12 +681,12 @@ class _SearchProviderSelector extends ConsumerWidget { void _showSearchProviderPicker( BuildContext context, WidgetRef ref, - dynamic settings, + AppSettings settings, List searchProviders, ) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, @@ -859,12 +862,12 @@ class _HomeFeedProviderSelector extends ConsumerWidget { void _showHomeFeedProviderPicker( BuildContext context, WidgetRef ref, - dynamic settings, + AppSettings settings, List homeFeedProviders, ) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index eae12522..83014ec9 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -255,7 +255,7 @@ class _LibrarySettingsPageState extends ConsumerState { void _showAutoScanPicker(BuildContext context, String current) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index b34e172a..06c0173b 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -92,7 +92,7 @@ class _LogScreenState extends State { } void _clearLogs() { - showDialog( + showDialog( context: context, builder: (context) => AlertDialog( title: Text(context.l10n.logClearLogsTitle), diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index 1025f1b1..30fbb798 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -241,7 +241,7 @@ class OptionsSettingsPage extends ConsumerWidget { WidgetRef ref, ColorScheme colorScheme, ) { - showDialog( + showDialog( context: context, builder: (context) => AlertDialog( title: Text(context.l10n.dialogClearHistoryTitle), @@ -273,7 +273,7 @@ class OptionsSettingsPage extends ConsumerWidget { BuildContext context, WidgetRef ref, ) async { - showDialog( + showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index 7e4d03f2..f08d967a 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -151,6 +151,6 @@ class SettingsTab extends ConsumerWidget { void _navigateTo(BuildContext context, Widget page) { FocusManager.instance.primaryFocus?.unfocus(); - Navigator.of(context).push(slidePageRoute(page: page)); + Navigator.of(context).push(slidePageRoute(page: page)); } } diff --git a/lib/screens/setup_screen.dart b/lib/screens/setup_screen.dart index bc72a18d..9a9ddd9f 100644 --- a/lib/screens/setup_screen.dart +++ b/lib/screens/setup_screen.dart @@ -124,7 +124,7 @@ class _SetupScreenState extends ConsumerState { final shouldOpen = await _showAndroid11StorageDialog(); if (shouldOpen == true) { await Permission.manageExternalStorage.request(); - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); manageStatus = await Permission.manageExternalStorage.status; } } @@ -203,7 +203,7 @@ class _SetupScreenState extends ConsumerState { } Future _showPermissionDeniedDialog(String permissionType) async { - await showDialog( + await showDialog( context: context, builder: (context) => AlertDialog( title: Text(context.l10n.setupPermissionRequired(permissionType)), @@ -286,7 +286,7 @@ class _SetupScreenState extends ConsumerState { Future _showIOSDirectoryOptions() async { final colorScheme = Theme.of(context).colorScheme; - await showModalBottomSheet( + await showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index 2d149f87..8270bec3 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -416,7 +416,7 @@ class _StoreTabState extends ConsumerState { void _showChangeRepoDialog(String currentUrl) { final changeUrlController = TextEditingController(text: currentUrl); - showDialog( + showDialog( context: context, builder: (context) => AlertDialog( title: Text(context.l10n.storeRepoDialogTitle), @@ -583,7 +583,7 @@ class _StoreTabState extends ConsumerState { void _showExtensionDetails(StoreExtension ext) { Navigator.of(context).push( - MaterialPageRoute( + MaterialPageRoute( builder: (context) => ExtensionDetailsScreen(extension: ext), ), ); diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index 254d2f06..0e3538a8 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -2135,7 +2135,7 @@ class _TrackMetadataScreenState extends ConsumerState { treeUri: treeUri, relativeDir: relativeDir, fileName: '$baseName.lrc', - mimeType: 'text/plain', + mimeType: 'application/octet-stream', srcPath: tempOutput, ); try { @@ -2533,7 +2533,7 @@ class _TrackMetadataScreenState extends ConsumerState { WidgetRef ref, ColorScheme colorScheme, ) { - showModalBottomSheet( + showModalBottomSheet( context: screenContext, useRootNavigator: true, shape: const RoundedRectangleBorder( @@ -2824,7 +2824,7 @@ class _TrackMetadataScreenState extends ConsumerState { bool isLosslessTarget = selectedFormat == 'ALAC' || selectedFormat == 'FLAC'; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, shape: const RoundedRectangleBorder( @@ -3023,7 +3023,7 @@ class _TrackMetadataScreenState extends ConsumerState { if (!mounted) return; - showModalBottomSheet( + showModalBottomSheet( context: this.context, useRootNavigator: true, isScrollControlled: true, @@ -3186,7 +3186,7 @@ class _TrackMetadataScreenState extends ConsumerState { required String date, required List tracks, }) { - showDialog( + showDialog( context: context, builder: (dialogContext) { return AlertDialog( @@ -3442,7 +3442,7 @@ class _TrackMetadataScreenState extends ConsumerState { final isLossless = targetFormat.toUpperCase() == 'ALAC' || targetFormat.toUpperCase() == 'FLAC'; - showDialog( + showDialog( context: context, builder: (dialogContext) { return AlertDialog( @@ -3792,7 +3792,7 @@ class _TrackMetadataScreenState extends ConsumerState { WidgetRef ref, ColorScheme colorScheme, ) { - showDialog( + showDialog( context: screenContext, useRootNavigator: true, builder: (dialogContext) => AlertDialog( diff --git a/lib/screens/tutorial_screen.dart b/lib/screens/tutorial_screen.dart index a944f6a8..a8d35705 100644 --- a/lib/screens/tutorial_screen.dart +++ b/lib/screens/tutorial_screen.dart @@ -527,7 +527,7 @@ class _InteractiveDownloadExampleState for (int i = 0; i <= 100; i += 5) { if (!mounted) return; - await Future.delayed(const Duration(milliseconds: 50)); + await Future.delayed(const Duration(milliseconds: 50)); setState(() => _progress = i / 100); } @@ -536,7 +536,7 @@ class _InteractiveDownloadExampleState _isCompleted = true; }); - await Future.delayed(const Duration(seconds: 2)); + await Future.delayed(const Duration(seconds: 2)); if (mounted) { setState(() { _isCompleted = false; diff --git a/lib/services/app_state_database.dart b/lib/services/app_state_database.dart index 92ca49f8..471c251c 100644 --- a/lib/services/app_state_database.dart +++ b/lib/services/app_state_database.dart @@ -119,7 +119,7 @@ class AppStateDatabase { final db = await database; await db.transaction((txn) async { final batch = txn.batch(); - for (final entry in decoded.whereType()) { + for (final entry in decoded.whereType>()) { final map = Map.from(entry); final id = map['id'] as String?; if (id == null || id.isEmpty) continue; @@ -179,7 +179,7 @@ class AppStateDatabase { final decoded = jsonDecode(rawRecent); if (decoded is List) { final batch = txn.batch(); - for (final entry in decoded.whereType()) { + for (final entry in decoded.whereType>()) { final map = Map.from(entry); final type = map['type'] as String?; final id = map['id'] as String?; diff --git a/lib/services/csv_import_service.dart b/lib/services/csv_import_service.dart index 551acd17..45d59d18 100644 --- a/lib/services/csv_import_service.dart +++ b/lib/services/csv_import_service.dart @@ -124,7 +124,7 @@ class CsvImportService { ); if (i < tracks.length - 1) { - await Future.delayed(const Duration(milliseconds: 100)); + await Future.delayed(const Duration(milliseconds: 100)); } continue; } diff --git a/lib/services/history_database.dart b/lib/services/history_database.dart index 59b7f883..4c851305 100644 --- a/lib/services/history_database.dart +++ b/lib/services/history_database.dart @@ -224,7 +224,7 @@ class HistoryDatabase { } try { - final List jsonList = jsonDecode(jsonStr); + final jsonList = List.from(jsonDecode(jsonStr) as List); _log.i( 'Migrating ${jsonList.length} items from SharedPreferences to SQLite', ); @@ -233,7 +233,7 @@ class HistoryDatabase { final batch = db.batch(); for (final json in jsonList) { - final map = json as Map; + final map = Map.from(json as Map); batch.insert( 'history', _jsonToDbRow(map), diff --git a/lib/services/library_collections_database.dart b/lib/services/library_collections_database.dart index 65577cce..db813114 100644 --- a/lib/services/library_collections_database.dart +++ b/lib/services/library_collections_database.dart @@ -155,11 +155,11 @@ class LibraryCollectionsDatabase { final db = await database; await db.transaction((txn) async { - for (final entry in wishlistRaw.whereType()) { + for (final entry in wishlistRaw.whereType>()) { final map = Map.from(entry); final trackKey = map['key'] as String?; final track = map['track']; - if (trackKey == null || track is! Map) continue; + if (trackKey == null || track is! Map) continue; final addedAt = (map['addedAt'] as String?) ?? nowIso; await txn.insert(_tableWishlist, { 'track_key': trackKey, @@ -168,11 +168,11 @@ class LibraryCollectionsDatabase { }, conflictAlgorithm: ConflictAlgorithm.replace); } - for (final entry in lovedRaw.whereType()) { + for (final entry in lovedRaw.whereType>()) { final map = Map.from(entry); final trackKey = map['key'] as String?; final track = map['track']; - if (trackKey == null || track is! Map) continue; + if (trackKey == null || track is! Map) continue; final addedAt = (map['addedAt'] as String?) ?? nowIso; await txn.insert(_tableLoved, { 'track_key': trackKey, @@ -181,7 +181,8 @@ class LibraryCollectionsDatabase { }, conflictAlgorithm: ConflictAlgorithm.replace); } - for (final playlistEntry in playlistsRaw.whereType()) { + for (final playlistEntry + in playlistsRaw.whereType>()) { final playlist = Map.from(playlistEntry); final playlistId = playlist['id'] as String?; if (playlistId == null || playlistId.isEmpty) continue; @@ -197,11 +198,12 @@ class LibraryCollectionsDatabase { }, conflictAlgorithm: ConflictAlgorithm.replace); final tracksRaw = (playlist['tracks'] as List?) ?? const []; - for (final trackEntry in tracksRaw.whereType()) { + for (final trackEntry + in tracksRaw.whereType>()) { final trackMap = Map.from(trackEntry); final trackKey = trackMap['key'] as String?; final track = trackMap['track']; - if (trackKey == null || track is! Map) continue; + if (trackKey == null || track is! Map) continue; final addedAt = (trackMap['addedAt'] as String?) ?? nowIso; await txn.insert(_tablePlaylistTracks, { 'playlist_id': playlistId, diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 401a3afc..0dfa2bcf 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -67,8 +67,8 @@ class PlatformBridge { if (response['success'] == true) { final service = response['service'] ?? payload.service; final filePath = response['file_path'] ?? ''; - final bitDepth = response['actual_bit_depth']; - final sampleRate = response['actual_sample_rate']; + final bitDepth = response['actual_bit_depth'] as num?; + final sampleRate = response['actual_sample_rate'] as num?; final qualityStr = bitDepth != null && sampleRate != null ? ' ($bitDepth-bit/${(sampleRate / 1000).toStringAsFixed(1)}kHz)' : ''; diff --git a/lib/services/share_intent_service.dart b/lib/services/share_intent_service.dart index 1040c738..c5f174d0 100644 --- a/lib/services/share_intent_service.dart +++ b/lib/services/share_intent_service.dart @@ -65,7 +65,7 @@ class ShareIntentService { _mediaSubscription = ReceiveSharingIntent.instance.getMediaStream().listen( _handleSharedMedia, - onError: (err) => _log.e('Error: $err'), + onError: (Object err) => _log.e('Error: $err'), ); final initialMedia = await ReceiveSharingIntent.instance.getInitialMedia(); diff --git a/lib/services/update_checker.dart b/lib/services/update_checker.dart index c24b47f0..7ea63307 100644 --- a/lib/services/update_checker.dart +++ b/lib/services/update_checker.dart @@ -1,11 +1,26 @@ import 'dart:convert'; import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:http/http.dart' as http; import 'package:spotiflac_android/constants/app_info.dart'; import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('UpdateChecker'); +enum _ApkVariant { arm64, arm32, universal } + +class _ApkAsset { + final String name; + final String url; + final _ApkVariant variant; + + const _ApkAsset({ + required this.name, + required this.url, + required this.variant, + }); +} + class UpdateInfo { final String version; final String changelog; @@ -94,32 +109,15 @@ class UpdateChecker { DateTime.tryParse(releaseData['published_at'] as String? ?? '') ?? DateTime.now(); - String? arm64Url; - String? universalUrl; - - final assets = releaseData['assets'] as List? ?? []; - for (final asset in assets) { - final name = (asset['name'] as String? ?? '').toLowerCase(); - if (name.endsWith('.apk')) { - final downloadUrl = asset['browser_download_url'] as String?; - final uri = downloadUrl != null ? Uri.tryParse(downloadUrl) : null; - if (uri == null || uri.scheme != 'https') { - _log.w('Skipping non-HTTPS APK URL: $downloadUrl'); - continue; - } - if (name.contains('arm64') || name.contains('v8a')) { - arm64Url = downloadUrl; - } else if (name.contains('universal')) { - universalUrl = downloadUrl; - } - } - } - - // Only arm64 is supported; fall back to universal if available - final apkUrl = arm64Url ?? universalUrl; + final assets = _collectApkAssets( + releaseData['assets'] as List? ?? const [], + ); + final selectedAsset = await _selectApkForCurrentDevice(assets); + final apkUrl = selectedAsset?.url; _log.i( - 'Update available: $latestVersion (prerelease: $isPrerelease), APK URL: $apkUrl', + 'Update available: $latestVersion (prerelease: $isPrerelease), ' + 'APK asset: ${selectedAsset?.name ?? 'none'}, APK URL: $apkUrl', ); return UpdateInfo( @@ -169,4 +167,128 @@ class UpdateChecker { } static String get currentVersion => AppInfo.version; + + static List<_ApkAsset> _collectApkAssets(List assets) { + final apkAssets = <_ApkAsset>[]; + + for (final asset in assets.whereType>()) { + final assetMap = Map.from(asset); + final name = (assetMap['name'] as String? ?? '').trim(); + final normalizedName = name.toLowerCase(); + if (!normalizedName.endsWith('.apk')) { + continue; + } + + final downloadUrl = assetMap['browser_download_url'] as String?; + final uri = downloadUrl != null ? Uri.tryParse(downloadUrl) : null; + if (uri == null || uri.scheme != 'https') { + _log.w('Skipping non-HTTPS APK URL: $downloadUrl'); + continue; + } + + final variant = _apkVariantFromName(normalizedName); + if (variant == null) { + _log.w('Skipping APK with unknown variant: $name'); + continue; + } + + apkAssets.add( + _ApkAsset(name: name, url: uri.toString(), variant: variant), + ); + } + + return apkAssets; + } + + static _ApkVariant? _apkVariantFromName(String name) { + if (name.contains('universal')) { + return _ApkVariant.universal; + } + if (name.contains('arm64') || name.contains('arm64-v8a')) { + return _ApkVariant.arm64; + } + if (name.contains('arm32') || + name.contains('armeabi') || + name.contains('armv7') || + name.contains('v7a')) { + return _ApkVariant.arm32; + } + return null; + } + + static Future<_ApkAsset?> _selectApkForCurrentDevice( + List<_ApkAsset> assets, + ) async { + if (assets.isEmpty) { + return null; + } + + _ApkAsset? arm64Asset; + _ApkAsset? arm32Asset; + _ApkAsset? universalAsset; + for (final asset in assets) { + switch (asset.variant) { + case _ApkVariant.arm64: + arm64Asset ??= asset; + break; + case _ApkVariant.arm32: + arm32Asset ??= asset; + break; + case _ApkVariant.universal: + universalAsset ??= asset; + break; + } + } + + final supportedAbis = await _getSupportedAndroidAbis(); + final hasArm64 = supportedAbis.any(_isArm64Abi); + final hasArm32 = supportedAbis.any(_isArm32Abi); + + if (hasArm64) { + return arm64Asset ?? universalAsset ?? arm32Asset; + } + if (hasArm32) { + return arm32Asset ?? universalAsset; + } + + if (universalAsset != null) { + _log.w( + 'Could not match APK asset to supported ABIs ${supportedAbis.join(', ')}; ' + 'falling back to universal APK.', + ); + return universalAsset; + } + + _log.w( + 'Could not match APK asset to supported ABIs ${supportedAbis.join(', ')}; ' + 'no universal APK available.', + ); + return null; + } + + static Future> _getSupportedAndroidAbis() async { + if (!Platform.isAndroid) { + return const []; + } + + try { + final androidInfo = await DeviceInfoPlugin().androidInfo; + final supportedAbis = androidInfo.supportedAbis + .map((abi) => abi.toLowerCase()) + .where((abi) => abi.isNotEmpty) + .toSet() + .toList(); + _log.i('Detected supported Android ABIs: ${supportedAbis.join(', ')}'); + return supportedAbis; + } catch (e) { + _log.w('Failed to detect supported Android ABIs: $e'); + return const []; + } + } + + static bool _isArm64Abi(String abi) => + abi.contains('arm64') || abi.contains('aarch64'); + + static bool _isArm32Abi(String abi) => + abi.contains('armeabi') || abi.contains('armv7') || abi.contains('arm'); } diff --git a/lib/utils/clickable_metadata.dart b/lib/utils/clickable_metadata.dart index 91e40501..b39f1bd1 100644 --- a/lib/utils/clickable_metadata.dart +++ b/lib/utils/clickable_metadata.dart @@ -252,7 +252,7 @@ void _pushViaPreferredNavigator(BuildContext context, WidgetBuilder builder) { identical(currentNavigator, rootNavigator) && activeTabNavigator != null; if (!shouldRouteToTabNavigator) { - currentNavigator.push(MaterialPageRoute(builder: builder)); + currentNavigator.push(MaterialPageRoute(builder: builder)); return; } @@ -264,12 +264,12 @@ void _pushViaPreferredNavigator(BuildContext context, WidgetBuilder builder) { currentNavigator.pop(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!activeTabNavigator.mounted) return; - activeTabNavigator.push(MaterialPageRoute(builder: builder)); + activeTabNavigator.push(MaterialPageRoute(builder: builder)); }); return; } - activeTabNavigator.push(MaterialPageRoute(builder: builder)); + activeTabNavigator.push(MaterialPageRoute(builder: builder)); } void _showLoadingSnackBar(BuildContext context, String message) { diff --git a/lib/utils/logger.dart b/lib/utils/logger.dart index 3a32375c..2601c70c 100644 --- a/lib/utils/logger.dart +++ b/lib/utils/logger.dart @@ -179,15 +179,16 @@ class LogBuffer extends ChangeNotifier { final nextIndex = result['next_index'] as int? ?? _lastGoLogIndex; final keepNonErrorLogs = _loggingEnabled; - for (final log in logs) { - final level = log['level'] as String? ?? 'INFO'; + for (final log in logs.whereType>()) { + final logMap = Map.from(log); + final level = logMap['level'] as String? ?? 'INFO'; if (!keepNonErrorLogs && level != 'ERROR' && level != 'FATAL') { continue; } - final timestamp = log['timestamp'] as String? ?? ''; - final tag = log['tag'] as String? ?? 'Go'; - final message = log['message'] as String? ?? ''; + final timestamp = logMap['timestamp'] as String? ?? ''; + final tag = logMap['tag'] as String? ?? 'Go'; + final message = logMap['message'] as String? ?? ''; DateTime parsedTime = DateTime.now(); if (timestamp.isNotEmpty) { diff --git a/lib/widgets/audio_analysis_widget.dart b/lib/widgets/audio_analysis_widget.dart index 41a9ba1b..d88c55b0 100644 --- a/lib/widgets/audio_analysis_widget.dart +++ b/lib/widgets/audio_analysis_widget.dart @@ -239,7 +239,9 @@ class _AudioAnalysisCardState extends State { final file = File('${dir.path}/$key.json'); if (!await file.exists()) return null; - final json = jsonDecode(await file.readAsString()); + final json = Map.from( + jsonDecode(await file.readAsString()) as Map, + ); final cachedSize = json['fileSize'] as int; if (!filePath.startsWith('content://')) { diff --git a/lib/widgets/download_service_picker.dart b/lib/widgets/download_service_picker.dart index 51f4aa37..dfff48cb 100644 --- a/lib/widgets/download_service_picker.dart +++ b/lib/widgets/download_service_picker.dart @@ -110,7 +110,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget { }) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, backgroundColor: colorScheme.surfaceContainerHigh, diff --git a/lib/widgets/track_collection_quick_actions.dart b/lib/widgets/track_collection_quick_actions.dart index b8f75708..5fe9d3d4 100644 --- a/lib/widgets/track_collection_quick_actions.dart +++ b/lib/widgets/track_collection_quick_actions.dart @@ -19,7 +19,7 @@ class TrackCollectionQuickActions extends ConsumerWidget { Track track, ) { final colorScheme = Theme.of(context).colorScheme; - showModalBottomSheet( + showModalBottomSheet( context: context, useRootNavigator: true, isScrollControlled: true, diff --git a/pubspec.lock b/pubspec.lock index 5f81e3da..cc6cea93 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,14 +9,22 @@ packages: url: "https://pub.dev" source: hosted version: "91.0.0" + analysis_server_plugin: + dependency: transitive + description: + name: analysis_server_plugin + sha256: "26844e7f977087567135d62532b67d5639fe206c5194c3f410ba75e1a04a2747" + url: "https://pub.dev" + source: hosted + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 + sha256: a40a0cee526a7e1f387c6847bd8a5ccbf510a75952ef8a28338e989558072cb0 url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "8.4.0" analyzer_buffer: dependency: transitive description: @@ -25,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.11" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "08cfefa90b4f4dd3b447bda831cecf644029f9f8e22820f6ee310213ebe2dd53" + url: "https://pub.dev" + source: hosted + version: "0.13.10" archive: dependency: transitive description: @@ -145,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" cli_config: dependency: transitive description: @@ -241,6 +265,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "751ee9440920f808266c3ec2553420dea56d3c7837dd2d62af76b11be3fcece5" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "85b339346154d5646952d44d682965dfe9e12cae5febd706f0db3aa5010d6423" + url: "https://pub.dev" + source: hosted + version: "0.8.1" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "91f2a81e9f0abb4b9f3bb529f78b6227ce6050300d1ae5b1e2c69c66c7a566d8" + url: "https://pub.dev" + source: hosted + version: "1.0.0+8.4.0" dart_style: dependency: transitive description: @@ -925,6 +973,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0+1" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "4d2eb0d19bbe7e3323bd0ce4553b2e6170d161a13914bfdd85a3612329edcb43" + url: "https://pub.dev" + source: hosted + version: "3.1.0" rxdart: dependency: transitive description: @@ -1402,6 +1458,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488" + url: "https://pub.dev" + source: hosted + version: "2.2.4" sdks: dart: ">=3.10.0 <4.0.0" flutter: ">=3.38.1" diff --git a/pubspec.yaml b/pubspec.yaml index 3a146a5d..8821346b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: # Localization flutter_localizations: sdk: flutter - intl: any + intl: ^0.20.2 # State Management flutter_riverpod: ^3.1.0 @@ -68,7 +68,9 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 build_runner: ^2.10.4 + custom_lint: ^0.8.1 riverpod_generator: ^4.0.0 + riverpod_lint: ^3.1.0 json_serializable: ^6.11.2 flutter_launcher_icons: ^0.14.3