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