songs list tab

This commit is contained in:
austinried 2025-12-06 09:42:53 +09:00
parent 16a79c81cb
commit a4e4c6fa57
6 changed files with 113 additions and 32 deletions

View File

@ -10,6 +10,7 @@ import '../state/source.dart';
import '../ui/cover_art_theme.dart'; import '../ui/cover_art_theme.dart';
import '../ui/gradient.dart'; import '../ui/gradient.dart';
import '../ui/lists/header.dart'; import '../ui/lists/header.dart';
import '../ui/lists/items.dart';
import '../ui/lists/songs_list.dart'; import '../ui/lists/songs_list.dart';
class AlbumScreen extends HookConsumerWidget { class AlbumScreen extends HookConsumerWidget {
@ -61,7 +62,13 @@ class AlbumScreen extends HookConsumerWidget {
onMore: () {}, onMore: () {},
), ),
), ),
SongsList(query: query), SongsList(
query: query,
itemBuilder: (context, item, index) => SongListTile(
song: item.song,
onTap: () {},
),
),
], ],
), ),
), ),

View File

@ -1,13 +1,18 @@
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:flutter_hooks/flutter_hooks.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';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../database/query.dart';
import '../../l10n/generated/app_localizations.dart'; import '../../l10n/generated/app_localizations.dart';
import '../state/services.dart'; import '../state/services.dart';
import '../state/source.dart';
import '../ui/lists/albums_grid.dart'; import '../ui/lists/albums_grid.dart';
import '../ui/lists/artists_list.dart'; import '../ui/lists/artists_list.dart';
import '../ui/lists/items.dart';
import '../ui/lists/songs_list.dart';
import '../util/custom_scroll_fix.dart'; import '../util/custom_scroll_fix.dart';
const kIconSize = 26.0; const kIconSize = 26.0;
@ -51,20 +56,7 @@ class LibraryScreen extends HookConsumerWidget {
builder: (context) => CustomScrollProvider( builder: (context) => CustomScrollProvider(
tabController: tabController, tabController: tabController,
parent: PrimaryScrollController.of(context), parent: PrimaryScrollController.of(context),
child: TabBarView( child: LibraryTabBarView(tabController: tabController),
controller: tabController,
children: LibraryTab.values
.map(
(tab) => TabScrollView(
index: LibraryTab.values.indexOf(tab),
sliver: switch (tab) {
LibraryTab.albums => AlbumsGrid(),
_ => ArtistsList(),
},
),
)
.toList(),
),
), ),
), ),
), ),
@ -72,6 +64,56 @@ class LibraryScreen extends HookConsumerWidget {
} }
} }
class LibraryTabBarView extends HookConsumerWidget {
const LibraryTabBarView({
super.key,
required this.tabController,
});
final TabController tabController;
@override
Widget build(BuildContext context, WidgetRef ref) {
final sourceId = ref.watch(sourceIdProvider);
final songsQuery = SongsQuery(
sourceId: sourceId,
sort: IList([
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.albumArtist),
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.album),
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.disc),
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.track),
SongsSortingTerm(dir: SortDirection.asc, by: SongsColumn.title),
]),
);
return TabBarView(
controller: tabController,
children: LibraryTab.values
.map(
(tab) => TabScrollView(
index: LibraryTab.values.indexOf(tab),
sliver: switch (tab) {
LibraryTab.albums => AlbumsGrid(),
LibraryTab.artists => ArtistsList(),
LibraryTab.songs => SongsList(
query: songsQuery,
itemBuilder: (context, item, index) => SongListTile(
song: item.song,
coverArt: item.albumCoverArt,
showLeading: true,
onTap: () {},
),
),
_ => ArtistsList(),
},
),
)
.toList(),
);
}
}
class LibraryTabsHeader extends HookConsumerWidget { class LibraryTabsHeader extends HookConsumerWidget {
const LibraryTabsHeader({ const LibraryTabsHeader({
super.key, super.key,

View File

@ -66,19 +66,25 @@ class SongListTile extends StatelessWidget {
const SongListTile({ const SongListTile({
super.key, super.key,
required this.song, required this.song,
this.coverArt,
this.showLeading = false,
this.onTap, this.onTap,
}); });
final Song song; final Song song;
final String? coverArt;
final bool showLeading;
final void Function()? onTap; final void Function()? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
// leading: CoverArtImage( leading: showLeading
// coverArt: song.coverArt, ? CoverArtImage(
// thumbnail: true, coverArt: coverArt,
// ), thumbnail: true,
)
: null,
title: Text(song.title), title: Text(song.title),
subtitle: Text(song.artist ?? ''), subtitle: Text(song.artist ?? ''),
onTap: onTap, onTap: onTap,

View File

@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import '../../../database/dao/library_dao.dart';
import '../../../database/query.dart'; import '../../../database/query.dart';
import '../../../sources/models.dart';
import '../../hooks/use_on_source.dart'; import '../../hooks/use_on_source.dart';
import '../../hooks/use_paging_controller.dart'; import '../../hooks/use_paging_controller.dart';
import '../../state/database.dart'; import '../../state/database.dart';
import 'items.dart';
const kPageSize = 30; const kPageSize = 30;
@ -15,14 +14,17 @@ class SongsList extends HookConsumerWidget {
const SongsList({ const SongsList({
super.key, super.key,
required this.query, required this.query,
required this.itemBuilder,
}); });
final SongsQuery query; final SongsQuery query;
final Widget Function(BuildContext context, SongListItem item, int index)
itemBuilder;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final db = ref.watch(databaseProvider); final db = ref.watch(databaseProvider);
final controller = usePagingController<int, Song>( final controller = usePagingController<int, SongListItem>(
getNextPageKey: (state) => getNextPageKey: (state) =>
state.lastPageIsEmpty ? null : state.nextIntPageKey, state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) => db.libraryDao.listSongs( fetchPage: (pageKey) => db.libraryDao.listSongs(
@ -42,13 +44,8 @@ class SongsList extends HookConsumerWidget {
return PagedSliverList( return PagedSliverList(
state: state, state: state,
fetchNextPage: fetchNextPage, fetchNextPage: fetchNextPage,
builderDelegate: PagedChildBuilderDelegate<Song>( builderDelegate: PagedChildBuilderDelegate<SongListItem>(
itemBuilder: (context, item, index) { itemBuilder: itemBuilder,
return SongListTile(
song: item,
onTap: () async {},
);
},
), ),
); );
}, },

View File

@ -6,7 +6,15 @@ import '../query.dart';
part 'library_dao.g.dart'; part 'library_dao.g.dart';
typedef AristListItem = ({models.Artist artist, int? albumCount}); typedef AristListItem = ({
models.Artist artist,
int? albumCount,
});
typedef SongListItem = ({
models.Song song,
String? albumCoverArt,
});
extension on SortDirection { extension on SortDirection {
OrderingMode toMode() => switch (this) { OrderingMode toMode() => switch (this) {
@ -111,7 +119,7 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
.get(); .get();
} }
Future<List<models.Song>> listSongs(SongsQuery q) { Future<List<SongListItem>> listSongs(SongsQuery q) {
var joinPlaylistSongs = false; var joinPlaylistSongs = false;
var filter = songs.sourceId.equals(q.sourceId); var filter = songs.sourceId.equals(q.sourceId);
@ -127,6 +135,11 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
final query = final query =
songs.select().join([ songs.select().join([
leftOuterJoin(
albums,
albums.id.equalsExp(songs.albumId) &
albums.sourceId.equals(q.sourceId),
),
if (joinPlaylistSongs) if (joinPlaylistSongs)
leftOuterJoin( leftOuterJoin(
playlistSongs, playlistSongs,
@ -134,6 +147,9 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
playlistSongs.songId.equalsExp(songs.id), playlistSongs.songId.equalsExp(songs.id),
), ),
]) ])
..addColumns([
albums.coverArt,
])
..where(filter) ..where(filter)
..orderBy( ..orderBy(
q.sort q.sort
@ -144,6 +160,9 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
SongsColumn.starred => songs.starred, SongsColumn.starred => songs.starred,
SongsColumn.disc => songs.disc, SongsColumn.disc => songs.disc,
SongsColumn.track => songs.track, SongsColumn.track => songs.track,
SongsColumn.album => songs.album,
SongsColumn.artist => songs.artist,
SongsColumn.albumArtist => albums.albumArtist,
}, },
mode: sort.dir.toMode(), mode: sort.dir.toMode(),
), ),
@ -153,7 +172,14 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
_limitQuery(query: query, limit: q.limit, offset: q.offset); _limitQuery(query: query, limit: q.limit, offset: q.offset);
return query.map((row) => (row.readTable(songs))).get(); return query
.map(
(row) => (
song: row.readTable(songs),
albumCoverArt: row.read(albums.coverArt),
),
)
.get();
} }
Selectable<models.Album> getAlbum(int sourceId, String id) { Selectable<models.Album> getAlbum(int sourceId, String id) {

View File

@ -27,6 +27,9 @@ enum SongsColumn {
starred, starred,
disc, disc,
track, track,
album,
artist,
albumArtist,
} }
@freezed @freezed