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.
This commit is contained in:
zarzet
2026-03-27 19:28:42 +07:00
parent 18d3612674
commit f29177216d
41 changed files with 397 additions and 174 deletions
+20
View File
@@ -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
@@ -304,6 +304,7 @@ class MainActivity: FlutterFragmentActivity() {
".mp3" -> "audio/mpeg"
".opus" -> "audio/ogg"
".flac" -> "audio/flac"
".lrc" -> "application/octet-stream"
else -> "application/octet-stream"
}
}
+8 -8
View File
@@ -510,7 +510,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
}
if ((c + 1) % _safRepairBatchSize == 0) {
await Future.delayed(const Duration(milliseconds: 16));
await Future<void>.delayed(const Duration(milliseconds: 16));
}
}
@@ -762,7 +762,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
_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<DownloadHistoryState> {
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<DownloadHistoryState> {
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<DownloadHistoryState> {
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<DownloadQueueState> {
_log.d('Queue is paused, waiting for active downloads...');
await Future.any([
Future.wait(activeDownloads.values),
Future.delayed(_queueSchedulingInterval),
Future<void>.delayed(_queueSchedulingInterval),
]);
continue;
}
@@ -3647,10 +3647,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (activeDownloads.isNotEmpty) {
await Future.any([
Future.any(activeDownloads.values),
Future.delayed(_queueSchedulingInterval),
Future<void>.delayed(_queueSchedulingInterval),
]);
} else {
await Future.delayed(_queueSchedulingInterval);
await Future<void>.delayed(_queueSchedulingInterval);
}
}
@@ -118,7 +118,7 @@ class UserPlaylistCollection {
createdAt: createdAt,
updatedAt: updatedAt,
tracks: tracksRaw
.whereType<Map>()
.whereType<Map<Object?, Object?>>()
.map(
(e) => CollectionTrackEntry.fromJson(Map<String, dynamic>.from(e)),
)
@@ -233,19 +233,19 @@ class LibraryCollectionsState {
return LibraryCollectionsState(
wishlist: wishlistRaw
.whereType<Map>()
.whereType<Map<Object?, Object?>>()
.map(
(e) => CollectionTrackEntry.fromJson(Map<String, dynamic>.from(e)),
)
.toList(growable: false),
loved: lovedRaw
.whereType<Map>()
.whereType<Map<Object?, Object?>>()
.map(
(e) => CollectionTrackEntry.fromJson(Map<String, dynamic>.from(e)),
)
.toList(growable: false),
playlists: playlistsRaw
.whereType<Map>()
.whereType<Map<Object?, Object?>>()
.map(
(e) =>
UserPlaylistCollection.fromJson(Map<String, dynamic>.from(e)),
+8 -4
View File
@@ -34,7 +34,9 @@ class SettingsNotifier extends Notifier<AppSettings> {
final prefs = await _prefs;
final json = prefs.getString(_settingsKey);
if (json != null) {
state = AppSettings.fromJson(jsonDecode(json));
state = AppSettings.fromJson(
Map<String, dynamic>.from(jsonDecode(json) as Map),
);
await _runMigrations(prefs);
await _normalizeIosDownloadDirectoryIfNeeded();
@@ -52,7 +54,9 @@ class SettingsNotifier extends Notifier<AppSettings> {
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<AppSettings> {
'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<AppSettings> {
PlatformBridge.setNetworkCompatibilityOptions(
allowHttp: compatibilityMode,
insecureTls: compatibilityMode,
).catchError((e) {
).catchError((Object e) {
_log.w('Failed to sync network compatibility options to backend: $e');
});
}
+7 -7
View File
@@ -234,7 +234,7 @@ class TrackNotifier extends Notifier<TrackState> {
}
if (attempt < 3) {
await Future.delayed(const Duration(milliseconds: 500));
await Future<void>.delayed(const Duration(milliseconds: 500));
}
}
@@ -275,10 +275,12 @@ class TrackNotifier extends Notifier<TrackState> {
state = TrackState(
tracks: tracks,
isLoading: false,
albumId: result['album']?['id'] as String?,
albumId:
(result['album'] as Map<String, dynamic>?)?['id'] as String?,
albumName:
result['name'] as String? ??
result['album']?['name'] as String?,
(result['album'] as Map<String, dynamic>?)?['name']
as String?,
playlistName: type == 'playlist'
? result['name'] as String?
: null,
@@ -825,8 +827,7 @@ class TrackNotifier extends Notifier<TrackState> {
isLoading: true,
hasSearchText: state.hasSearchText,
isShowingRecentAccess: state.isShowingRecentAccess,
selectedSearchFilter:
state.selectedSearchFilter,
selectedSearchFilter: state.selectedSearchFilter,
);
try {
@@ -921,8 +922,7 @@ class TrackNotifier extends Notifier<TrackState> {
final tracks = List<Track>.from(state.tracks);
tracks[index] = updatedTrack;
state = state.copyWith(tracks: tracks);
} catch (_) {
}
} catch (_) {}
}
void clear() {
+9 -7
View File
@@ -805,7 +805,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
);
final singleTracks = singles.fold<int>(0, (sum, a) => sum + a.totalTracks);
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -939,7 +939,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
return;
}
showDialog(
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (ctx) => _FetchingProgressDialog(
@@ -1121,6 +1121,10 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
Track _parseTrackFromDeezer(Map<String, dynamic> data, ArtistAlbum album) {
int durationMs = 0;
final durationValue = data['duration'];
final artistData = data['artist'];
final artistName = artistData is Map<String, dynamic>
? (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<ArtistScreen> {
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<ArtistScreen> {
if (album.providerId != null && album.providerId!.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionAlbumScreen(
extensionId: album.providerId!,
albumId: album.id,
@@ -1950,7 +1952,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
} else {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => AlbumScreen(
albumId: album.id,
albumName: album.name,
+2 -2
View File
@@ -309,7 +309,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
if (!mounted) return;
final result = await navigator.push(
slidePageRoute(page: TrackMetadataScreen(item: item)),
slidePageRoute<bool>(page: TrackMetadataScreen(item: item)),
);
await DownloadedEmbeddedCoverResolver.scheduleRefreshForPath(
item.filePath,
@@ -932,7 +932,7 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen> {
? '320k'
: (selectedFormat == 'Opus' ? '128k' : '320k');
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
shape: const RoundedRectangleBorder(
+24 -24
View File
@@ -556,7 +556,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
pending != query &&
mounted &&
_urlController.text.trim() == pending) {
await Future.delayed(const Duration(milliseconds: 100));
await Future<void>.delayed(const Duration(milliseconds: 100));
if (mounted && _urlController.text.trim() == pending) {
_executeLiveSearch(pending);
}
@@ -681,7 +681,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
final extensionId = trackState.searchExtensionId;
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => AlbumScreen(
albumId: trackState.albumId!,
albumName: trackState.albumName!,
@@ -708,7 +708,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => PlaylistScreen(
playlistName: trackState.playlistName!,
coverUrl: trackState.coverUrl,
@@ -729,7 +729,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
final extensionId = trackState.searchExtensionId;
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ArtistScreen(
artistId: trackState.artistId!,
artistName: trackState.artistName!,
@@ -798,7 +798,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
if (progressDialogInitialized || !mounted) return;
progressDialogInitialized = true;
progressDialogVisible = true;
showDialog(
showDialog<void>(
context: this.context,
useRootNavigator: false,
barrierDismissible: false,
@@ -1691,7 +1691,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
case 'album':
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionAlbumScreen(
extensionId: extensionId,
albumId: item.id,
@@ -1704,7 +1704,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
case 'playlist':
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionPlaylistScreen(
extensionId: extensionId,
playlistId: item.id,
@@ -1717,7 +1717,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
case 'artist':
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionArtistScreen(
extensionId: extensionId,
artistId: item.id,
@@ -1738,7 +1738,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
void _showTrackBottomSheet(ExploreItem item) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surface,
@@ -1884,7 +1884,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
if (item.albumId != null && item.albumId!.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionAlbumScreen(
extensionId: item.providerId ?? 'spotify-web',
albumId: item.albumId!,
@@ -2148,7 +2148,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
item.providerId != 'qobuz') {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionArtistScreen(
extensionId: item.providerId!,
artistId: item.id,
@@ -2160,7 +2160,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
} else {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ArtistScreen(
artistId: item.id,
artistName: item.name,
@@ -2174,7 +2174,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
if (item.providerId == 'download') {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => DownloadedAlbumScreen(
albumName: item.name,
artistName: item.subtitle ?? '',
@@ -2190,7 +2190,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
item.providerId != 'qobuz') {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionAlbumScreen(
extensionId: item.providerId!,
albumId: item.id,
@@ -2202,7 +2202,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
} else {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => AlbumScreen(
albumId: item.id,
albumName: item.name,
@@ -2240,7 +2240,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
item.providerId != 'qobuz') {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionPlaylistScreen(
extensionId: item.providerId!,
playlistId: item.id,
@@ -2252,7 +2252,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
} else {
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => PlaylistScreen(
playlistName: item.name,
coverUrl: item.imageUrl,
@@ -2275,7 +2275,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
);
if (!mounted) return;
final result = await navigator.push(
slidePageRoute(page: TrackMetadataScreen(item: item)),
slidePageRoute<bool>(page: TrackMetadataScreen(item: item)),
);
await DownloadedEmbeddedCoverResolver.scheduleRefreshForPath(
item.filePath,
@@ -2910,7 +2910,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ArtistScreen(
artistId: artistId,
artistName: artistName,
@@ -2936,7 +2936,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
// Keep the full ID with prefix (e.g., "deezer:123") for AlbumScreen to detect source
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => AlbumScreen(
albumId: album.id,
albumName: album.name,
@@ -2963,7 +2963,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
// Keep the full ID with prefix (e.g., "deezer:123") for PlaylistScreen to detect source
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => PlaylistScreen(
playlistName: playlist.name,
coverUrl: playlist.imageUrl,
@@ -2999,7 +2999,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionAlbumScreen(
extensionId: extensionId,
albumId: albumItem.id,
@@ -3035,7 +3035,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionPlaylistScreen(
extensionId: extensionId,
playlistId: playlistItem.id,
@@ -3070,7 +3070,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionArtistScreen(
extensionId: extensionId,
artistId: artistItem.id,
+2 -2
View File
@@ -119,7 +119,7 @@ class LibraryPlaylistsScreen extends ConsumerWidget {
),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (_) => LibraryTracksFolderScreen(
mode: LibraryTracksFolderMode.playlist,
playlistId: playlist.id,
@@ -149,7 +149,7 @@ class LibraryPlaylistsScreen extends ConsumerWidget {
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -847,7 +847,7 @@ class _LibraryTracksFolderScreenState
void _confirmDownloadAll(List<Track> tracks) {
if (tracks.isEmpty) return;
showDialog(
showDialog<void>(
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<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -1338,7 +1338,7 @@ class _CollectionTrackTile extends ConsumerWidget {
final showAddToPlaylist =
mode != LibraryTracksFolderMode.wishlist || isDownloaded;
showModalBottomSheet(
showModalBottomSheet<void>(
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<void>(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<void>(page: TrackMetadataScreen(localItem: localItem)),
);
return;
}
+1 -1
View File
@@ -1180,7 +1180,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen> {
? '320k'
: (selectedFormat == 'Opus' ? '128k' : '320k');
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
shape: const RoundedRectangleBorder(
+3 -3
View File
@@ -79,7 +79,7 @@ class _MainShellState extends ConsumerState<MainShell>
_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<MainShell>
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<void>.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<MainShell>
final colorScheme = Theme.of(context).colorScheme;
showDialog(
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
+1 -1
View File
@@ -578,7 +578,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen> {
void _confirmDownloadAll(BuildContext context) {
if (_tracks.isEmpty) return;
showDialog(
showDialog<void>(
context: context,
builder: (dialogContext) {
final colorScheme = Theme.of(dialogContext).colorScheme;
+7 -7
View File
@@ -656,8 +656,8 @@ final _queueFilteredAlbumsProvider =
});
Map<String, List<String>> _filterHistoryInIsolate(Map<String, Object> payload) {
final entries = (payload['entries'] as List).cast<List>();
final albumCounts = (payload['albumCounts'] as Map).cast<String, int>();
final entries = (payload['entries'] as List).cast<List<Object?>>();
final albumCounts = Map<String, int>.from(payload['albumCounts'] as Map);
final query = (payload['query'] as String?) ?? '';
final hasQuery = query.isNotEmpty;
@@ -1968,7 +1968,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
String? tempFormat = _filterFormat;
String tempSortMode = _sortMode;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
@@ -2280,7 +2280,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
final beforeModTime = await _readFileModTimeMillis(historyItem.filePath);
if (!mounted) return;
final result = await navigator.push(
slidePageRoute(page: TrackMetadataScreen(item: historyItem)),
slidePageRoute<bool>(page: TrackMetadataScreen(item: historyItem)),
);
_searchFocusNode.unfocus();
if (result == true) {
@@ -2306,7 +2306,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
final beforeModTime = await _readFileModTimeMillis(item.filePath);
if (!mounted) return;
final result = await navigator.push(
slidePageRoute(page: TrackMetadataScreen(item: item)),
slidePageRoute<bool>(page: TrackMetadataScreen(item: item)),
);
_searchFocusNode.unfocus();
if (result == true) {
@@ -2327,7 +2327,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
_searchFocusNode.unfocus();
Navigator.push(
context,
slidePageRoute(page: TrackMetadataScreen(localItem: item)),
slidePageRoute<void>(page: TrackMetadataScreen(localItem: item)),
).then((_) => _searchFocusNode.unfocus());
}
@@ -4711,7 +4711,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
_hideSelectionOverlay();
_hidePlaylistSelectionOverlay();
await showModalBottomSheet(
await showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
shape: const RoundedRectangleBorder(
@@ -770,7 +770,7 @@ class _LanguageSelector extends StatelessWidget {
void _showLanguagePicker(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surface,
@@ -510,7 +510,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (_) => const LyricsProviderPriorityPage(),
),
),
@@ -853,7 +853,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
WidgetRef ref,
String current,
) {
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
builder: (context) => SafeArea(
@@ -1002,7 +1002,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
);
}
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
@@ -1220,7 +1220,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
final settings = ref.read(settingsProvider);
final isSafMode =
settings.storageMode == 'saf' && settings.downloadTreeUri.isNotEmpty;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -1298,7 +1298,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
void _showIOSDirectoryOptions(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -1493,7 +1493,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
String current,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -1598,7 +1598,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
String current,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -1685,7 +1685,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
final colorScheme = Theme.of(context).colorScheme;
final controller = TextEditingController(text: currentLanguage);
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -1771,7 +1771,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
String current,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -1843,7 +1843,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
) {
final colorScheme = Theme.of(context).colorScheme;
final normalizedCurrent = current.trim().toUpperCase();
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -1911,7 +1911,7 @@ class _DownloadSettingsPageState extends ConsumerState<DownloadSettingsPage> {
String current,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -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<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(widget.setting.label),
+10 -7
View File
@@ -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<ExtensionsPage> {
showDivider: index < extState.extensions.length - 1,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (_) =>
ExtensionDetailPage(extensionId: ext.id),
),
@@ -469,7 +470,9 @@ class _DownloadPriorityItem extends ConsumerWidget {
onTap: hasDownloadExtensions
? () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ProviderPriorityPage()),
MaterialPageRoute<void>(
builder: (_) => const ProviderPriorityPage(),
),
)
: null,
child: Padding(
@@ -534,7 +537,7 @@ class _MetadataPriorityItem extends ConsumerWidget {
onTap: hasMetadataExtensions
? () => Navigator.push(
context,
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (_) => const MetadataProviderPriorityPage(),
),
)
@@ -678,12 +681,12 @@ class _SearchProviderSelector extends ConsumerWidget {
void _showSearchProviderPicker(
BuildContext context,
WidgetRef ref,
dynamic settings,
AppSettings settings,
List<Extension> searchProviders,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
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<Extension> homeFeedProviders,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -255,7 +255,7 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
void _showAutoScanPicker(BuildContext context, String current) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
+1 -1
View File
@@ -92,7 +92,7 @@ class _LogScreenState extends State<LogScreen> {
}
void _clearLogs() {
showDialog(
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.logClearLogsTitle),
@@ -241,7 +241,7 @@ class OptionsSettingsPage extends ConsumerWidget {
WidgetRef ref,
ColorScheme colorScheme,
) {
showDialog(
showDialog<void>(
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<void>(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
+1 -1
View File
@@ -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<void>(page: page));
}
}
+3 -3
View File
@@ -124,7 +124,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
final shouldOpen = await _showAndroid11StorageDialog();
if (shouldOpen == true) {
await Permission.manageExternalStorage.request();
await Future.delayed(const Duration(milliseconds: 500));
await Future<void>.delayed(const Duration(milliseconds: 500));
manageStatus = await Permission.manageExternalStorage.status;
}
}
@@ -203,7 +203,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
}
Future<void> _showPermissionDeniedDialog(String permissionType) async {
await showDialog(
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.setupPermissionRequired(permissionType)),
@@ -286,7 +286,7 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
Future<void> _showIOSDirectoryOptions() async {
final colorScheme = Theme.of(context).colorScheme;
await showModalBottomSheet(
await showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
+2 -2
View File
@@ -416,7 +416,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
void _showChangeRepoDialog(String currentUrl) {
final changeUrlController = TextEditingController(text: currentUrl);
showDialog(
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.storeRepoDialogTitle),
@@ -583,7 +583,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
void _showExtensionDetails(StoreExtension ext) {
Navigator.of(context).push(
MaterialPageRoute(
MaterialPageRoute<void>(
builder: (context) => ExtensionDetailsScreen(extension: ext),
),
);
+7 -7
View File
@@ -2135,7 +2135,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
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<TrackMetadataScreen> {
WidgetRef ref,
ColorScheme colorScheme,
) {
showModalBottomSheet(
showModalBottomSheet<void>(
context: screenContext,
useRootNavigator: true,
shape: const RoundedRectangleBorder(
@@ -2824,7 +2824,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
bool isLosslessTarget =
selectedFormat == 'ALAC' || selectedFormat == 'FLAC';
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
shape: const RoundedRectangleBorder(
@@ -3023,7 +3023,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
if (!mounted) return;
showModalBottomSheet(
showModalBottomSheet<void>(
context: this.context,
useRootNavigator: true,
isScrollControlled: true,
@@ -3186,7 +3186,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
required String date,
required List<CueSplitTrackInfo> tracks,
}) {
showDialog(
showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertDialog(
@@ -3442,7 +3442,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
final isLossless =
targetFormat.toUpperCase() == 'ALAC' ||
targetFormat.toUpperCase() == 'FLAC';
showDialog(
showDialog<void>(
context: context,
builder: (dialogContext) {
return AlertDialog(
@@ -3792,7 +3792,7 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
WidgetRef ref,
ColorScheme colorScheme,
) {
showDialog(
showDialog<void>(
context: screenContext,
useRootNavigator: true,
builder: (dialogContext) => AlertDialog(
+2 -2
View File
@@ -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<void>.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<void>.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() {
_isCompleted = false;
+2 -2
View File
@@ -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<Map>()) {
for (final entry in decoded.whereType<Map<Object?, Object?>>()) {
final map = Map<String, dynamic>.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<Map>()) {
for (final entry in decoded.whereType<Map<Object?, Object?>>()) {
final map = Map<String, dynamic>.from(entry);
final type = map['type'] as String?;
final id = map['id'] as String?;
+1 -1
View File
@@ -124,7 +124,7 @@ class CsvImportService {
);
if (i < tracks.length - 1) {
await Future.delayed(const Duration(milliseconds: 100));
await Future<void>.delayed(const Duration(milliseconds: 100));
}
continue;
}
+2 -2
View File
@@ -224,7 +224,7 @@ class HistoryDatabase {
}
try {
final List<dynamic> jsonList = jsonDecode(jsonStr);
final jsonList = List<dynamic>.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<String, dynamic>;
final map = Map<String, dynamic>.from(json as Map);
batch.insert(
'history',
_jsonToDbRow(map),
@@ -155,11 +155,11 @@ class LibraryCollectionsDatabase {
final db = await database;
await db.transaction((txn) async {
for (final entry in wishlistRaw.whereType<Map>()) {
for (final entry in wishlistRaw.whereType<Map<Object?, Object?>>()) {
final map = Map<String, dynamic>.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<Object?, Object?>) 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<Map>()) {
for (final entry in lovedRaw.whereType<Map<Object?, Object?>>()) {
final map = Map<String, dynamic>.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<Object?, Object?>) 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<Map>()) {
for (final playlistEntry
in playlistsRaw.whereType<Map<Object?, Object?>>()) {
final playlist = Map<String, dynamic>.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<Map>()) {
for (final trackEntry
in tracksRaw.whereType<Map<Object?, Object?>>()) {
final trackMap = Map<String, dynamic>.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<Object?, Object?>) continue;
final addedAt = (trackMap['addedAt'] as String?) ?? nowIso;
await txn.insert(_tablePlaylistTracks, {
'playlist_id': playlistId,
+2 -2
View File
@@ -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)'
: '';
+1 -1
View File
@@ -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();
+146 -24
View File
@@ -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<dynamic>? ?? [];
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<dynamic>? ?? 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<dynamic> assets) {
final apkAssets = <_ApkAsset>[];
for (final asset in assets.whereType<Map<Object?, Object?>>()) {
final assetMap = Map<String, dynamic>.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<List<String>> _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');
}
+3 -3
View File
@@ -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<void>(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<void>(builder: builder));
});
return;
}
activeTabNavigator.push(MaterialPageRoute(builder: builder));
activeTabNavigator.push(MaterialPageRoute<void>(builder: builder));
}
void _showLoadingSnackBar(BuildContext context, String message) {
+6 -5
View File
@@ -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<Map<Object?, Object?>>()) {
final logMap = Map<String, dynamic>.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) {
+3 -1
View File
@@ -239,7 +239,9 @@ class _AudioAnalysisCardState extends State<AudioAnalysisCard> {
final file = File('${dir.path}/$key.json');
if (!await file.exists()) return null;
final json = jsonDecode(await file.readAsString());
final json = Map<String, dynamic>.from(
jsonDecode(await file.readAsString()) as Map,
);
final cachedSize = json['fileSize'] as int;
if (!filePath.startsWith('content://')) {
+1 -1
View File
@@ -110,7 +110,7 @@ class DownloadServicePicker extends ConsumerStatefulWidget {
}) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
backgroundColor: colorScheme.surfaceContainerHigh,
@@ -19,7 +19,7 @@ class TrackCollectionQuickActions extends ConsumerWidget {
Track track,
) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
+66 -2
View File
@@ -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"
+3 -1
View File
@@ -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