mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 21:29:36 +02:00
284 lines
9.7 KiB
Dart
284 lines
9.7 KiB
Dart
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_history_provider.dart';
|
|
import 'package:spotiflac_android/providers/library_search_provider.dart';
|
|
import 'package:spotiflac_android/providers/music_player_provider.dart';
|
|
import 'package:spotiflac_android/providers/playback_provider.dart';
|
|
import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
|
|
import 'package:spotiflac_android/screens/library_tracks_folder_screen.dart';
|
|
import 'package:spotiflac_android/screens/local_album_screen.dart';
|
|
import 'package:spotiflac_android/services/history_database.dart';
|
|
import 'package:spotiflac_android/services/library_database.dart';
|
|
import 'package:spotiflac_android/services/library_search.dart';
|
|
import 'package:spotiflac_android/widgets/app_choice_chip.dart';
|
|
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
|
|
import 'package:spotiflac_android/widgets/track_card.dart';
|
|
|
|
/// Slivers shared by Material's Library and Mornye's Library/Songs screens.
|
|
/// Only the current query's provider families are watched; late results from
|
|
/// an earlier query cannot replace what the user is currently searching for.
|
|
class LibrarySearchResults extends ConsumerStatefulWidget {
|
|
const LibrarySearchResults({
|
|
super.key,
|
|
required this.query,
|
|
required this.onOpenArtist,
|
|
});
|
|
|
|
final String query;
|
|
final ValueChanged<String> onOpenArtist;
|
|
|
|
@override
|
|
ConsumerState<LibrarySearchResults> createState() =>
|
|
_LibrarySearchResultsState();
|
|
}
|
|
|
|
class _LibrarySearchResultsState extends ConsumerState<LibrarySearchResults> {
|
|
LibrarySearchKind? _kind;
|
|
int _pages = 1;
|
|
|
|
@override
|
|
void didUpdateWidget(covariant LibrarySearchResults oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.query != widget.query) _pages = 1;
|
|
}
|
|
|
|
String _label(LibrarySearchKind kind) => switch (kind) {
|
|
LibrarySearchKind.songs => context.l10n.searchSongs,
|
|
LibrarySearchKind.albums => context.l10n.searchAlbums,
|
|
LibrarySearchKind.artists => context.l10n.searchArtists,
|
|
LibrarySearchKind.playlists => context.l10n.searchPlaylists,
|
|
};
|
|
|
|
void _select(LibrarySearchKind? kind) => setState(() {
|
|
_kind = kind;
|
|
_pages = 1;
|
|
});
|
|
|
|
Future<void> _open(LibrarySearchHit hit) async {
|
|
FocusScope.of(context).unfocus();
|
|
try {
|
|
switch (hit.kind) {
|
|
case LibrarySearchKind.songs:
|
|
final row = hit.source == 'local'
|
|
? await LibraryDatabase.instance.getById(hit.id)
|
|
: await HistoryDatabase.instance.getById(hit.id);
|
|
if (!mounted || row == null) return;
|
|
final media = hit.source == 'local'
|
|
? playableFromLocal(LocalLibraryItem.fromJson(row))
|
|
: playableFromHistory(DownloadHistoryItem.fromJson(row));
|
|
await ref
|
|
.read(playbackProvider.notifier)
|
|
.playMediaQueue(
|
|
[media],
|
|
startIndex: 0,
|
|
externalPath: media.source,
|
|
);
|
|
case LibrarySearchKind.artists:
|
|
widget.onOpenArtist(hit.title);
|
|
case LibrarySearchKind.playlists:
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => LibraryTracksFolderScreen(
|
|
mode: LibraryTracksFolderMode.playlist,
|
|
playlistId: hit.id,
|
|
),
|
|
),
|
|
);
|
|
case LibrarySearchKind.albums:
|
|
if (hit.source == 'local') {
|
|
final rows = await LibraryDatabase.instance
|
|
.getQueueLocalAlbumTracksByKey(hit.id);
|
|
if (!mounted) return;
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => LocalAlbumScreen(
|
|
albumName: hit.title,
|
|
artistName: hit.artist,
|
|
coverPath: hit.cover,
|
|
tracks: rows.map(LocalLibraryItem.fromJson).toList(),
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
await Navigator.of(context).push(
|
|
MaterialPageRoute<void>(
|
|
builder: (_) => DownloadedAlbumScreen(
|
|
albumName: hit.title,
|
|
artistName: hit.artist,
|
|
coverUrl: hit.cover,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(context.l10n.snackbarCannotOpenFile('$error'))),
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _row(LibrarySearchHit hit) {
|
|
final icon = switch (hit.kind) {
|
|
LibrarySearchKind.songs => Icons.music_note_outlined,
|
|
LibrarySearchKind.albums => Icons.album_outlined,
|
|
LibrarySearchKind.artists => Icons.person_outline,
|
|
LibrarySearchKind.playlists => Icons.queue_music,
|
|
};
|
|
final subtitle = hit.kind == LibrarySearchKind.songs
|
|
? hit.artist
|
|
: [
|
|
if (hit.kind == LibrarySearchKind.albums) hit.artist,
|
|
context.l10n.queueTrackCount(hit.trackCount),
|
|
].join(' · ');
|
|
Widget fallback(BuildContext context) => ColoredBox(
|
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
child: Center(child: Icon(icon)),
|
|
);
|
|
return TrackCard(
|
|
key: ValueKey('${hit.kind.name}:${hit.source}:${hit.id}'),
|
|
style: TrackCardStyle.flat,
|
|
leading: ClipRRect(
|
|
borderRadius: BorderRadius.circular(
|
|
hit.kind == LibrarySearchKind.artists ? 28 : 8,
|
|
),
|
|
child: SizedBox.square(
|
|
dimension: 52,
|
|
child: hit.cover?.isNotEmpty == true
|
|
? LocalOrNetworkCoverImage(url: hit.cover!, placeholder: fallback)
|
|
: fallback(context),
|
|
),
|
|
),
|
|
title: hit.title,
|
|
subtitle: Text(subtitle, maxLines: 2, overflow: TextOverflow.ellipsis),
|
|
trailing: Icon(
|
|
hit.kind == LibrarySearchKind.songs
|
|
? Icons.play_arrow_rounded
|
|
: Icons.chevron_right,
|
|
),
|
|
onTap: () => _open(hit),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final slivers = <Widget>[
|
|
SliverToBoxAdapter(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
child: Row(
|
|
spacing: 8,
|
|
children: [
|
|
for (final kind in [null, ...LibrarySearchKind.values])
|
|
AppChoiceChip(
|
|
label: Text(
|
|
kind == null ? context.l10n.historyFilterAll : _label(kind),
|
|
),
|
|
selected: _kind == kind,
|
|
singleChoice: true,
|
|
onSelected: (_) => _select(kind),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
];
|
|
var hasResults = false;
|
|
var loading = false;
|
|
var failed = false;
|
|
for (final kind in _kind == null ? LibrarySearchKind.values : [_kind!]) {
|
|
for (var page = 0; page < (_kind == null ? 1 : _pages); page++) {
|
|
final pageSize = _kind == null ? 5 : 40;
|
|
final request = (
|
|
query: widget.query,
|
|
kind: kind,
|
|
limit: pageSize + 1,
|
|
offset: page * pageSize,
|
|
);
|
|
final result = ref.watch(librarySearchProvider(request));
|
|
loading |= result.isLoading;
|
|
failed |= result.hasError;
|
|
final hits = result.value ?? const <LibrarySearchHit>[];
|
|
if (hits.isNotEmpty) {
|
|
hasResults = true;
|
|
if (page == 0) {
|
|
slivers.add(
|
|
SliverToBoxAdapter(
|
|
child: ListTile(
|
|
title: Text(
|
|
_label(kind),
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
trailing: _kind == null
|
|
? const Icon(Icons.chevron_right)
|
|
: null,
|
|
onTap: _kind == null ? () => _select(kind) : null,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
slivers.add(
|
|
SliverList.builder(
|
|
itemCount: hits.length.clamp(0, pageSize),
|
|
itemBuilder: (context, index) => _row(hits[index]),
|
|
),
|
|
);
|
|
if (_kind != null && page == _pages - 1 && hits.length > pageSize) {
|
|
slivers.add(
|
|
SliverToBoxAdapter(
|
|
child: Center(
|
|
child: IconButton(
|
|
tooltip: _label(kind),
|
|
icon: const Icon(Icons.expand_more),
|
|
onPressed: () => setState(() => _pages++),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
if (result.hasError) {
|
|
slivers.add(
|
|
SliverToBoxAdapter(
|
|
child: Center(
|
|
child: TextButton.icon(
|
|
icon: const Icon(Icons.refresh),
|
|
label: Text('${_label(kind)} · ${context.l10n.dialogRetry}'),
|
|
onPressed: () =>
|
|
ref.invalidate(librarySearchProvider(request)),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (loading) {
|
|
slivers.add(
|
|
const SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(24),
|
|
child: Center(child: CircularProgressIndicator.adaptive()),
|
|
),
|
|
),
|
|
);
|
|
} else if (!hasResults && !failed) {
|
|
slivers.add(
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Text(
|
|
context.l10n.searchEmptyResultSubtitle,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return SliverMainAxisGroup(slivers: slivers);
|
|
}
|
|
}
|