fix(download): swap local track variant actions

This commit is contained in:
zarzet
2026-07-27 14:31:39 +07:00
parent 9b4a960324
commit af6ca12e85
6 changed files with 122 additions and 23 deletions
+5 -1
View File
@@ -433,6 +433,7 @@ extension _ArtistScreenSections on _ArtistScreenState {
context,
ref,
track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
@@ -527,7 +528,10 @@ extension _ArtistScreenSections on _ArtistScreenState {
],
),
),
TrackCollectionQuickActions(track: track),
TrackCollectionQuickActions(
track: track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
],
),
),
+5 -1
View File
@@ -264,6 +264,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
context,
ref,
track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
splashColor: colorScheme.primary.withValues(alpha: 0.12),
highlightColor: colorScheme.primary.withValues(alpha: 0.08),
@@ -336,7 +337,10 @@ class _TrackItemWithStatus extends ConsumerWidget {
),
),
PreviewButton(track: track),
TrackCollectionQuickActions(track: track),
TrackCollectionQuickActions(
track: track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
],
),
),
@@ -0,0 +1,15 @@
enum QualityVariantMenuAction { downloadAnotherQuality, playLocal }
/// Resolves the quality-variant entry shown in a remote track's options menu.
///
/// When tapping an already-local track is repurposed to download another
/// quality, the menu becomes the escape hatch for playing that local copy.
QualityVariantMenuAction? resolveQualityVariantMenuAction({
required bool allowQualityVariants,
required bool hasLocalPlaybackCandidate,
}) {
if (!allowQualityVariants) return null;
return hasLocalPlaybackCandidate
? QualityVariantMenuAction.playLocal
: QualityVariantMenuAction.downloadAnotherQuality;
}
+56 -20
View File
@@ -6,20 +6,28 @@ import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/services/cover_cache_manager.dart';
import 'package:spotiflac_android/utils/local_playback.dart';
import 'package:spotiflac_android/widgets/playlist_picker_sheet.dart';
import 'package:spotiflac_android/widgets/track_collection_action_policy.dart';
import 'package:spotiflac_android/widgets/track_detail_actions.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
class TrackCollectionQuickActions extends ConsumerWidget {
final Track track;
final bool hasLocalPlaybackCandidate;
const TrackCollectionQuickActions({super.key, required this.track});
const TrackCollectionQuickActions({
super.key,
required this.track,
this.hasLocalPlaybackCandidate = false,
});
static void showTrackOptionsSheet(
BuildContext context,
WidgetRef ref,
Track track,
) {
Track track, {
bool hasLocalPlaybackCandidate = false,
}) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet<void>(
context: context,
@@ -29,7 +37,10 @@ class TrackCollectionQuickActions extends ConsumerWidget {
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
),
builder: (sheetContext) => _TrackOptionsSheet(track: track),
builder: (sheetContext) => _TrackOptionsSheet(
track: track,
hasLocalPlaybackCandidate: hasLocalPlaybackCandidate,
),
);
}
@@ -44,7 +55,12 @@ class TrackCollectionQuickActions extends ConsumerWidget {
color: colorScheme.onSurfaceVariant,
size: 20,
),
onPressed: () => showTrackOptionsSheet(context, ref, track),
onPressed: () => showTrackOptionsSheet(
context,
ref,
track,
hasLocalPlaybackCandidate: hasLocalPlaybackCandidate,
),
padding: const EdgeInsets.only(left: 12),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
);
@@ -53,8 +69,12 @@ class TrackCollectionQuickActions extends ConsumerWidget {
class _TrackOptionsSheet extends ConsumerWidget {
final Track track;
final bool hasLocalPlaybackCandidate;
const _TrackOptionsSheet({required this.track});
const _TrackOptionsSheet({
required this.track,
required this.hasLocalPlaybackCandidate,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -69,6 +89,10 @@ class _TrackOptionsSheet extends ConsumerWidget {
final allowQualityVariants = ref.watch(
settingsProvider.select((settings) => settings.allowQualityVariants),
);
final qualityVariantAction = resolveQualityVariantMenuAction(
allowQualityVariants: allowQualityVariants,
hasLocalPlaybackCandidate: hasLocalPlaybackCandidate,
);
return SafeArea(
child: ConstrainedBox(
@@ -168,23 +192,18 @@ class _TrackOptionsSheet extends ConsumerWidget {
color: colorScheme.outlineVariant.withValues(alpha: 0.5),
),
if (allowQualityVariants)
if (qualityVariantAction == QualityVariantMenuAction.playLocal)
_OptionTile(
icon: Icons.play_arrow_rounded,
title: context.l10n.trackMetadataPlay,
onTap: () => _playLocal(context, ref),
)
else if (qualityVariantAction ==
QualityVariantMenuAction.downloadAnotherQuality)
_OptionTile(
icon: Icons.download_outlined,
title: context.l10n.trackOptionDownloadQualityVariant,
onTap: () {
final rootContext = Navigator.of(
context,
rootNavigator: true,
).context;
Navigator.pop(context);
downloadSingleTrack(
rootContext,
ref,
track,
forceQualityPicker: true,
);
},
onTap: () => _downloadQualityVariant(context, ref),
),
_OptionTile(
@@ -255,6 +274,23 @@ class _TrackOptionsSheet extends ConsumerWidget {
),
);
}
void _downloadQualityVariant(BuildContext context, WidgetRef ref) {
final rootContext = Navigator.of(context, rootNavigator: true).context;
Navigator.pop(context);
downloadSingleTrack(rootContext, ref, track, forceQualityPicker: true);
}
Future<void> _playLocal(BuildContext context, WidgetRef ref) async {
final rootContext = Navigator.of(context, rootNavigator: true).context;
Navigator.pop(context);
final played = await playLocalIfAvailable(rootContext, ref, track);
if (!played && rootContext.mounted) {
ScaffoldMessenger.of(rootContext).showSnackBar(
SnackBar(content: Text(rootContext.l10n.snackbarFileNotFound)),
);
}
}
}
class _OptionTile extends StatelessWidget {
+5 -1
View File
@@ -116,7 +116,10 @@ class TrackListTile extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
children: [
PreviewButton(track: track),
TrackCollectionQuickActions(track: track),
TrackCollectionQuickActions(
track: track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
],
),
onTap: () => _handleTap(
@@ -129,6 +132,7 @@ class TrackListTile extends ConsumerWidget {
context,
ref,
track,
hasLocalPlaybackCandidate: isInHistory || isInLocalLibrary,
),
),
),
@@ -0,0 +1,36 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/widgets/track_collection_action_policy.dart';
void main() {
group('quality variant track menu', () {
test('is hidden when quality variants are disabled', () {
expect(
resolveQualityVariantMenuAction(
allowQualityVariants: false,
hasLocalPlaybackCandidate: true,
),
isNull,
);
});
test('offers another download when the track is not local', () {
expect(
resolveQualityVariantMenuAction(
allowQualityVariants: true,
hasLocalPlaybackCandidate: false,
),
QualityVariantMenuAction.downloadAnotherQuality,
);
});
test('swaps to local playback when row tap handles the download', () {
expect(
resolveQualityVariantMenuAction(
allowQualityVariants: true,
hasLocalPlaybackCandidate: true,
),
QualityVariantMenuAction.playLocal,
);
});
});
}