Compare commits

..

No commits in common. "f7874bcead403adbc32b7ca89b4d2e89f76d03bd" and "3fcb938f2b94e5a7e3556630f094f74abd5a5842" have entirely different histories.

12 changed files with 67 additions and 364 deletions

View File

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

View File

@ -1,124 +1,12 @@
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';
import '../../database/query.dart'; class ArtistScreen extends StatelessWidget {
import '../../sources/models.dart'; const ArtistScreen({super.key});
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
Widget build(BuildContext context, WidgetRef ref) {
final db = ref.watch(databaseProvider);
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = TextTheme.of(context); return Scaffold(
final colorScheme = ColorScheme.of(context); body: Center(child: Text('Artist!')),
return SliverToBoxAdapter(
child: Stack(
fit: StackFit.passthrough,
alignment: AlignmentGeometry.bottomCenter,
children: [
CoverArtImage(
fit: BoxFit.cover,
coverArt: artist.coverArt,
thumbnail: false,
height: 350,
),
Column(
children: [
Container(
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: AlignmentGeometry.centerRight,
end: AlignmentGeometry.centerLeft,
colors: [
colorScheme.surface.withAlpha(220),
colorScheme.surface.withAlpha(150),
colorScheme.surface.withAlpha(20),
],
),
),
child: Padding(
padding: EdgeInsetsGeometry.symmetric(
vertical: 12,
horizontal: 16,
),
child: Text(
artist.name,
textAlign: TextAlign.right,
style: textTheme.headlineLarge?.copyWith(
shadows: [
Shadow(
blurRadius: 20,
color: colorScheme.surface,
),
],
),
),
),
),
],
),
],
),
); );
} }
} }

View File

@ -20,7 +20,7 @@ const kIconSize = 26.0;
const kTabHeight = 36.0; const kTabHeight = 36.0;
enum LibraryTab { enum LibraryTab {
// home(Icon(Symbols.home_rounded)), home(Icon(Symbols.home_rounded)),
albums(Icon(Symbols.album_rounded)), albums(Icon(Symbols.album_rounded)),
artists(Icon(Symbols.person_rounded)), artists(Icon(Symbols.person_rounded)),
songs(Icon(Symbols.music_note_rounded)), songs(Icon(Symbols.music_note_rounded)),
@ -41,7 +41,7 @@ class LibraryScreen extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tabController = useTabController( final tabController = useTabController(
initialLength: LibraryTab.values.length, initialLength: LibraryTab.values.length,
initialIndex: 0, initialIndex: 1,
); );
return Scaffold( return Scaffold(
@ -77,13 +77,6 @@ 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([
@ -102,7 +95,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(query: albumsQuery), LibraryTab.albums => AlbumsGrid(),
LibraryTab.artists => ArtistsList(), LibraryTab.artists => ArtistsList(),
LibraryTab.playlists => PlaylistsList(), LibraryTab.playlists => PlaylistsList(),
LibraryTab.songs => SongsList( LibraryTab.songs => SongsList(
@ -114,7 +107,7 @@ class LibraryTabBarView extends HookConsumerWidget {
onTap: () {}, onTap: () {},
), ),
), ),
// _ => SliverToBoxAdapter(child: Container()), _ => ArtistsList(),
}, },
), ),
) )
@ -216,7 +209,7 @@ class TabTitleText extends HookConsumerWidget {
String tabLocalization(LibraryTab tab) => switch (tab) { String tabLocalization(LibraryTab tab) => switch (tab) {
LibraryTab.albums => l.navigationTabsAlbums, LibraryTab.albums => l.navigationTabsAlbums,
// LibraryTab.home => l.navigationTabsHome, LibraryTab.home => l.navigationTabsHome,
LibraryTab.artists => l.navigationTabsArtists, LibraryTab.artists => l.navigationTabsArtists,
LibraryTab.songs => l.navigationTabsSongs, LibraryTab.songs => l.navigationTabsSongs,
LibraryTab.playlists => l.navigationTabsPlaylists, LibraryTab.playlists => l.navigationTabsPlaylists,

View File

@ -1,4 +1,4 @@
import 'package:drift/drift.dart' show InsertMode, Value; import 'package:drift/drift.dart' show Value;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../database/database.dart'; import '../../database/database.dart';
@ -7,43 +7,45 @@ final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
final db = SubtracksDatabase(); final db = SubtracksDatabase();
await db await db
.batch((batch) { .into(db.sources)
batch.insertAll( .insertOnConflictUpdate(
db.sources, SourcesCompanion.insert(
[ id: Value(1),
SourcesCompanion.insert( name: 'test subsonic',
id: Value(1), // isActive: Value(true),
name: 'test subsonic', ),
isActive: Value(true), );
), await db
SourcesCompanion.insert( .into(db.subsonicSettings)
id: Value(2), .insertOnConflictUpdate(
name: 'test navidrome', SubsonicSettingsCompanion.insert(
isActive: Value(null), sourceId: Value(1),
), address: Uri.parse('http://demo.subsonic.org'),
], username: 'guest1',
mode: InsertMode.insertOrIgnore, password: 'guest',
); useTokenAuth: Value(true),
batch.insertAllOnConflictUpdate(db.subsonicSettings, [ ),
SubsonicSettingsCompanion.insert( );
sourceId: Value(1), await db
address: Uri.parse('http://demo.subsonic.org'), .into(db.sources)
username: 'guest1', .insertOnConflictUpdate(
password: 'guest', SourcesCompanion.insert(
useTokenAuth: Value(true), id: Value(2),
), name: 'test navidrome',
SubsonicSettingsCompanion.insert( // isActive: Value(null),
sourceId: Value(2), ),
address: Uri.parse('http://10.0.2.2:4533'), );
username: 'admin', await db
password: 'password', .into(db.subsonicSettings)
useTokenAuth: Value(true), .insertOnConflictUpdate(
), SubsonicSettingsCompanion.insert(
]); sourceId: Value(2),
}) address: Uri.parse('http://10.0.2.2:4533'),
.onError((error, stack) { username: 'admin',
print(error); password: 'password',
}); useTokenAuth: Value(true),
),
);
return db; return db;
}); });

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 = BoxFit.cover, this.fit,
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: fit, fit: BoxFit.cover,
fadeOutDuration: Duration(milliseconds: 100), fadeOutDuration: Duration(milliseconds: 100),
fadeInDuration: Duration(milliseconds: 200), fadeInDuration: Duration(milliseconds: 200),
); );

