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
@@ -0,0 +1,462 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/library_browse_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/downloaded_embedded_cover_resolver.dart';
|
||||
import 'package:spotiflac_android/services/library_database.dart';
|
||||
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
|
||||
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/app_search_field.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
|
||||
|
||||
enum MornyeLibraryPage { overview, albums, artists }
|
||||
|
||||
class MornyeLibraryScreen extends ConsumerStatefulWidget {
|
||||
const MornyeLibraryScreen({
|
||||
super.key,
|
||||
required this.onOpenSection,
|
||||
this.page = MornyeLibraryPage.overview,
|
||||
this.artist,
|
||||
});
|
||||
|
||||
final ValueChanged<String> onOpenSection;
|
||||
final MornyeLibraryPage page;
|
||||
final String? artist;
|
||||
|
||||
@override
|
||||
ConsumerState<MornyeLibraryScreen> createState() =>
|
||||
_MornyeLibraryScreenState();
|
||||
}
|
||||
|
||||
class _MornyeLibraryScreenState extends ConsumerState<MornyeLibraryScreen> {
|
||||
final _scroll = ScrollController();
|
||||
final _search = TextEditingController();
|
||||
Timer? _debounce;
|
||||
bool _showSearch = false;
|
||||
String _query = '';
|
||||
late String _sort;
|
||||
int _limit = 40;
|
||||
List<LibraryBrowseEntry> _rows = const [];
|
||||
bool _loading = true;
|
||||
|
||||
bool get _overview => widget.page == MornyeLibraryPage.overview;
|
||||
bool get _artists => widget.page == MornyeLibraryPage.artists;
|
||||
LibraryBrowseRequest get _request => (
|
||||
artists: _artists,
|
||||
artist: widget.artist,
|
||||
search: _query,
|
||||
sort: _sort,
|
||||
limit: _limit,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_sort = _overview ? 'latest' : 'a-z';
|
||||
_scroll.addListener(() {
|
||||
if (_scroll.position.extentAfter < 500 &&
|
||||
!_loading &&
|
||||
_rows.length >= _limit) {
|
||||
setState(() => _limit += 40);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounce?.cancel();
|
||||
_scroll.dispose();
|
||||
_search.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _searchChanged(String value) {
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 300), () {
|
||||
if (!mounted || _query == value.trim()) return;
|
||||
setState(() {
|
||||
_query = value.trim();
|
||||
_limit = 40;
|
||||
_rows = const [];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _openPage(MornyeLibraryPage page, {String? artist}) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(
|
||||
body: MornyeLibraryScreen(
|
||||
page: page,
|
||||
artist: artist,
|
||||
onOpenSection: widget.onOpenSection,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _openAlbum(LibraryBrowseEntry entry) async {
|
||||
if (entry.source != 'local') {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => DownloadedAlbumScreen(
|
||||
albumName: entry.name,
|
||||
artistName: entry.artist,
|
||||
coverUrl: entry.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final rows = await LibraryDatabase.instance.getQueueLocalAlbumTracksByKey(
|
||||
entry.key,
|
||||
);
|
||||
if (!mounted) return;
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => LocalAlbumScreen(
|
||||
albumName: entry.name,
|
||||
artistName: entry.artist,
|
||||
coverPath: entry.cover,
|
||||
tracks: rows.map(LocalLibraryItem.fromJson).toList(growable: false),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _chooseSort() async {
|
||||
final selected = await showAppBottomSheet<String>(
|
||||
context: context,
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final option in [
|
||||
('a-z', context.l10n.searchSortTitleAZ),
|
||||
('latest', context.l10n.libraryRecentlyAdded),
|
||||
])
|
||||
ListTile(
|
||||
title: Text(option.$2),
|
||||
trailing: _sort == option.$1 ? const Icon(Icons.check) : null,
|
||||
onTap: () => Navigator.pop(context, option.$1),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (!mounted || selected == null || selected == _sort) return;
|
||||
setState(() {
|
||||
_sort = selected;
|
||||
_limit = 40;
|
||||
_rows = const [];
|
||||
});
|
||||
_scroll.jumpTo(0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final result = ref.watch(libraryBrowseProvider(_request));
|
||||
_loading = result.isLoading;
|
||||
if (result.hasValue) _rows = result.requireValue;
|
||||
final title =
|
||||
widget.artist ??
|
||||
(_overview
|
||||
? context.l10n.navLibrary
|
||||
: _artists
|
||||
? context.l10n.searchArtists
|
||||
: context.l10n.searchAlbums);
|
||||
final actions = <Widget>[
|
||||
IconButton(
|
||||
tooltip: context.l10n.librarySearchHint,
|
||||
icon: const Icon(Icons.search),
|
||||
onPressed: () => setState(() => _showSearch = !_showSearch),
|
||||
),
|
||||
if (_overview)
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final count = ref.watch(
|
||||
downloadQueueLookupProvider.select(
|
||||
(s) => s.notCompletedItemIds.length,
|
||||
),
|
||||
);
|
||||
return TextButton.icon(
|
||||
onPressed: () => widget.onOpenSection('downloads'),
|
||||
icon: Badge(
|
||||
isLabelVisible: count > 0,
|
||||
label: Text('$count'),
|
||||
child: const Icon(Icons.downloading_outlined),
|
||||
),
|
||||
label: Text(context.l10n.libraryDownloads),
|
||||
);
|
||||
},
|
||||
)
|
||||
else if (!_artists)
|
||||
IconButton(
|
||||
tooltip: context.l10n.searchSortTitle,
|
||||
icon: const Icon(Icons.sort),
|
||||
onPressed: _chooseSort,
|
||||
),
|
||||
];
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
ref.invalidate(libraryBrowseProvider(_request));
|
||||
await ref.read(libraryBrowseProvider(_request).future);
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: _scroll,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
if (_overview)
|
||||
AppSliverHeader.tabRoot(title: title, actions: actions)
|
||||
else
|
||||
AppSliverHeader.page(title: title, actions: actions),
|
||||
if (_showSearch)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 8, 24, 16),
|
||||
child: AppSearchField(
|
||||
controller: _search,
|
||||
autofocus: true,
|
||||
hintText: context.l10n.librarySearchHint,
|
||||
clearTooltip: context.l10n.dialogClear,
|
||||
onChanged: _searchChanged,
|
||||
onClear: () {
|
||||
_search.clear();
|
||||
_searchChanged('');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_overview && _query.isEmpty)
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 20),
|
||||
sliver: SliverList.list(
|
||||
children: [
|
||||
for (final category in [
|
||||
(
|
||||
Icons.queue_music,
|
||||
context.l10n.searchPlaylists,
|
||||
() => widget.onOpenSection('playlists'),
|
||||
),
|
||||
(
|
||||
Icons.mic_none,
|
||||
context.l10n.searchArtists,
|
||||
() => _openPage(MornyeLibraryPage.artists),
|
||||
),
|
||||
(
|
||||
Icons.album_outlined,
|
||||
context.l10n.searchAlbums,
|
||||
() => _openPage(MornyeLibraryPage.albums),
|
||||
),
|
||||
(
|
||||
Icons.music_note_outlined,
|
||||
context.l10n.searchSongs,
|
||||
() => widget.onOpenSection('all'),
|
||||
),
|
||||
]) ...[
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
minVerticalPadding: 12,
|
||||
leading: Icon(
|
||||
category.$1,
|
||||
color: colors.primary,
|
||||
size: 28,
|
||||
),
|
||||
title: Text(
|
||||
category.$2,
|
||||
style: const TextStyle(fontSize: 22),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
onTap: category.$3,
|
||||
),
|
||||
const Divider(height: 1, indent: 48),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_overview)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 8, 24, 16),
|
||||
child: Text(
|
||||
_query.isEmpty
|
||||
? context.l10n.libraryRecentlyAdded
|
||||
: context.l10n.searchAlbums,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_rows.isNotEmpty)
|
||||
if (_artists)
|
||||
SliverList.builder(
|
||||
itemCount: _rows.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = _rows[index];
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 8,
|
||||
),
|
||||
leading: ClipOval(
|
||||
child: SizedBox.square(
|
||||
dimension: 56,
|
||||
child: _LibraryBrowseArtwork(entry: entry),
|
||||
),
|
||||
),
|
||||
title: Text(entry.artist),
|
||||
subtitle: Text(
|
||||
context.l10n.queueTrackCount(entry.trackCount),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _openPage(
|
||||
MornyeLibraryPage.albums,
|
||||
artist: entry.artist,
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, indent: 96, endIndent: 24),
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
sliver: SliverLayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final columns = (constraints.crossAxisExtent / 200)
|
||||
.floor()
|
||||
.clamp(2, 6);
|
||||
final width =
|
||||
(constraints.crossAxisExtent - 20 * (columns - 1)) /
|
||||
columns;
|
||||
final textHeight =
|
||||
MediaQuery.textScalerOf(context).scale(16) * 3.2 + 24;
|
||||
return SliverGrid.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: 20,
|
||||
mainAxisSpacing: 16,
|
||||
mainAxisExtent: width + textHeight,
|
||||
),
|
||||
itemCount: _rows.length,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = _rows[index];
|
||||
return Semantics(
|
||||
button: true,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _openAlbum(entry),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: _LibraryBrowseArtwork(entry: entry),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
entry.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
entry.artist,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (result.hasError)
|
||||
SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
ref.invalidate(libraryBrowseProvider(_request)),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(context.l10n.dialogRetry),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (_loading)
|
||||
const SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Center(child: CircularProgressIndicator.adaptive()),
|
||||
),
|
||||
)
|
||||
else if (_rows.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: Text(context.l10n.libraryEmptyCollection)),
|
||||
),
|
||||
const NavBarSliverSpacer(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LibraryBrowseArtwork extends StatefulWidget {
|
||||
const _LibraryBrowseArtwork({required this.entry});
|
||||
final LibraryBrowseEntry entry;
|
||||
|
||||
@override
|
||||
State<_LibraryBrowseArtwork> createState() => _LibraryBrowseArtworkState();
|
||||
}
|
||||
|
||||
class _LibraryBrowseArtworkState extends State<_LibraryBrowseArtwork> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final entry = widget.entry;
|
||||
final embedded = entry.cover?.isNotEmpty == true
|
||||
? null
|
||||
: DownloadedEmbeddedCoverResolver.resolve(
|
||||
entry.samplePath,
|
||||
onChanged: () {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
);
|
||||
Widget fallback(BuildContext context) => ColoredBox(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.album_outlined,
|
||||
size: 40,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
final cover = entry.cover?.isNotEmpty == true ? entry.cover! : embedded;
|
||||
return cover == null
|
||||
? fallback(context)
|
||||
: LocalOrNetworkCoverImage(url: cover, placeholder: fallback);
|
||||
}
|
||||
}
|
||||
+101
-34
@@ -50,6 +50,7 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:spotiflac_android/services/cover_cache_manager.dart';
|
||||
import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart';
|
||||
import 'package:spotiflac_android/screens/local_album_screen.dart';
|
||||
import 'package:spotiflac_android/screens/mornye_library_screen.dart';
|
||||
import 'package:spotiflac_android/screens/queue_library_refresh_policy.dart';
|
||||
import 'package:spotiflac_android/utils/clickable_metadata.dart';
|
||||
import 'package:spotiflac_android/utils/string_utils.dart';
|
||||
@@ -207,12 +208,14 @@ DownloadHistoryItem? _historyItemForCompletionBridge(
|
||||
}
|
||||
|
||||
class QueueTab extends ConsumerStatefulWidget {
|
||||
final String? librarySection;
|
||||
final PageController? parentPageController;
|
||||
final int parentPageIndex;
|
||||
final int? nextPageIndex;
|
||||
|
||||
const QueueTab({
|
||||
super.key,
|
||||
this.librarySection,
|
||||
this.parentPageController,
|
||||
this.parentPageIndex = 1,
|
||||
this.nextPageIndex,
|
||||
@@ -349,13 +352,16 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
if (_isPageControllerInitialized) return;
|
||||
_isPageControllerInitialized = true;
|
||||
final settings = ref.read(settingsProvider);
|
||||
final initialFilter = settings.defaultLibraryView == 'last'
|
||||
? settings.historyFilterMode
|
||||
: settings.defaultLibraryView;
|
||||
final initialFilter =
|
||||
widget.librarySection ??
|
||||
(settings.defaultLibraryView == 'last'
|
||||
? settings.historyFilterMode
|
||||
: settings.defaultLibraryView);
|
||||
final initialPage = _filterModes
|
||||
.indexOf(initialFilter)
|
||||
.clamp(0, _filterModes.length - 1);
|
||||
if (settings.historyFilterMode != _filterModes[initialPage]) {
|
||||
if (widget.librarySection == null &&
|
||||
settings.historyFilterMode != _filterModes[initialPage]) {
|
||||
Future.microtask(() {
|
||||
if (!mounted) return;
|
||||
ref
|
||||
@@ -369,6 +375,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
/// When the shell switches back to this tab and a fixed default view is
|
||||
/// configured, jump the filter pager to it.
|
||||
void _applyDefaultLibraryViewOnTabVisible() {
|
||||
if (widget.librarySection != null) return;
|
||||
final isVisible = TickerMode.valuesOf(context).enabled;
|
||||
final becameVisible = isVisible && !_wasTabVisible;
|
||||
_wasTabVisible = isVisible;
|
||||
@@ -651,6 +658,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
}
|
||||
|
||||
void _onFilterPageChanged(int index) {
|
||||
if (widget.librarySection != null) return;
|
||||
HapticFeedback.selectionClick();
|
||||
final filterMode = _filterModes[index];
|
||||
ref.read(settingsProvider.notifier).setHistoryFilterMode(filterMode);
|
||||
@@ -1220,6 +1228,15 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (context.isMornye && widget.librarySection == null) {
|
||||
return MornyeLibraryScreen(
|
||||
onOpenSection: (section) => Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) => Scaffold(body: QueueTab(librarySection: section)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
_initializePageController();
|
||||
_applyDefaultLibraryViewOnTabVisible();
|
||||
|
||||
@@ -1278,6 +1295,34 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.librarySection == 'downloads') {
|
||||
final ids = ref.watch(
|
||||
downloadQueueLookupProvider.select((s) => s.notCompletedItemIds),
|
||||
);
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
AppSliverHeader.page(title: context.l10n.libraryDownloads),
|
||||
_buildQueueHeaderSliver(context, colors),
|
||||
SliverList.builder(
|
||||
itemCount: ids.length,
|
||||
itemBuilder: (context, index) => _QueueItemSliverRow(
|
||||
key: ValueKey(ids[index]),
|
||||
itemId: ids[index],
|
||||
colorScheme: colors,
|
||||
itemBuilder: _buildQueueItem,
|
||||
),
|
||||
),
|
||||
if (ids.isEmpty)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(child: Text(context.l10n.libraryNoActiveDownloads)),
|
||||
),
|
||||
const NavBarSliverSpacer(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final hasQueueItems = ref.watch(
|
||||
downloadQueueLookupProvider.select((lookup) => lookup.itemIds.isNotEmpty),
|
||||
);
|
||||
@@ -1314,9 +1359,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
final historyViewMode = ref.watch(
|
||||
settingsProvider.select((s) => s.historyViewMode),
|
||||
);
|
||||
final historyFilterMode = ref.watch(
|
||||
settingsProvider.select((s) => s.historyFilterMode),
|
||||
);
|
||||
final String historyFilterMode =
|
||||
widget.librarySection ??
|
||||
ref.watch<String>(settingsProvider.select((s) => s.historyFilterMode));
|
||||
// Keep this mode out of the page-provider request: changing only badge
|
||||
// text must not re-query the database or reset Library pagination.
|
||||
_libraryQualityLabelMode = ref.watch(
|
||||
@@ -1493,7 +1538,14 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
).copyWith(overscroll: false),
|
||||
child: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
AppSliverHeader.tabRoot(title: context.l10n.navLibrary),
|
||||
if (widget.librarySection == null)
|
||||
AppSliverHeader.tabRoot(title: context.l10n.navLibrary)
|
||||
else
|
||||
AppSliverHeader.page(
|
||||
title: widget.librarySection == 'playlists'
|
||||
? context.l10n.searchPlaylists
|
||||
: context.l10n.searchSongs,
|
||||
),
|
||||
|
||||
if (shouldShowLibraryControls || hasQueueItems)
|
||||
SliverToBoxAdapter(
|
||||
@@ -1504,7 +1556,9 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
child: AppSearchField(
|
||||
controller: _searchController,
|
||||
focusNode: _searchFocusNode,
|
||||
hintText: context.l10n.historySearchHint,
|
||||
hintText: widget.librarySection == null
|
||||
? context.l10n.historySearchHint
|
||||
: context.l10n.librarySearchHint,
|
||||
clearTooltip: context.l10n.dialogClear,
|
||||
onChanged: _onSearchChanged,
|
||||
onClear: () {
|
||||
@@ -1516,7 +1570,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
),
|
||||
|
||||
if (shouldShowLibraryControls)
|
||||
if (shouldShowLibraryControls && widget.librarySection == null)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
@@ -1585,30 +1639,43 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
),
|
||||
],
|
||||
body: PageView.builder(
|
||||
controller: _filterPageController!,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
onPageChanged: _onFilterPageChanged,
|
||||
itemCount: _filterModes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filterMode = _filterModes[index];
|
||||
final filterData = getFilterData(filterMode);
|
||||
return _buildFilterContent(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
filterMode: filterMode,
|
||||
historyViewMode: historyViewMode,
|
||||
hasQueueItems: hasQueueItems,
|
||||
filterData: filterData,
|
||||
collectionState: collectionState,
|
||||
hasMoreLibrary: filterMode == historyFilterMode
|
||||
? hasMoreLibrary
|
||||
: false,
|
||||
isPageLoading: isLibraryPageLoading,
|
||||
inMemoryHistoryItems: inMemoryHistoryItems,
|
||||
);
|
||||
},
|
||||
),
|
||||
body: widget.librarySection != null
|
||||
? _buildFilterContent(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
filterMode: historyFilterMode,
|
||||
historyViewMode: 'list',
|
||||
hasQueueItems: false,
|
||||
filterData: getFilterData(historyFilterMode),
|
||||
collectionState: collectionState,
|
||||
hasMoreLibrary: hasMoreLibrary,
|
||||
isPageLoading: isLibraryPageLoading,
|
||||
inMemoryHistoryItems: inMemoryHistoryItems,
|
||||
)
|
||||
: PageView.builder(
|
||||
controller: _filterPageController!,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
onPageChanged: _onFilterPageChanged,
|
||||
itemCount: _filterModes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final filterMode = _filterModes[index];
|
||||
final filterData = getFilterData(filterMode);
|
||||
return _buildFilterContent(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
filterMode: filterMode,
|
||||
historyViewMode: historyViewMode,
|
||||
hasQueueItems: hasQueueItems,
|
||||
filterData: filterData,
|
||||
collectionState: collectionState,
|
||||
hasMoreLibrary: filterMode == historyFilterMode
|
||||
? hasMoreLibrary
|
||||
: false,
|
||||
isPageLoading: isLibraryPageLoading,
|
||||
inMemoryHistoryItems: inMemoryHistoryItems,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
), // ScrollConfiguration
|
||||
],
|
||||
|
||||
@@ -52,7 +52,7 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
final totalTrackCount = filterData.totalTrackCount;
|
||||
final totalAlbumCount = filterData.totalAlbumCount;
|
||||
|
||||
final activeDownloadIds = filterMode == 'albums'
|
||||
final activeDownloadIds = !hasQueueItems || filterMode == 'albums'
|
||||
? const <String>[]
|
||||
: ref
|
||||
.watch(
|
||||
@@ -74,7 +74,9 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
),
|
||||
};
|
||||
List<String> bridgeIds = const [];
|
||||
if (filterMode != 'albums' && _completionBridge.isNotEmpty) {
|
||||
if (hasQueueItems &&
|
||||
filterMode != 'albums' &&
|
||||
_completionBridge.isNotEmpty) {
|
||||
final now = DateTime.now();
|
||||
final stale = <String>[];
|
||||
final pending = <String>[];
|
||||
@@ -394,7 +396,8 @@ extension _QueueTabFilterWidgets on _QueueTabState {
|
||||
),
|
||||
),
|
||||
|
||||
if (filterMode == 'all') _buildQueueHeaderSliver(context, colorScheme),
|
||||
if (filterMode == 'all' && hasQueueItems)
|
||||
_buildQueueHeaderSliver(context, colorScheme),
|
||||
|
||||
if (filterMode == 'albums' &&
|
||||
(filteredGroupedAlbums.isNotEmpty ||
|
||||
|
||||
@@ -1067,9 +1067,12 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
(result['lyrics'] ?? result['lyrics_lrc'])?.toString() ?? '',
|
||||
),
|
||||
);
|
||||
await _checkEmbeddedLyrics();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.trackReEnrichSuccess)),
|
||||
SnackBar(
|
||||
content: Text(reEnrichCompletionMessage(context.l10n, result)),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (method == 'ffmpeg') {
|
||||
@@ -1168,9 +1171,14 @@ extension _TrackMetadataLyricsAndSaving on _TrackMetadataScreenState {
|
||||
(result['lyrics'] ?? result['lyrics_lrc'])?.toString() ?? '',
|
||||
),
|
||||
);
|
||||
await _checkEmbeddedLyrics();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.trackReEnrichSuccess)),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
reEnrichCompletionMessage(context.l10n, result),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (mounted) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:path/path.dart' as p;
|
||||
import 'package:spotiflac_android/services/conversion_library_service.dart';
|
||||
import 'package:spotiflac_android/services/library_database.dart';
|
||||
import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/utils/re_enrich_result.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
Reference in new issue
Block a user