artist screen

This commit is contained in:
austinried 2025-12-07 11:26:21 +09:00
parent d245fc7fef
commit 805e6fff7a
10 changed files with 293 additions and 19 deletions

View File

@ -29,8 +29,9 @@ final router = GoRouter(
AlbumScreen(id: state.pathParameters['id']!), AlbumScreen(id: state.pathParameters['id']!),
), ),
GoRoute( GoRoute(
path: 'artists', path: 'artists/:id',
builder: (context, state) => ArtistScreen(), builder: (context, state) =>
ArtistScreen(id: state.pathParameters['id']!),
), ),
GoRoute( GoRoute(
path: 'playlists/:id', path: 'playlists/:id',

View File

@ -1,12 +1,105 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class ArtistScreen extends StatelessWidget { import '../../database/query.dart';
const ArtistScreen({super.key}); import '../../sources/models.dart';
import '../state/database.dart';
import '../state/source.dart';
import '../ui/cover_art_theme.dart';
import '../ui/gradient.dart';
import '../ui/images.dart';
import '../ui/lists/albums_list.dart';
class ArtistScreen extends HookConsumerWidget {
const ArtistScreen({
super.key,
required this.id,
});
final String id;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return Scaffold( final db = ref.watch(databaseProvider);
body: Center(child: Text('Artist!')), final sourceId = ref.watch(sourceIdProvider);
final getArtist = useMemoized(
() => db.libraryDao.getArtist(sourceId, id).getSingle(),
);
final artist = useFuture(getArtist).data;
if (artist == null) {
return Container();
}
final query = AlbumsQuery(
sourceId: sourceId,
// filter: IList([AlbumsFilter.artistId(artist.id)]),
sort: IList([
SortingTerm.albumsDesc(AlbumsColumn.year),
SortingTerm.albumsAsc(AlbumsColumn.name),
]),
);
return CoverArtTheme(
coverArt: artist.coverArt,
child: Scaffold(
body: GradientScrollView(
slivers: [
ArtistHeader(artist: artist),
AlbumsList(query: query),
],
),
),
);
}
}
class ArtistHeader extends StatelessWidget {
const ArtistHeader({
super.key,
required this.artist,
});
final Artist artist;
@override
Widget build(BuildContext context) {
final textTheme = TextTheme.of(context);
final colorScheme = ColorScheme.of(context);
return SliverToBoxAdapter(
child: Stack(
fit: StackFit.passthrough,
alignment: AlignmentGeometry.bottomCenter,
children: [
CoverArtImage(
fit: BoxFit.cover,
coverArt: artist.coverArt,
thumbnail: false,
),
Container(
color: colorScheme.surface.withAlpha(120),
child: Padding(
padding: EdgeInsetsGeometry.symmetric(vertical: 12),
child: Text(
artist.name,
textAlign: TextAlign.center,
style: textTheme.headlineLarge?.copyWith(
shadows: [
Shadow(
blurRadius: 20,
color: colorScheme.surface,
),
],
),
),
),
),
],
),
); );
} }
} }

View File

@ -77,6 +77,13 @@ class LibraryTabBarView extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final sourceId = ref.watch(sourceIdProvider); final sourceId = ref.watch(sourceIdProvider);
final albumsQuery = AlbumsQuery(
sourceId: sourceId,
sort: IList([
SortingTerm.albumsDesc(AlbumsColumn.created),
]),
);
final songsQuery = SongsQuery( final songsQuery = SongsQuery(
sourceId: sourceId, sourceId: sourceId,
sort: IList([ sort: IList([
@ -95,7 +102,7 @@ class LibraryTabBarView extends HookConsumerWidget {
(tab) => TabScrollView( (tab) => TabScrollView(
index: LibraryTab.values.indexOf(tab), index: LibraryTab.values.indexOf(tab),
sliver: switch (tab) { sliver: switch (tab) {
LibraryTab.albums => AlbumsGrid(), LibraryTab.albums => AlbumsGrid(query: albumsQuery),
LibraryTab.artists => ArtistsList(), LibraryTab.artists => ArtistsList(),
LibraryTab.playlists => PlaylistsList(), LibraryTab.playlists => PlaylistsList(),
LibraryTab.songs => SongsList( LibraryTab.songs => SongsList(
@ -107,7 +114,7 @@ class LibraryTabBarView extends HookConsumerWidget {
onTap: () {}, onTap: () {},
), ),
), ),
_ => ArtistsList(), _ => SliverToBoxAdapter(child: Container()),
}, },
), ),
) )

View File

@ -10,14 +10,14 @@ class CoverArtImage extends HookConsumerWidget {
super.key, super.key,
this.coverArt, this.coverArt,
this.thumbnail = true, this.thumbnail = true,
this.fit, this.fit = BoxFit.cover,
this.height, this.height,
this.width, this.width,
}); });
final String? coverArt; final String? coverArt;
final bool thumbnail; final bool thumbnail;
final BoxFit? fit; final BoxFit fit;
final double? height; final double? height;
final double? width; final double? width;
@ -37,7 +37,7 @@ class CoverArtImage extends HookConsumerWidget {
cacheKey: '$sourceId$coverArt$thumbnail', cacheKey: '$sourceId$coverArt$thumbnail',
placeholder: (context, url) => Icon(Symbols.cached_rounded), placeholder: (context, url) => Icon(Symbols.cached_rounded),
errorWidget: (context, url, error) => Icon(Icons.error), errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover, fit: fit,
fadeOutDuration: Duration(milliseconds: 100), fadeOutDuration: Duration(milliseconds: 100),
fadeInDuration: Duration(milliseconds: 200), fadeInDuration: Duration(milliseconds: 200),
); );

