feat: rebuild cross-extension sharing and queue controls

Co-authored-by: Amonoman <musaauron87@gmail.com>
This commit is contained in:
zarzet
2026-06-02 00:58:41 +07:00
co-authored by Amonoman
parent f52527a41b
commit ffdaf14ba5
38 changed files with 1850 additions and 122 deletions
+78
View File
@@ -6994,6 +6994,84 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Releases'**
String get artistReleases;
/// Button to clear selected fields for auto-fill
///
/// In en, this message translates to:
/// **'None'**
String get editMetadataSelectNone;
/// Button to retry every failed download in the queue
///
/// In en, this message translates to:
/// **'Retry {count} failed'**
String queueRetryAllFailed(int count);
/// Settings switch title for storing completed downloads in history
///
/// In en, this message translates to:
/// **'Save download history'**
String get settingsSaveDownloadHistory;
/// Settings switch subtitle for storing completed downloads in history
///
/// In en, this message translates to:
/// **'Keep completed downloads in history and library views'**
String get settingsSaveDownloadHistorySubtitle;
/// Confirmation dialog title shown before disabling download history
///
/// In en, this message translates to:
/// **'Turn off download history?'**
String get dialogDisableHistoryTitle;
/// Confirmation dialog message shown before disabling download history
///
/// In en, this message translates to:
/// **'Existing history will be cleared. Downloaded files will not be deleted.'**
String get dialogDisableHistoryMessage;
/// Confirmation action to disable download history and clear existing entries
///
/// In en, this message translates to:
/// **'Turn off and clear'**
String get dialogDisableAndClear;
/// Title and tooltip for finding the current collection in other services
///
/// In en, this message translates to:
/// **'Open in other services'**
String get openInOtherServices;
/// Empty state when no extensions can be searched for cross-service links
///
/// In en, this message translates to:
/// **'No other compatible services'**
String get shareSheetNoExtensions;
/// Cross-service share sheet row subtitle when a service has no match
///
/// In en, this message translates to:
/// **'Not found'**
String get shareSheetNotFound;
/// Tooltip for copying a cross-service link
///
/// In en, this message translates to:
/// **'Copy link'**
String get shareSheetCopyLink;
/// Snackbar after copying a cross-service link
///
/// In en, this message translates to:
/// **'{service} link copied'**
String shareSheetLinkCopied(Object service);
/// Tooltip for opening a cross-service link inside the app
///
/// In en, this message translates to:
/// **'Open'**
String get shareSheetOpen;
}
class _AppLocalizationsDelegate
+45
View File
@@ -4229,4 +4229,49 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4200,4 +4200,49 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4194,6 +4194,51 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
/// The translations for Spanish Castilian, as used in Spain (`es_ES`).
+45
View File
@@ -4198,4 +4198,49 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4195,4 +4195,49 @@ class AppLocalizationsHi extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4186,4 +4186,49 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'Tidak ada';
@override
String queueRetryAllFailed(int count) {
return 'Coba ulang $count gagal';
}
@override
String get settingsSaveDownloadHistory => 'Simpan riwayat unduhan';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Simpan unduhan selesai di riwayat dan tampilan pustaka';
@override
String get dialogDisableHistoryTitle => 'Matikan riwayat unduhan?';
@override
String get dialogDisableHistoryMessage =>
'Riwayat yang ada akan dihapus. File unduhan tidak akan dihapus.';
@override
String get dialogDisableAndClear => 'Matikan dan hapus';
@override
String get openInOtherServices => 'Buka di layanan lain';
@override
String get shareSheetNoExtensions => 'Tidak ada layanan lain yang kompatibel';
@override
String get shareSheetNotFound => 'Tidak ditemukan';
@override
String get shareSheetCopyLink => 'Salin tautan';
@override
String shareSheetLinkCopied(Object service) {
return 'Tautan $service disalin';
}
@override
String get shareSheetOpen => 'Buka';
}
+45
View File
@@ -4182,4 +4182,49 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4175,4 +4175,49 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4195,4 +4195,49 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4194,6 +4194,51 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
/// The translations for Portuguese, as used in Portugal (`pt_PT`).
+45
View File
@@ -4254,4 +4254,49 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4221,4 +4221,49 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4254,4 +4254,49 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
+45
View File
@@ -4194,6 +4194,51 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get artistReleases => 'Releases';
@override
String get editMetadataSelectNone => 'None';
@override
String queueRetryAllFailed(int count) {
return 'Retry $count failed';
}
@override
String get settingsSaveDownloadHistory => 'Save download history';
@override
String get settingsSaveDownloadHistorySubtitle =>
'Keep completed downloads in history and library views';
@override
String get dialogDisableHistoryTitle => 'Turn off download history?';
@override
String get dialogDisableHistoryMessage =>
'Existing history will be cleared. Downloaded files will not be deleted.';
@override
String get dialogDisableAndClear => 'Turn off and clear';
@override
String get openInOtherServices => 'Open in other services';
@override
String get shareSheetNoExtensions => 'No other compatible services';
@override
String get shareSheetNotFound => 'Not found';
@override
String get shareSheetCopyLink => 'Copy link';
@override
String shareSheetLinkCopied(Object service) {
return '$service link copied';
}
@override
String get shareSheetOpen => 'Open';
}
/// The translations for Chinese, as used in China (`zh_CN`).
+60
View File
@@ -5494,5 +5494,65 @@
"artistReleases": "Releases",
"@artistReleases": {
"description": "Section header for all artist releases"
},
"editMetadataSelectNone": "None",
"@editMetadataSelectNone": {
"description": "Button to clear selected fields for auto-fill"
},
"queueRetryAllFailed": "Retry {count} failed",
"@queueRetryAllFailed": {
"description": "Button to retry every failed download in the queue",
"placeholders": {
"count": {
"type": "int"
}
}
},
"settingsSaveDownloadHistory": "Save download history",
"@settingsSaveDownloadHistory": {
"description": "Settings switch title for storing completed downloads in history"
},
"settingsSaveDownloadHistorySubtitle": "Keep completed downloads in history and library views",
"@settingsSaveDownloadHistorySubtitle": {
"description": "Settings switch subtitle for storing completed downloads in history"
},
"dialogDisableHistoryTitle": "Turn off download history?",
"@dialogDisableHistoryTitle": {
"description": "Confirmation dialog title shown before disabling download history"
},
"dialogDisableHistoryMessage": "Existing history will be cleared. Downloaded files will not be deleted.",
"@dialogDisableHistoryMessage": {
"description": "Confirmation dialog message shown before disabling download history"
},
"dialogDisableAndClear": "Turn off and clear",
"@dialogDisableAndClear": {
"description": "Confirmation action to disable download history and clear existing entries"
},
"openInOtherServices": "Open in other services",
"@openInOtherServices": {
"description": "Title and tooltip for finding the current collection in other services"
},
"shareSheetNoExtensions": "No other compatible services",
"@shareSheetNoExtensions": {
"description": "Empty state when no extensions can be searched for cross-service links"
},
"shareSheetNotFound": "Not found",
"@shareSheetNotFound": {
"description": "Cross-service share sheet row subtitle when a service has no match"
},
"shareSheetCopyLink": "Copy link",
"@shareSheetCopyLink": {
"description": "Tooltip for copying a cross-service link"
},
"shareSheetLinkCopied": "{service} link copied",
"@shareSheetLinkCopied": {
"description": "Snackbar after copying a cross-service link",
"placeholders": {
"service": {}
}
},
"shareSheetOpen": "Open",
"@shareSheetOpen": {
"description": "Tooltip for opening a cross-service link inside the app"
}
}
+60
View File
@@ -4622,5 +4622,65 @@
"queueRateLimitMessage": "Lagu ini mungkin masih tersedia. Tunggu beberapa menit, kurangi unduhan paralel, lalu coba lagi.",
"@queueRateLimitMessage": {
"description": "Explanation shown on a failed queue item when the download service rate limits requests"
},
"editMetadataSelectNone": "Tidak ada",
"@editMetadataSelectNone": {
"description": "Button to clear selected fields for auto-fill"
},
"queueRetryAllFailed": "Coba ulang {count} gagal",
"@queueRetryAllFailed": {
"description": "Button to retry every failed download in the queue",
"placeholders": {
"count": {
"type": "int"
}
}
},
"settingsSaveDownloadHistory": "Simpan riwayat unduhan",
"@settingsSaveDownloadHistory": {
"description": "Settings switch title for storing completed downloads in history"
},
"settingsSaveDownloadHistorySubtitle": "Simpan unduhan selesai di riwayat dan tampilan pustaka",
"@settingsSaveDownloadHistorySubtitle": {
"description": "Settings switch subtitle for storing completed downloads in history"
},
"dialogDisableHistoryTitle": "Matikan riwayat unduhan?",
"@dialogDisableHistoryTitle": {
"description": "Confirmation dialog title shown before disabling download history"
},
"dialogDisableHistoryMessage": "Riwayat yang ada akan dihapus. File unduhan tidak akan dihapus.",
"@dialogDisableHistoryMessage": {
"description": "Confirmation dialog message shown before disabling download history"
},
"dialogDisableAndClear": "Matikan dan hapus",
"@dialogDisableAndClear": {
"description": "Confirmation action to disable download history and clear existing entries"
},
"openInOtherServices": "Buka di layanan lain",
"@openInOtherServices": {
"description": "Title and tooltip for finding the current collection in other services"
},
"shareSheetNoExtensions": "Tidak ada layanan lain yang kompatibel",
"@shareSheetNoExtensions": {
"description": "Empty state when no extensions can be searched for cross-service links"
},
"shareSheetNotFound": "Tidak ditemukan",
"@shareSheetNotFound": {
"description": "Cross-service share sheet row subtitle when a service has no match"
},
"shareSheetCopyLink": "Salin tautan",
"@shareSheetCopyLink": {
"description": "Tooltip for copying a cross-service link"
},
"shareSheetLinkCopied": "Tautan {service} disalin",
"@shareSheetLinkCopied": {
"description": "Snackbar after copying a cross-service link",
"placeholders": {
"service": {}
}
},
"shareSheetOpen": "Buka",
"@shareSheetOpen": {
"description": "Tooltip for opening a cross-service link inside the app"
}
}
+4
View File
@@ -91,6 +91,7 @@ class AppSettings {
final bool
deduplicateDownloads; // Skip downloading tracks already present in history
final bool saveDownloadHistory; // Record completed downloads in local history
const AppSettings({
this.defaultService = '',
@@ -152,6 +153,7 @@ class AppSettings {
this.musixmatchLanguage = '',
this.lastSeenVersion = '',
this.deduplicateDownloads = true,
this.saveDownloadHistory = true,
});
AppSettings copyWith({
@@ -217,6 +219,7 @@ class AppSettings {
String? musixmatchLanguage,
String? lastSeenVersion,
bool? deduplicateDownloads,
bool? saveDownloadHistory,
}) {
return AppSettings(
defaultService: defaultService ?? this.defaultService,
@@ -300,6 +303,7 @@ class AppSettings {
musixmatchLanguage: musixmatchLanguage ?? this.musixmatchLanguage,
lastSeenVersion: lastSeenVersion ?? this.lastSeenVersion,
deduplicateDownloads: deduplicateDownloads ?? this.deduplicateDownloads,
saveDownloadHistory: saveDownloadHistory ?? this.saveDownloadHistory,
);
}
+2
View File
@@ -82,6 +82,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
musixmatchLanguage: json['musixmatchLanguage'] as String? ?? '',
lastSeenVersion: json['lastSeenVersion'] as String? ?? '',
deduplicateDownloads: json['deduplicateDownloads'] as bool? ?? true,
saveDownloadHistory: json['saveDownloadHistory'] as bool? ?? true,
);
Map<String, dynamic> _$AppSettingsToJson(
@@ -147,4 +148,5 @@ Map<String, dynamic> _$AppSettingsToJson(
'musixmatchLanguage': instance.musixmatchLanguage,
'lastSeenVersion': instance.lastSeenVersion,
'deduplicateDownloads': instance.deduplicateDownloads,
'saveDownloadHistory': instance.saveDownloadHistory,
};
+162 -100
View File
@@ -4023,6 +4023,57 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
}
void retryAllFailed() {
final failedIds = state.items
.where(
(item) =>
item.status == DownloadStatus.failed ||
item.status == DownloadStatus.skipped,
)
.map((item) => item.id)
.toSet();
if (failedIds.isEmpty) {
_log.d('retryAllFailed: no failed downloads to retry');
return;
}
_log.i('Retrying ${failedIds.length} failed download(s)');
_locallyCancelledItemIds.removeAll(failedIds);
_pausePendingItemIds.removeAll(failedIds);
for (final item in state.items) {
if (!failedIds.contains(item.id)) continue;
final rgKey = _albumRgKey(item.track);
final rgAcc = _albumRgData[rgKey];
if (rgAcc == null) continue;
rgAcc.entries.removeWhere((entry) => entry.trackId == item.track.id);
if (rgAcc.entries.isEmpty) {
_albumRgData.remove(rgKey);
}
}
final items = state.items
.map((item) {
if (!failedIds.contains(item.id)) return item;
return item.copyWith(
status: DownloadStatus.queued,
progress: 0,
speedMBps: 0,
bytesReceived: 0,
bytesTotal: 0,
error: null,
);
})
.toList(growable: false);
state = state.copyWith(items: items, isPaused: false);
_saveQueueToStorage();
if (!state.isProcessing) {
Future.microtask(() => _processQueue());
}
}
void removeItem(String id) {
final removedItem = state.items.where((item) => item.id == id).firstOrNull;
_locallyCancelledItemIds.remove(id);
@@ -5336,6 +5387,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
DownloadRequestPayload.nativeWorkerContractVersion,
'run_id': runId,
'created_at': DateTime.now().toIso8601String(),
'save_download_history': settings.saveDownloadHistory,
},
);
@@ -5769,22 +5821,26 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
progress: 1.0,
filePath: filePath,
);
final historyItem = result['history_item'];
if (historyItem is Map) {
try {
ref
.read(downloadHistoryProvider.notifier)
.adoptNativeHistoryItem(
DownloadHistoryItem.fromJson(
Map<String, dynamic>.from(historyItem),
),
);
} catch (e) {
_log.w('Failed to adopt native history item: $e');
if (settings.saveDownloadHistory) {
final historyItem = result['history_item'];
if (historyItem is Map) {
try {
ref
.read(downloadHistoryProvider.notifier)
.adoptNativeHistoryItem(
DownloadHistoryItem.fromJson(
Map<String, dynamic>.from(historyItem),
),
);
} catch (e) {
_log.w('Failed to adopt native history item: $e');
await ref
.read(downloadHistoryProvider.notifier)
.reloadFromStorage();
}
} else if (result['history_written'] == true) {
await ref.read(downloadHistoryProvider.notifier).reloadFromStorage();
}
} else if (result['history_written'] == true) {
await ref.read(downloadHistoryProvider.notifier).reloadFromStorage();
}
_completedInSession++;
await _notificationService.showDownloadComplete(
@@ -5989,51 +6045,53 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
backendComposer,
);
ref
.read(downloadHistoryProvider.notifier)
.addToHistory(
DownloadHistoryItem(
id: item.id,
trackName: historyTitle,
artistName: historyArtist,
albumName: historyAlbum,
albumArtist: normalizeOptionalString(trackToDownload.albumArtist),
coverUrl: normalizeCoverReference(trackToDownload.coverUrl),
filePath: filePath,
storageMode: context.storageMode,
downloadTreeUri: context.storageMode == 'saf'
? context.downloadTreeUri
: null,
safRelativeDir: context.storageMode == 'saf'
? context.safRelativeDir
: null,
safFileName: context.storageMode == 'saf'
? ((resultSafFileName != null && resultSafFileName.isNotEmpty)
? resultSafFileName
: context.safFileName)
: null,
safRepaired: false,
service: result['service'] as String? ?? item.service,
downloadedAt: DateTime.now(),
isrc: historyIsrc,
spotifyId: trackToDownload.id,
trackNumber: historyTrackNumber,
totalTracks: historyTotalTracks,
discNumber: historyDiscNumber,
totalDiscs: historyTotalDiscs,
duration: trackToDownload.duration,
releaseDate: historyReleaseDate,
quality: actualQuality,
bitDepth: isLossyOutput ? null : actualBitDepth,
sampleRate: isLossyOutput ? null : actualSampleRate,
bitrate: isLossyOutput ? actualBitrate : null,
format: historyFormat,
genre: normalizeOptionalString(backendGenre),
composer: historyComposer,
label: normalizeOptionalString(backendLabel),
copyright: normalizeOptionalString(backendCopyright),
),
);
if (settings.saveDownloadHistory) {
ref
.read(downloadHistoryProvider.notifier)
.addToHistory(
DownloadHistoryItem(
id: item.id,
trackName: historyTitle,
artistName: historyArtist,
albumName: historyAlbum,
albumArtist: normalizeOptionalString(trackToDownload.albumArtist),
coverUrl: normalizeCoverReference(trackToDownload.coverUrl),
filePath: filePath,
storageMode: context.storageMode,
downloadTreeUri: context.storageMode == 'saf'
? context.downloadTreeUri
: null,
safRelativeDir: context.storageMode == 'saf'
? context.safRelativeDir
: null,
safFileName: context.storageMode == 'saf'
? ((resultSafFileName != null && resultSafFileName.isNotEmpty)
? resultSafFileName
: context.safFileName)
: null,
safRepaired: false,
service: result['service'] as String? ?? item.service,
downloadedAt: DateTime.now(),
isrc: historyIsrc,
spotifyId: trackToDownload.id,
trackNumber: historyTrackNumber,
totalTracks: historyTotalTracks,
discNumber: historyDiscNumber,
totalDiscs: historyTotalDiscs,
duration: trackToDownload.duration,
releaseDate: historyReleaseDate,
quality: actualQuality,
bitDepth: isLossyOutput ? null : actualBitDepth,
sampleRate: isLossyOutput ? null : actualSampleRate,
bitrate: isLossyOutput ? actualBitrate : null,
format: historyFormat,
genre: normalizeOptionalString(backendGenre),
composer: historyComposer,
label: normalizeOptionalString(backendLabel),
copyright: normalizeOptionalString(backendCopyright),
),
);
}
removeItem(item.id);
}
@@ -8659,47 +8717,51 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
backendComposer,
);
ref
.read(downloadHistoryProvider.notifier)
.addToHistory(
DownloadHistoryItem(
id: item.id,
trackName: historyTitle,
artistName: historyArtist,
albumName: historyAlbum,
albumArtist: historyAlbumArtist,
coverUrl: normalizeCoverReference(trackToDownload.coverUrl),
filePath: filePath,
storageMode: effectiveSafMode ? 'saf' : 'app',
downloadTreeUri: effectiveSafMode
? settings.downloadTreeUri
: null,
safRelativeDir: effectiveSafMode ? effectiveOutputDir : null,
safFileName: effectiveSafMode
? (finalSafFileName ?? safFileName)
: null,
safRepaired: false,
service: result['service'] as String? ?? item.service,
downloadedAt: DateTime.now(),
isrc: historyIsrc,
spotifyId: trackToDownload.id,
trackNumber: historyTrackNumber,
totalTracks: historyTotalTracks,
discNumber: historyDiscNumber,
totalDiscs: historyTotalDiscs,
duration: trackToDownload.duration,
releaseDate: historyReleaseDate,
quality: actualQuality,
bitDepth: historyBitDepth,
sampleRate: historySampleRate,
bitrate: historyBitrate,
format: finalFormat,
genre: effectiveGenre,
composer: historyComposer,
label: effectiveLabel,
copyright: effectiveCopyright,
),
);
if (settings.saveDownloadHistory) {
ref
.read(downloadHistoryProvider.notifier)
.addToHistory(
DownloadHistoryItem(
id: item.id,
trackName: historyTitle,
artistName: historyArtist,
albumName: historyAlbum,
albumArtist: historyAlbumArtist,
coverUrl: normalizeCoverReference(trackToDownload.coverUrl),
filePath: filePath,
storageMode: effectiveSafMode ? 'saf' : 'app',
downloadTreeUri: effectiveSafMode
? settings.downloadTreeUri
: null,
safRelativeDir: effectiveSafMode
? effectiveOutputDir
: null,
safFileName: effectiveSafMode
? (finalSafFileName ?? safFileName)
: null,
safRepaired: false,
service: result['service'] as String? ?? item.service,
downloadedAt: DateTime.now(),
isrc: historyIsrc,
spotifyId: trackToDownload.id,
trackNumber: historyTrackNumber,
totalTracks: historyTotalTracks,
discNumber: historyDiscNumber,
totalDiscs: historyTotalDiscs,
duration: trackToDownload.duration,
releaseDate: historyReleaseDate,
quality: actualQuality,
bitDepth: historyBitDepth,
sampleRate: historySampleRate,
bitrate: historyBitrate,
format: finalFormat,
genre: effectiveGenre,
composer: historyComposer,
label: effectiveLabel,
copyright: effectiveCopyright,
),
);
}
removeItem(item.id);
}
+5
View File
@@ -600,6 +600,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
state = state.copyWith(deduplicateDownloads: enabled);
_saveSettings();
}
void setSaveDownloadHistory(bool enabled) {
state = state.copyWith(saveDownloadHistory: enabled);
_saveSettings();
}
}
final settingsProvider = NotifierProvider<SettingsNotifier, AppSettings>(
+42
View File
@@ -21,6 +21,7 @@ import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
import 'package:spotiflac_android/widgets/audio_quality_badges.dart';
import 'package:spotiflac_android/widgets/cross_extension_share_sheet.dart';
class _AlbumCache {
static final Map<String, _CacheEntry> _cache = {};
@@ -566,6 +567,8 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
children: [
_buildLoveAllButton(),
const SizedBox(width: 12),
_buildShareButton(context, tracks, artistName),
const SizedBox(width: 12),
FilledButton.icon(
onPressed: () => _downloadAll(context),
icon: Icon(Icons.download, size: 18),
@@ -846,6 +849,45 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
);
}
Widget _buildShareButton(
BuildContext context,
List<Track> tracks,
String? artistName,
) {
final sourceExtensionId = _directMetadataProviderId() ?? '';
final resolvedArtists =
artistName ??
tracks.firstOrNull?.albumArtist ??
tracks.firstOrNull?.artistName ??
'';
return Container(
width: 48,
height: 48,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withValues(alpha: 0.15),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1,
),
),
child: IconButton(
onPressed: () => CrossExtensionShareSheet.show(
context,
name: widget.albumName,
artists: resolvedArtists,
type: 'album',
sourceExtensionId: sourceExtensionId,
),
icon: const Icon(Icons.open_in_new_rounded, size: 22),
color: Colors.white,
tooltip: context.l10n.openInOtherServices,
padding: EdgeInsets.zero,
),
);
}
Future<void> _loveAll(List<Track> tracks) async {
final notifier = ref.read(libraryCollectionsProvider.notifier);
final state = ref.read(libraryCollectionsProvider);
+26 -4
View File
@@ -23,6 +23,7 @@ import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
import 'package:spotiflac_android/widgets/cross_extension_share_sheet.dart';
class _ArtistCache {
static final Map<String, _CacheEntry> _cache = {};
@@ -1421,10 +1422,31 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
),
);
}),
),
),
),
],
),
),
),
const SizedBox(width: 12),
Container(
width: 52,
height: 52,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: IconButton(
onPressed: () => CrossExtensionShareSheet.show(
context,
name: widget.artistName,
artists: '',
type: 'artist',
sourceExtensionId: _directMetadataProviderId() ?? '',
),
icon: const Icon(Icons.open_in_new_rounded, size: 24),
color: Colors.black87,
tooltip: context.l10n.openInOtherServices,
),
),
],
);
}
+51 -10
View File
@@ -3018,24 +3018,47 @@ class _QueueTabState extends ConsumerState<QueueTab> {
final queueCount = ref.watch(
downloadQueueLookupProvider.select((lookup) => lookup.itemIds.length),
);
final failedCount = ref.watch(
downloadQueueProvider.select((state) => state.failedCount),
);
final isProcessing = ref.watch(
downloadQueueProvider.select((state) => state.isProcessing),
);
if (queueCount == 0) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Row(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
context.l10n.queueDownloadingCount(queueCount),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
Row(
children: [
Text(
context.l10n.queueDownloadingCount(queueCount),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
_buildPauseResumeButton(context, ref, colorScheme),
const SizedBox(width: 4),
_buildClearAllButton(context, ref, colorScheme),
],
),
const Spacer(),
_buildPauseResumeButton(context, ref, colorScheme),
const SizedBox(width: 4),
_buildClearAllButton(context, ref, colorScheme),
if (failedCount > 0 && !isProcessing) ...[
const SizedBox(height: 6),
Align(
alignment: Alignment.centerLeft,
child: _buildRetryAllFailedButton(
context,
ref,
colorScheme,
failedCount,
),
),
],
],
),
),
@@ -4007,6 +4030,24 @@ class _QueueTabState extends ConsumerState<QueueTab> {
);
}
Widget _buildRetryAllFailedButton(
BuildContext context,
WidgetRef ref,
ColorScheme colorScheme,
int failedCount,
) {
return TextButton.icon(
onPressed: () =>
ref.read(downloadQueueProvider.notifier).retryAllFailed(),
icon: const Icon(Icons.replay_rounded, size: 18),
label: Text(context.l10n.queueRetryAllFailed(failedCount)),
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
foregroundColor: colorScheme.primary,
),
);
}
Future<void> _showClearAllDialog(
BuildContext context,
WidgetRef ref,
@@ -114,6 +114,33 @@ class AppSettingsPage extends ConsumerWidget {
subtitle: context.l10n.optionsClearHistorySubtitle,
onTap: () =>
_showClearHistoryDialog(context, ref, colorScheme),
),
SettingsSwitchItem(
icon: Icons.history_toggle_off_outlined,
title: context.l10n.settingsSaveDownloadHistory,
subtitle: context.l10n.settingsSaveDownloadHistorySubtitle,
value: settings.saveDownloadHistory,
onChanged: (enabled) {
if (enabled) {
ref
.read(settingsProvider.notifier)
.setSaveDownloadHistory(true);
return;
}
final hasHistory = ref
.read(downloadHistoryProvider)
.items
.isNotEmpty;
if (hasHistory) {
_showDisableHistoryDialog(context, ref, colorScheme);
return;
}
ref
.read(settingsProvider.notifier)
.setSaveDownloadHistory(false);
},
showDivider: false,
),
],
@@ -148,6 +175,40 @@ class AppSettingsPage extends ConsumerWidget {
);
}
void _showDisableHistoryDialog(
BuildContext context,
WidgetRef ref,
ColorScheme colorScheme,
) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.dialogDisableHistoryTitle),
content: Text(context.l10n.dialogDisableHistoryMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.dialogCancel),
),
TextButton(
onPressed: () {
ref.read(downloadHistoryProvider.notifier).clearHistory();
ref.read(settingsProvider.notifier).setSaveDownloadHistory(false);
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.snackbarHistoryCleared)),
);
},
child: Text(
context.l10n.dialogDisableAndClear,
style: TextStyle(color: colorScheme.error),
),
),
],
),
);
}
void _showClearHistoryDialog(
BuildContext context,
WidgetRef ref,
@@ -350,6 +350,10 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
});
}
void _selectNoFields() {
setState(_autoFillFields.clear);
}
String _normalizeMetadataText(String value) {
final collapsed = value
.toLowerCase()
@@ -1474,6 +1478,12 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
onTap: _selectEmptyFields,
cs: cs,
),
const SizedBox(width: 8),
_quickSelectButton(
label: context.l10n.editMetadataSelectNone,
onTap: _selectNoFields,
cs: cs,
),
],
),
),
@@ -0,0 +1,52 @@
import 'package:spotiflac_android/services/platform_bridge.dart';
class CrossExtensionShareResult {
final String extensionId;
final String displayName;
final bool found;
final String? url;
final String? itemName;
final String? itemArtists;
final String? error;
const CrossExtensionShareResult({
required this.extensionId,
required this.displayName,
required this.found,
this.url,
this.itemName,
this.itemArtists,
this.error,
});
factory CrossExtensionShareResult.fromJson(Map<String, dynamic> json) {
return CrossExtensionShareResult(
extensionId: json['extension_id'] as String? ?? '',
displayName: json['display_name'] as String? ?? '',
found: json['found'] as bool? ?? false,
url: json['url'] as String?,
itemName: json['item_name'] as String?,
itemArtists: json['item_artists'] as String?,
error: json['error'] as String?,
);
}
}
class CrossExtensionShareService {
const CrossExtensionShareService();
Future<List<CrossExtensionShareResult>> findAcrossExtensions({
required String name,
required String artists,
required String type,
required String sourceExtensionId,
}) async {
final results = await PlatformBridge.findCollectionAcrossExtensions(
name: name,
artists: artists,
type: type,
sourceExtensionId: sourceExtensionId,
);
return results.map(CrossExtensionShareResult.fromJson).toList();
}
}
+8
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@@ -236,6 +237,7 @@ class NotificationService {
bool alreadyInLibrary = false,
}) async {
if (!_isInitialized) await initialize();
unawaited(HapticFeedback.mediumImpact());
String title;
if (alreadyInLibrary) {
@@ -286,6 +288,11 @@ class NotificationService {
}) async {
if (!_isInitialized) await initialize();
if (completedCount <= 0 && failedCount <= 0) return;
unawaited(
failedCount > 0
? HapticFeedback.heavyImpact()
: HapticFeedback.mediumImpact(),
);
final title = failedCount > 0
? (_l10n?.notifDownloadsFinished(completedCount, failedCount) ??
@@ -330,6 +337,7 @@ class NotificationService {
Future<void> showQueueCanceled({required int canceledCount}) async {
if (!_isInitialized) await initialize();
if (canceledCount <= 0) return;
unawaited(HapticFeedback.lightImpact());
final title = _l10n?.notifDownloadsCanceledTitle ?? 'Downloads canceled';
final body =
+19
View File
@@ -1309,6 +1309,25 @@ class PlatformBridge {
return _decodeMapListResult(result, 'searchTracksWithMetadataProviders');
}
static Future<List<Map<String, dynamic>>> findCollectionAcrossExtensions({
required String name,
required String artists,
required String type,
required String sourceExtensionId,
}) async {
final requestJson = jsonEncode({
'name': name,
'artists': artists,
'type': type,
'source_extension_id': sourceExtensionId,
});
final result = await _channel.invokeMethod(
'findCollectionAcrossExtensions',
requestJson,
);
return _decodeMapListResult(result, 'findCollectionAcrossExtensions');
}
static Future<void> cleanupExtensions() async {
_log.d('cleanupExtensions');
await _channel.invokeMethod('cleanupExtensions');
+6
View File
@@ -31,6 +31,12 @@ class ShareIntentService {
return url;
}
void injectUrl(String url) {
if (url.isEmpty) return;
_pendingUrl = url;
_sharedUrlController.add(url);
}
Future<void> initialize() async {
if (_initialized) return;
_initialized = true;
+2 -3
View File
@@ -36,15 +36,14 @@ class AppAnnouncementDialog extends StatelessWidget {
} catch (_) {
launched = false;
}
if (!context.mounted) return;
if (!launched) {
_showCtaOpenFailed(context);
return;
}
onDismiss();
if (context.mounted) {
Navigator.pop(context);
}
Navigator.pop(context);
}
void _showCtaOpenFailed(BuildContext context) {
@@ -0,0 +1,248 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/services/cross_extension_share_service.dart';
import 'package:spotiflac_android/services/share_intent_service.dart';
class CrossExtensionShareSheet extends StatefulWidget {
final String name;
final String artists;
final String type;
final String sourceExtensionId;
const CrossExtensionShareSheet({
super.key,
required this.name,
required this.artists,
required this.type,
required this.sourceExtensionId,
});
static Future<void> show(
BuildContext context, {
required String name,
required String artists,
required String type,
required String sourceExtensionId,
}) {
final colorScheme = Theme.of(context).colorScheme;
return showModalBottomSheet<void>(
context: context,
useRootNavigator: true,
isScrollControlled: true,
backgroundColor: colorScheme.surfaceContainerHigh,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
builder: (_) => CrossExtensionShareSheet(
name: name,
artists: artists,
type: type,
sourceExtensionId: sourceExtensionId,
),
);
}
@override
State<CrossExtensionShareSheet> createState() =>
_CrossExtensionShareSheetState();
}
class _CrossExtensionShareSheetState extends State<CrossExtensionShareSheet> {
late final Future<List<CrossExtensionShareResult>> _future;
@override
void initState() {
super.initState();
_future = const CrossExtensionShareService()
.findAcrossExtensions(
name: widget.name,
artists: widget.artists,
type: widget.type,
sourceExtensionId: widget.sourceExtensionId,
)
.then((results) {
final sorted = [...results];
sorted.sort((a, b) {
if (a.found != b.found) return a.found ? -1 : 1;
return a.displayName.compareTo(b.displayName);
});
return sorted;
});
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return SafeArea(
top: false,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.sizeOf(context).height * 0.82,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(2),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 4),
child: Text(
context.l10n.openInOtherServices,
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
child: Text(
widget.artists.isNotEmpty
? '${widget.name} - ${widget.artists}'
: widget.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
Flexible(
child: FutureBuilder<List<CrossExtensionShareResult>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const SizedBox(
height: 180,
child: Center(child: CircularProgressIndicator()),
);
}
final results = snapshot.data ?? const [];
if (results.isEmpty) {
return SizedBox(
height: 180,
child: Center(
child: Text(
context.l10n.shareSheetNoExtensions,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
);
}
return ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(12, 4, 12, 16),
itemBuilder: (context, index) {
return _CrossExtensionShareTile(result: results[index]);
},
separatorBuilder: (_, _) =>
const Divider(height: 1, indent: 72),
itemCount: results.length,
);
},
),
),
],
),
),
);
}
}
class _CrossExtensionShareTile extends StatelessWidget {
final CrossExtensionShareResult result;
const _CrossExtensionShareTile({required this.result});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final url = result.found ? result.url : null;
final hasUrl = url != null && url.isNotEmpty;
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: hasUrl
? colorScheme.primaryContainer
: colorScheme.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(
hasUrl ? Icons.link_rounded : Icons.link_off_rounded,
color: hasUrl
? colorScheme.onPrimaryContainer
: colorScheme.onSurfaceVariant,
),
),
title: Text(
result.displayName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600),
),
subtitle: Text(
hasUrl
? (result.itemName?.isNotEmpty == true ? result.itemName! : url)
: context.l10n.shareSheetNotFound,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.bodySmall?.copyWith(
color: hasUrl ? colorScheme.primary : colorScheme.onSurfaceVariant,
),
),
trailing: hasUrl
? Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: context.l10n.shareSheetCopyLink,
icon: const Icon(Icons.copy_rounded, size: 20),
onPressed: () {
Clipboard.setData(ClipboardData(text: url));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.shareSheetLinkCopied(result.displayName),
),
),
);
},
),
IconButton(
tooltip: context.l10n.shareSheetOpen,
icon: const Icon(Icons.open_in_new_rounded, size: 20),
color: colorScheme.primary,
onPressed: () {
Navigator.pop(context);
ShareIntentService().injectUrl(url);
},
),
],
)
: null,
);
}
}