From 75bace61632e660cf939aa9981e63068841dfbb1 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 10:09:24 +0200 Subject: [PATCH 01/15] Front: Packages/Extras pages: remove emptysticky header --- front/lib/pages/src/extras.dart | 2 +- front/lib/pages/src/packages.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/front/lib/pages/src/extras.dart b/front/lib/pages/src/extras.dart index a0266f7..01ab71a 100644 --- a/front/lib/pages/src/extras.dart +++ b/front/lib/pages/src/extras.dart @@ -25,7 +25,7 @@ class ExtrasPage extends ConsumerWidget { thumbnail: item?.thumbnail, ), query: (q) => client.getExtras(page: q), - header: Container()) + header: null) ])); } } diff --git a/front/lib/pages/src/packages.dart b/front/lib/pages/src/packages.dart index c0a3ced..7657baa 100644 --- a/front/lib/pages/src/packages.dart +++ b/front/lib/pages/src/packages.dart @@ -25,7 +25,7 @@ class PackagesPage extends ConsumerWidget { thumbnail: item?.poster, ), query: (q) => client.getPackages(page: q), - header: Container()) + header: null) ])); } } From edd48376b1b2eba5c81cad253a73b687dfb66951 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 10:26:31 +0200 Subject: [PATCH 02/15] Front: Setup Artist Page --- front/lib/api/src/client.dart | 8 ++++ front/lib/pages/src/artist.dart | 32 ++++++++++++++ front/lib/pages/src/package.dart | 62 +-------------------------- front/lib/providers.dart | 13 ++++++ front/lib/router.dart | 6 +++ front/lib/ui/src/description_box.dart | 61 ++++++++++++++++++++++++++ 6 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 front/lib/pages/src/artist.dart create mode 100644 front/lib/ui/src/description_box.dart diff --git a/front/lib/api/src/client.dart b/front/lib/api/src/client.dart index d05aa22..8d3a097 100644 --- a/front/lib/api/src/client.dart +++ b/front/lib/api/src/client.dart @@ -64,6 +64,14 @@ class APIClient { responseBody, (x) => ExternalId.fromJson(x as Map)); } + Future> getArtistExternalIds(String artistUuid, + {PageQuery page = const PageQuery()}) async { + var responseBody = await _request(RequestType.get, + '/external_ids?artist=$artistUuid&take=${page.take}&skip=${page.skip}'); + return Page.fromJson( + responseBody, (x) => ExternalId.fromJson(x as Map)); + } + Future> getMovies(String packageUuid) async { var responseBody = await _request(RequestType.get, '/movies?package=$packageUuid'); diff --git a/front/lib/pages/src/artist.dart b/front/lib/pages/src/artist.dart new file mode 100644 index 0000000..4264346 --- /dev/null +++ b/front/lib/pages/src/artist.dart @@ -0,0 +1,32 @@ +import 'package:blee/providers.dart'; +import 'package:blee/ui/src/breakpoints.dart'; +import 'package:blee/ui/src/description_box.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:responsive_framework/responsive_framework.dart'; + +class ArtistPage extends ConsumerWidget { + final String artistUuid; + const ArtistPage({super.key, required this.artistUuid}); + + @override + Widget build(BuildContext context, ref) { + // final artist = ref.watch(getArtistProvider(artistUuid)); + final externalIds = ref.watch(getArtistExternalIdsProvider(artistUuid)); + return MaxWidthBox( + maxWidth: Breakpoints.getSized(BreakpointEnum.sm), + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: DescriptionBox( + description: externalIds.value?.items + .map((item) => item.description) + .firstWhereOrNull((description) => description != null), + skeletonize: externalIds.value == null, + ), + ) + ], + )); + } +} diff --git a/front/lib/pages/src/package.dart b/front/lib/pages/src/package.dart index 22079eb..4470ef2 100644 --- a/front/lib/pages/src/package.dart +++ b/front/lib/pages/src/package.dart @@ -3,6 +3,7 @@ import 'package:blee/api/src/models/page.dart' as page; import 'package:blee/api/src/models/image.dart' as blee_image; import 'package:blee/providers.dart'; import 'package:blee/ui/src/breakpoints.dart'; +import 'package:blee/ui/src/description_box.dart'; import 'package:blee/ui/src/image.dart'; import 'package:blee/ui/src/infinite_scroll.dart'; import 'package:blee/ui/src/tile.dart'; @@ -114,65 +115,6 @@ class _PackagePageHeader extends StatelessWidget { } } -class _PackageDescriptionBox extends StatelessWidget { - final String? description; - final bool skeletonize; - - const _PackageDescriptionBox( - {required this.description, required this.skeletonize}); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), - child: skeletonize - ? Column( - children: List.generate(3, (index) => index) - .map((i) => Skeletonizer( - child: Text( - List.generate(200, (index) => ' ').toString(), - maxLines: 1, - ))) - .toList()) - : Text( - description ?? '', - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Material( - color: Colors.transparent, - child: description == null - ? Container() - : InkWell( - onTap: () => _showFullTextDialog(context), - )))), - ], - ); - } - - Future _showFullTextDialog(BuildContext context) { - return showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Description'), - content: Text(description!), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Close')) - ], - ); - }); - } -} - class PackagePage extends ConsumerWidget { final String packageUuid; const PackagePage({super.key, required this.packageUuid}); @@ -201,7 +143,7 @@ class PackagePage extends ConsumerWidget { releaseDate: package?.releaseDate, poster: package?.poster), SizedBox.fromSize(size: const Size.fromHeight(8)), - _PackageDescriptionBox( + DescriptionBox( description: externalIds?.items.firstOrNull?.description, skeletonize: externalIds == null, ) diff --git a/front/lib/providers.dart b/front/lib/providers.dart index 3cf9a76..2967145 100644 --- a/front/lib/providers.dart +++ b/front/lib/providers.dart @@ -18,6 +18,12 @@ Future getPackage(GetPackageRef ref, String packageUuid) async { return await client.getPackage(packageUuid); } +@riverpod +Future getArtist(GetArtistRef ref, String artistUuid) async { + APIClient client = ref.watch(apiClientProvider); + return await client.getArtist(artistUuid); +} + @riverpod Future getFile(GetFileRef ref, String fileUuid) async { APIClient client = ref.watch(apiClientProvider); @@ -63,6 +69,13 @@ Future> getPackageExternalIds( return await client.getPackageExternalIds(packageUuid); } +@riverpod +Future> getArtistExternalIds( + GetArtistExternalIdsRef ref, String artistUuid) async { + APIClient client = ref.watch(apiClientProvider); + return await client.getArtistExternalIds(artistUuid); +} + //// Player @riverpod diff --git a/front/lib/router.dart b/front/lib/router.dart index 6d65b41..a815433 100644 --- a/front/lib/router.dart +++ b/front/lib/router.dart @@ -1,5 +1,6 @@ import 'package:blee/navigation.dart'; import 'package:blee/pages/pages.dart'; +import 'package:blee/pages/src/artist.dart'; import 'package:blee/pages/src/extras.dart'; import 'package:blee/pages/src/player.dart'; import 'package:flutter/material.dart'; @@ -28,6 +29,11 @@ final router = GoRouter( )); }, routes: [ + GoRoute( + path: '/artists/:id', + pageBuilder: (context, state) => NoTransitionPage( + child: ArtistPage(artistUuid: state.pathParameters['id']!)), + ), GoRoute( path: '/packages', pageBuilder: (context, state) => diff --git a/front/lib/ui/src/description_box.dart b/front/lib/ui/src/description_box.dart new file mode 100644 index 0000000..11d71fe --- /dev/null +++ b/front/lib/ui/src/description_box.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +class DescriptionBox extends StatelessWidget { + final String? description; + final bool skeletonize; + + const DescriptionBox( + {super.key, required this.description, required this.skeletonize}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4), + child: skeletonize + ? Column( + children: List.generate(3, (index) => index) + .map((i) => Skeletonizer( + child: Text( + List.generate(200, (index) => ' ').toString(), + maxLines: 1, + ))) + .toList()) + : Text( + description ?? '', + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Material( + color: Colors.transparent, + child: description == null + ? Container() + : InkWell( + onTap: () => _showFullTextDialog(context), + )))), + ], + ); + } + + Future _showFullTextDialog(BuildContext context) { + return showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Description'), + content: Text(description!), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Close')) + ], + ); + }); + } +} From eb5936f6ecce38a8b2d2676501336345bdee8114 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 10:59:34 +0200 Subject: [PATCH 03/15] Front: Artist Page Header --- front/lib/pages/src/artist.dart | 32 +++++-- front/lib/pages/src/package.dart | 106 ++++------------------- front/lib/ui/src/poster_page_header.dart | 71 +++++++++++++++ 3 files changed, 112 insertions(+), 97 deletions(-) create mode 100644 front/lib/ui/src/poster_page_header.dart diff --git a/front/lib/pages/src/artist.dart b/front/lib/pages/src/artist.dart index 4264346..51ab69f 100644 --- a/front/lib/pages/src/artist.dart +++ b/front/lib/pages/src/artist.dart @@ -1,6 +1,7 @@ import 'package:blee/providers.dart'; import 'package:blee/ui/src/breakpoints.dart'; import 'package:blee/ui/src/description_box.dart'; +import 'package:blee/ui/src/poster_page_header.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -12,20 +13,33 @@ class ArtistPage extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - // final artist = ref.watch(getArtistProvider(artistUuid)); + final artist = ref.watch(getArtistProvider(artistUuid)); final externalIds = ref.watch(getArtistExternalIdsProvider(artistUuid)); return MaxWidthBox( maxWidth: Breakpoints.getSized(BreakpointEnum.sm), child: CustomScrollView( slivers: [ - SliverToBoxAdapter( - child: DescriptionBox( - description: externalIds.value?.items - .map((item) => item.description) - .firstWhereOrNull((description) => description != null), - skeletonize: externalIds.value == null, - ), - ) + SliverList.list( + children: [ + SizedBox.fromSize(size: const Size.fromHeight(16)), + PosterPageHeader( + isLoading: artist.value == null, + title: Text( + artist.value?.name ?? 'No Artist Name', + style: Theme.of(context).textTheme.titleLarge, + ), + subtitle: null, + thirdTitle: null, + poster: artist.value?.poster, + ), + DescriptionBox( + description: externalIds.value?.items + .map((item) => item.description) + .firstWhereOrNull((description) => description != null && description.length > 1), + skeletonize: externalIds.value == null, + ) + ], + ), ], )); } diff --git a/front/lib/pages/src/package.dart b/front/lib/pages/src/package.dart index 4470ef2..ce29736 100644 --- a/front/lib/pages/src/package.dart +++ b/front/lib/pages/src/package.dart @@ -6,6 +6,7 @@ import 'package:blee/ui/src/breakpoints.dart'; import 'package:blee/ui/src/description_box.dart'; import 'package:blee/ui/src/image.dart'; import 'package:blee/ui/src/infinite_scroll.dart'; +import 'package:blee/ui/src/poster_page_header.dart'; import 'package:blee/ui/src/tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -33,88 +34,6 @@ Future<(Package, page.Page, page.Page)> getPackagePageData( return (package, movies, externalId); } -class _PackagePageHeader extends StatelessWidget { - final String? packageTitle; - final String? artistName; - final String? artistUuid; - final DateTime? releaseDate; - final blee_image.Image? poster; - final bool isCompilation; - const _PackagePageHeader( - {super.key, - required this.packageTitle, - required this.artistName, - required this.artistUuid, - required this.releaseDate, - required this.isCompilation, - required this.poster}); - - @override - Widget build(BuildContext context) { - var isLoading = packageTitle == null; - var title = Skeletonizer( - enabled: isLoading, - child: Text( - packageTitle ?? 'No Package Name', - style: Theme.of(context).textTheme.titleLarge, - )); - var subtitle = Skeletonizer( - enabled: isLoading, - child: TextButton( - onPressed: () => context.push('/artists/$artistUuid'), - child: Text( - artistName ?? 'No Artist Name', - style: Theme.of(context).textTheme.titleMedium, - ))); - var info = Text( - releaseDate?.year.toString() ?? '', - style: Theme.of(context).textTheme.labelLarge, - ); - paddingForVerticalText(Widget w) => Padding( - padding: const EdgeInsets.only(left: 14), - child: w, - ); - if (ResponsiveBreakpoints.of(context).smallerThan(BreakpointEnum.sm.name)) { - return Column(mainAxisSize: MainAxisSize.min, children: [ - Center( - child: AspectRatio( - aspectRatio: 16 / 9, - child: Poster(image: poster), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16, bottom: 4), - child: title, - ), - subtitle, - info - ]); - } - return IntrinsicHeight( - child: Row( - children: [ - Flexible(child: Poster(image: poster)), - Flexible( - flex: 2, - child: Padding( - padding: const EdgeInsets.only(left: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - paddingForVerticalText(title), - subtitle, - paddingForVerticalText(info) - ], - ), - ), - ) - ], - ), - ); - } -} - class PackagePage extends ConsumerWidget { final String packageUuid; const PackagePage({super.key, required this.packageUuid}); @@ -134,13 +53,24 @@ class PackagePage extends ConsumerWidget { SliverList.list( children: [ SizedBox.fromSize(size: const Size.fromHeight(16)), - _PackagePageHeader( + PosterPageHeader( key: Key('$packageUuid-header'), - packageTitle: package?.name, - artistName: package?.artistName, - artistUuid: package?.artistId, - isCompilation: package?.artistId == null, - releaseDate: package?.releaseDate, + isLoading: package == null, + title: Text( + package?.name ?? 'No Package Name', + style: Theme.of(context).textTheme.titleLarge, + ), + subtitle: TextButton( + onPressed: () => + context.push('/artists/${package?.artistId}'), + child: Text( + package?.artistName ?? 'No Artist Name', + style: Theme.of(context).textTheme.titleMedium, + )), + thirdTitle: Text( + package?.releaseDate?.year.toString() ?? '', + style: Theme.of(context).textTheme.labelLarge, + ), poster: package?.poster), SizedBox.fromSize(size: const Size.fromHeight(8)), DescriptionBox( diff --git a/front/lib/ui/src/poster_page_header.dart b/front/lib/ui/src/poster_page_header.dart new file mode 100644 index 0000000..e3d8054 --- /dev/null +++ b/front/lib/ui/src/poster_page_header.dart @@ -0,0 +1,71 @@ +import 'package:blee/ui/src/breakpoints.dart'; +import 'package:blee/ui/src/image.dart'; +import 'package:flutter/material.dart'; +import 'package:responsive_framework/responsive_framework.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:blee/api/src/models/image.dart' as blee_image; + +class PosterPageHeader extends StatelessWidget { + final Widget title; + final Widget? subtitle; + final Widget? thirdTitle; + final blee_image.Image? poster; + final bool isLoading; + const PosterPageHeader( + {super.key, + required this.title, + required this.subtitle, + required this.thirdTitle, + required this.isLoading, + required this.poster}); + + @override + Widget build(BuildContext context) { + var title = Skeletonizer(enabled: isLoading, child: this.title); + var subtitle = + Skeletonizer(enabled: isLoading, child: this.subtitle ?? Container()); + var info = thirdTitle ?? Container(); + paddingForVerticalText(Widget w) => Padding( + padding: const EdgeInsets.only(left: 14), + child: w, + ); + if (ResponsiveBreakpoints.of(context).smallerThan(BreakpointEnum.sm.name)) { + return Column(mainAxisSize: MainAxisSize.min, children: [ + Center( + child: AspectRatio( + aspectRatio: 16 / 9, + child: Poster(image: poster), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16, bottom: 4), + child: title, + ), + subtitle, + info + ]); + } + return IntrinsicHeight( + child: Row( + children: [ + Flexible(child: Poster(image: poster)), + Flexible( + flex: 2, + child: Padding( + padding: const EdgeInsets.only(left: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + paddingForVerticalText(title), + subtitle, + paddingForVerticalText(info) + ], + ), + ), + ) + ], + ), + ); + } +} From 0f8fc2690b03b3e91c5186910de4cc83368a20a4 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 11:06:11 +0200 Subject: [PATCH 04/15] Front: Artists page --- front/lib/api/src/client.dart | 8 ++++++++ front/lib/navigation.dart | 5 +++++ front/lib/pages/src/artists.dart | 31 +++++++++++++++++++++++++++++++ front/lib/providers.dart | 7 +++++++ front/lib/router.dart | 6 ++++++ 5 files changed, 57 insertions(+) create mode 100644 front/lib/pages/src/artists.dart diff --git a/front/lib/api/src/client.dart b/front/lib/api/src/client.dart index 8d3a097..960fc3f 100644 --- a/front/lib/api/src/client.dart +++ b/front/lib/api/src/client.dart @@ -56,6 +56,14 @@ class APIClient { responseBody, (x) => Package.fromJson(x as Map)); } + Future> getArtists( + {PageQuery page = const PageQuery(), String? package}) async { + var responseBody = await _request( + RequestType.get, '/artists?package=$package&take=${page.take}&skip=${page.skip}'); + return Page.fromJson( + responseBody, (x) => Artist.fromJson(x as Map)); + } + Future> getPackageExternalIds(String packageUuid, {PageQuery page = const PageQuery()}) async { var responseBody = await _request(RequestType.get, diff --git a/front/lib/navigation.dart b/front/lib/navigation.dart index dcd2565..bfe1f13 100644 --- a/front/lib/navigation.dart +++ b/front/lib/navigation.dart @@ -29,6 +29,11 @@ class _ScaffoldWithNavBarState extends State { } static const List tabs = [ + MyNavigationDestination( + icon: FaIcon(FontAwesomeIcons.user), + label: 'Artists', + initialLocation: '/artists', + ), MyNavigationDestination( icon: FaIcon(FontAwesomeIcons.film), label: 'Movies', diff --git a/front/lib/pages/src/artists.dart b/front/lib/pages/src/artists.dart new file mode 100644 index 0000000..0b68ac7 --- /dev/null +++ b/front/lib/pages/src/artists.dart @@ -0,0 +1,31 @@ +import 'package:blee/api/api.dart'; +import 'package:blee/providers.dart'; +import 'package:blee/ui/src/breakpoints.dart'; +import 'package:blee/ui/src/infinite_scroll.dart'; +import 'package:blee/ui/src/tile.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:responsive_framework/responsive_framework.dart'; + +class ArtistsPage extends ConsumerWidget { + const ArtistsPage({super.key}); + @override + Widget build(BuildContext context, ref) { + APIClient client = ref.read(apiClientProvider); + + return MaxWidthBox( + maxWidth: Breakpoints.getSized(BreakpointEnum.lg), + child: CustomScrollView(slivers: [ + PosterTileGridView( + tileBuilder: (context, item, index) => PosterTile( + onTap: () => context.push('/artists/${item?.id}'), + title: item?.name, + subtitle: '', + thumbnail: item?.poster, + ), + query: (q) => client.getArtists(page: q), + header: null) + ])); + } +} diff --git a/front/lib/providers.dart b/front/lib/providers.dart index 2967145..8a9b164 100644 --- a/front/lib/providers.dart +++ b/front/lib/providers.dart @@ -62,6 +62,13 @@ Future> getExtras(GetExtrasRef ref, return await client.getExtras(packageUuid: packageUuid, page: page); } +@riverpod +Future> getArtists(GetArtistsRef ref, + {String? packageUuid, PageQuery page = const PageQuery()}) async { + APIClient client = ref.watch(apiClientProvider); + return await client.getArtists(page: page, package: packageUuid); +} + @riverpod Future> getPackageExternalIds( GetPackageExternalIdsRef ref, String packageUuid) async { diff --git a/front/lib/router.dart b/front/lib/router.dart index a815433..00e1a0c 100644 --- a/front/lib/router.dart +++ b/front/lib/router.dart @@ -1,6 +1,7 @@ import 'package:blee/navigation.dart'; import 'package:blee/pages/pages.dart'; import 'package:blee/pages/src/artist.dart'; +import 'package:blee/pages/src/artists.dart'; import 'package:blee/pages/src/extras.dart'; import 'package:blee/pages/src/player.dart'; import 'package:flutter/material.dart'; @@ -29,6 +30,11 @@ final router = GoRouter( )); }, routes: [ + GoRoute( + path: '/artists', + pageBuilder: (context, state) => + const NoTransitionPage(child: ArtistsPage()), + ), GoRoute( path: '/artists/:id', pageBuilder: (context, state) => NoTransitionPage( From ec99444f6236133ae27b19cf40a866ddcd81fb05 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 11:42:49 +0200 Subject: [PATCH 05/15] Front: Artist Page: Package List --- front/lib/api/src/client.dart | 10 +-- front/lib/pages/src/artist.dart | 65 ++++++++++++-------- front/lib/ui/src/infinite_scroll.dart | 88 +++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 29 deletions(-) diff --git a/front/lib/api/src/client.dart b/front/lib/api/src/client.dart index 960fc3f..47f5c6e 100644 --- a/front/lib/api/src/client.dart +++ b/front/lib/api/src/client.dart @@ -49,17 +49,17 @@ class APIClient { } Future> getPackages( - {PageQuery page = const PageQuery()}) async { - var responseBody = await _request( - RequestType.get, '/packages?take=${page.take}&skip=${page.skip}'); + {PageQuery page = const PageQuery(), String? artistUuid}) async { + var responseBody = await _request(RequestType.get, + '/packages?artist=${artistUuid ?? ''}&take=${page.take}&skip=${page.skip}'); return Page.fromJson( responseBody, (x) => Package.fromJson(x as Map)); } Future> getArtists( {PageQuery page = const PageQuery(), String? package}) async { - var responseBody = await _request( - RequestType.get, '/artists?package=$package&take=${page.take}&skip=${page.skip}'); + var responseBody = await _request(RequestType.get, + '/artists?package=$package&take=${page.take}&skip=${page.skip}'); return Page.fromJson( responseBody, (x) => Artist.fromJson(x as Map)); } diff --git a/front/lib/pages/src/artist.dart b/front/lib/pages/src/artist.dart index 51ab69f..7c6347e 100644 --- a/front/lib/pages/src/artist.dart +++ b/front/lib/pages/src/artist.dart @@ -1,10 +1,14 @@ +import 'package:blee/api/api.dart'; import 'package:blee/providers.dart'; import 'package:blee/ui/src/breakpoints.dart'; import 'package:blee/ui/src/description_box.dart'; +import 'package:blee/ui/src/infinite_scroll.dart'; import 'package:blee/ui/src/poster_page_header.dart'; +import 'package:blee/ui/src/tile.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:responsive_framework/responsive_framework.dart'; class ArtistPage extends ConsumerWidget { @@ -13,34 +17,47 @@ class ArtistPage extends ConsumerWidget { @override Widget build(BuildContext context, ref) { + final client = ref.watch(apiClientProvider); final artist = ref.watch(getArtistProvider(artistUuid)); final externalIds = ref.watch(getArtistExternalIdsProvider(artistUuid)); return MaxWidthBox( maxWidth: Breakpoints.getSized(BreakpointEnum.sm), - child: CustomScrollView( - slivers: [ - SliverList.list( - children: [ - SizedBox.fromSize(size: const Size.fromHeight(16)), - PosterPageHeader( - isLoading: artist.value == null, - title: Text( - artist.value?.name ?? 'No Artist Name', - style: Theme.of(context).textTheme.titleLarge, - ), - subtitle: null, - thirdTitle: null, - poster: artist.value?.poster, - ), - DescriptionBox( - description: externalIds.value?.items - .map((item) => item.description) - .firstWhereOrNull((description) => description != null && description.length > 1), - skeletonize: externalIds.value == null, - ) - ], + child: ListView(children: [ + SizedBox.fromSize(size: const Size.fromHeight(16)), + PosterPageHeader( + isLoading: artist.value == null, + title: Text( + artist.value?.name ?? 'No Artist Name', + style: Theme.of(context).textTheme.titleLarge, ), - ], - )); + subtitle: null, + thirdTitle: null, + poster: artist.value?.poster, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: DescriptionBox( + description: externalIds.value?.items + .map((item) => item.description) + .firstWhereOrNull((description) => + description != null && description.length > 1), + skeletonize: externalIds.value == null, + ), + ), + artist.value == null + ? Container() + : SizedBox( + height: 200, + child: PosterTileListView( + tileBuilder: (context, item, index) => PosterTile( + onTap: () => context.push('/packages/${item?.id}'), + title: item?.name, + subtitle: item?.releaseDate?.year.toString() ?? '', + thumbnail: item?.poster, + ), + query: (q) => client.getPackages( + page: q, artistUuid: artist.value?.id), + header: Text('Packages'))) + ])); } } diff --git a/front/lib/ui/src/infinite_scroll.dart b/front/lib/ui/src/infinite_scroll.dart index 4f7651e..11442e3 100644 --- a/front/lib/ui/src/infinite_scroll.dart +++ b/front/lib/ui/src/infinite_scroll.dart @@ -112,3 +112,91 @@ class _AbstractGridViewState extends State> { super.dispose(); } } + +class PosterTileListView extends AbstractHorizontalListView { + const PosterTileListView( + {super.key, + required super.tileBuilder, + required super.query, + super.skeletonHeader, + required super.header}); +} + +abstract class AbstractHorizontalListView extends StatefulWidget { + final Future> Function(PageQuery) query; + final Widget Function(BuildContext, T?, int) tileBuilder; + final Widget? header; + final bool? skeletonHeader; + const AbstractHorizontalListView( + {super.key, + required this.query, + required this.tileBuilder, + required this.header, + this.skeletonHeader}); + @override + State> createState() => + _AbstractHorizontalListViewState(); +} + +class _AbstractHorizontalListViewState + extends State> { + static const _pageSize = 20; + + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + @override + void initState() { + _pagingController.addPageRequestListener((pageKey) { + _fetchPage(pageKey); + }); + super.initState(); + } + + Future _fetchPage(int pageKey) async { + try { + final newPage = + await widget.query(PageQuery(skip: pageKey, take: _pageSize)); + final isLastPage = newPage.metadata.count < _pageSize; + if (isLastPage) { + _pagingController.appendLastPage(newPage.items); + } else { + final nextPageKey = pageKey + newPage.metadata.count; + _pagingController.appendPage(newPage.items, nextPageKey); + } + } catch (error) { + _pagingController.error = error; + } + } + + @override + Widget build(BuildContext context) => PagedListView( + primary: false, + pagingController: _pagingController, + scrollDirection: Axis.horizontal, + builderDelegate: PagedChildBuilderDelegate( + noItemsFoundIndicatorBuilder: (_) => Container(), + firstPageProgressIndicatorBuilder: (context) => Row( + children: [0, 1] + .map((index) => Padding( + padding: const EdgeInsets.all(4), + child: AspectRatio( + aspectRatio: 0.55, + child: widget.tileBuilder(context, null, index)))) + .toList(), + ), + itemBuilder: (context, item, index) => Padding( + padding: const EdgeInsets.all(4), + child: AspectRatio( + aspectRatio: 0.55, + child: widget.tileBuilder(context, item, index)), + ), + ), + ); + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } +} From 73fb6265d91140c4baf7e56b04ab7bd3004f2737 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 12:36:31 +0200 Subject: [PATCH 06/15] Front: More generic infinite scrollers --- front/lib/pages/src/artist.dart | 6 +- front/lib/pages/src/artists.dart | 2 +- front/lib/pages/src/extras.dart | 2 +- front/lib/pages/src/package.dart | 4 +- front/lib/pages/src/packages.dart | 2 +- front/lib/ui/src/infinite_scroll.dart | 202 ------------------ .../ui/src/infinite_scroll/infinite_grid.dart | 99 +++++++++ .../infinite_horizontal_list.dart | 82 +++++++ .../{grid.dart => infinite_scroll/utils.dart} | 40 +++- front/lib/ui/src/tile.dart | 2 + 10 files changed, 223 insertions(+), 218 deletions(-) delete mode 100644 front/lib/ui/src/infinite_scroll.dart create mode 100644 front/lib/ui/src/infinite_scroll/infinite_grid.dart create mode 100644 front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart rename front/lib/ui/src/{grid.dart => infinite_scroll/utils.dart} (54%) diff --git a/front/lib/pages/src/artist.dart b/front/lib/pages/src/artist.dart index 7c6347e..c16f2f1 100644 --- a/front/lib/pages/src/artist.dart +++ b/front/lib/pages/src/artist.dart @@ -2,7 +2,7 @@ import 'package:blee/api/api.dart'; import 'package:blee/providers.dart'; import 'package:blee/ui/src/breakpoints.dart'; import 'package:blee/ui/src/description_box.dart'; -import 'package:blee/ui/src/infinite_scroll.dart'; +import 'package:blee/ui/src/infinite_scroll/infinite_horizontal_list.dart'; import 'package:blee/ui/src/poster_page_header.dart'; import 'package:blee/ui/src/tile.dart'; import 'package:collection/collection.dart'; @@ -49,7 +49,7 @@ class ArtistPage extends ConsumerWidget { : SizedBox( height: 200, child: PosterTileListView( - tileBuilder: (context, item, index) => PosterTile( + itemBuilder: (context, item, index) => PosterTile( onTap: () => context.push('/packages/${item?.id}'), title: item?.name, subtitle: item?.releaseDate?.year.toString() ?? '', @@ -57,7 +57,7 @@ class ArtistPage extends ConsumerWidget { ), query: (q) => client.getPackages( page: q, artistUuid: artist.value?.id), - header: Text('Packages'))) + header: const Text('Packages'))) ])); } } diff --git a/front/lib/pages/src/artists.dart b/front/lib/pages/src/artists.dart index 0b68ac7..dce145b 100644 --- a/front/lib/pages/src/artists.dart +++ b/front/lib/pages/src/artists.dart @@ -1,7 +1,7 @@ import 'package:blee/api/api.dart'; import 'package:blee/providers.dart'; import 'package:blee/ui/src/breakpoints.dart'; -import 'package:blee/ui/src/infinite_scroll.dart'; +import 'package:blee/ui/src/infinite_scroll/infinite_grid.dart'; import 'package:blee/ui/src/tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/front/lib/pages/src/extras.dart b/front/lib/pages/src/extras.dart index 01ab71a..d1e6236 100644 --- a/front/lib/pages/src/extras.dart +++ b/front/lib/pages/src/extras.dart @@ -1,7 +1,7 @@ import 'package:blee/api/api.dart'; import 'package:blee/providers.dart'; import 'package:blee/ui/src/breakpoints.dart'; -import 'package:blee/ui/src/infinite_scroll.dart'; +import 'package:blee/ui/src/infinite_scroll/infinite_grid.dart'; import 'package:blee/ui/src/tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/front/lib/pages/src/package.dart b/front/lib/pages/src/package.dart index ce29736..fa3808b 100644 --- a/front/lib/pages/src/package.dart +++ b/front/lib/pages/src/package.dart @@ -1,11 +1,9 @@ import 'package:blee/api/api.dart'; import 'package:blee/api/src/models/page.dart' as page; -import 'package:blee/api/src/models/image.dart' as blee_image; import 'package:blee/providers.dart'; import 'package:blee/ui/src/breakpoints.dart'; import 'package:blee/ui/src/description_box.dart'; -import 'package:blee/ui/src/image.dart'; -import 'package:blee/ui/src/infinite_scroll.dart'; +import 'package:blee/ui/src/infinite_scroll/infinite_grid.dart'; import 'package:blee/ui/src/poster_page_header.dart'; import 'package:blee/ui/src/tile.dart'; import 'package:flutter/material.dart'; diff --git a/front/lib/pages/src/packages.dart b/front/lib/pages/src/packages.dart index 7657baa..4c004f8 100644 --- a/front/lib/pages/src/packages.dart +++ b/front/lib/pages/src/packages.dart @@ -1,7 +1,7 @@ import 'package:blee/api/api.dart'; import 'package:blee/providers.dart'; import 'package:blee/ui/src/breakpoints.dart'; -import 'package:blee/ui/src/infinite_scroll.dart'; +import 'package:blee/ui/src/infinite_scroll/infinite_grid.dart'; import 'package:blee/ui/src/tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/front/lib/ui/src/infinite_scroll.dart b/front/lib/ui/src/infinite_scroll.dart deleted file mode 100644 index 11442e3..0000000 --- a/front/lib/ui/src/infinite_scroll.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'package:blee/api/api.dart'; -import 'package:blee/api/src/models/page.dart' as page; -import 'package:blee/ui/src/grid.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_sticky_header/flutter_sticky_header.dart'; -import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:skeletonizer/skeletonizer.dart'; - -class ThumbnailTileGridView extends AbstractGridView { - const ThumbnailTileGridView( - {super.key, - required super.tileBuilder, - required super.query, - super.skeletonHeader, - required super.header}) - : super(delegate: DefaultThumbnailTileGridDelegate); -} - -class PosterTileGridView extends AbstractGridView { - const PosterTileGridView( - {super.key, - required super.tileBuilder, - required super.query, - super.skeletonHeader, - required super.header}) - : super(delegate: DefaultPosterTileGridDelegate); -} - -abstract class AbstractGridView extends StatefulWidget { - final Future> Function(PageQuery) query; - final Widget Function(BuildContext, T?, int) tileBuilder; - final SliverGridDelegate Function(BuildContext) delegate; - final Widget? header; - final bool? skeletonHeader; - const AbstractGridView( - {super.key, - required this.query, - required this.tileBuilder, - required this.header, - required this.delegate, - this.skeletonHeader}); - @override - State> createState() => _AbstractGridViewState(); -} - -class _AbstractGridViewState extends State> { - static const _pageSize = 20; - - final PagingController _pagingController = - PagingController(firstPageKey: 0); - - @override - void initState() { - _pagingController.addPageRequestListener((pageKey) { - _fetchPage(pageKey); - }); - super.initState(); - } - - Future _fetchPage(int pageKey) async { - try { - final newPage = - await widget.query(PageQuery(skip: pageKey, take: _pageSize)); - final isLastPage = newPage.metadata.count < _pageSize; - if (isLastPage) { - _pagingController.appendLastPage(newPage.items); - } else { - final nextPageKey = pageKey + newPage.metadata.count; - _pagingController.appendPage(newPage.items, nextPageKey); - } - } catch (error) { - _pagingController.error = error; - } - } - - @override - Widget build(BuildContext context) => SliverStickyHeader( - sticky: widget.header != null, - header: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Skeletonizer( - enabled: widget.skeletonHeader ?? _pagingController.itemList == null, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), - child: (_pagingController.itemList?.isEmpty ?? false) - ? Container() - : widget.header ?? Container()), - ), - ), - sliver: PagedSliverGrid( - pagingController: _pagingController, - gridDelegate: widget.delegate(context), - shrinkWrapFirstPageIndicators: true, - builderDelegate: PagedChildBuilderDelegate( - noItemsFoundIndicatorBuilder: (_) => Container(), - firstPageProgressIndicatorBuilder: (context) => GridView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: widget.delegate(context), - children: [0, 1] - .map((index) => widget.tileBuilder(context, null, index)) - .toList(), - ), - itemBuilder: (context, item, index) => - widget.tileBuilder(context, item, index), - ), - )); - - @override - void dispose() { - _pagingController.dispose(); - super.dispose(); - } -} - -class PosterTileListView extends AbstractHorizontalListView { - const PosterTileListView( - {super.key, - required super.tileBuilder, - required super.query, - super.skeletonHeader, - required super.header}); -} - -abstract class AbstractHorizontalListView extends StatefulWidget { - final Future> Function(PageQuery) query; - final Widget Function(BuildContext, T?, int) tileBuilder; - final Widget? header; - final bool? skeletonHeader; - const AbstractHorizontalListView( - {super.key, - required this.query, - required this.tileBuilder, - required this.header, - this.skeletonHeader}); - @override - State> createState() => - _AbstractHorizontalListViewState(); -} - -class _AbstractHorizontalListViewState - extends State> { - static const _pageSize = 20; - - final PagingController _pagingController = - PagingController(firstPageKey: 0); - - @override - void initState() { - _pagingController.addPageRequestListener((pageKey) { - _fetchPage(pageKey); - }); - super.initState(); - } - - Future _fetchPage(int pageKey) async { - try { - final newPage = - await widget.query(PageQuery(skip: pageKey, take: _pageSize)); - final isLastPage = newPage.metadata.count < _pageSize; - if (isLastPage) { - _pagingController.appendLastPage(newPage.items); - } else { - final nextPageKey = pageKey + newPage.metadata.count; - _pagingController.appendPage(newPage.items, nextPageKey); - } - } catch (error) { - _pagingController.error = error; - } - } - - @override - Widget build(BuildContext context) => PagedListView( - primary: false, - pagingController: _pagingController, - scrollDirection: Axis.horizontal, - builderDelegate: PagedChildBuilderDelegate( - noItemsFoundIndicatorBuilder: (_) => Container(), - firstPageProgressIndicatorBuilder: (context) => Row( - children: [0, 1] - .map((index) => Padding( - padding: const EdgeInsets.all(4), - child: AspectRatio( - aspectRatio: 0.55, - child: widget.tileBuilder(context, null, index)))) - .toList(), - ), - itemBuilder: (context, item, index) => Padding( - padding: const EdgeInsets.all(4), - child: AspectRatio( - aspectRatio: 0.55, - child: widget.tileBuilder(context, item, index)), - ), - ), - ); - - @override - void dispose() { - _pagingController.dispose(); - super.dispose(); - } -} diff --git a/front/lib/ui/src/infinite_scroll/infinite_grid.dart b/front/lib/ui/src/infinite_scroll/infinite_grid.dart new file mode 100644 index 0000000..ec17461 --- /dev/null +++ b/front/lib/ui/src/infinite_scroll/infinite_grid.dart @@ -0,0 +1,99 @@ +import 'package:blee/api/api.dart'; +import 'package:blee/api/src/models/page.dart' as page; +import 'package:blee/ui/src/infinite_scroll/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +class ThumbnailTileGridView extends AbstractGridView { + const ThumbnailTileGridView( + {super.key, + required super.tileBuilder, + required super.query, + super.skeletonHeader, + required super.header}) + : super(delegate: DefaultThumbnailTileGridDelegate); +} + +class PosterTileGridView extends AbstractGridView { + const PosterTileGridView( + {super.key, + required super.tileBuilder, + required super.query, + super.skeletonHeader, + required super.header}) + : super(delegate: DefaultPosterTileGridDelegate); +} + +abstract class AbstractGridView extends StatefulWidget { + final Future> Function(PageQuery) query; + final Widget Function(BuildContext, T?, int) tileBuilder; + final SliverGridDelegate Function(BuildContext) delegate; + final Widget? header; + final bool? skeletonHeader; + const AbstractGridView( + {super.key, + required this.query, + required this.tileBuilder, + required this.header, + required this.delegate, + this.skeletonHeader}); + @override + State> createState() => _AbstractGridViewState(); +} + +class _AbstractGridViewState extends State> { + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + @override + void initState() { + _pagingController.addPageRequestListener((pageKey) { + InfiniteScrollHelper.fetchPage( + query: widget.query, + skip: pageKey, + pagingController: _pagingController); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) => SliverStickyHeader( + sticky: widget.header != null, + header: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Skeletonizer( + enabled: widget.skeletonHeader ?? _pagingController.itemList == null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: (_pagingController.itemList?.isEmpty ?? false) + ? Container() + : widget.header ?? Container()), + ), + ), + sliver: PagedSliverGrid( + pagingController: _pagingController, + gridDelegate: widget.delegate(context), + shrinkWrapFirstPageIndicators: true, + builderDelegate: PagedChildBuilderDelegate( + noItemsFoundIndicatorBuilder: (_) => Container(), + firstPageProgressIndicatorBuilder: (context) => GridView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: widget.delegate(context), + children: [0, 1] + .map((index) => widget.tileBuilder(context, null, index)) + .toList(), + ), + itemBuilder: (context, item, index) => + widget.tileBuilder(context, item, index), + ), + )); + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } +} diff --git a/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart b/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart new file mode 100644 index 0000000..563b719 --- /dev/null +++ b/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart @@ -0,0 +1,82 @@ +import 'package:blee/api/src/models/page.dart'; +import 'package:blee/ui/src/infinite_scroll/utils.dart'; +import 'package:blee/ui/src/tile.dart'; +import 'package:flutter/material.dart'; +import 'package:blee/api/src/models/page.dart' as page; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +class PosterTileListView extends AbstractHorizontalListView { + final Widget Function(BuildContext, T?, int) itemBuilder; + PosterTileListView( + {super.key, + required this.itemBuilder, + required super.query, + super.skeletonHeader, + required super.header}) + : super( + tileBuilder: (context, item, index) => AspectRatio( + aspectRatio: PosterTile.aspectRatio, + child: itemBuilder(context, item, index))); +} + +abstract class AbstractHorizontalListView extends StatefulWidget { + final Future> Function(PageQuery) query; + final Widget Function(BuildContext, T?, int) tileBuilder; + final Widget? header; + final bool? skeletonHeader; + const AbstractHorizontalListView( + {super.key, + required this.query, + required this.tileBuilder, + required this.header, + this.skeletonHeader}); + @override + State> createState() => + _AbstractHorizontalListViewState(); +} + +class _AbstractHorizontalListViewState + extends State> { + final PagingController _pagingController = + PagingController(firstPageKey: 0); + + @override + void initState() { + _pagingController.addPageRequestListener((pageKey) { + InfiniteScrollHelper.fetchPage( + query: widget.query, + skip: pageKey, + pagingController: _pagingController); + }); + super.initState(); + } + + Widget _itemBuilder(Widget item) => + Padding(padding: const EdgeInsets.all(4), child: item); + + @override + Widget build(BuildContext context) { + return PagedListView( + primary: false, + pagingController: _pagingController, + scrollDirection: Axis.horizontal, + builderDelegate: PagedChildBuilderDelegate( + noItemsFoundIndicatorBuilder: (_) => Container(), + firstPageProgressIndicatorBuilder: (context) => Row( + children: [0, 1] + .map((index) => + _itemBuilder(widget.tileBuilder(context, null, index))) + .toList(), + ), + itemBuilder: (context, item, index) => + _itemBuilder(widget.tileBuilder(context, item, index)), + ), + ); + } + + @override + void dispose() { + _pagingController.dispose(); + super.dispose(); + } +} diff --git a/front/lib/ui/src/grid.dart b/front/lib/ui/src/infinite_scroll/utils.dart similarity index 54% rename from front/lib/ui/src/grid.dart rename to front/lib/ui/src/infinite_scroll/utils.dart index 47e20c9..1e148ad 100644 --- a/front/lib/ui/src/grid.dart +++ b/front/lib/ui/src/infinite_scroll/utils.dart @@ -1,14 +1,40 @@ // ignore_for_file: non_constant_identifier_names - +import 'package:blee/api/src/models/page.dart'; +import 'package:blee/api/src/models/page.dart' as page; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:blee/ui/src/breakpoints.dart'; +import 'package:blee/ui/src/tile.dart'; import 'package:flutter/material.dart'; import 'package:responsive_framework/responsive_framework.dart'; +class InfiniteScrollHelper { + static Future fetchPage( + {required Future> Function(PageQuery) query, + required PagingController pagingController, + required int skip, + int pageSize = 20}) async { + try { + final newPage = await query(PageQuery(skip: skip, take: pageSize)); + final isLastPage = newPage.metadata.count < pageSize; + if (isLastPage) { + pagingController.appendLastPage(newPage.items); + } else { + final nextPageKey = skip + newPage.metadata.count; + pagingController.appendPage(newPage.items, nextPageKey); + } + } catch (error) { + pagingController.error = error; + } + } +} + +const padding = 8.0; + SliverGridDelegate DefaultThumbnailTileGridDelegate(BuildContext context) { return SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1.25, - crossAxisSpacing: 8, - mainAxisSpacing: 8, + childAspectRatio: ThumbnailTile.aspectRatio, + crossAxisSpacing: padding, + mainAxisSpacing: padding, crossAxisCount: ResponsiveBreakpoints.of(context) .largerOrEqualTo(BreakpointEnum.xl.name) ? 5 @@ -24,9 +50,9 @@ SliverGridDelegate DefaultThumbnailTileGridDelegate(BuildContext context) { SliverGridDelegate DefaultPosterTileGridDelegate(BuildContext context) { return SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 0.55, - crossAxisSpacing: 8, - mainAxisSpacing: 8, + childAspectRatio: PosterTile.aspectRatio, + crossAxisSpacing: padding, + mainAxisSpacing: padding, crossAxisCount: ResponsiveBreakpoints.of(context) .largerOrEqualTo(BreakpointEnum.xl.name) ? 9 diff --git a/front/lib/ui/src/tile.dart b/front/lib/ui/src/tile.dart index e80289e..734eb0e 100644 --- a/front/lib/ui/src/tile.dart +++ b/front/lib/ui/src/tile.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:skeletonizer/skeletonizer.dart'; class PosterTile extends Tile { + static double aspectRatio = 0.55; PosterTile( {super.key, required super.title, @@ -14,6 +15,7 @@ class PosterTile extends Tile { } class ThumbnailTile extends Tile { + static double aspectRatio = 1.25; ThumbnailTile( {super.key, required super.title, From 0d51a9f12779c48da8bd4241f5ba6cb6a8d76d76 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 12:44:04 +0200 Subject: [PATCH 07/15] Front: Add header to infinite lists --- front/lib/pages/src/artist.dart | 23 ++++---- .../infinite_horizontal_list.dart | 52 ++++++++++++++----- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/front/lib/pages/src/artist.dart b/front/lib/pages/src/artist.dart index c16f2f1..6f2dd82 100644 --- a/front/lib/pages/src/artist.dart +++ b/front/lib/pages/src/artist.dart @@ -46,18 +46,17 @@ class ArtistPage extends ConsumerWidget { ), artist.value == null ? Container() - : SizedBox( - height: 200, - child: PosterTileListView( - itemBuilder: (context, item, index) => PosterTile( - onTap: () => context.push('/packages/${item?.id}'), - title: item?.name, - subtitle: item?.releaseDate?.year.toString() ?? '', - thumbnail: item?.poster, - ), - query: (q) => client.getPackages( - page: q, artistUuid: artist.value?.id), - header: const Text('Packages'))) + : PosterTileListView( + itemBuilder: (context, item, index) => PosterTile( + onTap: () => context.push('/packages/${item?.id}'), + title: item?.name, + subtitle: item?.releaseDate?.year.toString() ?? '', + thumbnail: item?.poster, + ), + query: (q) => + client.getPackages(page: q, artistUuid: artist.value?.id), + skeletonHeader: false, + header: const Text('Packages')) ])); } } diff --git a/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart b/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart index 563b719..e16958f 100644 --- a/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart +++ b/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart @@ -4,6 +4,7 @@ import 'package:blee/ui/src/tile.dart'; import 'package:flutter/material.dart'; import 'package:blee/api/src/models/page.dart' as page; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:skeletonizer/skeletonizer.dart'; class PosterTileListView extends AbstractHorizontalListView { final Widget Function(BuildContext, T?, int) itemBuilder; @@ -12,6 +13,7 @@ class PosterTileListView extends AbstractHorizontalListView { required this.itemBuilder, required super.query, super.skeletonHeader, + super.height = 200, required super.header}) : super( tileBuilder: (context, item, index) => AspectRatio( @@ -24,8 +26,10 @@ abstract class AbstractHorizontalListView extends StatefulWidget { final Widget Function(BuildContext, T?, int) tileBuilder; final Widget? header; final bool? skeletonHeader; + final int height; const AbstractHorizontalListView( {super.key, + required this.height, required this.query, required this.tileBuilder, required this.header, @@ -56,21 +60,41 @@ class _AbstractHorizontalListViewState @override Widget build(BuildContext context) { - return PagedListView( - primary: false, - pagingController: _pagingController, - scrollDirection: Axis.horizontal, - builderDelegate: PagedChildBuilderDelegate( - noItemsFoundIndicatorBuilder: (_) => Container(), - firstPageProgressIndicatorBuilder: (context) => Row( - children: [0, 1] - .map((index) => - _itemBuilder(widget.tileBuilder(context, null, index))) - .toList(), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Skeletonizer( + enabled: + widget.skeletonHeader ?? (_pagingController.itemList == null), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: (_pagingController.itemList?.isEmpty ?? false) + ? Container() + : widget.header ?? Container()), + ), ), - itemBuilder: (context, item, index) => - _itemBuilder(widget.tileBuilder(context, item, index)), - ), + SizedBox( + height: widget.height.ceilToDouble(), + child: PagedListView( + primary: false, + pagingController: _pagingController, + scrollDirection: Axis.horizontal, + builderDelegate: PagedChildBuilderDelegate( + noItemsFoundIndicatorBuilder: (_) => Container(), + firstPageProgressIndicatorBuilder: (context) => Row( + children: [0, 1] + .map((index) => + _itemBuilder(widget.tileBuilder(context, null, index))) + .toList(), + ), + itemBuilder: (context, item, index) => + _itemBuilder(widget.tileBuilder(context, item, index)), + ), + ), + ), + ], ); } From 3972b1ca22d7f3ea4134f1e0ac365e1a2ae37ba4 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 14:02:05 +0200 Subject: [PATCH 08/15] Front: Add video section to artist page --- front/lib/api/src/client.dart | 6 ++- front/lib/pages/src/artist.dart | 41 +++++++++++++------ front/lib/pages/src/artists.dart | 2 +- front/lib/pages/src/extras.dart | 2 +- front/lib/pages/src/package.dart | 4 +- front/lib/pages/src/packages.dart | 2 +- .../ui/src/infinite_scroll/infinite_grid.dart | 8 ++-- .../infinite_horizontal_list.dart | 26 +++++++++--- 8 files changed, 62 insertions(+), 29 deletions(-) diff --git a/front/lib/api/src/client.dart b/front/lib/api/src/client.dart index 47f5c6e..95f95b0 100644 --- a/front/lib/api/src/client.dart +++ b/front/lib/api/src/client.dart @@ -95,9 +95,11 @@ class APIClient { } Future> getExtras( - {String? packageUuid, PageQuery page = const PageQuery()}) async { + {String? packageUuid, + String? artistUuid, + PageQuery page = const PageQuery()}) async { var responseBody = await _request(RequestType.get, - '/extras?package=$packageUuid&take=${page.take}&skip=${page.skip}'); + '/extras?package=$packageUuid&artist=$artistUuid&take=${page.take}&skip=${page.skip}'); return Page.fromJson( responseBody, (x) => Extra.fromJson(x as Map)); } diff --git a/front/lib/pages/src/artist.dart b/front/lib/pages/src/artist.dart index 6f2dd82..e30b242 100644 --- a/front/lib/pages/src/artist.dart +++ b/front/lib/pages/src/artist.dart @@ -5,6 +5,7 @@ import 'package:blee/ui/src/description_box.dart'; import 'package:blee/ui/src/infinite_scroll/infinite_horizontal_list.dart'; import 'package:blee/ui/src/poster_page_header.dart'; import 'package:blee/ui/src/tile.dart'; +import 'package:blee/utils/format_duration.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -44,19 +45,33 @@ class ArtistPage extends ConsumerWidget { skeletonize: externalIds.value == null, ), ), - artist.value == null - ? Container() - : PosterTileListView( - itemBuilder: (context, item, index) => PosterTile( - onTap: () => context.push('/packages/${item?.id}'), - title: item?.name, - subtitle: item?.releaseDate?.year.toString() ?? '', - thumbnail: item?.poster, - ), - query: (q) => - client.getPackages(page: q, artistUuid: artist.value?.id), - skeletonHeader: false, - header: const Text('Packages')) + ...(artist.value == null + ? [] + : [ + PosterListView( + itemBuilder: (context, item, index) => PosterTile( + onTap: () => context.push('/packages/${item?.id}'), + title: item?.name, + subtitle: item?.releaseDate?.year.toString() ?? '', + thumbnail: item?.poster, + ), + query: (q) => client.getPackages( + page: q, artistUuid: artist.value?.id), + header: const Text('Movies')), + ThumbnailListView( + itemBuilder: (context, item, index) => ThumbnailTile( + onTap: () => + context.push('/player/extra:${item?.id}'), + title: item?.name, + subtitle: item != null + ? formatDuration(item.duration) + : '', + thumbnail: item?.thumbnail, + ), + query: (q) => client.getExtras( + page: q, artistUuid: artist.value?.id), + header: const Text('Videos')) + ]) ])); } } diff --git a/front/lib/pages/src/artists.dart b/front/lib/pages/src/artists.dart index dce145b..d819b71 100644 --- a/front/lib/pages/src/artists.dart +++ b/front/lib/pages/src/artists.dart @@ -17,7 +17,7 @@ class ArtistsPage extends ConsumerWidget { return MaxWidthBox( maxWidth: Breakpoints.getSized(BreakpointEnum.lg), child: CustomScrollView(slivers: [ - PosterTileGridView( + PosterGridView( tileBuilder: (context, item, index) => PosterTile( onTap: () => context.push('/artists/${item?.id}'), title: item?.name, diff --git a/front/lib/pages/src/extras.dart b/front/lib/pages/src/extras.dart index d1e6236..72eaa7f 100644 --- a/front/lib/pages/src/extras.dart +++ b/front/lib/pages/src/extras.dart @@ -17,7 +17,7 @@ class ExtrasPage extends ConsumerWidget { return MaxWidthBox( maxWidth: Breakpoints.getSized(BreakpointEnum.sm), child: CustomScrollView(slivers: [ - ThumbnailTileGridView( + ThumbnailGridView( tileBuilder: (context, item, index) => ThumbnailTile( onTap: () => context.push('/player/extra:${item!.id}'), title: item?.name, diff --git a/front/lib/pages/src/package.dart b/front/lib/pages/src/package.dart index fa3808b..34816f9 100644 --- a/front/lib/pages/src/package.dart +++ b/front/lib/pages/src/package.dart @@ -93,7 +93,7 @@ class PackagePage extends ConsumerWidget { ), ...(movies?.items.map((movie) { var isOnlyMovie = movies.metadata.count == 1; - return ThumbnailTileGridView( + return ThumbnailGridView( key: Key('$movie-chapters'), header: Text( isOnlyMovie ? 'Chapters' : movie.name, @@ -115,7 +115,7 @@ class PackagePage extends ConsumerWidget { )); }).toList()) ?? [], - ThumbnailTileGridView( + ThumbnailGridView( key: Key('$packageUuid-extras'), header: (movies?.metadata.count ?? 1) > 0 ? Text( diff --git a/front/lib/pages/src/packages.dart b/front/lib/pages/src/packages.dart index 4c004f8..a614e5d 100644 --- a/front/lib/pages/src/packages.dart +++ b/front/lib/pages/src/packages.dart @@ -17,7 +17,7 @@ class PackagesPage extends ConsumerWidget { return MaxWidthBox( maxWidth: Breakpoints.getSized(BreakpointEnum.lg), child: CustomScrollView(slivers: [ - PosterTileGridView( + PosterGridView( tileBuilder: (context, item, index) => PosterTile( onTap: () => context.push('/packages/${item?.id}'), title: item?.name, diff --git a/front/lib/ui/src/infinite_scroll/infinite_grid.dart b/front/lib/ui/src/infinite_scroll/infinite_grid.dart index ec17461..5ecb368 100644 --- a/front/lib/ui/src/infinite_scroll/infinite_grid.dart +++ b/front/lib/ui/src/infinite_scroll/infinite_grid.dart @@ -6,8 +6,8 @@ import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:skeletonizer/skeletonizer.dart'; -class ThumbnailTileGridView extends AbstractGridView { - const ThumbnailTileGridView( +class ThumbnailGridView extends AbstractGridView { + const ThumbnailGridView( {super.key, required super.tileBuilder, required super.query, @@ -16,8 +16,8 @@ class ThumbnailTileGridView extends AbstractGridView { : super(delegate: DefaultThumbnailTileGridDelegate); } -class PosterTileGridView extends AbstractGridView { - const PosterTileGridView( +class PosterGridView extends AbstractGridView { + const PosterGridView( {super.key, required super.tileBuilder, required super.query, diff --git a/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart b/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart index e16958f..806a56c 100644 --- a/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart +++ b/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart @@ -6,9 +6,9 @@ import 'package:blee/api/src/models/page.dart' as page; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:skeletonizer/skeletonizer.dart'; -class PosterTileListView extends AbstractHorizontalListView { +class PosterListView extends AbstractHorizontalListView { final Widget Function(BuildContext, T?, int) itemBuilder; - PosterTileListView( + PosterListView( {super.key, required this.itemBuilder, required super.query, @@ -21,6 +21,21 @@ class PosterTileListView extends AbstractHorizontalListView { child: itemBuilder(context, item, index))); } +class ThumbnailListView extends AbstractHorizontalListView { + final Widget Function(BuildContext, T?, int) itemBuilder; + ThumbnailListView( + {super.key, + required this.itemBuilder, + required super.query, + super.skeletonHeader, + super.height = 150, + required super.header}) + : super( + tileBuilder: (context, item, index) => AspectRatio( + aspectRatio: ThumbnailTile.aspectRatio, + child: itemBuilder(context, item, index))); +} + abstract class AbstractHorizontalListView extends StatefulWidget { final Future> Function(PageQuery) query; final Widget Function(BuildContext, T?, int) tileBuilder; @@ -48,9 +63,10 @@ class _AbstractHorizontalListViewState void initState() { _pagingController.addPageRequestListener((pageKey) { InfiniteScrollHelper.fetchPage( - query: widget.query, - skip: pageKey, - pagingController: _pagingController); + query: widget.query, + skip: pageKey, + pagingController: _pagingController) + .then((value) => setState(() {})); }); super.initState(); } From bcd151095a0adfcc4bdfb14fbee83a38b919d774 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 14:43:16 +0200 Subject: [PATCH 09/15] Front: Navigation Rail --- front/lib/navigation.dart | 53 ++++++++++++--- .../infinite_horizontal_list.dart | 67 ++++++++++--------- front/lib/ui/src/infinite_scroll/utils.dart | 5 +- front/lib/ui/src/player_controls.dart | 2 + front/lib/ui/src/tile.dart | 2 +- 5 files changed, 84 insertions(+), 45 deletions(-) diff --git a/front/lib/navigation.dart b/front/lib/navigation.dart index bfe1f13..f707c67 100644 --- a/front/lib/navigation.dart +++ b/front/lib/navigation.dart @@ -1,6 +1,8 @@ +import 'package:blee/ui/src/breakpoints.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:responsive_framework/responsive_framework.dart'; class ScaffoldWithNavBar extends StatefulWidget { final String location; @@ -14,7 +16,7 @@ class ScaffoldWithNavBar extends StatefulWidget { } class _ScaffoldWithNavBarState extends State { - int _currentIndex = 0; + int _currentIndex = 1; @override void initState() { @@ -49,14 +51,42 @@ class _ScaffoldWithNavBarState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SafeArea(child: widget.child), - bottomNavigationBar: NavigationBar( - destinations: tabs, - onDestinationSelected: (int index) { - _goOtherTab(context, index); - }, - selectedIndex: _currentIndex, + body: SafeArea( + child: Row( + children: [ + ResponsiveBreakpoints.of(context) + .largerOrEqualTo(BreakpointEnum.sm.name) + ? NavigationRail( + labelType: NavigationRailLabelType.all, + onDestinationSelected: (int index) { + _goOtherTab(context, index); + }, + destinations: tabs + .map((tab) => tab.toRailDestination(context)) + .toList(), + selectedIndex: _currentIndex) + : Container(), + Expanded(child: widget.child) + ], + )), + appBar: AppBar( + centerTitle: false, + title: const Text( + 'Blee', + style: TextStyle( + fontWeight: FontWeight.w900, fontStyle: FontStyle.italic), + ), ), + bottomNavigationBar: ResponsiveBreakpoints.of(context) + .smallerOrEqualTo(BreakpointEnum.xs.name) + ? NavigationBar( + destinations: tabs, + onDestinationSelected: (int index) { + _goOtherTab(context, index); + }, + selectedIndex: _currentIndex, + ) + : null, ); } @@ -78,6 +108,11 @@ class MyNavigationDestination extends NavigationDestination { {super.key, required this.initialLocation, required super.icon, + super.selectedIcon, required super.label}) - : super(selectedIcon: icon); + : super(); + NavigationRailDestination toRailDestination(BuildContext context) { + return NavigationRailDestination( + icon: icon, label: Text(label), selectedIcon: selectedIcon); + } } diff --git a/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart b/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart index 806a56c..589615c 100644 --- a/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart +++ b/front/lib/ui/src/infinite_scroll/infinite_horizontal_list.dart @@ -78,39 +78,42 @@ class _AbstractHorizontalListViewState Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: Skeletonizer( - enabled: - widget.skeletonHeader ?? (_pagingController.itemList == null), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), - child: (_pagingController.itemList?.isEmpty ?? false) - ? Container() - : widget.header ?? Container()), - ), - ), - SizedBox( - height: widget.height.ceilToDouble(), - child: PagedListView( - primary: false, - pagingController: _pagingController, - scrollDirection: Axis.horizontal, - builderDelegate: PagedChildBuilderDelegate( - noItemsFoundIndicatorBuilder: (_) => Container(), - firstPageProgressIndicatorBuilder: (context) => Row( - children: [0, 1] - .map((index) => - _itemBuilder(widget.tileBuilder(context, null, index))) - .toList(), + children: (_pagingController.itemList?.isNotEmpty ?? true) + ? [ + Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Skeletonizer( + enabled: widget.skeletonHeader ?? + (_pagingController.itemList == null), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 4), + child: (_pagingController.itemList?.isEmpty ?? false) + ? Container() + : widget.header ?? Container()), + ), ), - itemBuilder: (context, item, index) => - _itemBuilder(widget.tileBuilder(context, item, index)), - ), - ), - ), - ], + SizedBox( + height: widget.height.ceilToDouble(), + child: PagedListView( + primary: false, + pagingController: _pagingController, + scrollDirection: Axis.horizontal, + builderDelegate: PagedChildBuilderDelegate( + noItemsFoundIndicatorBuilder: (_) => Container(), + firstPageProgressIndicatorBuilder: (context) => Row( + children: [0, 1] + .map((index) => _itemBuilder( + widget.tileBuilder(context, null, index))) + .toList(), + ), + itemBuilder: (context, item, index) => + _itemBuilder(widget.tileBuilder(context, item, index)), + ), + ), + ), + ] + : [], ); } diff --git a/front/lib/ui/src/infinite_scroll/utils.dart b/front/lib/ui/src/infinite_scroll/utils.dart index 1e148ad..99921d9 100644 --- a/front/lib/ui/src/infinite_scroll/utils.dart +++ b/front/lib/ui/src/infinite_scroll/utils.dart @@ -34,7 +34,6 @@ SliverGridDelegate DefaultThumbnailTileGridDelegate(BuildContext context) { return SliverGridDelegateWithFixedCrossAxisCount( childAspectRatio: ThumbnailTile.aspectRatio, crossAxisSpacing: padding, - mainAxisSpacing: padding, crossAxisCount: ResponsiveBreakpoints.of(context) .largerOrEqualTo(BreakpointEnum.xl.name) ? 5 @@ -58,13 +57,13 @@ SliverGridDelegate DefaultPosterTileGridDelegate(BuildContext context) { ? 9 : ResponsiveBreakpoints.of(context) .largerOrEqualTo(BreakpointEnum.lg.name) - ? 7 + ? 8 : ResponsiveBreakpoints.of(context) .largerOrEqualTo(BreakpointEnum.md.name) ? 6 : ResponsiveBreakpoints.of(context) .largerOrEqualTo(BreakpointEnum.sm.name) - ? 5 + ? 4 : 3, ); } diff --git a/front/lib/ui/src/player_controls.dart b/front/lib/ui/src/player_controls.dart index 69daa66..25134cb 100644 --- a/front/lib/ui/src/player_controls.dart +++ b/front/lib/ui/src/player_controls.dart @@ -128,6 +128,8 @@ class _PlayerControlsState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(widget.title ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( color: textColor, fontSize: Theme.of(context) diff --git a/front/lib/ui/src/tile.dart b/front/lib/ui/src/tile.dart index 734eb0e..90a3d0f 100644 --- a/front/lib/ui/src/tile.dart +++ b/front/lib/ui/src/tile.dart @@ -15,7 +15,7 @@ class PosterTile extends Tile { } class ThumbnailTile extends Tile { - static double aspectRatio = 1.25; + static double aspectRatio = 1.22; ThumbnailTile( {super.key, required super.title, From d61f0d21e714b3392750a503542bcb110b2d2f8c Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 15:31:48 +0200 Subject: [PATCH 10/15] Scanner: Prevent Crash on file open failure --- scanner/pkg/actions/register.go | 6 +++--- scanner/pkg/parser/media_info.go | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/scanner/pkg/actions/register.go b/scanner/pkg/actions/register.go index 02093a9..0b9cab0 100644 --- a/scanner/pkg/actions/register.go +++ b/scanner/pkg/actions/register.go @@ -26,12 +26,12 @@ func RegisterFile(path string, c *config.Config) error { if err != nil { glg.Failf("Getting MediaInfo on '%s' failed:", filepath.Base(path)) glg.Fail(err) + return err } var resourceUuid = "" var packageUuid = "" if parsedPath.Movie != nil { - dto, err := buildMovieDto(path, parsedPath.Movie, mediainfo) - + dto, err := buildMovieDto(path, parsedPath.Movie, &mediainfo) if err != nil { glg.Failf(err.Error()) return err @@ -51,7 +51,7 @@ func RegisterFile(path string, c *config.Config) error { } } } else if parsedPath.Extra != nil { - dto, err := buildExtraDto(path, parsedPath.Extra, mediainfo) + dto, err := buildExtraDto(path, parsedPath.Extra, &mediainfo) if err != nil { glg.Failf(err.Error()) diff --git a/scanner/pkg/parser/media_info.go b/scanner/pkg/parser/media_info.go index 4f4116e..026aa05 100644 --- a/scanner/pkg/parser/media_info.go +++ b/scanner/pkg/parser/media_info.go @@ -20,10 +20,10 @@ type MediaChapter struct { Types []models.ChapterType `validate:"required,dive,required"` } -func GetMediaInfo(path string) (*MediaInfo, error) { +func GetMediaInfo(path string) (MediaInfo, error) { mi, err := mediainfo.Open(path) if err != nil { - return nil, err + return MediaInfo{}, err } defer mi.Close() @@ -45,8 +45,7 @@ func GetMediaInfo(path string) (*MediaInfo, error) { } }), } - - return &info, nil + return info, nil } func qualityFromHeight(height uint64) models.Quality { From 15840efb2fbc378a28c78a99cc303e23a7e9deec Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 15:51:58 +0200 Subject: [PATCH 11/15] Docker-compose for user + define public port --- .env.example | 2 + docker-compose.prod.yml | 154 ++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 +- front/Dockerfile | 2 +- 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 docker-compose.prod.yml diff --git a/.env.example b/.env.example index 2240642..0d754b3 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,6 @@ TMDB_API_KEY= +# Port on which Blee will be exposed +PUBLIC_PORT= # Database. Fill in the missing fields with secure values POSTGRES_USER= POSTGRES_PASSWORD= diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..c4c3034 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,154 @@ +x-transcoder: &transcoder-base + image: ghcr.io/zoriya/kyoo_transcoder:4.5.0 + networks: + default: + aliases: + - transcoder + restart: unless-stopped + environment: + - GOCODER_PREFIX=/videos + volumes: + - ${DATA_DIR}:/videos:ro + - ${CACHE_ROOT}:/cache + - metadata:/metadata + +# This compose allows to build the whole project in a "production"-like environment +services: + front: + image: arthichaud/blee-front:edge + restart: on-failure + depends_on: + api: + condition: service_healthy + environment: + - PORT=3000 + ports: + - "3000:3000" + api: + image: arthichaud/blee-api:edge + ports: + - "8000:8000" + restart: on-failure + depends_on: + db: + condition: service_healthy + mq: + condition: service_healthy + volumes: + - data:${CONFIG_DIR} + environment: + - POSTGRES_HOST=db + - POSTGRES_PORT=5432 + - RABBIT_HOST=mq + - RABBIT_PORT=5672 + env_file: + - .env + healthcheck: + test: ["CMD-SHELL", "wget -qO- localhost:8000"] + interval: 5s + retries: 3 + mq: + image: rabbitmq:3.13-alpine + environment: + - RABBITMQ_DEFAULT_USER=${RABBIT_USER} + - RABBITMQ_DEFAULT_PASS=${RABBIT_PASS} + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 10s + timeout: 3s + retries: 10 + scanner: + image: arthichaud/blee-scanner:edge + depends_on: + api: + condition: service_healthy + volumes: + - ${DATA_DIR}:/videos:ro + - ./scanner.json:/app/scanner.json + environment: + - API_URL=http://api:8000 + - WATCH_DIR=/videos + - SCANNER_API_KEY=${SCANNER_API_KEY} + - CONFIG_DIR=/app + matcher: + image: arthichaud/blee-matcher:edge + restart: on-failure + depends_on: + api: + condition: service_healthy + environment: + - RABBIT_HOST=mq + - RABBIT_PORT=5672 + - API_URL=http://api:8000 + - MATCHER_API_KEY=${MATCHER_API_KEY} + env_file: + - .env + db: + image: postgres:alpine3.16 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 3s + timeout: 5s + retries: 5 + env_file: + - .env + expose: + - 5432 + volumes: + - db:/var/lib/postgresql/data + nginx: + restart: on-failure + image: nginx:1.24.0-alpine + depends_on: + api: + condition: service_started + front: + condition: service_started + ports: + - ${PUBLIC_PORT}:5000 + environment: + - PORT=5000 + - FRONT_URL=http://front:3000 + - SERVER_URL=http://api:8000 + - TRANSCODER_URL=http://transcoder:7666 + volumes: + - ./nginx.conf.template:/etc/nginx/templates/blee.conf.template:ro + transcoder: + <<: *transcoder-base + profiles: ['', 'cpu'] + + transcoder-nvidia: + <<: *transcoder-base + deploy: + resources: + reservations: + devices: + - capabilities: [gpu] + environment: + - GOCODER_PREFIX=/video + - GOCODER_HWACCEL=nvidia + profiles: ['nvidia'] + + transcoder-vaapi: + <<: *transcoder-base + devices: + - /dev/dri:/dev/dri + environment: + - GOCODER_PREFIX=/video + - GOCODER_HWACCEL=vaapi + - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} + profiles: ['vaapi'] + # qsv is the same setup as vaapi but with the hwaccel env var different + transcoder-qsv: + <<: *transcoder-base + devices: + - /dev/dri:/dev/dri + environment: + - GOCODER_PREFIX=/video + - GOCODER_HWACCEL=qsv + - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} + profiles: ['qsv'] +volumes: + db: + data: + metadata: diff --git a/docker-compose.yml b/docker-compose.yml index bdd4cec..85364ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -109,7 +109,7 @@ services: front: condition: service_started ports: - - 5000:5000 + - ${PUBLIC_PORT}:5000 environment: - PORT=5000 - FRONT_URL=http://front:3000 diff --git a/front/Dockerfile b/front/Dockerfile index 7cf457e..949405a 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -18,7 +18,7 @@ WORKDIR $APP RUN flutter clean RUN flutter pub get -RUN dart run build_runner build +RUN dart run build_runner build --delete-conflicting-outputs RUN flutter build web FROM nginx:1.25.2-alpine From 1ced167fa0f4ba234b7fee9e6f3f5b1f0bce6216 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 15:58:16 +0200 Subject: [PATCH 12/15] Front: Fix host url in api client in prod --- front/lib/api/src/client.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/front/lib/api/src/client.dart b/front/lib/api/src/client.dart index 95f95b0..5f29b62 100644 --- a/front/lib/api/src/client.dart +++ b/front/lib/api/src/client.dart @@ -7,7 +7,8 @@ import 'package:http/http.dart' as http; enum RequestType { get, post, put, delete } class APIClient { - String _host = Uri.base.toString(); + String _host = + Uri.base.toString().substring(0, Uri.base.toString().indexOf('/#/')); final http.Client client = http.Client(); @@ -20,7 +21,7 @@ class APIClient { String buildImageUrl(String uuid) { final route = "/images/$uuid"; - return _host + (kDebugMode ? route : "api$route"); + return _host + (kDebugMode ? route : "/api$route"); } Future getArtist(String uuid) async { From e63904a0a90f79293e9126cf75d0577024f52152 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 16:43:37 +0200 Subject: [PATCH 13/15] Front: Set Transcoder URL for prod --- front/Dockerfile | 2 +- front/lib/api/src/client.dart | 9 ++++++++- front/lib/pages/src/player.dart | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/front/Dockerfile b/front/Dockerfile index 949405a..2394df2 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -19,7 +19,7 @@ WORKDIR $APP RUN flutter clean RUN flutter pub get RUN dart run build_runner build --delete-conflicting-outputs -RUN flutter build web +RUN flutter build web --release --source-maps FROM nginx:1.25.2-alpine COPY --from=build-env /app/build/web /etc/nginx/html diff --git a/front/lib/api/src/client.dart b/front/lib/api/src/client.dart index 5f29b62..de2aad1 100644 --- a/front/lib/api/src/client.dart +++ b/front/lib/api/src/client.dart @@ -24,6 +24,13 @@ class APIClient { return _host + (kDebugMode ? route : "/api$route"); } + String buildTranscoderUrl(String transcoderRoute) { + if (kDebugMode) { + return 'http://localhost:7666$transcoderRoute'; + } + return "$_host/transcoder$transcoderRoute"; + } + Future getArtist(String uuid) async { var responseBody = await _request(RequestType.get, '/artists/$uuid'); return Artist.fromJson(responseBody); @@ -111,7 +118,7 @@ class APIClient { params ?? {}; http.Response response; Uri fullRoute = Uri.parse(_host + - (kDebugMode ? route : "api$route") + + (kDebugMode ? route : "/api$route") + (params == null ? "" : "?${Uri(queryParameters: params).query}")); final Map headers = { 'Content-type': 'application/json', diff --git a/front/lib/pages/src/player.dart b/front/lib/pages/src/player.dart index 88317a0..fd04c6e 100644 --- a/front/lib/pages/src/player.dart +++ b/front/lib/pages/src/player.dart @@ -101,8 +101,8 @@ class PlayerPageState extends ConsumerState { VideoPlayerController setupPlayer(PlayerMetadata metadata, {StreamMode streamMode = StreamMode.hls}) { - final baseUrl = - 'http://localhost:7666/${base64Encode(utf8.encode(metadata.videoFile.path))}'; + final baseUrl = ref.read(apiClientProvider).buildTranscoderUrl( + "/${base64Encode(utf8.encode(metadata.videoFile.path))}"); return VideoPlayerController.networkUrl( Uri.parse(streamMode == StreamMode.direct ? '$baseUrl/direct' From b65ea297588e993dc01486d0bcae7d63c3220914 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 16:56:40 +0200 Subject: [PATCH 14/15] Matcher: Docker: Add openssl --- matcher/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/matcher/Dockerfile b/matcher/Dockerfile index 9111226..6253143 100644 --- a/matcher/Dockerfile +++ b/matcher/Dockerfile @@ -22,6 +22,9 @@ RUN mv "$(stack path --local-install-root --system-ghc)/bin" /opt/build/bin # stage should run # We use the same base image as the base of the haskell image FROM debian:buster-slim as runner +RUN apt-get update; apt-get install -y --no-install-recommends ca-certificates openssl +ENV SSL_CERT_DIR=/etc/ssl/certs +RUN update-ca-certificates RUN mkdir -p /opt/app WORKDIR /opt/app COPY --from=build /opt/build/bin /opt/app From 28a5d0d6620f84f4b42ab4f95319996b74251205 Mon Sep 17 00:00:00 2001 From: Arthur Jamet Date: Thu, 9 May 2024 17:19:15 +0200 Subject: [PATCH 15/15] Matcher: Fix tests --- matcher/test/Matcher/TestTMDB.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matcher/test/Matcher/TestTMDB.hs b/matcher/test/Matcher/TestTMDB.hs index a9c4947..271f03c 100644 --- a/matcher/test/Matcher/TestTMDB.hs +++ b/matcher/test/Matcher/TestTMDB.hs @@ -22,7 +22,7 @@ specs = describe "TMDB" $ do name res `shouldBe` "Madonna" originalName res `shouldBe` "Madonna" profilePath res - `shouldBe` Just "https://image.tmdb.org/t/p/original/pI6g1iVlUy7cUAZ6AspVXWq4kli.jpg" + `shouldBe` Just "https://image.tmdb.org/t/p/original/8XtGxpB4z428QDgwKlFYPktYHFC.jpg" ) it "Should Fail to find band" $ do searchArtist tmdbClient "Garbage" @@ -38,7 +38,7 @@ specs = describe "TMDB" $ do Right (ArtistDetails Nothing) -> expectationFailure "No biography found" Right (ArtistDetails (Just description)) -> do take 10 description `shouldBe` "Madonna (b" - reverse (take 10 $ reverse description) `shouldBe` " industry." + reverse (take 10 $ reverse description) `shouldBe` "le artist." ) describe "Search Package" $ do it "Should Get Package" $ do