View File

@ -1,4 +1,3 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -15,7 +14,12 @@ import 'items.dart';
const kPageSize = 60; const kPageSize = 60;
class AlbumsGrid extends HookConsumerWidget { class AlbumsGrid extends HookConsumerWidget {
const AlbumsGrid({super.key}); const AlbumsGrid({
super.key,
required this.query,
});
final AlbumsQuery query;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -24,11 +28,8 @@ class AlbumsGrid extends HookConsumerWidget {
getNextPageKey: (state) => getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey, state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) => db.libraryDao.listAlbums( fetchPage: (pageKey) => db.libraryDao.listAlbums(
AlbumsQuery( query.copyWith(
sourceId: ref.read(sourceIdProvider), sourceId: ref.read(sourceIdProvider),
sort: IList([
SortingTerm.albumsDesc(AlbumsColumn.created),
]),
limit: kPageSize, limit: kPageSize,
offset: (pageKey - 1) * kPageSize, offset: (pageKey - 1) * kPageSize,
), ),

View File

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import '../../../database/query.dart';
import '../../../sources/models.dart';
import '../../hooks/use_on_source.dart';
import '../../hooks/use_paging_controller.dart';
import '../../state/database.dart';
import '../../state/source.dart';
import 'items.dart';
const kPageSize = 30;
class AlbumsList extends HookConsumerWidget {
const AlbumsList({
super.key,
required this.query,
});
final AlbumsQuery query;
@override
Widget build(BuildContext context, WidgetRef ref) {
final db = ref.watch(databaseProvider);
final controller = usePagingController<int, Album>(
getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) => db.libraryDao.listAlbums(
query.copyWith(
sourceId: ref.read(sourceIdProvider),
limit: kPageSize,
offset: (pageKey - 1) * kPageSize,
),
),
);
useOnSourceChange(ref, (_) => controller.refresh());
useOnSourceSync(ref, controller.refresh);
return PagingListener(
controller: controller,
builder: (context, state, fetchNextPage) {
return PagedSliverList(
state: state,
fetchNextPage: fetchNextPage,
builderDelegate: PagedChildBuilderDelegate<Album>(
itemBuilder: (context, item, index) {
final tile = AlbumListTile(
album: item,
onTap: () {
context.push('/albums/${item.id}');
},
);
final currentItemYear = item.year;
final previousItemYear = index == 0
? currentItemYear
: controller.items?.elementAtOrNull(index - 1)?.year;
if (index == 0 || currentItemYear != previousItemYear) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
top: 24,
bottom: 8,
left: 16,
right: 16,
),
child: Text(
item.year?.toString() ?? 'Unknown year',
style: TextTheme.of(context).headlineMedium,
),
),
tile,
],
);
}
return tile;
},
),
);
},
);
}
}

View File

@ -36,6 +36,8 @@ class SongsListHeader extends HookConsumerWidget {
children: [ children: [
const SizedBox(height: 24), const SizedBox(height: 24),
Container( Container(
height: 300,
width: 300,
decoration: BoxDecoration( decoration: BoxDecoration(
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@ -48,7 +50,6 @@ class SongsListHeader extends HookConsumerWidget {
], ],
), ),
child: CoverArtImage( child: CoverArtImage(
height: 300,
thumbnail: false, thumbnail: false,
coverArt: coverArt, coverArt: coverArt,
fit: BoxFit.contain, fit: BoxFit.contain,

View File

@ -62,6 +62,63 @@ class ArtistListTile extends StatelessWidget {
} }
} }
class AlbumListTile extends StatelessWidget {
const AlbumListTile({
super.key,
required this.album,
this.onTap,
});
final Album album;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
final textTheme = TextTheme.of(context);
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 8, right: 18),
child: RoundedBoxClip(
child: CoverArtImage(
coverArt: album.coverArt,
thumbnail: true,
width: 80,
height: 80,
),
),
),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
album.name,
style: textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
Text(
album.albumArtist ?? 'Unknown album artist',
style: textTheme.bodyMedium,
),
],
),
),
],
),
),
);
}
}
class PlaylistListTile extends StatelessWidget { class PlaylistListTile extends StatelessWidget {
const PlaylistListTile({ const PlaylistListTile({
super.key, super.key,

View File

@ -19,3 +19,21 @@ class CircleClip extends StatelessWidget {
); );
} }
} }
class RoundedBoxClip extends StatelessWidget {
const RoundedBoxClip({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
return ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadiusGeometry.circular(3),
child: child,
);
}
}

View File

@ -225,6 +225,12 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
); );
} }
Selectable<models.Artist> getArtist(int sourceId, String id) {
return db.managers.artists.filter(
(f) => f.sourceId.equals(sourceId) & f.id.equals(id),
);
}
Selectable<models.Playlist> getPlaylist(int sourceId, String id) { Selectable<models.Playlist> getPlaylist(int sourceId, String id) {
return db.managers.playlists.filter( return db.managers.playlists.filter(
(f) => f.sourceId.equals(sourceId) & f.id.equals(id), (f) => f.sourceId.equals(sourceId) & f.id.equals(id),