mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 17:39:27 +01:00
Compare commits
6 Commits
3fcb938f2b
...
f7874bcead
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7874bcead | ||
|
|
ba169092fd | ||
|
|
4183e2d3b9 | ||
|
|
c3bb14edbf | ||
|
|
805e6fff7a | ||
|
|
d245fc7fef |
@ -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,124 @@
|
|||||||
|
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,
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: 1,
|
initialIndex: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -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()),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -209,7 +216,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,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show InsertMode, Value;
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import '../../database/database.dart';
|
import '../../database/database.dart';
|
||||||
@ -7,17 +7,24 @@ final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
|||||||
final db = SubtracksDatabase();
|
final db = SubtracksDatabase();
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.into(db.sources)
|
.batch((batch) {
|
||||||
.insertOnConflictUpdate(
|
batch.insertAll(
|
||||||
|
db.sources,
|
||||||
|
[
|
||||||
SourcesCompanion.insert(
|
SourcesCompanion.insert(
|
||||||
id: Value(1),
|
id: Value(1),
|
||||||
name: 'test subsonic',
|
name: 'test subsonic',
|
||||||
// isActive: Value(true),
|
isActive: Value(true),
|
||||||
),
|
),
|
||||||
|
SourcesCompanion.insert(
|
||||||
|
id: Value(2),
|
||||||
|
name: 'test navidrome',
|
||||||
|
isActive: Value(null),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
mode: InsertMode.insertOrIgnore,
|
||||||
);
|
);
|
||||||
await db
|
batch.insertAllOnConflictUpdate(db.subsonicSettings, [
|
||||||
.into(db.subsonicSettings)
|
|
||||||
.insertOnConflictUpdate(
|
|
||||||
SubsonicSettingsCompanion.insert(
|
SubsonicSettingsCompanion.insert(
|
||||||
sourceId: Value(1),
|
sourceId: Value(1),
|
||||||
address: Uri.parse('http://demo.subsonic.org'),
|
address: Uri.parse('http://demo.subsonic.org'),
|
||||||
@ -25,19 +32,6 @@ final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
|||||||
password: 'guest',
|
password: 'guest',
|
||||||
useTokenAuth: Value(true),
|
useTokenAuth: Value(true),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
await db
|
|
||||||
.into(db.sources)
|
|
||||||
.insertOnConflictUpdate(
|
|
||||||
SourcesCompanion.insert(
|
|
||||||
id: Value(2),
|
|
||||||
name: 'test navidrome',
|
|
||||||
// isActive: Value(null),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.into(db.subsonicSettings)
|
|
||||||
.insertOnConflictUpdate(
|
|
||||||
SubsonicSettingsCompanion.insert(
|
SubsonicSettingsCompanion.insert(
|
||||||
sourceId: Value(2),
|
sourceId: Value(2),
|
||||||
address: Uri.parse('http://10.0.2.2:4533'),
|
address: Uri.parse('http://10.0.2.2:4533'),
|
||||||
@ -45,7 +39,11 @@ final databaseInitializer = FutureProvider<SubtracksDatabase>((ref) async {
|
|||||||
password: 'password',
|
password: 'password',
|
||||||
useTokenAuth: Value(true),
|
useTokenAuth: Value(true),
|
||||||
),
|
),
|
||||||
);
|
]);
|
||||||
|
})
|
||||||
|
.onError((error, stack) {
|
||||||
|
print(error);
|
||||||
|
});
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
@ -77,10 +134,12 @@ class PlaylistListTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CoverArtImage(
|
leading: RoundedBoxClip(
|
||||||
|
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 ?? ''),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@ -106,9 +165,11 @@ class SongListTile extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: showLeading
|
leading: showLeading
|
||||||
? CoverArtImage(
|
? RoundedBoxClip(
|
||||||
|
child: CoverArtImage(
|
||||||
coverArt: coverArt,
|
coverArt: coverArt,
|
||||||
thumbnail: true,
|
thumbnail: true,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
title: Text(song.title),
|
title: Text(song.title),
|
||||||
|
|||||||
@ -7,6 +7,7 @@ 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;
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ 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,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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