mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-07 05:07:59 +02:00
d034144e9c
Resolve API (api.zarz.moe): - Refactor songlink.go: Spotify URLs use resolve API, non-Spotify uses SongLink API - Add SongLink fallback when resolve API fails for Spotify (two-layer resilience) - Remove dead code: page parser, XOR-obfuscated keys, legacy helpers Multi-artist tag fix (#288): - Add RewriteSplitArtistTags() in Go to rewrite ARTIST/ALBUMARTIST as split Vorbis comments - Wire method channel handler in Android (MainActivity.kt) and iOS (AppDelegate.swift) - Add PlatformBridge.rewriteSplitArtistTags() in Dart - Call native FLAC rewriter after FFmpeg embed when split_vorbis mode is active - Extract deezerTrackArtistDisplay() helper to use Contributors in album/playlist tracks Code cleanup: - Remove unused imports, dead code, and redundant comments across Go and Dart - Fix build: remove stale getQobuzDebugKey() reference in deezer_download.go
270 lines
9.9 KiB
Dart
270 lines
9.9 KiB
Dart
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:spotiflac_android/l10n/l10n.dart';
|
|
import 'package:spotiflac_android/models/track.dart';
|
|
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
|
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
|
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
|
|
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
|
|
|
class TrackCollectionQuickActions extends ConsumerWidget {
|
|
final Track track;
|
|
|
|
const TrackCollectionQuickActions({super.key, required this.track});
|
|
|
|
static void showTrackOptionsSheet(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
Track track,
|
|
) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
useRootNavigator: true,
|
|
isScrollControlled: true,
|
|
backgroundColor: colorScheme.surfaceContainerHigh,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
|
),
|
|
builder: (sheetContext) => _TrackOptionsSheet(track: track),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
return IconButton(
|
|
tooltip: MaterialLocalizations.of(context).showMenuTooltip,
|
|
icon: Icon(
|
|
Icons.more_vert,
|
|
color: colorScheme.onSurfaceVariant,
|
|
size: 20,
|
|
),
|
|
onPressed: () => showTrackOptionsSheet(context, ref, track),
|
|
padding: const EdgeInsets.only(left: 12),
|
|
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TrackOptionsSheet extends ConsumerWidget {
|
|
final Track track;
|
|
|
|
const _TrackOptionsSheet({required this.track});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
|
|
final isLoved = ref.watch(
|
|
libraryCollectionsProvider.select((state) => state.isLoved(track)),
|
|
);
|
|
final isInWishlist = ref.watch(
|
|
libraryCollectionsProvider.select((state) => state.isInWishlist(track)),
|
|
);
|
|
|
|
return SafeArea(
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxHeight: MediaQuery.of(context).size.height * 0.82,
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Column(
|
|
children: [
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.onSurfaceVariant.withValues(
|
|
alpha: 0.4,
|
|
),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
|
child: Row(
|
|
children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child:
|
|
track.coverUrl != null &&
|
|
track.coverUrl!.isNotEmpty
|
|
? CachedNetworkImage(
|
|
imageUrl: track.coverUrl!,
|
|
width: 56,
|
|
height: 56,
|
|
fit: BoxFit.cover,
|
|
memCacheWidth: 112,
|
|
cacheManager: CoverCacheManager.instance,
|
|
errorWidget: (context, url, error) =>
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
color:
|
|
colorScheme.surfaceContainerHighest,
|
|
child: Icon(
|
|
Icons.music_note,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
)
|
|
: Container(
|
|
width: 56,
|
|
height: 56,
|
|
color: colorScheme.surfaceContainerHighest,
|
|
child: Icon(
|
|
Icons.music_note,
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
track.name,
|
|
style: Theme.of(context).textTheme.titleMedium
|
|
?.copyWith(fontWeight: FontWeight.w600),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
ClickableArtistName(
|
|
artistName: track.artistName,
|
|
artistId: track.artistId,
|
|
coverUrl: track.coverUrl,
|
|
style: Theme.of(context).textTheme.bodyMedium
|
|
?.copyWith(
|
|
color: colorScheme.onSurfaceVariant,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Divider(
|
|
height: 1,
|
|
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
|
|
),
|
|
|
|
_OptionTile(
|
|
icon: isLoved ? Icons.favorite : Icons.favorite_border,
|
|
iconColor: isLoved ? colorScheme.error : null,
|
|
title: isLoved
|
|
? context.l10n.trackOptionRemoveFromLoved
|
|
: context.l10n.trackOptionAddToLoved,
|
|
onTap: () async {
|
|
Navigator.pop(context);
|
|
final added = await ref
|
|
.read(libraryCollectionsProvider.notifier)
|
|
.toggleLoved(track);
|
|
if (!context.mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
added
|
|
? context.l10n.collectionAddedToLoved(track.name)
|
|
: context.l10n.collectionRemovedFromLoved(
|
|
track.name,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
_OptionTile(
|
|
icon: isInWishlist
|
|
? Icons.playlist_add_check_circle
|
|
: Icons.add_circle_outline,
|
|
iconColor: isInWishlist ? colorScheme.primary : null,
|
|
title: isInWishlist
|
|
? context.l10n.trackOptionRemoveFromWishlist
|
|
: context.l10n.trackOptionAddToWishlist,
|
|
onTap: () async {
|
|
Navigator.pop(context);
|
|
final added = await ref
|
|
.read(libraryCollectionsProvider.notifier)
|
|
.toggleWishlist(track);
|
|
if (!context.mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
added
|
|
? context.l10n.collectionAddedToWishlist(track.name)
|
|
: context.l10n.collectionRemovedFromWishlist(
|
|
track.name,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
_OptionTile(
|
|
icon: Icons.playlist_add,
|
|
title: context.l10n.collectionAddToPlaylist,
|
|
onTap: () {
|
|
Navigator.pop(context);
|
|
showAddTrackToPlaylistSheet(context, ref, track);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _OptionTile extends StatelessWidget {
|
|
final IconData icon;
|
|
final Color? iconColor;
|
|
final String title;
|
|
final VoidCallback onTap;
|
|
|
|
const _OptionTile({
|
|
required this.icon,
|
|
this.iconColor,
|
|
required this.title,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final colorScheme = Theme.of(context).colorScheme;
|
|
return ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
|
leading: Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(
|
|
color: colorScheme.primaryContainer,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: iconColor ?? colorScheme.onPrimaryContainer,
|
|
size: 20,
|
|
),
|
|
),
|
|
title: Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
|
|
onTap: onTap,
|
|
);
|
|
}
|
|
}
|