mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
feat(library): add Mornye browsing and contextual compact navigation
Add category browsing, a two-column Recently Added grid, and a dedicated download queue. Keep compact navigation icons and their animations aligned with the active tab. Keep profile avatars circular and report lyric enrichment outcomes accurately, preserving existing lyrics and supporting instrumental markers.
This commit is contained in:
1 parent
4c5af08cdd
commit
c87c9ff848
23 files changed
+1257
-128
No files matched your search
@@ -1604,6 +1604,55 @@ class LibraryDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
/// Album artists across downloaded and scanned music, without loading tracks
|
||||
/// into Dart. Scanned paths already represented by downloads are excluded.
|
||||
Future<List<Map<String, dynamic>>> getQueueArtistPage(
|
||||
QueueLibraryDbQuery request,
|
||||
) async {
|
||||
final db = await database;
|
||||
await _ensureHistoryAttached(db);
|
||||
final parts = <String>[
|
||||
'''
|
||||
SELECT sort_album_artist AS artist_key,
|
||||
COALESCE(NULLIF(album_artist, ''), artist_name) AS artist_name,
|
||||
cover_url, NULL AS cover_path, file_path AS sample_file_path
|
||||
FROM history_db.history
|
||||
''',
|
||||
if (request.includeLocal)
|
||||
'''
|
||||
SELECT album_artist_norm AS artist_key,
|
||||
COALESCE(NULLIF(album_artist, ''), artist_name) AS artist_name,
|
||||
NULL AS cover_url, cover_path, file_path AS sample_file_path
|
||||
FROM $visibleLibraryView l
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM library_path_keys lpk
|
||||
JOIN history_db.history_path_keys hpk ON hpk.path_key = lpk.path_key
|
||||
WHERE lpk.item_id = l.id
|
||||
)
|
||||
''',
|
||||
];
|
||||
final search = normalizeLookupText(request.searchQuery);
|
||||
return db.rawQuery(
|
||||
'''
|
||||
SELECT artist_key, MIN(artist_name) AS artist_name,
|
||||
MAX(NULLIF(cover_url, '')) AS cover_url,
|
||||
MAX(NULLIF(cover_path, '')) AS cover_path,
|
||||
MAX(sample_file_path) AS sample_file_path,
|
||||
COUNT(*) AS track_count
|
||||
FROM (${parts.join(' UNION ALL ')})
|
||||
WHERE artist_key != '' ${search.isEmpty ? '' : "AND artist_key LIKE ? ESCAPE '\\'"}
|
||||
GROUP BY artist_key
|
||||
ORDER BY artist_key
|
||||
LIMIT ? OFFSET ?
|
||||
''',
|
||||
[
|
||||
if (search.isNotEmpty) '%${_escapeLikePattern(search)}%',
|
||||
request.limit,
|
||||
request.offset,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getQueueLocalAlbumTracks(
|
||||
String albumName,
|
||||
String artistName,
|
||||
|
||||
@@ -298,6 +298,8 @@ class IsrcDuplicateGroup {
|
||||
}
|
||||
|
||||
class QueueLibraryDbQuery {
|
||||
final bool includeSingleTrackAlbums;
|
||||
final String? albumArtist;
|
||||
final int limit;
|
||||
final int offset;
|
||||
final String filterMode;
|
||||
@@ -311,6 +313,8 @@ class QueueLibraryDbQuery {
|
||||
final QueueLibraryDbCursor? cursor;
|
||||
|
||||
const QueueLibraryDbQuery({
|
||||
this.includeSingleTrackAlbums = false,
|
||||
this.albumArtist,
|
||||
this.limit = 100,
|
||||
this.offset = 0,
|
||||
this.filterMode = 'all',
|
||||
|
||||
@@ -266,7 +266,7 @@ extension _LibraryDbQueueSql on LibraryDatabase {
|
||||
MAX(COALESCE(sort_added, 0)) AS latest_added
|
||||
FROM history_db.history
|
||||
GROUP BY album_key
|
||||
HAVING COUNT(*) > 1
|
||||
HAVING COUNT(*) > ${request.includeSingleTrackAlbums ? 0 : 1}
|
||||
) c
|
||||
ON c.album_key = h.album_key
|
||||
${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')}'}
|
||||
@@ -325,7 +325,7 @@ extension _LibraryDbQueueSql on LibraryDatabase {
|
||||
WHERE lpk.item_id = candidate.id
|
||||
)
|
||||
GROUP BY album_key
|
||||
HAVING COUNT(*) > 1
|
||||
HAVING COUNT(*) > ${request.includeSingleTrackAlbums ? 0 : 1}
|
||||
) c ON c.album_key = l.album_key
|
||||
${where.isEmpty ? '' : 'WHERE ${where.join(' AND ')}'}
|
||||
GROUP BY c.album_key
|
||||
@@ -368,6 +368,10 @@ extension _LibraryDbQueueSql on LibraryDatabase {
|
||||
List<Object?> args,
|
||||
QueueLibraryDbQuery request,
|
||||
) {
|
||||
if (request.albumArtist != null) {
|
||||
where.add('h.sort_album_artist = ?');
|
||||
args.add(LibraryDatabase.normalizeLookupText(request.albumArtist));
|
||||
}
|
||||
final query = LibraryDatabase.normalizeLookupText(request.searchQuery);
|
||||
if (query.isNotEmpty) {
|
||||
final ftsQuery = sqlite.ftsPhraseSearchQuery(query);
|
||||
@@ -412,6 +416,10 @@ extension _LibraryDbQueueSql on LibraryDatabase {
|
||||
List<Object?> args,
|
||||
QueueLibraryDbQuery request,
|
||||
) {
|
||||
if (request.albumArtist != null) {
|
||||
where.add('l.album_artist_norm = ?');
|
||||
args.add(LibraryDatabase.normalizeLookupText(request.albumArtist));
|
||||
}
|
||||
final query = LibraryDatabase.normalizeLookupText(request.searchQuery);
|
||||
if (query.isNotEmpty) {
|
||||
final ftsQuery = sqlite.ftsPhraseSearchQuery(query);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/widgets/app_alert_dialog.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/utils/re_enrich_result.dart';
|
||||
import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
@@ -228,8 +229,10 @@ Future<void> reEnrichLocalTracks(
|
||||
},
|
||||
);
|
||||
|
||||
final lyricsSummary = ReEnrichLyricsSummary();
|
||||
final successCount = await runner.apply(
|
||||
previews,
|
||||
onResult: lyricsSummary.add,
|
||||
shouldStop: () => cancelled || !context.mounted || !isActive(),
|
||||
onProgress: (index, item) => BatchProgressDialog.update(
|
||||
current: index + 1,
|
||||
@@ -254,14 +257,7 @@ Future<void> reEnrichLocalTracks(
|
||||
onComplete();
|
||||
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
final failedCount = total - successCount;
|
||||
final summary = failedCount <= 0
|
||||
? '${context.l10n.trackReEnrichSuccess} ($successCount/$total)'
|
||||
: context.l10n.trackReEnrichSuccessWithFailures(
|
||||
successCount,
|
||||
total,
|
||||
failedCount,
|
||||
);
|
||||
final summary = lyricsSummary.message(context.l10n, successCount, total);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(summary)));
|
||||
}
|
||||
|
||||
@@ -345,6 +341,7 @@ class BatchReEnrichRunner {
|
||||
List<BatchReEnrichPreview> previews, {
|
||||
required bool Function() shouldStop,
|
||||
required void Function(int, LocalLibraryItem) onProgress,
|
||||
void Function(Map<String, dynamic>)? onResult,
|
||||
}) async {
|
||||
if (shouldStop()) return 0;
|
||||
// Settings may have changed while the review sheet was open.
|
||||
@@ -370,6 +367,7 @@ class BatchReEnrichRunner {
|
||||
reEnrichResult: result,
|
||||
);
|
||||
successes++;
|
||||
onResult?.call(result);
|
||||
case 'ffmpeg':
|
||||
if (await applyFfmpeg(
|
||||
item: preview.item,
|
||||
@@ -377,6 +375,7 @@ class BatchReEnrichRunner {
|
||||
artistTagMode: settings.artistTagMode,
|
||||
)) {
|
||||
successes++;
|
||||
onResult?.call(result);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
|
||||
Reference in new issue
Block a user