feat(library): add lyrics filtering and metadata actions

This commit is contained in:
zarzet
2026-08-27 01:44:06 +07:00
parent 04c42bd511
commit 1a7c20111a
60 changed files with 1454 additions and 276 deletions
+115
View File
@@ -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,