subtracks/lib/app/pages/songs_page.dart
austinried f0f812e66a v2
2023-04-28 12:26:02 +09:00

512 lines
13 KiB
Dart

import 'dart:async';
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:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.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 '../../state/theme.dart';
import '../buttons.dart';
import '../context_menus.dart';
import '../gradient.dart';
import '../hooks/use_download_actions.dart';
import '../hooks/use_list_query_paging_controller.dart';
import '../images.dart';
import '../items.dart';
import '../lists.dart';
class AlbumSongsPage extends HookConsumerWidget {
final String id;
const AlbumSongsPage({
super.key,
@pathParam required this.id,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final album = ref.watch(albumProvider(id)).valueOrNull;
final audio = ref.watch(audioControlProvider);
final colors = ref.watch(albumArtThemeProvider(id)).valueOrNull;
final key = useState(GlobalKey());
if (album == null) {
return Container();
}
final query = useMemoized(() => const ListQuery(
page: Pagination(limit: 30),
sort: SortBy(column: 'disc, track'),
));
final getSongs = useCallback(
(ListQuery query) => ref.read(albumSongsListProvider(id, query).future),
[id],
);
final play = useCallback(
({int? index, bool? shuffle}) => audio.playSongs(
query: query,
getSongs: getSongs,
startIndex: index,
context: QueueContextType.album,
contextId: id,
shuffle: shuffle,
),
[id, query, getSongs],
);
return QueueContext(
id: id,
type: QueueContextType.album,
child: _SongsPage(
query: query,
getSongs: getSongs,
fab: ShuffleFab(onPressed: () => play(shuffle: true)),
onSongTap: (song, index) => play(index: index),
background: AlbumArtGradient(key: key.value, id: id),
colors: colors,
header: _AlbumHeader(
album: album,
play: () => play(shuffle: false),
),
),
);
}
}
class _AlbumHeader extends HookConsumerWidget {
final Album album;
final void Function() play;
const _AlbumHeader({
required this.album,
required this.play,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final cache = ref.watch(cacheServiceProvider);
final downloadActions = useAlbumDownloadActions(
context: context,
ref: ref,
album: album,
);
final l = AppLocalizations.of(context);
return _Header(
title: album.name,
subtitle: album.albumArtist,
imageCache: cache.albumArt(album, thumbnail: false),
playText: l.resourcesAlbumActionsPlay,
onPlay: play,
onMore: () => showContextMenu(
context: context,
ref: ref,
builder: (context) => BottomSheetMenu(
child: AlbumContextMenu(album: album),
),
),
downloadActions: downloadActions,
);
}
}
class PlaylistSongsPage extends HookConsumerWidget {
final String id;
const PlaylistSongsPage({
super.key,
@pathParam required this.id,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final playlist = ref.watch(playlistProvider(id)).valueOrNull;
final audio = ref.watch(audioControlProvider);
final colors = ref.watch(playlistArtThemeProvider(id)).valueOrNull;
if (playlist == null) {
return Container();
}
final query = useMemoized(() => const ListQuery(
page: Pagination(limit: 30),
sort: SortBy(column: 'playlist_songs.position'),
));
final getSongs = useCallback(
(ListQuery query) =>
ref.read(playlistSongsListProvider(id, query).future),
[id],
);
final play = useCallback(
({int? index, bool? shuffle}) => audio.playSongs(
query: query,
getSongs: getSongs,
startIndex: index,
context: QueueContextType.playlist,
contextId: id,
shuffle: shuffle,
),
[id, query, getSongs],
);
return QueueContext(
id: id,
type: QueueContextType.playlist,
child: _SongsPage(
query: query,
getSongs: getSongs,
fab: ShuffleFab(onPressed: () => play(shuffle: true)),
onSongTap: (song, index) => play(index: index),
songImage: true,
background: PlaylistArtGradient(id: id),
colors: colors,
header: _PlaylistHeader(
playlist: playlist,
play: () => play(shuffle: false),
),
),
);
}
}
class _PlaylistHeader extends HookConsumerWidget {
final Playlist playlist;
final void Function() play;
const _PlaylistHeader({
required this.playlist,
required this.play,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final cache = ref.watch(cacheServiceProvider);
final downloadActions = usePlaylistDownloadActions(
context: context,
ref: ref,
playlist: playlist,
);
final l = AppLocalizations.of(context);
return _Header(
title: playlist.name,
subtitle: playlist.comment,
imageCache: cache.playlistArt(playlist, thumbnail: false),
playText: l.resourcesPlaylistActionsPlay,
onPlay: play,
onMore: () {
showContextMenu(
context: context,
ref: ref,
builder: (context) => BottomSheetMenu(
size: MenuSize.small,
child: PlaylistContextMenu(playlist: playlist),
),
);
},
downloadActions: downloadActions,
);
}
}
class GenreSongsPage extends HookConsumerWidget {
final String genre;
const GenreSongsPage({
super.key,
@pathParam required this.genre,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final query = useMemoized(
() => ListQuery(
page: const Pagination(limit: 30),
sort: const SortBy(
column: 'albums.created DESC, albums.name, songs.disc, songs.track',
),
filters: IList(
[FilterWith.equals(column: 'songs.genre', value: genre)],
),
),
[genre],
);
final getSongs = useCallback(
(ListQuery query) => ref.read(songsByAlbumListProvider(query).future),
[],
);
final play = useCallback(
({int? index, bool? shuffle}) => ref.read(audioControlProvider).playRadio(
context: QueueContextType.genre,
contextId: genre,
query: query,
getSongs: getSongs,
),
[query, getSongs],
);
return QueueContext(
id: genre,
type: QueueContextType.album,
child: _SongsPage(
query: query,
getSongs: getSongs,
// onSongTap: (song, index) => play(index: index),
songImage: true,
background: const BackgroundGradient(),
fab: RadioPlayFab(
onPressed: () => play(),
),
header: _GenreHeader(genre: genre),
),
);
}
}
class _GenreHeader extends HookConsumerWidget {
final String genre;
const _GenreHeader({
required this.genre,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(songsByGenreCountProvider(genre)).valueOrNull ?? 0;
final l = AppLocalizations.of(context);
return _Header(
title: genre,
subtitle: l.resourcesSongCount(count),
downloadActions: const [],
);
}
}
class QueueContext extends InheritedWidget {
final QueueContextType type;
final String? id;
const QueueContext({
super.key,
required this.type,
this.id,
required super.child,
});
static QueueContext? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<QueueContext>();
}
static QueueContext of(BuildContext context) {
final QueueContext? result = maybeOf(context);
assert(result != null, 'No QueueContext found in context');
return result!;
}
@override
bool updateShouldNotify(covariant QueueContext oldWidget) =>
oldWidget.id != id || oldWidget.type != type;
}
class _SongsPage extends HookConsumerWidget {
final ListQuery query;
final FutureOr<List<Song>> Function(ListQuery query) getSongs;
final void Function(Song song, int index)? onSongTap;
final bool songImage;
final Widget background;
final Widget fab;
final ColorTheme? colors;
final Widget header;
const _SongsPage({
required this.query,
required this.getSongs,
this.onSongTap,
this.songImage = false,
required this.background,
required this.fab,
this.colors,
required this.header,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final base = ref.watch(baseThemeProvider);
ref.listen(musicSourceProvider, (previous, next) {
if (next.id != previous?.id) {
context.router.popUntilRoot();
}
});
final pagingController = useListQueryPagingController(
ref,
query: query,
getItems: getSongs,
);
final widget = Scaffold(
floatingActionButton: fab,
body: CustomScrollView(
slivers: [
SliverStack(
children: [
SliverPositioned.fill(
child: Container(
color: base.gradientLow,
),
),
SliverPositioned.directional(
textDirection: TextDirection.ltr,
start: 0,
end: 0,
top: 0,
child: background,
),
MultiSliver(
children: [
SliverSafeArea(
sliver: SliverToBoxAdapter(
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: const EdgeInsets.all(16),
child: header,
),
),
),
),
PagedListQueryView(
pagingController: pagingController,
useSliver: true,
itemBuilder: (context, item, index) => SongListTile(
song: item,
image: songImage,
onTap: () =>
onSongTap != null ? onSongTap!(item, index) : null,
),
),
],
),
],
),
],
),
);
if (colors != null) {
return Theme(data: colors!.theme, child: widget);
} else {
return widget;
}
}
}
class _Header extends HookConsumerWidget {
final UriCacheInfo? imageCache;
final String title;
final String? subtitle;
final String? playText;
final void Function()? onPlay;
final FutureOr<void> Function()? onMore;
final List<DownloadAction> downloadActions;
const _Header({
this.imageCache,
required this.title,
this.subtitle,
this.playText,
this.onPlay,
this.onMore,
required this.downloadActions,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final inheritedStyle = DefaultTextStyle.of(context).style;
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 10),
if (imageCache != null)
CardClip(
square: false,
child: UriCacheInfoImage(
height: 300,
fit: BoxFit.contain,
placeholderStyle: PlaceholderStyle.spinner,
cache: imageCache!,
),
),
const SizedBox(height: 16),
Column(
children: [
Text(
title,
style: theme.textTheme.titleLarge!.copyWith(
color: inheritedStyle.color,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
Text(
subtitle ?? '',
style: theme.textTheme.titleMedium!.copyWith(
color: inheritedStyle.color,
),
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
if (downloadActions.isNotEmpty)
IconButton(
onPressed: downloadActions.first.action,
icon: downloadActions.first.type == DownloadActionType.delete
? const Icon(Icons.download_done_rounded)
: downloadActions.first.iconBuilder(context),
),
if (onPlay != null)
FilledButton.icon(
onPressed: onPlay,
icon: const Icon(Icons.play_arrow_rounded),
label: Text(playText ?? ''),
),
if (onMore != null)
IconButton(
onPressed: onMore,
icon: const Icon(Icons.more_horiz),
)
],
),
],
);
}
}