mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-03 08:40:44 +02:00
feat(library): add lyrics filtering and metadata actions
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/services/cover_download_service.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
|
||||
/// Adds the app-wide long-press-to-save behavior to a remote cover image.
|
||||
///
|
||||
/// The child keeps its normal tap behavior. Only the long-press gesture is
|
||||
/// claimed here, so a surrounding track or collection card can still open its
|
||||
/// usual options when the user presses outside the artwork.
|
||||
class DownloadableCover extends ConsumerStatefulWidget {
|
||||
final String? coverUrl;
|
||||
final String baseName;
|
||||
final Widget child;
|
||||
final bool enabled;
|
||||
|
||||
const DownloadableCover({
|
||||
super.key,
|
||||
required this.coverUrl,
|
||||
required this.baseName,
|
||||
required this.child,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<DownloadableCover> createState() => _DownloadableCoverState();
|
||||
}
|
||||
|
||||
class _DownloadableCoverState extends ConsumerState<DownloadableCover> {
|
||||
bool _saving = false;
|
||||
|
||||
String get _coverUrl => normalizeRemoteHttpUrl(widget.coverUrl) ?? '';
|
||||
|
||||
Future<void> _saveCover() async {
|
||||
if (_saving) return;
|
||||
final coverUrl = _coverUrl;
|
||||
if (coverUrl.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(context.l10n.trackCoverNoSource)));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _saving = true);
|
||||
ScaffoldMessenger.of(context)
|
||||
..hideCurrentSnackBar()
|
||||
..showSnackBar(SnackBar(content: Text(context.l10n.updateDownloading)));
|
||||
|
||||
try {
|
||||
final saved = await CoverDownloadService.saveRemoteCover(
|
||||
coverUrl: coverUrl,
|
||||
baseName: widget.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 {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canSave = widget.enabled && _coverUrl.isNotEmpty;
|
||||
final semanticLabel =
|
||||
'${context.l10n.dialogDownload} '
|
||||
'${context.l10n.editMetadataFieldCover}: ${widget.baseName}';
|
||||
|
||||
return Semantics(
|
||||
button: canSave,
|
||||
label: semanticLabel,
|
||||
onLongPress: canSave ? _saveCover : null,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
excludeFromSemantics: true,
|
||||
onLongPress: canSave ? _saveCover : null,
|
||||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
widget.child,
|
||||
if (_saving)
|
||||
const Positioned.fill(
|
||||
child: ColoredBox(
|
||||
color: Color(0x33000000),
|
||||
child: Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 22,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
@@ -194,6 +195,23 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
||||
onTap: () => _downloadQualityVariant(context, ref),
|
||||
),
|
||||
|
||||
_OptionTile(
|
||||
icon: Icons.content_copy_rounded,
|
||||
title: context.l10n.trackOptionCopyTrackName,
|
||||
onTap: () => _copyText(context, track.name),
|
||||
),
|
||||
_OptionTile(
|
||||
icon: Icons.person_outline_rounded,
|
||||
title: context.l10n.trackOptionCopyArtist,
|
||||
onTap: () => _copyText(context, track.artistName),
|
||||
),
|
||||
_OptionTile(
|
||||
icon: Icons.copy_all_rounded,
|
||||
title: context.l10n.trackOptionCopyTrackAndArtist,
|
||||
onTap: () =>
|
||||
_copyText(context, '${track.name} - ${track.artistName}'),
|
||||
),
|
||||
|
||||
_OptionTile(
|
||||
icon: Icons.album_outlined,
|
||||
title: context.l10n.homeGoToAlbum,
|
||||
@@ -275,6 +293,15 @@ class _TrackOptionsSheet extends ConsumerWidget {
|
||||
downloadSingleTrack(rootContext, ref, track, forceQualityPicker: true);
|
||||
}
|
||||
|
||||
void _copyText(BuildContext context, String text) {
|
||||
final rootContext = Navigator.of(context, rootNavigator: true).context;
|
||||
Navigator.pop(context);
|
||||
Clipboard.setData(ClipboardData(text: text));
|
||||
ScaffoldMessenger.of(rootContext).showSnackBar(
|
||||
SnackBar(content: Text(rootContext.l10n.trackCopiedToClipboard)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _goToAlbum(BuildContext context) async {
|
||||
final navigationContext = Navigator.of(
|
||||
context,
|
||||
|
||||
Reference in New Issue
Block a user