refactor: split large screen files into part files and DRY platform bridge

- Extract home_tab.dart helpers/widgets into home_tab_helpers.dart and home_tab_widgets.dart using Dart part files
- Extract queue_tab.dart helpers/widgets into queue_tab_helpers.dart and queue_tab_widgets.dart using Dart part files
- Extract track_metadata_edit_sheet.dart from track_metadata_screen.dart using Dart part file
- Refactor _FileExistsListenableCache into a standalone class in queue_tab_helpers.dart
- Fix artist_screen.dart: replace unreliable findAncestorStateOfType with GlobalKey for _FetchingProgressDialog progress updates
- DRY platform_bridge.dart: extract common JSON decode patterns into reusable helper methods (_decodeRequiredMapResult, _decodeNullableMapResult, _decodeMapListResult, _decodeStringListResult)
This commit is contained in:
zarzet 2026-05-02 00:27:51 +07:00
parent 01c7c9cc3a
commit 3a7419ec9f
10 changed files with 5081 additions and 5013 deletions

View file

@ -926,10 +926,12 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
return;
}
final progressDialogKey = GlobalKey<_FetchingProgressDialogState>();
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (ctx) => _FetchingProgressDialog(
key: progressDialogKey,
totalAlbums: albums.length,
onCancel: () {
setState(() => _isFetchingDiscography = false);
@ -955,8 +957,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
fetchedCount++;
if (mounted) {
_FetchingProgressDialog.updateProgress(
context,
progressDialogKey.currentState?.updateProgress(
fetchedCount,
albums.length,
);
@ -2001,16 +2002,11 @@ class _FetchingProgressDialog extends StatefulWidget {
final VoidCallback onCancel;
const _FetchingProgressDialog({
super.key,
required this.totalAlbums,
required this.onCancel,
});
static void updateProgress(BuildContext context, int current, int total) {
final state = context
.findAncestorStateOfType<_FetchingProgressDialogState>();
state?._updateProgress(current, total);
}
@override
State<_FetchingProgressDialog> createState() =>
_FetchingProgressDialogState();
@ -2026,7 +2022,7 @@ class _FetchingProgressDialogState extends State<_FetchingProgressDialog> {
_total = widget.totalAlbums;
}
void _updateProgress(int current, int total) {
void updateProgress(int current, int total) {
if (mounted) {
setState(() {
_current = current;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,188 @@
part of 'home_tab.dart';
class _RecentAccessView {
final List<RecentAccessItem> uniqueItems;
final List<String> downloadIds;
final Map<String, String> downloadFilePathByRecentKey;
final bool hasHiddenDownloads;
const _RecentAccessView({
required this.uniqueItems,
required this.downloadIds,
required this.downloadFilePathByRecentKey,
required this.hasHiddenDownloads,
});
}
class _RecentAlbumAggregate {
int count;
DownloadHistoryItem mostRecent;
_RecentAlbumAggregate({required this.count, required this.mostRecent});
}
class _CsvImportOptions {
final bool confirmed;
final bool skipDownloaded;
const _CsvImportOptions({
required this.confirmed,
required this.skipDownloaded,
});
}
class _SearchResultBuckets {
final List<Track> realTracks;
final List<int> realTrackIndexes;
final List<Track> albumItems;
final List<Track> playlistItems;
final List<Track> artistItems;
const _SearchResultBuckets({
required this.realTracks,
required this.realTrackIndexes,
required this.albumItems,
required this.playlistItems,
required this.artistItems,
});
}
enum _SearchSortOption {
defaultOrder,
titleAsc,
titleDesc,
artistAsc,
artistDesc,
durationAsc,
durationDesc,
dateAsc,
dateDesc,
}
const _homeHistoryPreviewLimit = 48;
class _HomeHistoryPreview {
final List<DownloadHistoryItem> items;
const _HomeHistoryPreview(this.items);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _HomeHistoryPreview && listEquals(items, other.items);
@override
int get hashCode => Object.hashAll(items);
}
final _homeHistoryPreviewProvider = Provider<List<DownloadHistoryItem>>((ref) {
final preview = ref.watch(
downloadHistoryProvider.select((s) {
final items = s.items;
if (items.length <= _homeHistoryPreviewLimit) {
return _HomeHistoryPreview(items);
}
return _HomeHistoryPreview(
items.take(_homeHistoryPreviewLimit).toList(growable: false),
);
}),
);
return preview.items;
});
_RecentAccessView _buildRecentAccessViewData(
List<RecentAccessItem> items,
List<DownloadHistoryItem> historyItems,
Set<String> hiddenIds,
) {
final albumGroups = <String, _RecentAlbumAggregate>{};
for (final h in historyItems) {
final artistForKey = (h.albumArtist != null && h.albumArtist!.isNotEmpty)
? h.albumArtist!
: h.artistName;
final albumKey = '${h.albumName}|$artistForKey';
final existing = albumGroups[albumKey];
if (existing == null) {
albumGroups[albumKey] = _RecentAlbumAggregate(count: 1, mostRecent: h);
} else {
existing.count++;
if (h.downloadedAt.isAfter(existing.mostRecent.downloadedAt)) {
existing.mostRecent = h;
}
}
}
final downloadIds = <String>[];
final visibleDownloads = <RecentAccessItem>[];
final downloadFilePathByRecentKey = <String, String>{};
for (final aggregate in albumGroups.values) {
final mostRecent = aggregate.mostRecent;
final artistForKey =
(mostRecent.albumArtist != null && mostRecent.albumArtist!.isNotEmpty)
? mostRecent.albumArtist!
: mostRecent.artistName;
final isSingleTrack = aggregate.count == 1;
final recentId = isSingleTrack
? (mostRecent.spotifyId ?? mostRecent.id)
: '${mostRecent.albumName}|$artistForKey';
final recent = RecentAccessItem(
id: recentId,
name: isSingleTrack ? mostRecent.trackName : mostRecent.albumName,
subtitle: isSingleTrack ? mostRecent.artistName : artistForKey,
imageUrl: mostRecent.coverUrl,
type: isSingleTrack ? RecentAccessType.track : RecentAccessType.album,
accessedAt: mostRecent.downloadedAt,
providerId: 'download',
);
downloadIds.add(recentId);
downloadFilePathByRecentKey['${recent.type.name}:${recent.id}'] =
mostRecent.filePath;
if (!hiddenIds.contains(recentId)) {
visibleDownloads.add(recent);
}
}
visibleDownloads.sort((a, b) => b.accessedAt.compareTo(a.accessedAt));
if (visibleDownloads.length > 10) {
visibleDownloads.removeRange(10, visibleDownloads.length);
}
final allItems = <RecentAccessItem>[...items, ...visibleDownloads];
allItems.sort((a, b) => b.accessedAt.compareTo(a.accessedAt));
final seen = <String>{};
final uniqueItems = <RecentAccessItem>[];
for (final item in allItems) {
final key = '${item.type.name}:${item.id}';
if (seen.add(key)) {
uniqueItems.add(item);
if (uniqueItems.length >= 10) {
break;
}
}
}
return _RecentAccessView(
uniqueItems: uniqueItems,
downloadIds: downloadIds,
downloadFilePathByRecentKey: downloadFilePathByRecentKey,
hasHiddenDownloads: hiddenIds.isNotEmpty,
);
}
final recentAccessViewProvider = Provider<_RecentAccessView>((ref) {
final historyItems = ref.watch(_homeHistoryPreviewProvider);
final recentAccessItems = ref.watch(
recentAccessProvider.select((s) => s.items),
);
final hiddenDownloadIds = ref.watch(
recentAccessProvider.select((s) => s.hiddenDownloadIds),
);
return _buildRecentAccessViewData(
recentAccessItems,
historyItems,
hiddenDownloadIds,
);
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,200 @@
part of 'queue_tab.dart';
class _QueueItemSliverRow extends ConsumerWidget {
final String itemId;
final ColorScheme colorScheme;
final Widget Function(BuildContext, DownloadItem, ColorScheme) itemBuilder;
const _QueueItemSliverRow({
super.key,
required this.itemId,
required this.colorScheme,
required this.itemBuilder,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(
downloadQueueLookupProvider.select((lookup) => lookup.byItemId[itemId]),
);
if (item == null) {
return const SizedBox.shrink();
}
return RepaintBoundary(child: itemBuilder(context, item, colorScheme));
}
}
enum _CollectionEntryType { wishlist, loved, playlist }
class _CollectionEntry {
final _CollectionEntryType type;
final int playlistIndex;
const _CollectionEntry._(this.type, [this.playlistIndex = -1]);
static const wishlist = _CollectionEntry._(_CollectionEntryType.wishlist);
static const loved = _CollectionEntry._(_CollectionEntryType.loved);
static _CollectionEntry playlist(int index) =>
_CollectionEntry._(_CollectionEntryType.playlist, index);
}
class _FilterChip extends StatelessWidget {
final String label;
final int count;
final bool isSelected;
final VoidCallback onTap;
const _FilterChip({
required this.label,
required this.count,
required this.isSelected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return FilterChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(label),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isSelected
? colorScheme.primary.withValues(alpha: 0.2)
: colorScheme.outline.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(10),
),
child: Text(
count.toString(),
style: TextStyle(
fontSize: 11,
color: isSelected
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
),
],
),
selected: isSelected,
onSelected: (_) => onTap(),
showCheckmark: false,
);
}
}
class _SelectionActionButton extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback? onPressed;
final ColorScheme colorScheme;
const _SelectionActionButton({
required this.icon,
required this.label,
required this.onPressed,
required this.colorScheme,
});
@override
Widget build(BuildContext context) {
final isDisabled = onPressed == null;
return Material(
color: isDisabled
? colorScheme.surfaceContainerHighest.withValues(alpha: 0.5)
: colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: onPressed,
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 18,
color: isDisabled
? colorScheme.onSurfaceVariant.withValues(alpha: 0.5)
: colorScheme.onSecondaryContainer,
),
const SizedBox(width: 6),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: isDisabled
? colorScheme.onSurfaceVariant.withValues(alpha: 0.5)
: colorScheme.onSecondaryContainer,
),
),
),
],
),
),
),
);
}
}
class _AnimatedOverlayBottomBar extends StatefulWidget {
final Widget child;
const _AnimatedOverlayBottomBar({required this.child});
@override
State<_AnimatedOverlayBottomBar> createState() =>
_AnimatedOverlayBottomBarState();
}
class _AnimatedOverlayBottomBarState extends State<_AnimatedOverlayBottomBar>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<Offset> _slideAnimation;
late final Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 240),
);
final curve = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutCubic,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.08),
end: Offset.zero,
).animate(curve);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(curve);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: SlideTransition(position: _slideAnimation, child: widget.child),
);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,7 @@ class PlatformBridge {
'spotify_id': spotifyId,
'isrc': isrc,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'checkAvailability');
}
static Future<Map<String, dynamic>> _invokeDownloadMethod(
@ -38,7 +38,7 @@ class PlatformBridge {
) async {
final request = jsonEncode(payload.toJson());
final result = await _channel.invokeMethod(method, request);
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, method);
}
static Future<Map<String, dynamic>> downloadByStrategy({
@ -133,7 +133,7 @@ class PlatformBridge {
'output_dir': outputDir,
'isrc': isrc,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'checkDuplicate');
}
static Future<String> buildFilename(
@ -156,8 +156,7 @@ class PlatformBridge {
static Future<Map<String, dynamic>?> pickSafTree() async {
final result = await _channel.invokeMethod('pickSafTree');
if (result == null) return null;
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeNullableMapResult(result, 'pickSafTree');
}
static Future<bool> safExists(String uri) async {
@ -172,7 +171,7 @@ class PlatformBridge {
static Future<Map<String, dynamic>> safStat(String uri) async {
final result = await _channel.invokeMethod('safStat', {'uri': uri});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'safStat');
}
static Future<Map<String, dynamic>> resolveSafFile({
@ -185,7 +184,7 @@ class PlatformBridge {
'relative_dir': relativeDir,
'file_name': fileName,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'resolveSafFile');
}
static Future<String?> copyContentUriToTemp(String uri) async {
@ -259,7 +258,7 @@ class PlatformBridge {
'artist_name': artistName,
'duration_ms': durationMs,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'fetchLyrics');
}
static Future<String> getLyricsLRC(
@ -293,7 +292,7 @@ class PlatformBridge {
'file_path': filePath ?? '',
'duration_ms': durationMs,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'getLyricsLRCWithSource');
}
static Future<Map<String, dynamic>> embedLyricsToFile(
@ -304,7 +303,7 @@ class PlatformBridge {
'file_path': filePath,
'lyrics': lyrics,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'embedLyricsToFile');
}
static Future<void> cleanupConnections() async {
@ -321,7 +320,7 @@ class PlatformBridge {
'output_path': outputPath,
'max_quality': maxQuality,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'downloadCoverToFile');
}
static Future<Map<String, dynamic>> extractCoverToFile(
@ -332,7 +331,7 @@ class PlatformBridge {
'audio_path': audioPath,
'output_path': outputPath,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'extractCoverToFile');
}
static Future<Map<String, dynamic>> fetchAndSaveLyrics({
@ -351,7 +350,7 @@ class PlatformBridge {
'output_path': outputPath,
'audio_file_path': audioFilePath,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'fetchAndSaveLyrics');
}
/// Providers not in the list are disabled.
@ -364,15 +363,13 @@ class PlatformBridge {
static Future<List<String>> getLyricsProviders() async {
final result = await _channel.invokeMethod('getLyricsProviders');
final List<dynamic> decoded = jsonDecode(result as String) as List<dynamic>;
return decoded.cast<String>();
return _decodeStringListResult(result, 'getLyricsProviders');
}
static Future<List<Map<String, dynamic>>>
getAvailableLyricsProviders() async {
final result = await _channel.invokeMethod('getAvailableLyricsProviders');
final List<dynamic> decoded = jsonDecode(result as String) as List<dynamic>;
return decoded.cast<Map<String, dynamic>>();
return _decodeMapListResult(result, 'getAvailableLyricsProviders');
}
/// Sets advanced lyrics fetch options used by provider-specific integrations.
@ -387,7 +384,7 @@ class PlatformBridge {
static Future<Map<String, dynamic>> getLyricsFetchOptions() async {
final result = await _channel.invokeMethod('getLyricsFetchOptions');
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'getLyricsFetchOptions');
}
static Future<Map<String, dynamic>> reEnrichFile(
@ -397,14 +394,14 @@ class PlatformBridge {
final result = await _channel.invokeMethod('reEnrichFile', {
'request_json': requestJSON,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'reEnrichFile');
}
static Future<Map<String, dynamic>> readFileMetadata(String filePath) async {
final result = await _channel.invokeMethod('readFileMetadata', {
'file_path': filePath,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'readFileMetadata');
}
static Future<Map<String, dynamic>> editFileMetadata(
@ -416,7 +413,7 @@ class PlatformBridge {
'file_path': filePath,
'metadata_json': metadataJSON,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'editFileMetadata');
}
/// Rewrites ARTIST/ALBUMARTIST Vorbis comments as multiple split entries
@ -431,7 +428,7 @@ class PlatformBridge {
'artist': artist,
'album_artist': albumArtist,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'rewriteSplitArtistTags');
}
static Future<bool> writeTempToSaf(String tempPath, String safUri) async {
@ -439,7 +436,7 @@ class PlatformBridge {
'temp_path': tempPath,
'saf_uri': safUri,
});
final map = jsonDecode(result as String) as Map<String, dynamic>;
final map = _decodeRequiredMapResult(result, 'writeTempToSaf');
return map['success'] == true;
}
@ -510,7 +507,7 @@ class PlatformBridge {
'artist_limit': artistLimit,
'filter': filter ?? '',
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'searchProviderAll');
}
static Future<Map<String, dynamic>> getDeezerRelatedArtists(
@ -521,14 +518,14 @@ class PlatformBridge {
'artist_id': artistId,
'limit': limit,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'getDeezerRelatedArtists');
}
static Future<Map<String, dynamic>> parseProviderUrl(String url) async {
final result = await _channel.invokeMethod('parseProviderUrl', {
'url': url,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'parseProviderUrl');
}
static Future<Map<String, dynamic>> getProviderMetadata(
@ -546,7 +543,7 @@ class PlatformBridge {
'getProviderMetadata returned null for $providerId:$resourceType:$resourceId',
);
}
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'getProviderMetadata');
}
static Future<Map<String, dynamic>> searchDeezerByISRC(
@ -557,7 +554,7 @@ class PlatformBridge {
'isrc': isrc,
'item_id': itemId ?? '',
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'searchDeezerByISRC');
}
static Future<Map<String, String>?> getDeezerExtendedMetadata(
@ -568,7 +565,10 @@ class PlatformBridge {
'track_id': trackId,
});
if (result == null) return null;
final data = jsonDecode(result as String) as Map<String, dynamic>;
final data = _decodeRequiredMapResult(
result,
'getDeezerExtendedMetadata',
);
return {
'genre': data['genre'] as String? ?? '',
'label': data['label'] as String? ?? '',
@ -588,20 +588,19 @@ class PlatformBridge {
'resource_type': resourceType,
'spotify_id': spotifyId,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'convertSpotifyToDeezer');
}
static Future<List<Map<String, dynamic>>> getGoLogs() async {
final result = await _channel.invokeMethod('getLogs');
final logs = jsonDecode(result as String) as List<dynamic>;
return logs.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'getGoLogs');
}
static Future<Map<String, dynamic>> getGoLogsSince(int index) async {
final result = await _channel.invokeMethod('getLogsSince', {
'index': index,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'getGoLogsSince');
}
static Future<void> clearGoLogs() async {
@ -635,7 +634,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('loadExtensionsFromDir', {
'dir_path': dirPath,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'loadExtensionsFromDir');
}
static Future<Map<String, dynamic>> loadExtensionFromPath(
@ -645,7 +644,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('loadExtensionFromPath', {
'file_path': filePath,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'loadExtensionFromPath');
}
static Future<void> unloadExtension(String extensionId) async {
@ -667,7 +666,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('upgradeExtension', {
'file_path': filePath,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'upgradeExtension');
}
static Future<Map<String, dynamic>> checkExtensionUpgrade(
@ -677,13 +676,12 @@ class PlatformBridge {
final result = await _channel.invokeMethod('checkExtensionUpgrade', {
'file_path': filePath,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'checkExtensionUpgrade');
}
static Future<List<Map<String, dynamic>>> getInstalledExtensions() async {
final result = await _channel.invokeMethod('getInstalledExtensions');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'getInstalledExtensions');
}
static Future<void> setExtensionEnabled(
@ -706,8 +704,7 @@ class PlatformBridge {
static Future<List<String>> getProviderPriority() async {
final result = await _channel.invokeMethod('getProviderPriority');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as String).toList();
return _decodeStringListResult(result, 'getProviderPriority');
}
static Future<void> setDownloadFallbackExtensionIds(
@ -730,8 +727,7 @@ class PlatformBridge {
static Future<List<String>> getMetadataProviderPriority() async {
final result = await _channel.invokeMethod('getMetadataProviderPriority');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as String).toList();
return _decodeStringListResult(result, 'getMetadataProviderPriority');
}
static Future<Map<String, dynamic>> getExtensionSettings(
@ -740,7 +736,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('getExtensionSettings', {
'extension_id': extensionId,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'getExtensionSettings');
}
static Future<void> setExtensionSettings(
@ -766,7 +762,7 @@ class PlatformBridge {
if (result == null || (result as String).isEmpty) {
return {'success': true};
}
return jsonDecode(result) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'invokeExtensionAction');
}
static Future<List<Map<String, dynamic>>> searchTracksWithExtensions(
@ -778,8 +774,7 @@ class PlatformBridge {
'query': query,
'limit': limit,
});
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'searchTracksWithExtensions');
}
static Future<List<Map<String, dynamic>>> searchTracksWithMetadataProviders(
@ -794,8 +789,7 @@ class PlatformBridge {
'searchTracksWithMetadataProviders',
{'query': query, 'limit': limit, 'include_extensions': includeExtensions},
);
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'searchTracksWithMetadataProviders');
}
static Future<void> cleanupExtensions() async {
@ -809,8 +803,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('getExtensionPendingAuth', {
'extension_id': extensionId,
});
if (result == null) return null;
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeNullableMapResult(result, 'getExtensionPendingAuth');
}
static Future<void> setExtensionAuthCode(
@ -854,8 +847,7 @@ class PlatformBridge {
static Future<List<Map<String, dynamic>>> getAllPendingAuthRequests() async {
final result = await _channel.invokeMethod('getAllPendingAuthRequests');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'getAllPendingAuthRequests');
}
static Future<Map<String, dynamic>?> getPendingFFmpegCommand(
@ -864,8 +856,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('getPendingFFmpegCommand', {
'command_id': commandId,
});
if (result == null) return null;
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeNullableMapResult(result, 'getPendingFFmpegCommand');
}
static Future<void> setFFmpegCommandResult(
@ -885,8 +876,7 @@ class PlatformBridge {
static Future<List<Map<String, dynamic>>>
getAllPendingFFmpegCommands() async {
final result = await _channel.invokeMethod('getAllPendingFFmpegCommands');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'setFFmpegCommandResult');
}
static Future<List<Map<String, dynamic>>> customSearchWithExtension(
@ -899,20 +889,17 @@ class PlatformBridge {
'query': query,
'options': options != null ? jsonEncode(options) : '',
});
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'customSearchWithExtension');
}
static Future<List<Map<String, dynamic>>> getSearchProviders() async {
final result = await _channel.invokeMethod('getSearchProviders');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'getSearchProviders');
}
static Future<List<Map<String, dynamic>>> getBuiltInProviders() async {
final result = await _channel.invokeMethod('getBuiltInProviders');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'getBuiltInProviders');
}
static Future<Map<String, dynamic>?> handleURLWithExtension(
@ -922,8 +909,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('handleURLWithExtension', {
'url': url,
});
if (result == null || result == '') return null;
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeNullableMapResult(result, 'handleURLWithExtension');
} catch (e) {
return null;
}
@ -937,8 +923,7 @@ class PlatformBridge {
static Future<List<Map<String, dynamic>>> getURLHandlers() async {
final result = await _channel.invokeMethod('getURLHandlers');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'getURLHandlers');
}
static Future<Map<String, dynamic>?> getExtensionHomeFeed(
@ -948,8 +933,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('getExtensionHomeFeed', {
'extension_id': extensionId,
});
if (result == null || result == '') return null;
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeNullableMapResult(result, 'getExtensionHomeFeed');
} catch (e) {
_log.e('getExtensionHomeFeed failed: $e');
return null;
@ -964,8 +948,7 @@ class PlatformBridge {
'getExtensionBrowseCategories',
{'extension_id': extensionId},
);
if (result == null || result == '') return null;
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeNullableMapResult(result, 'getExtensionBrowseCategories');
} catch (e) {
_log.e('getExtensionBrowseCategories failed: $e');
return null;
@ -986,8 +969,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('scanLibraryFolder', {
'folder_path': folderPath,
});
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'scanLibraryFolder');
}
static Future<Map<String, dynamic>> scanLibraryFolderIncremental(
@ -1001,7 +983,7 @@ class PlatformBridge {
'folder_path': folderPath,
'existing_files': jsonEncode(existingFiles),
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'scanLibraryFolderIncremental');
}
static Future<Map<String, dynamic>> scanLibraryFolderIncrementalFromSnapshot(
@ -1012,7 +994,10 @@ class PlatformBridge {
'scanLibraryFolderIncrementalFromSnapshot',
{'folder_path': folderPath, 'snapshot_path': snapshotPath},
);
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(
result,
'scanLibraryFolderIncrementalFromSnapshot',
);
}
static Future<List<Map<String, dynamic>>> scanSafTree(String treeUri) async {
@ -1020,8 +1005,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('scanSafTree', {
'tree_uri': treeUri,
});
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'scanSafTree');
}
static Future<Map<String, dynamic>> scanSafTreeIncremental(
@ -1035,7 +1019,7 @@ class PlatformBridge {
'tree_uri': treeUri,
'existing_files': jsonEncode(existingFiles),
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'scanSafTreeIncremental');
}
static Future<Map<String, dynamic>> scanSafTreeIncrementalFromSnapshot(
@ -1046,14 +1030,17 @@ class PlatformBridge {
'scanSafTreeIncrementalFromSnapshot',
{'tree_uri': treeUri, 'snapshot_path': snapshotPath},
);
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(
result,
'scanSafTreeIncrementalFromSnapshot',
);
}
static Future<Map<String, int>> getSafFileModTimes(List<String> uris) async {
final result = await _channel.invokeMethod('getSafFileModTimes', {
'uris': jsonEncode(uris),
});
final map = jsonDecode(result as String) as Map<String, dynamic>;
final map = _decodeRequiredMapResult(result, 'getSafFileModTimes');
return map.map((key, value) => MapEntry(key, (value as num).toInt()));
}
@ -1072,6 +1059,73 @@ class PlatformBridge {
await _channel.invokeMethod('cancelLibraryScan');
}
static Object? _decodeJsonResult(dynamic result) {
if (result is String) {
if (result.isEmpty) return null;
return jsonDecode(result);
}
return result;
}
static Map<String, dynamic> _decodeRequiredMapResult(
dynamic result,
String method,
) {
final decoded = _decodeJsonResult(result);
if (decoded is Map) {
return decoded.cast<String, dynamic>();
}
throw FormatException(
'Expected map result from $method, got ${decoded.runtimeType}',
);
}
static Map<String, dynamic>? _decodeNullableMapResult(
dynamic result,
String method,
) {
final decoded = _decodeJsonResult(result);
if (decoded == null) return null;
if (decoded is Map) {
return decoded.cast<String, dynamic>();
}
throw FormatException(
'Expected nullable map result from $method, got ${decoded.runtimeType}',
);
}
static List<dynamic> _decodeRequiredListResult(
dynamic result,
String method,
) {
final decoded = _decodeJsonResult(result);
if (decoded is List) return decoded;
throw FormatException(
'Expected list result from $method, got ${decoded.runtimeType}',
);
}
static List<Map<String, dynamic>> _decodeMapListResult(
dynamic result,
String method,
) {
return _decodeRequiredListResult(result, method).map((entry) {
if (entry is Map) return entry.cast<String, dynamic>();
throw FormatException(
'Expected map entry from $method, got ${entry.runtimeType}',
);
}).toList();
}
static List<String> _decodeStringListResult(dynamic result, String method) {
return _decodeRequiredListResult(result, method).map((entry) {
if (entry is String) return entry;
throw FormatException(
'Expected string entry from $method, got ${entry.runtimeType}',
);
}).toList();
}
static Map<String, dynamic> _decodeMapResult(dynamic result) {
if (result is Map) {
return result.cast<String, dynamic>();
@ -1134,8 +1188,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('readAudioMetadata', {
'file_path': filePath,
});
if (result == null || result == '') return null;
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeNullableMapResult(result, 'readAudioMetadata');
} catch (e) {
_log.w('Failed to read audio metadata: $e');
return null;
@ -1150,7 +1203,7 @@ class PlatformBridge {
'file_path': filePath,
'metadata': metadata != null ? jsonEncode(metadata) : '',
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'runPostProcessing');
}
static Future<Map<String, dynamic>> runPostProcessingV2(
@ -1167,13 +1220,12 @@ class PlatformBridge {
'input': jsonEncode(input),
'metadata': metadata != null ? jsonEncode(metadata) : '',
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'runPostProcessingV2');
}
static Future<List<Map<String, dynamic>>> getPostProcessingProviders() async {
final result = await _channel.invokeMethod('getPostProcessingProviders');
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'getPostProcessingProviders');
}
static Future<void> initExtensionStore(String cacheDir) async {
@ -1206,8 +1258,7 @@ class PlatformBridge {
final result = await _channel.invokeMethod('getStoreExtensions', {
'force_refresh': forceRefresh,
});
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'getStoreExtensions');
}
static Future<List<Map<String, dynamic>>> searchStoreExtensions(
@ -1219,14 +1270,12 @@ class PlatformBridge {
'query': query,
'category': category ?? '',
});
final list = jsonDecode(result as String) as List<dynamic>;
return list.map((e) => e as Map<String, dynamic>).toList();
return _decodeMapListResult(result, 'searchStoreExtensions');
}
static Future<List<String>> getStoreCategories() async {
final result = await _channel.invokeMethod('getStoreCategories');
final list = jsonDecode(result as String) as List<dynamic>;
return list.cast<String>();
return _decodeStringListResult(result, 'getStoreCategories');
}
static Future<String> downloadStoreExtension(
@ -1255,6 +1304,6 @@ class PlatformBridge {
'cue_path': cuePath,
'audio_dir': audioDir,
});
return jsonDecode(result as String) as Map<String, dynamic>;
return _decodeRequiredMapResult(result, 'parseCueSheet');
}
}