mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 13:22:49 +02:00
feat(search): save max-quality cover on long press #511
This commit is contained in:
@@ -22,6 +22,7 @@ import 'package:spotiflac_android/screens/album_screen.dart';
|
||||
import 'package:spotiflac_android/screens/artist_screen.dart';
|
||||
import 'package:spotiflac_android/screens/home_search_logic.dart';
|
||||
import 'package:spotiflac_android/services/csv_import_service.dart';
|
||||
import 'package:spotiflac_android/services/cover_download_service.dart';
|
||||
import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/utils/adaptive_layout.dart';
|
||||
@@ -75,6 +76,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
bool _embeddedCoverRefreshScheduled = false;
|
||||
List<Extension>? _thumbnailSizesExtensionsCache;
|
||||
bool _isCsvImporting = false;
|
||||
final Set<String> _activeCoverDownloads = <String>{};
|
||||
|
||||
void _setCsvImporting(bool value) {
|
||||
if (_isCsvImporting == value) return;
|
||||
|
||||
@@ -2,6 +2,51 @@
|
||||
part of 'home_tab.dart';
|
||||
|
||||
extension _HomeTabSearchResultsUI on _HomeTabState {
|
||||
Future<void> _saveSearchResultCover(Track item) async {
|
||||
final coverUrl = item.coverUrl?.trim() ?? '';
|
||||
if (coverUrl.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.trackCoverNoSource)));
|
||||
return;
|
||||
}
|
||||
if (!_activeCoverDownloads.add(coverUrl)) return;
|
||||
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(content: Text(context.l10n.updateDownloading)));
|
||||
|
||||
final baseName = item.isCollection || item.artistName.trim().isEmpty
|
||||
? item.name
|
||||
: '${item.artistName} - ${item.name}';
|
||||
try {
|
||||
final saved = await CoverDownloadService.saveRemoteCover(
|
||||
coverUrl: coverUrl,
|
||||
baseName: baseName,
|
||||
settings: ref.read(settingsProvider),
|
||||
);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.trackCoverSaved(saved.fileName))),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.trackSaveFailed(context.friendlyError(error)),
|
||||
),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
_activeCoverDownloads.remove(coverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildErrorWidget(String error, ColorScheme colorScheme) {
|
||||
final l10n = context.l10n;
|
||||
final isRateLimit =
|
||||
@@ -312,6 +357,7 @@ extension _HomeTabSearchResultsUI on _HomeTabState {
|
||||
item: artistItems[index],
|
||||
showDivider: showDivider,
|
||||
onTap: () => _navigateToExtensionArtist(artistItems[index]),
|
||||
onSaveCover: () => _saveSearchResultCover(artistItems[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -330,6 +376,7 @@ extension _HomeTabSearchResultsUI on _HomeTabState {
|
||||
item: albumItems[index],
|
||||
showDivider: showDivider,
|
||||
onTap: () => _navigateToExtensionAlbum(albumItems[index]),
|
||||
onSaveCover: () => _saveSearchResultCover(albumItems[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -348,6 +395,7 @@ extension _HomeTabSearchResultsUI on _HomeTabState {
|
||||
item: playlistItems[index],
|
||||
showDivider: showDivider,
|
||||
onTap: () => _navigateToExtensionPlaylist(playlistItems[index]),
|
||||
onSaveCover: () => _saveSearchResultCover(playlistItems[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -386,6 +434,7 @@ extension _HomeTabSearchResultsUI on _HomeTabState {
|
||||
isInHistory: existingHistoryKeys.contains(
|
||||
historyLookups[index].lookupKey,
|
||||
),
|
||||
onSaveCover: () => _saveSearchResultCover(sortedTracks[index]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -196,6 +196,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
final String? searchExtensionId;
|
||||
final bool showLocalLibraryIndicator;
|
||||
final Map<String, (double, double)> thumbnailSizesByExtensionId;
|
||||
final VoidCallback onSaveCover;
|
||||
|
||||
/// Resolved by the result page via one batch lookup instead of a per-row
|
||||
/// exists query (which in SAF mode also costs a bridge call per row).
|
||||
@@ -211,6 +212,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
required this.showLocalLibraryIndicator,
|
||||
required this.thumbnailSizesByExtensionId,
|
||||
required this.isInHistory,
|
||||
required this.onSaveCover,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -248,6 +250,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
}
|
||||
|
||||
final isQueued = queueItem != null;
|
||||
final hasCover = track.coverUrl?.isNotEmpty == true;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -272,24 +275,36 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 6, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: track.coverUrl != null
|
||||
? CachedCoverImage(
|
||||
imageUrl: track.coverUrl!,
|
||||
width: thumbWidth,
|
||||
height: thumbHeight,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
width: thumbWidth,
|
||||
height: thumbHeight,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.music_note,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
label:
|
||||
'${context.l10n.dialogDownload} '
|
||||
'${context.l10n.editMetadataFieldCover}: ${track.name}',
|
||||
button: hasCover,
|
||||
onLongPress: hasCover ? onSaveCover : null,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
excludeFromSemantics: true,
|
||||
onLongPress: hasCover ? onSaveCover : null,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: hasCover
|
||||
? CachedCoverImage(
|
||||
imageUrl: track.coverUrl!,
|
||||
width: thumbWidth,
|
||||
height: thumbHeight,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
width: thumbWidth,
|
||||
height: thumbHeight,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.music_note,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
@@ -417,12 +432,14 @@ class _CollectionItemWidget extends StatelessWidget {
|
||||
final Track item;
|
||||
final bool showDivider;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onSaveCover;
|
||||
|
||||
const _CollectionItemWidget({
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.showDivider,
|
||||
required this.onTap,
|
||||
required this.onSaveCover,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -435,21 +452,37 @@ class _CollectionItemWidget extends StatelessWidget {
|
||||
if (isPlaylist) placeholderIcon = Icons.playlist_play;
|
||||
if (isArtist) placeholderIcon = Icons.person;
|
||||
|
||||
final cover = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(isArtist ? 28 : 10),
|
||||
child: item.coverUrl != null && item.coverUrl!.isNotEmpty
|
||||
? CachedCoverImage(
|
||||
imageUrl: item.coverUrl!,
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(placeholderIcon, color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
final hasCover = item.coverUrl != null && item.coverUrl!.isNotEmpty;
|
||||
final cover = Semantics(
|
||||
label:
|
||||
'${context.l10n.dialogDownload} '
|
||||
'${context.l10n.editMetadataFieldCover}: ${item.name}',
|
||||
button: hasCover,
|
||||
onLongPress: hasCover ? onSaveCover : null,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
excludeFromSemantics: true,
|
||||
onLongPress: hasCover ? onSaveCover : null,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(isArtist ? 28 : 10),
|
||||
child: hasCover
|
||||
? CachedCoverImage(
|
||||
imageUrl: item.coverUrl!,
|
||||
width: 56,
|
||||
height: 56,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
placeholderIcon,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
Reference in New Issue
Block a user