mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
artist screen
This commit is contained in:
parent
d245fc7fef
commit
805e6fff7a
@ -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',
|
||||||
|
|||||||
@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
90
lib/app/ui/lists/albums_list.dart
Normal file
90
lib/app/ui/lists/albums_list.dart
Normal 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;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user