mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-19 14:07:26 +02:00
refactor(metadata): flatten track actions and remove similar songs
This commit is contained in:
1 parent
2e71e88a9a
commit
cc3fe42df2
10 files changed
+113
-429
No files matched your search
@@ -7039,11 +7039,6 @@
|
||||
"artistAllUpcomingConcerts": "All Upcoming Concerts",
|
||||
"artistNoUpcomingConcerts": "No upcoming concerts.",
|
||||
"motionArtwork": "Animated artwork",
|
||||
"trackEditAudioFile": "Edit audio file",
|
||||
"relatedLibraryTracks": "Similar songs in Library",
|
||||
"relatedLibraryDescription": "Songs you already have with related artist, album or genre tags. No online music is searched.",
|
||||
"relatedLibraryEmpty": "No related songs found in your Library.",
|
||||
"relatedLibraryLoadError": "Could not find related songs. Try again.",
|
||||
"listeningStats": "Listening statistics",
|
||||
"listeningStatsRecord": "Record listening",
|
||||
"listeningStatsDescription": "Stored only on this device, starting now. Records the built-in player only; pauses, buffering and seeking add no listening time. A play counts after 30 seconds.",
|
||||
|
||||
@@ -6958,11 +6958,6 @@
|
||||
"hiResCheckRateEvidence": "Penurunan tajam di sekitar {cutoff} dan noise floor yang terukur mengindikasikan sumber dengan sample rate lebih rendah.",
|
||||
"hiResCheckSteadyEnergy": "Energi di atas {cutoff} relatif konstan pada bagian yang dianalisis dan mungkin berupa noise.",
|
||||
"motionArtwork": "Artwork bergerak",
|
||||
"trackEditAudioFile": "Ubah file audio",
|
||||
"relatedLibraryTracks": "Lagu serupa di Library",
|
||||
"relatedLibraryDescription": "Lagu yang sudah Anda miliki dengan tag artis, album, atau genre terkait. Tidak mencari musik online.",
|
||||
"relatedLibraryEmpty": "Tidak ada lagu terkait di Library Anda.",
|
||||
"relatedLibraryLoadError": "Tidak dapat mencari lagu terkait. Coba lagi.",
|
||||
"listeningStats": "Statistik mendengarkan",
|
||||
"listeningStatsRecord": "Catat waktu mendengarkan",
|
||||
"listeningStatsDescription": "Disimpan hanya di perangkat ini, mulai sekarang. Hanya mencatat player bawaan; pause, buffering, dan seek tidak menambah waktu dengar. Pemutaran dihitung setelah 30 detik.",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/unified_library_item.dart';
|
||||
import 'package:spotiflac_android/providers/music_player_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
|
||||
import 'package:spotiflac_android/services/related_library_tracks.dart';
|
||||
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
|
||||
import 'package:spotiflac_android/widgets/app_loading_indicator.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
|
||||
|
||||
final relatedLibraryTracksProvider = FutureProvider.autoDispose
|
||||
.family<List<UnifiedLibraryItem>, UnifiedLibraryItem>((ref, seed) {
|
||||
final includeLocal = ref.watch(
|
||||
settingsProvider.select((s) => s.localLibraryEnabled),
|
||||
);
|
||||
return loadRelatedLibraryTracks(seed, includeLocal: includeLocal);
|
||||
});
|
||||
|
||||
class RelatedLibraryTracksScreen extends ConsumerWidget {
|
||||
const RelatedLibraryTracksScreen({super.key, required this.seed});
|
||||
|
||||
final UnifiedLibraryItem seed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tracks = ref.watch(relatedLibraryTracksProvider(seed));
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
AppSliverHeader.page(title: context.l10n.relatedLibraryTracks),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(context.l10n.relatedLibraryDescription),
|
||||
),
|
||||
),
|
||||
...tracks.when(
|
||||
loading: () => [
|
||||
const SliverToBoxAdapter(
|
||||
child: Center(child: AppLoadingIndicator()),
|
||||
),
|
||||
],
|
||||
error: (_, _) => [
|
||||
SliverToBoxAdapter(
|
||||
child: ListTile(
|
||||
title: Text(context.l10n.relatedLibraryLoadError),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () =>
|
||||
ref.invalidate(relatedLibraryTracksProvider(seed)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
data: (items) => [
|
||||
if (items.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(context.l10n.relatedLibraryEmpty),
|
||||
),
|
||||
),
|
||||
SliverList.list(
|
||||
children: [
|
||||
for (final item in items)
|
||||
ListTile(
|
||||
leading: CachedCoverImage(
|
||||
imageUrl: item.localCoverPath ?? item.coverUrl ?? '',
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
title: Text(item.trackName),
|
||||
subtitle: Text(item.artistName),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => TrackMetadataScreen(
|
||||
item: item.historyItem,
|
||||
localItem: item.localItem,
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: IconButton(
|
||||
tooltip: context.l10n.trackMetadataPlay,
|
||||
icon: const Icon(Icons.play_arrow),
|
||||
onPressed: () =>
|
||||
ref.read(musicPlayerControllerProvider).playAll([
|
||||
if (item.localItem != null)
|
||||
playableFromLocal(item.localItem!)
|
||||
else if (item.historyItem != null)
|
||||
playableFromHistory(item.historyItem!),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const NavBarSliverSpacer(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -76,8 +76,6 @@ import 'package:spotiflac_android/widgets/metadata_barcode.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/related_library_tracks_screen.dart';
|
||||
import 'package:spotiflac_android/models/unified_library_item.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
|
||||
part 'track_metadata_screen_cover.dart';
|
||||
|
||||
@@ -8,148 +8,118 @@ extension _TrackMetadataMenu on _TrackMetadataScreenState {
|
||||
WidgetRef ref,
|
||||
ColorScheme colorScheme, {
|
||||
Rect? anchor,
|
||||
bool fileActionsOnly = false,
|
||||
}) {
|
||||
Widget buildMenu(BuildContext sheetContext) {
|
||||
final l10n = sheetContext.l10n;
|
||||
|
||||
final options = <_MetadataOption>[
|
||||
if (!fileActionsOnly) ...[
|
||||
if (_fileExists)
|
||||
_MetadataOption(
|
||||
icon: Icons.playlist_play,
|
||||
label: l10n.trackPlayNext,
|
||||
onTap: () => _enqueueThis(ref, playNext: true),
|
||||
),
|
||||
if (_fileExists)
|
||||
_MetadataOption(
|
||||
icon: Icons.queue_music,
|
||||
label: l10n.trackAddToQueue,
|
||||
onTap: () => _enqueueThis(ref, playNext: false),
|
||||
),
|
||||
if (albumName.trim().isNotEmpty)
|
||||
_MetadataOption(
|
||||
icon: Icons.album_outlined,
|
||||
label: l10n.homeGoToAlbum,
|
||||
onTap: () => navigateToAlbum(
|
||||
screenContext,
|
||||
albumName: albumName,
|
||||
artistName: albumArtist ?? artistName,
|
||||
coverUrl: _coverUrl,
|
||||
),
|
||||
),
|
||||
if (_fileExists)
|
||||
_MetadataOption(
|
||||
icon: Icons.person_outline,
|
||||
label: l10n.mornyeGoToArtist,
|
||||
onTap: () =>
|
||||
navigateToArtistCredits(screenContext, artistNames: artistName),
|
||||
icon: Icons.playlist_play,
|
||||
label: l10n.trackPlayNext,
|
||||
onTap: () => _enqueueThis(ref, playNext: true),
|
||||
),
|
||||
if (_fileExists)
|
||||
_MetadataOption(
|
||||
icon: Icons.tune,
|
||||
label: l10n.trackEditAudioFile,
|
||||
onTap: () => _showOptionsMenu(
|
||||
icon: Icons.queue_music,
|
||||
label: l10n.trackAddToQueue,
|
||||
onTap: () => _enqueueThis(ref, playNext: false),
|
||||
),
|
||||
if (albumName.trim().isNotEmpty)
|
||||
_MetadataOption(
|
||||
icon: Icons.album_outlined,
|
||||
label: l10n.homeGoToAlbum,
|
||||
onTap: () => navigateToAlbum(
|
||||
screenContext,
|
||||
ref,
|
||||
colorScheme,
|
||||
fileActionsOnly: true,
|
||||
albumName: albumName,
|
||||
artistName: albumArtist ?? artistName,
|
||||
coverUrl: _coverUrl,
|
||||
),
|
||||
),
|
||||
_MetadataOption(
|
||||
icon: Icons.person_outline,
|
||||
label: l10n.mornyeGoToArtist,
|
||||
onTap: () =>
|
||||
navigateToArtistCredits(screenContext, artistNames: artistName),
|
||||
),
|
||||
_MetadataOption(
|
||||
icon: Icons.copy_outlined,
|
||||
label: l10n.trackCopyFilePath,
|
||||
dividerAbove: _fileExists,
|
||||
onTap: () => _copyToClipboard(screenContext, cleanFilePath),
|
||||
),
|
||||
if (_fileExists)
|
||||
_MetadataOption(
|
||||
icon: Icons.library_music_outlined,
|
||||
label: l10n.relatedLibraryTracks,
|
||||
onTap: () => Navigator.of(screenContext).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => RelatedLibraryTracksScreen(
|
||||
seed: widget.localItem != null
|
||||
? UnifiedLibraryItem.fromLocalLibrary(widget.localItem!)
|
||||
: UnifiedLibraryItem.fromDownloadHistory(widget.item!),
|
||||
),
|
||||
),
|
||||
),
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.trackEditMetadata,
|
||||
onTap: () =>
|
||||
_showEditMetadataSheet(screenContext, ref, colorScheme),
|
||||
),
|
||||
],
|
||||
if (fileActionsOnly) ...[
|
||||
if (!_isLocalItem && (_coverUrl != null || _fileExists))
|
||||
_MetadataOption(
|
||||
icon: Icons.copy_outlined,
|
||||
label: l10n.trackCopyFilePath,
|
||||
dividerAbove: _fileExists,
|
||||
onTap: () => _copyToClipboard(screenContext, cleanFilePath),
|
||||
icon: Icons.image_outlined,
|
||||
label: l10n.trackSaveCoverArt,
|
||||
onTap: _saveCoverArt,
|
||||
),
|
||||
if (_fileExists)
|
||||
_MetadataOption(
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.trackEditMetadata,
|
||||
onTap: () =>
|
||||
_showEditMetadataSheet(screenContext, ref, colorScheme),
|
||||
),
|
||||
if (!_isLocalItem && (_coverUrl != null || _fileExists))
|
||||
_MetadataOption(
|
||||
icon: Icons.image_outlined,
|
||||
label: l10n.trackSaveCoverArt,
|
||||
onTap: _saveCoverArt,
|
||||
),
|
||||
if (!_isLocalItem)
|
||||
_MetadataOption(
|
||||
icon: Icons.lyrics_outlined,
|
||||
label: l10n.trackSaveLyrics,
|
||||
onTap: _saveLyrics,
|
||||
),
|
||||
if (_fileExists)
|
||||
_MetadataOption(
|
||||
icon: Icons.travel_explore,
|
||||
label: l10n.trackReEnrich,
|
||||
onTap: _reEnrichMetadata,
|
||||
),
|
||||
if (_fileExists && _isConvertibleFormat)
|
||||
_MetadataOption(
|
||||
icon: Icons.swap_horiz,
|
||||
label: l10n.trackConvertFormat,
|
||||
onTap: () => _showConvertSheet(screenContext),
|
||||
),
|
||||
if (_fileExists && !_isCueFile)
|
||||
_MetadataOption(
|
||||
icon: Icons.graphic_eq,
|
||||
label: l10n.trackReplayGain,
|
||||
onTap: () => _updateReplayGain(),
|
||||
),
|
||||
if (_fileExists && !_isCueFile)
|
||||
_MetadataOption(
|
||||
icon: Icons.remove_circle_outline,
|
||||
label: l10n.trackRemoveReplayGain,
|
||||
onTap: () => _removeReplayGain(),
|
||||
),
|
||||
if (_fileExists && _isCueFile)
|
||||
_MetadataOption(
|
||||
icon: Icons.call_split,
|
||||
label: l10n.cueSplitTitle,
|
||||
onTap: () => _showCueSplitSheet(screenContext),
|
||||
),
|
||||
],
|
||||
if (!fileActionsOnly) ...[
|
||||
if (_spotifyId != null || (isrc?.isNotEmpty ?? false))
|
||||
_MetadataOption(
|
||||
icon: Icons.open_in_new,
|
||||
label: l10n.trackOpenOn,
|
||||
dividerAbove: true,
|
||||
onTap: () => OpenOnPlatformSheet.show(
|
||||
screenContext,
|
||||
spotifyId: _spotifyId ?? '',
|
||||
isrc: isrc ?? '',
|
||||
),
|
||||
),
|
||||
if (!_isLocalItem)
|
||||
_MetadataOption(
|
||||
icon: Icons.share_outlined,
|
||||
label: l10n.trackMetadataShare,
|
||||
dividerAbove: _spotifyId == null && !(isrc?.isNotEmpty ?? false),
|
||||
onTap: () => _shareFile(screenContext),
|
||||
icon: Icons.lyrics_outlined,
|
||||
label: l10n.trackSaveLyrics,
|
||||
onTap: _saveLyrics,
|
||||
),
|
||||
if (_fileExists)
|
||||
_MetadataOption(
|
||||
icon: Icons.delete_outline,
|
||||
label: l10n.trackRemoveFromDevice,
|
||||
destructive: true,
|
||||
onTap: () => _confirmDelete(screenContext, ref, colorScheme),
|
||||
icon: Icons.travel_explore,
|
||||
label: l10n.trackReEnrich,
|
||||
onTap: _reEnrichMetadata,
|
||||
),
|
||||
],
|
||||
if (_fileExists && _isConvertibleFormat)
|
||||
_MetadataOption(
|
||||
icon: Icons.swap_horiz,
|
||||
label: l10n.trackConvertFormat,
|
||||
onTap: () => _showConvertSheet(screenContext),
|
||||
),
|
||||
if (_fileExists && !_isCueFile)
|
||||
_MetadataOption(
|
||||
icon: Icons.graphic_eq,
|
||||
label: l10n.trackReplayGain,
|
||||
onTap: () => _updateReplayGain(),
|
||||
),
|
||||
if (_fileExists && !_isCueFile)
|
||||
_MetadataOption(
|
||||
icon: Icons.remove_circle_outline,
|
||||
label: l10n.trackRemoveReplayGain,
|
||||
onTap: () => _removeReplayGain(),
|
||||
),
|
||||
if (_fileExists && _isCueFile)
|
||||
_MetadataOption(
|
||||
icon: Icons.call_split,
|
||||
label: l10n.cueSplitTitle,
|
||||
onTap: () => _showCueSplitSheet(screenContext),
|
||||
),
|
||||
if (_spotifyId != null || (isrc?.isNotEmpty ?? false))
|
||||
_MetadataOption(
|
||||
icon: Icons.open_in_new,
|
||||
label: l10n.trackOpenOn,
|
||||
dividerAbove: true,
|
||||
onTap: () => OpenOnPlatformSheet.show(
|
||||
screenContext,
|
||||
spotifyId: _spotifyId ?? '',
|
||||
isrc: isrc ?? '',
|
||||
),
|
||||
),
|
||||
_MetadataOption(
|
||||
icon: Icons.share_outlined,
|
||||
label: l10n.trackMetadataShare,
|
||||
dividerAbove: _spotifyId == null && !(isrc?.isNotEmpty ?? false),
|
||||
onTap: () => _shareFile(screenContext),
|
||||
),
|
||||
_MetadataOption(
|
||||
icon: Icons.delete_outline,
|
||||
label: l10n.trackRemoveFromDevice,
|
||||
destructive: true,
|
||||
onTap: () => _confirmDelete(screenContext, ref, colorScheme),
|
||||
),
|
||||
];
|
||||
|
||||
if (sheetContext.isMornye) {
|
||||
@@ -173,26 +143,24 @@ extension _TrackMetadataMenu on _TrackMetadataScreenState {
|
||||
}
|
||||
return MornyeContextMenu(
|
||||
quickActions: [
|
||||
if (!fileActionsOnly) ...[
|
||||
if (_fileExists) ...[
|
||||
MornyeMenuAction(
|
||||
icon: CupertinoIcons.play_fill,
|
||||
label: l10n.trackMetadataPlay,
|
||||
onPressed: () => _closeOptionsMenuAndRun(
|
||||
sheetContext,
|
||||
() => _openFile(screenContext, rawFilePath),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_fileExists) ...[
|
||||
MornyeMenuAction(
|
||||
icon: CupertinoIcons.share_solid,
|
||||
label: l10n.trackMetadataShare,
|
||||
icon: CupertinoIcons.play_fill,
|
||||
label: l10n.trackMetadataPlay,
|
||||
onPressed: () => _closeOptionsMenuAndRun(
|
||||
sheetContext,
|
||||
() => _shareFile(screenContext),
|
||||
() => _openFile(screenContext, rawFilePath),
|
||||
),
|
||||
),
|
||||
],
|
||||
MornyeMenuAction(
|
||||
icon: CupertinoIcons.share_solid,
|
||||
label: l10n.trackMetadataShare,
|
||||
onPressed: () => _closeOptionsMenuAndRun(
|
||||
sheetContext,
|
||||
() => _shareFile(screenContext),
|
||||
),
|
||||
),
|
||||
],
|
||||
groups: groups,
|
||||
);
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:spotiflac_android/models/unified_library_item.dart';
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
import 'package:spotiflac_android/services/library_database.dart';
|
||||
import 'package:spotiflac_android/utils/artist_utils.dart';
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
|
||||
String _normalized(String? value) => (value ?? '').trim().toLowerCase();
|
||||
|
||||
/// Only related, locally stored songs qualify. A sparse library can return
|
||||
/// an empty result instead of silently looking up online tracks.
|
||||
List<UnifiedLibraryItem> rankRelatedLibraryTracks(
|
||||
UnifiedLibraryItem seed,
|
||||
List<UnifiedLibraryItem> candidates,
|
||||
) {
|
||||
final artists = splitArtistNames(seed.artistName).map(_normalized).toSet();
|
||||
final seenPaths = {seed.filePath};
|
||||
final seenSongs = {
|
||||
'${_normalized(seed.artistName)}|${_normalized(seed.trackName)}',
|
||||
};
|
||||
final scored = <(UnifiedLibraryItem, int)>[];
|
||||
for (final item in candidates) {
|
||||
final path = item.filePath.trim();
|
||||
if (path.isEmpty ||
|
||||
path.startsWith('http://') ||
|
||||
path.startsWith('https://')) {
|
||||
continue;
|
||||
}
|
||||
final sameArtist = splitArtistNames(
|
||||
item.artistName,
|
||||
).map(_normalized).any(artists.contains);
|
||||
final sameGenre =
|
||||
_normalized(seed.genre).isNotEmpty &&
|
||||
_normalized(seed.genre) == _normalized(item.genre);
|
||||
final sameAlbum =
|
||||
sameArtist &&
|
||||
_normalized(seed.albumName).isNotEmpty &&
|
||||
_normalized(seed.albumName) == _normalized(item.albumName);
|
||||
final score =
|
||||
(sameArtist ? 5 : 0) + (sameGenre ? 4 : 0) + (sameAlbum ? 3 : 0);
|
||||
if (score == 0 || !seenPaths.add(path)) continue;
|
||||
final song =
|
||||
'${_normalized(item.artistName)}|${_normalized(item.trackName)}';
|
||||
if (!seenSongs.add(song)) continue;
|
||||
scored.add((item, score));
|
||||
}
|
||||
scored.sort((a, b) {
|
||||
final score = b.$2.compareTo(a.$2);
|
||||
return score != 0 ? score : a.$1.trackName.compareTo(b.$1.trackName);
|
||||
});
|
||||
return scored.map((entry) => entry.$1).toList(growable: false);
|
||||
}
|
||||
|
||||
Future<List<UnifiedLibraryItem>> loadRelatedLibraryTracks(
|
||||
UnifiedLibraryItem seed, {
|
||||
required bool includeLocal,
|
||||
}) async {
|
||||
final database = LibraryDatabase.instance;
|
||||
final count = (await database.getQueueCounts(
|
||||
QueueLibraryDbQuery(includeLocal: includeLocal),
|
||||
)).allTrackCount;
|
||||
final rows = <Map<String, dynamic>>[];
|
||||
for (final artist in splitArtistNames(seed.artistName).take(4)) {
|
||||
rows.addAll(
|
||||
await database.getQueueTrackPage(
|
||||
QueueLibraryDbQuery(
|
||||
includeLocal: includeLocal,
|
||||
searchQuery: artist,
|
||||
limit: 96,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
rows.addAll(
|
||||
await database.getQueueTrackPage(
|
||||
QueueLibraryDbQuery(
|
||||
includeLocal: includeLocal,
|
||||
offset: count > 256 ? Random().nextInt(count - 255) : 0,
|
||||
limit: 256,
|
||||
),
|
||||
),
|
||||
);
|
||||
final candidates = <UnifiedLibraryItem>[];
|
||||
for (final row in rows) {
|
||||
final json = Map<String, dynamic>.from(row['item'] as Map);
|
||||
if (row['source'] == 'local') {
|
||||
candidates.add(
|
||||
UnifiedLibraryItem.fromLocalLibrary(LocalLibraryItem.fromJson(json)),
|
||||
);
|
||||
} else if (row['source'] == 'downloaded') {
|
||||
candidates.add(
|
||||
UnifiedLibraryItem.fromDownloadHistory(
|
||||
DownloadHistoryItem.fromJson(json),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Bound storage-provider probes as well as database pages.
|
||||
final ranked = rankRelatedLibraryTracks(seed, candidates).take(48).toList();
|
||||
final exists = await fileExistenceByPath(
|
||||
ranked.map((item) => item.filePath).toList(),
|
||||
);
|
||||
return ranked
|
||||
.where((item) => exists[item.filePath] == true)
|
||||
.take(24)
|
||||
.toList(growable: false);
|
||||
}
|
||||
@@ -3,69 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/models/unified_library_item.dart';
|
||||
import 'package:spotiflac_android/services/library_database.dart';
|
||||
import 'package:spotiflac_android/services/related_library_tracks.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
|
||||
UnifiedLibraryItem _track(
|
||||
String id, {
|
||||
String artist = 'First Artist',
|
||||
String album = 'Album',
|
||||
String? genre,
|
||||
String? path,
|
||||
}) => UnifiedLibraryItem.fromLocalLibrary(
|
||||
LocalLibraryItem(
|
||||
id: id,
|
||||
trackName: id,
|
||||
artistName: artist,
|
||||
albumName: album,
|
||||
genre: genre,
|
||||
filePath: path ?? '/$id.flac',
|
||||
scannedAt: DateTime(2026),
|
||||
),
|
||||
);
|
||||
|
||||
void main() {
|
||||
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||
test(
|
||||
'related Library songs rank credits, deduplicate and exclude remote files',
|
||||
() {
|
||||
final seed = _track(
|
||||
'seed',
|
||||
artist: 'First Artist feat. Second Artist',
|
||||
genre: 'Rock',
|
||||
);
|
||||
final album = _track('album', genre: 'Rock');
|
||||
final genre = _track(
|
||||
'genre',
|
||||
artist: 'Other',
|
||||
album: 'Other Album',
|
||||
genre: 'Rock',
|
||||
);
|
||||
final collaborator = _track(
|
||||
'collaborator',
|
||||
artist: 'Second Artist',
|
||||
album: 'Other Album',
|
||||
);
|
||||
final results = rankRelatedLibraryTracks(seed, [
|
||||
seed,
|
||||
album,
|
||||
album,
|
||||
genre,
|
||||
collaborator,
|
||||
_track('unrelated', artist: 'Other', album: 'Other Album'),
|
||||
_track('remote', path: 'https://example.com/music.flac'),
|
||||
_track('album', path: '/duplicate.flac', genre: 'Rock'),
|
||||
]);
|
||||
expect(results.map((item) => item.trackName), [
|
||||
'album',
|
||||
'collaborator',
|
||||
'genre',
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
for (final mornye in [false, true]) {
|
||||
testWidgets('artist picker offers each featured credit ($mornye)', (
|
||||
@@ -264,6 +264,9 @@ void main() {
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await _tapVisible(tester, find.byIcon(CupertinoIcons.ellipsis));
|
||||
expect(find.text('Edit audio file'), findsNothing);
|
||||
expect(find.text('Similar songs in Library'), findsNothing);
|
||||
expect(find.text('Edit Metadata'), findsOneWidget);
|
||||
final lastAction = find.text('Remove from device');
|
||||
expect(lastAction.hitTestable(), findsNothing);
|
||||
expect(find.byType(MornyeContextMenu), findsOneWidget);
|
||||
|
||||
@@ -165,9 +165,8 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byIcon(Icons.more_vert));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.ensureVisible(find.text('Edit audio file'));
|
||||
await tester.tap(find.text('Edit audio file'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Edit audio file'), findsNothing);
|
||||
expect(find.text('Similar songs in Library'), findsNothing);
|
||||
await tester.ensureVisible(find.text('Edit Metadata'));
|
||||
await tester.tap(find.text('Edit Metadata'));
|
||||
await _settleFileIo(tester);
|
||||
|
||||
@@ -142,10 +142,9 @@ void main() {
|
||||
Future<void> openRemoval() async {
|
||||
await tester.tap(find.byIcon(Icons.more_vert));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Remove ReplayGain'), findsNothing);
|
||||
await tester.ensureVisible(find.text('Edit audio file'));
|
||||
await tester.tap(find.text('Edit audio file'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Edit audio file'), findsNothing);
|
||||
expect(find.text('Similar songs in Library'), findsNothing);
|
||||
expect(find.text('Remove ReplayGain'), findsOneWidget);
|
||||
await tester.ensureVisible(find.text('Remove ReplayGain'));
|
||||
await tester.tap(find.text('Remove ReplayGain'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Reference in new issue
Block a user