View File

@ -1,3 +1,4 @@
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';
@ -14,12 +15,7 @@ import 'items.dart';
const kPageSize = 60; const kPageSize = 60;
class AlbumsGrid extends HookConsumerWidget { class AlbumsGrid extends HookConsumerWidget {
const AlbumsGrid({ const AlbumsGrid({super.key});
super.key,
required this.query,
});
final AlbumsQuery query;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -28,8 +24,11 @@ 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(
query.copyWith( AlbumsQuery(
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

@ -1,90 +0,0 @@
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,8 +36,6 @@ 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(
@ -50,6 +48,7 @@ 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,63 +62,6 @@ 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,
@ -134,11 +77,9 @@ class PlaylistListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
leading: RoundedBoxClip( leading: CoverArtImage(
child: CoverArtImage( coverArt: playlist.coverArt,
coverArt: playlist.coverArt, thumbnail: true,
thumbnail: true,
),
), ),
title: Text(playlist.name), title: Text(playlist.name),
subtitle: Text(playlist.comment ?? ''), subtitle: Text(playlist.comment ?? ''),
@ -165,11 +106,9 @@ class SongListTile extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
leading: showLeading leading: showLeading
? RoundedBoxClip( ? CoverArtImage(
child: CoverArtImage( coverArt: coverArt,
coverArt: coverArt, thumbnail: true,
thumbnail: true,
),
) )
: null, : null,
title: Text(song.title), title: Text(song.title),

View File

@ -7,7 +7,6 @@ import '../../../database/query.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 '../../state/source.dart';
const kPageSize = 30; const kPageSize = 30;
@ -30,7 +29,6 @@ class SongsList extends HookConsumerWidget {
state.lastPageIsEmpty ? null : state.nextIntPageKey, state.lastPageIsEmpty ? null : state.nextIntPageKey,
fetchPage: (pageKey) => db.libraryDao.listSongs( fetchPage: (pageKey) => db.libraryDao.listSongs(
query.copyWith( query.copyWith(
sourceId: ref.read(sourceIdProvider),
limit: kPageSize, limit: kPageSize,
offset: (pageKey - 1) * kPageSize, offset: (pageKey - 1) * kPageSize,
), ),

View File

@ -19,21 +19,3 @@ 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,12 +225,6 @@ 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),