songs list and serializable list query

This commit is contained in:
austinried
2025-12-05 21:16:48 +09:00
parent 6609671ae2
commit 16a79c81cb
14 changed files with 3107 additions and 148 deletions

View File

@@ -1,14 +1,16 @@
import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../database/query.dart';
import '../../l10n/generated/app_localizations.dart';
import '../state/database.dart';
import '../state/source.dart';
import '../ui/cover_art_theme.dart';
import '../ui/images.dart';
import '../ui/gradient.dart';
import '../ui/lists/header.dart';
import '../ui/lists/songs_list.dart';
class AlbumScreen extends HookConsumerWidget {
const AlbumScreen({
@@ -34,111 +36,35 @@ class AlbumScreen extends HookConsumerWidget {
return Container();
}
final query = SongsQuery(
sourceId: sourceId,
filter: IList([SongsFilter.albumId(album.id)]),
sort: IList([
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.disc),
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.track),
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.title),
]),
);
return CoverArtTheme(
coverArt: album.coverArt,
child: Scaffold(
body: Center(
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SafeArea(
child: Padding(
padding: EdgeInsetsGeometry.symmetric(horizontal: 16),
child: _Header(
title: album.name,
subtitle: album.albumArtist,
coverArt: album.coverArt,
playText: l.resourcesAlbumActionsPlay,
onPlay: () {},
onMore: () {},
),
),
),
body: GradientScrollView(
slivers: [
SliverToBoxAdapter(
child: SongsListHeader(
title: album.name,
subtitle: album.albumArtist,
coverArt: album.coverArt,
playText: l.resourcesAlbumActionsPlay,
onPlay: () {},
onMore: () {},
),
],
),
),
SongsList(query: query),
],
),
),
);
}
}
class _Header extends HookConsumerWidget {
const _Header({
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 inheritedStyle = DefaultTextStyle.of(context).style;
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 16),
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),
),
],
),
],
);
}
}

View File

@@ -5,9 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../l10n/generated/app_localizations.dart';
import '../lists/albums_grid.dart';
import '../lists/artists_list.dart';
import '../state/services.dart';
import '../ui/lists/albums_grid.dart';
import '../ui/lists/artists_list.dart';
import '../util/custom_scroll_fix.dart';
const kIconSize = 26.0;

63
lib/app/ui/gradient.dart Normal file
View 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),
],
),
],
);
}
}

View File

@@ -1,14 +1,16 @@
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 '../../sources/models.dart';
import '../hooks/use_on_source.dart';
import '../hooks/use_paging_controller.dart';
import '../state/database.dart';
import '../state/source.dart';
import 'list_items.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;
@@ -22,9 +24,17 @@ class AlbumsGrid extends HookConsumerWidget {
getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) => db.libraryDao.listAlbums(
sourceId: ref.read(sourceIdProvider),
limit: kPageSize,
offset: (pageKey - 1) * kPageSize,
AlbumsQuery(
sourceId: ref.read(sourceIdProvider),
sort: IList([
AlbumsSortingTerm(
dir: SortDirection.desc,
by: AlbumsColumn.created,
),
]),
limit: kPageSize,
offset: (pageKey - 1) * kPageSize,
),
),
);

View File

@@ -1,14 +1,16 @@
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 '../hooks/use_on_source.dart';
import '../hooks/use_paging_controller.dart';
import '../state/database.dart';
import '../state/source.dart';
import 'list_items.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;
@@ -22,9 +24,17 @@ class ArtistsList extends HookConsumerWidget {
getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) => db.libraryDao.listArtists(
sourceId: ref.read(sourceIdProvider),
limit: kPageSize,
offset: (pageKey - 1) * kPageSize,
ArtistsQuery(
sourceId: ref.read(sourceIdProvider),
sort: IList([
ArtistsSortingTerm(
dir: SortDirection.asc,
by: ArtistsColumn.name,
),
]),
limit: kPageSize,
offset: (pageKey - 1) * kPageSize,
),
),
);

View 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),
],
),
);
}
}

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../sources/models.dart';
import '../ui/images.dart';
import '../util/clip.dart';
import '../../../sources/models.dart';
import '../../util/clip.dart';
import '../images.dart';
class AlbumGridTile extends HookConsumerWidget {
const AlbumGridTile({
@@ -62,6 +62,30 @@ class ArtistListTile extends StatelessWidget {
}
}
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,

View 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 {},
);
},
),
);
},
);
}
}