mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 06:52:43 +01:00
songs list and serializable list query
This commit is contained in:
63
lib/app/ui/gradient.dart
Normal file
63
lib/app/ui/gradient.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sliver_tools/sliver_tools.dart';
|
||||
|
||||
class ThemedGradient extends LinearGradient {
|
||||
const ThemedGradient({
|
||||
required super.colors,
|
||||
super.begin,
|
||||
super.end,
|
||||
});
|
||||
|
||||
factory ThemedGradient.of(
|
||||
BuildContext context, {
|
||||
AlignmentGeometry begin = Alignment.topCenter,
|
||||
AlignmentGeometry end = Alignment.bottomCenter,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return ThemedGradient(
|
||||
begin: begin,
|
||||
end: end,
|
||||
colors: [
|
||||
colorScheme.primaryContainer,
|
||||
colorScheme.surface,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GradientScrollView extends HookConsumerWidget {
|
||||
const GradientScrollView({
|
||||
super.key,
|
||||
required this.slivers,
|
||||
});
|
||||
|
||||
final List<Widget> slivers;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverStack(
|
||||
children: [
|
||||
SliverPositioned.directional(
|
||||
textDirection: TextDirection.ltr,
|
||||
start: 0,
|
||||
end: 0,
|
||||
top: 0,
|
||||
child: Ink(
|
||||
width: double.infinity,
|
||||
height: MediaQuery.heightOf(context),
|
||||
decoration: BoxDecoration(
|
||||
gradient: ThemedGradient.of(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
MultiSliver(children: slivers),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/app/ui/lists/albums_grid.dart
Normal file
68
lib/app/ui/lists/albums_grid.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
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 = 60;
|
||||
|
||||
class AlbumsGrid extends HookConsumerWidget {
|
||||
const AlbumsGrid({super.key});
|
||||
|
||||
@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(
|
||||
AlbumsQuery(
|
||||
sourceId: ref.read(sourceIdProvider),
|
||||
sort: IList([
|
||||
AlbumsSortingTerm(
|
||||
dir: SortDirection.desc,
|
||||
by: AlbumsColumn.created,
|
||||
),
|
||||
]),
|
||||
limit: kPageSize,
|
||||
offset: (pageKey - 1) * kPageSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
useOnSourceChange(ref, (_) => controller.refresh());
|
||||
useOnSourceSync(ref, controller.refresh);
|
||||
|
||||
return PagingListener(
|
||||
controller: controller,
|
||||
builder: (context, state, fetchNextPage) {
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
sliver: PagedSliverGrid(
|
||||
state: state,
|
||||
fetchNextPage: fetchNextPage,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
),
|
||||
builderDelegate: PagedChildBuilderDelegate<Album>(
|
||||
itemBuilder: (context, item, index) => AlbumGridTile(
|
||||
album: item,
|
||||
onTap: () async {
|
||||
context.push('/album/${item.id}');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
67
lib/app/ui/lists/artists_list.dart
Normal file
67
lib/app/ui/lists/artists_list.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
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/dao/library_dao.dart';
|
||||
import '../../../database/query.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 ArtistsList extends HookConsumerWidget {
|
||||
const ArtistsList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final controller = usePagingController<int, AristListItem>(
|
||||
getNextPageKey: (state) =>
|
||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||
fetchPage: (pageKey) => db.libraryDao.listArtists(
|
||||
ArtistsQuery(
|
||||
sourceId: ref.read(sourceIdProvider),
|
||||
sort: IList([
|
||||
ArtistsSortingTerm(
|
||||
dir: SortDirection.asc,
|
||||
by: ArtistsColumn.name,
|
||||
),
|
||||
]),
|
||||
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<AristListItem>(
|
||||
itemBuilder: (context, item, index) {
|
||||
final (:artist, :albumCount) = item;
|
||||
|
||||
return ArtistListTile(
|
||||
artist: artist,
|
||||
albumCount: albumCount,
|
||||
onTap: () async {
|
||||
context.push('/artist/${artist.id}');
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
103
lib/app/ui/lists/header.dart
Normal file
103
lib/app/ui/lists/header.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../images.dart';
|
||||
|
||||
class SongsListHeader extends HookConsumerWidget {
|
||||
const SongsListHeader({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.coverArt,
|
||||
this.playText,
|
||||
this.onPlay,
|
||||
this.onMore,
|
||||
// required this.downloadActions,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final String? coverArt;
|
||||
final String? playText;
|
||||
final void Function()? onPlay;
|
||||
final FutureOr<void> Function()? onMore;
|
||||
// final List<DownloadAction> downloadActions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return SafeArea(
|
||||
minimum: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 20,
|
||||
blurStyle: BlurStyle.normal,
|
||||
color: Colors.black.withAlpha(100),
|
||||
offset: Offset.zero,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: CoverArtImage(
|
||||
height: 300,
|
||||
thumbnail: false,
|
||||
coverArt: coverArt,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.headlineMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
subtitle ?? '',
|
||||
style: theme.textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.download_done_rounded),
|
||||
),
|
||||
if (onPlay != null)
|
||||
FilledButton.icon(
|
||||
onPressed: onPlay,
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: Text(
|
||||
playText ?? '',
|
||||
// style: theme.textTheme.bodyLarge?.copyWith(
|
||||
// color: theme.colorScheme.onPrimary,
|
||||
// ),
|
||||
),
|
||||
),
|
||||
if (onMore != null)
|
||||
IconButton(
|
||||
onPressed: onMore,
|
||||
icon: const Icon(Icons.more_horiz),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
122
lib/app/ui/lists/items.dart
Normal file
122
lib/app/ui/lists/items.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
import '../../../sources/models.dart';
|
||||
import '../../util/clip.dart';
|
||||
import '../images.dart';
|
||||
|
||||
class AlbumGridTile extends HookConsumerWidget {
|
||||
const AlbumGridTile({
|
||||
super.key,
|
||||
required this.album,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Album album;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return CardTheme(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadiusGeometry.circular(3),
|
||||
),
|
||||
margin: EdgeInsets.all(2),
|
||||
child: ImageCard(
|
||||
onTap: onTap,
|
||||
child: CoverArtImage(
|
||||
coverArt: album.coverArt,
|
||||
thumbnail: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistListTile extends StatelessWidget {
|
||||
const ArtistListTile({
|
||||
super.key,
|
||||
required this.artist,
|
||||
this.albumCount,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Artist artist;
|
||||
final int? albumCount;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: CircleClip(
|
||||
child: CoverArtImage(
|
||||
coverArt: artist.coverArt,
|
||||
thumbnail: true,
|
||||
),
|
||||
),
|
||||
title: Text(artist.name),
|
||||
subtitle: albumCount != null ? Text('$albumCount albums') : null,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SongListTile extends StatelessWidget {
|
||||
const SongListTile({
|
||||
super.key,
|
||||
required this.song,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final Song song;
|
||||
final void Function()? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
// leading: CoverArtImage(
|
||||
// coverArt: song.coverArt,
|
||||
// thumbnail: true,
|
||||
// ),
|
||||
title: Text(song.title),
|
||||
subtitle: Text(song.artist ?? ''),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ImageCard extends StatelessWidget {
|
||||
const ImageCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final void Function()? onTap;
|
||||
final void Function()? onLongPress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
child,
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
57
lib/app/ui/lists/songs_list.dart
Normal file
57
lib/app/ui/lists/songs_list.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.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 'items.dart';
|
||||
|
||||
const kPageSize = 30;
|
||||
|
||||
class SongsList extends HookConsumerWidget {
|
||||
const SongsList({
|
||||
super.key,
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final SongsQuery query;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final db = ref.watch(databaseProvider);
|
||||
final controller = usePagingController<int, Song>(
|
||||
getNextPageKey: (state) =>
|
||||
state.lastPageIsEmpty ? null : state.nextIntPageKey,
|
||||
fetchPage: (pageKey) => db.libraryDao.listSongs(
|
||||
query.copyWith(
|
||||
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<Song>(
|
||||
itemBuilder: (context, item, index) {
|
||||
return SongListTile(
|
||||
song: item,
|
||||
onTap: () async {},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user