feat(navigation): add go to album actions

This commit is contained in:
zarzet
2026-08-01 16:06:08 +07:00
parent 6a3a85f7c9
commit 645a324a5f
7 changed files with 254 additions and 22 deletions
+87
View File
@@ -5,9 +5,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/download_history_provider.dart';
import 'package:spotiflac_android/providers/music_player_provider.dart';
import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
import 'package:spotiflac_android/screens/local_album_screen.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/services/music_player_service.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/int_utils.dart';
import 'package:spotiflac_android/utils/lyrics_parser.dart';
@@ -729,6 +733,12 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
padding: const EdgeInsets.only(bottom: 16),
child: SettingsGroup(
children: [
if ((mediaItem.album ?? '').trim().isNotEmpty)
SettingsItem(
icon: Icons.album_outlined,
title: sheetContext.l10n.homeGoToAlbum,
onTap: () => Navigator.of(sheetContext).pop('album'),
),
SettingsItem(
icon: Icons.info_outline,
title: sheetContext.l10n.nowPlayingDetails,
@@ -751,6 +761,9 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
);
if (!mounted) return;
switch (action) {
case 'album':
await _goToCurrentAlbum(mediaItem: mediaItem, source: source);
break;
case 'details':
_showDetailsSheet(colorScheme);
break;
@@ -760,6 +773,80 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
}
}
Future<void> _goToCurrentAlbum({
required MediaItem mediaItem,
required String source,
}) async {
final albumName = (mediaItem.album ?? '').trim();
if (albumName.isEmpty) return;
// Prefer the stored collection so this action remains useful offline and
// opens the exact files the user is currently playing.
try {
final historyItem = source.isEmpty
? null
: await ref
.read(downloadHistoryProvider.notifier)
.getByFilePathAsync(source);
if (!mounted) return;
if (historyItem != null) {
final albumArtist = (historyItem.albumArtist ?? '').trim();
pushViaPreferredNavigator(
context,
(_) => DownloadedAlbumScreen(
albumName: historyItem.albumName,
artistName: albumArtist.isNotEmpty
? albumArtist
: historyItem.artistName,
coverUrl: historyItem.coverUrl,
),
);
return;
}
} catch (e) {
_log.w('Failed to resolve downloaded album: $e');
}
try {
final row = await LibraryDatabase.instance.getById(mediaItem.id);
if (!mounted) return;
if (row != null) {
final item = LocalLibraryItem.fromJson(row);
final rows = await LibraryDatabase.instance
.getQueueLocalAlbumTracksByKey(item.albumKey);
if (!mounted) return;
final tracks = rows
.map(LocalLibraryItem.fromJson)
.toList(growable: false);
if (tracks.isNotEmpty) {
final albumArtist = (item.albumArtist ?? '').trim();
pushViaPreferredNavigator(
context,
(_) => LocalAlbumScreen(
albumName: item.albumName,
artistName: albumArtist.isNotEmpty
? albumArtist
: item.artistName,
coverPath: item.coverPath,
tracks: tracks,
),
);
return;
}
}
} catch (e) {
_log.w('Failed to resolve local album: $e');
}
if (!mounted) return;
await navigateToAlbum(
context,
albumName: albumName,
artistName: mediaItem.artist,
coverUrl: mediaItem.artUri?.toString(),
);
}
Future<void> _shuffleLibrary(MusicPlayerController controller) async {
try {
final rows = await LibraryDatabase.instance.getAll();
+3
View File
@@ -45,6 +45,9 @@ import 'package:spotiflac_android/widgets/open_on_platform_sheet.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/constants/music_services.dart';
import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart';
import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
import 'package:spotiflac_android/screens/local_album_screen.dart';
import 'package:spotiflac_android/utils/clickable_metadata.dart';
part 'track_metadata_screen_cover.dart';
part 'track_metadata_screen_display.dart';
@@ -29,6 +29,12 @@ extension _TrackMetadataMenu on _TrackMetadataScreenState {
label: l10n.trackAddToQueue,
onTap: () => _enqueueThis(ref, playNext: false),
),
if (albumName.trim().isNotEmpty)
_MetadataOption(
icon: Icons.album_outlined,
label: l10n.homeGoToAlbum,
onTap: () => _goToStoredAlbum(screenContext),
),
_MetadataOption(
icon: Icons.copy_outlined,
label: l10n.trackCopyFilePath,
@@ -226,4 +232,55 @@ extension _TrackMetadataMenu on _TrackMetadataScreenState {
return placeholder();
}
Future<void> _goToStoredAlbum(BuildContext screenContext) async {
final resolvedAlbumArtist = (albumArtist ?? '').trim();
final artist = resolvedAlbumArtist.isNotEmpty
? resolvedAlbumArtist
: artistName;
if (!_isLocalItem) {
pushViaPreferredNavigator(
screenContext,
(_) => DownloadedAlbumScreen(
albumName: albumName,
artistName: artist,
coverUrl: _coverUrl,
),
);
return;
}
try {
final rows = await LibraryDatabase.instance.getQueueLocalAlbumTracksByKey(
_localLibraryItem!.albumKey,
);
if (!mounted || !screenContext.mounted) return;
final tracks = rows
.map(LocalLibraryItem.fromJson)
.toList(growable: false);
if (tracks.isNotEmpty) {
pushViaPreferredNavigator(
screenContext,
(_) => LocalAlbumScreen(
albumName: albumName,
artistName: artist,
coverPath: _localCoverPath,
tracks: tracks,
),
);
return;
}
} catch (e) {
_log.w('Failed to resolve local album: $e');
}
if (!mounted || !screenContext.mounted) return;
await navigateToAlbum(
screenContext,
albumName: albumName,
artistName: artist,
coverUrl: _coverUrl,
);
}
}
+6 -3
View File
@@ -292,7 +292,7 @@ void _pushArtistScreen(
final isExtension = extensionId != null;
final resolvedProviderId = extensionId;
_pushViaPreferredNavigator(
pushViaPreferredNavigator(
context,
(context) => isExtension && resolvedProviderId != null
? ExtensionArtistScreen(
@@ -320,7 +320,7 @@ void _pushAlbumScreen(
final isExtension = extensionId != null;
final resolvedExtensionId = extensionId;
_pushViaPreferredNavigator(
pushViaPreferredNavigator(
context,
(context) => isExtension && resolvedExtensionId != null
? ExtensionAlbumScreen(
@@ -339,7 +339,10 @@ void _pushAlbumScreen(
);
}
void _pushViaPreferredNavigator(BuildContext context, WidgetBuilder builder) {
/// Pushes detail pages into the active tab navigator when invoked from a root
/// modal such as Now Playing. The root modal is dismissed first so the album
/// is immediately visible instead of being hidden behind it.
void pushViaPreferredNavigator(BuildContext context, WidgetBuilder builder) {
final currentNavigator = Navigator.of(context);
final rootNavigator = Navigator.of(context, rootNavigator: true);
final activeTabNavigator = ShellNavigationService.activeTabNavigator();
@@ -194,6 +194,13 @@ class _TrackOptionsSheet extends ConsumerWidget {
onTap: () => _downloadQualityVariant(context, ref),
),
if (track.albumName.trim().isNotEmpty)
_OptionTile(
icon: Icons.album_outlined,
title: context.l10n.homeGoToAlbum,
onTap: () => _goToAlbum(context),
),
_OptionTile(
icon: isLoved ? Icons.favorite : Icons.favorite_border,
iconColor: isLoved ? colorScheme.error : null,
@@ -269,6 +276,23 @@ class _TrackOptionsSheet extends ConsumerWidget {
downloadSingleTrack(rootContext, ref, track, forceQualityPicker: true);
}
Future<void> _goToAlbum(BuildContext context) async {
final navigationContext = Navigator.of(
context,
rootNavigator: true,
).context;
final albumArtist = (track.albumArtist ?? '').trim();
Navigator.pop(context);
await navigateToAlbum(
navigationContext,
albumName: track.albumName,
albumId: track.albumId,
artistName: albumArtist.isNotEmpty ? albumArtist : track.artistName,
coverUrl: track.coverUrl,
extensionId: track.source,
);
}
Future<void> _playLocal(BuildContext context, WidgetRef ref) async {
final rootContext = Navigator.of(context, rootNavigator: true).context;
Navigator.pop(context);