mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
282 lines
7.4 KiB
Dart
282 lines
7.4 KiB
Dart
import 'package:auto_route/auto_route.dart';
|
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:sliver_tools/sliver_tools.dart';
|
|
|
|
import '../../database/database.dart';
|
|
import '../../models/music.dart';
|
|
import '../../models/query.dart';
|
|
import '../../models/support.dart';
|
|
import '../../services/audio_service.dart';
|
|
import '../../services/cache_service.dart';
|
|
import '../../state/music.dart';
|
|
import '../../state/settings.dart';
|
|
import '../app_router.dart';
|
|
import '../buttons.dart';
|
|
import '../images.dart';
|
|
import '../items.dart';
|
|
|
|
part 'browse_page.g.dart';
|
|
|
|
@riverpod
|
|
Stream<List<Album>> albumsCategoryList(
|
|
AlbumsCategoryListRef ref,
|
|
ListQuery opt,
|
|
) {
|
|
final db = ref.watch(databaseProvider);
|
|
final sourceId = ref.watch(sourceIdProvider);
|
|
|
|
return db.albumsList(sourceId, opt).watch();
|
|
}
|
|
|
|
class BrowsePage extends HookConsumerWidget {
|
|
const BrowsePage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final l = AppLocalizations.of(context);
|
|
|
|
final frequent = ref
|
|
.watch(albumsCategoryListProvider(const ListQuery(
|
|
page: Pagination(limit: 20),
|
|
sort: SortBy(column: 'frequent_rank'),
|
|
filters: IListConst([
|
|
FilterWith.isNull(column: 'frequent_rank', invert: true),
|
|
]),
|
|
)))
|
|
.valueOrNull;
|
|
final recent = ref
|
|
.watch(albumsCategoryListProvider(const ListQuery(
|
|
page: Pagination(limit: 20),
|
|
sort: SortBy(column: 'recent_rank'),
|
|
filters: IListConst([
|
|
FilterWith.isNull(column: 'recent_rank', invert: true),
|
|
]),
|
|
)))
|
|
.valueOrNull;
|
|
final starred = ref
|
|
.watch(albumsCategoryListProvider(const ListQuery(
|
|
page: Pagination(limit: 20),
|
|
sort: SortBy(column: 'starred'),
|
|
filters: IListConst([
|
|
FilterWith.isNull(column: 'starred', invert: true),
|
|
]),
|
|
)))
|
|
.valueOrNull;
|
|
final random = ref
|
|
.watch(albumsCategoryListProvider(const ListQuery(
|
|
page: Pagination(limit: 20),
|
|
sort: SortBy(column: 'RANDOM()'),
|
|
)))
|
|
.valueOrNull;
|
|
|
|
final genres = ref
|
|
.watch(albumGenresProvider(const Pagination(
|
|
limit: 20,
|
|
)))
|
|
.valueOrNull;
|
|
|
|
return Scaffold(
|
|
floatingActionButton: RadioPlayFab(
|
|
onPressed: () {
|
|
ref.read(audioControlProvider).playRadio(
|
|
context: QueueContextType.library,
|
|
getSongs: (query) => ref
|
|
.read(databaseProvider)
|
|
.songsList(ref.read(sourceIdProvider), query)
|
|
.get(),
|
|
);
|
|
},
|
|
),
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
const SliverSafeArea(
|
|
sliver: SliverPadding(padding: EdgeInsets.only(top: 8)),
|
|
),
|
|
_GenreCategory(
|
|
title: 'Genres',
|
|
items: genres?.toList() ?? [],
|
|
),
|
|
_AlbumCategory(
|
|
title: l.resourcesSortByFrequentlyPlayed,
|
|
items: frequent ?? [],
|
|
),
|
|
_AlbumCategory(
|
|
title: l.resourcesSortByRecentlyPlayed,
|
|
items: recent ?? [],
|
|
),
|
|
_AlbumCategory(
|
|
title: l.resourcesFilterStarred,
|
|
items: starred ?? [],
|
|
),
|
|
_AlbumCategory(
|
|
title: l.resourcesSortByRandom,
|
|
items: random ?? [],
|
|
),
|
|
const SliverToBoxAdapter(child: FabPadding()),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _GenreCategory extends HookConsumerWidget {
|
|
final String title;
|
|
final List<String> items;
|
|
|
|
const _GenreCategory({
|
|
required this.title,
|
|
required this.items,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return SliverPadding(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
sliver: _Category(
|
|
title: title,
|
|
height: 140,
|
|
itemWidth: 140,
|
|
items: items.map((genre) => _GenreItem(genre: genre)).toList(),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _GenreItem extends HookConsumerWidget {
|
|
final String genre;
|
|
|
|
const _GenreItem({
|
|
required this.genre,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final albums = ref
|
|
.watch(albumsByGenreProvider(
|
|
genre,
|
|
const Pagination(limit: 4),
|
|
))
|
|
.valueOrNull;
|
|
final cache = ref.watch(cacheServiceProvider);
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
if (albums == null) {
|
|
return Container();
|
|
}
|
|
|
|
return ImageCard(
|
|
onTap: () {
|
|
context.navigateTo(GenreSongsRoute(genre: genre));
|
|
},
|
|
child: Stack(
|
|
alignment: AlignmentDirectional.center,
|
|
children: [
|
|
CardClip(
|
|
child: MultiImage(
|
|
cacheInfo: albums.map((album) => cache.albumArt(album)),
|
|
),
|
|
),
|
|
Material(
|
|
type: MaterialType.canvas,
|
|
color: theme.colorScheme.secondaryContainer,
|
|
elevation: 5,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: Text(
|
|
genre,
|
|
textAlign: TextAlign.center,
|
|
style: theme.textTheme.labelLarge,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AlbumCategory extends HookConsumerWidget {
|
|
final String title;
|
|
final List<Album> items;
|
|
|
|
const _AlbumCategory({
|
|
required this.title,
|
|
required this.items,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return _Category(
|
|
title: title,
|
|
height: 190,
|
|
itemWidth: 140,
|
|
items: items
|
|
.map(
|
|
(album) => AlbumCard(
|
|
album: album,
|
|
onTap: () => context.navigateTo(
|
|
AlbumSongsRoute(
|
|
id: album.id,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
.toList(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Category extends HookConsumerWidget {
|
|
final String title;
|
|
final List<Widget> items;
|
|
final double height;
|
|
final double itemWidth;
|
|
|
|
const _Category({
|
|
required this.title,
|
|
required this.items,
|
|
required this.height,
|
|
required this.itemWidth,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return MultiSliver(
|
|
children: [
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
|
child: Text(
|
|
title,
|
|
style: Theme.of(context).textTheme.headlineMedium,
|
|
),
|
|
),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: SizedBox(
|
|
height: height,
|
|
child: ListView.separated(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: items.length,
|
|
itemBuilder: (context, index) => SizedBox(
|
|
width: itemWidth,
|
|
child: items[index],
|
|
),
|
|
separatorBuilder: (context, index) => const SizedBox(width: 8),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|