fix: Small improvement to library screen

This commit is contained in:
PartyDonut 2025-10-28 21:44:36 +01:00
parent b2657f6408
commit 16bf5e8a32
3 changed files with 204 additions and 136 deletions

View file

@ -12,6 +12,13 @@ sealed class NameSwitch {
String label(BuildContext context); String label(BuildContext context);
} }
class Resume extends NameSwitch {
const Resume();
@override
String label(BuildContext context) => context.localized.dashboardContinue;
}
class NextUp extends NameSwitch { class NextUp extends NameSwitch {
const NextUp(); const NextUp();

View file

@ -90,23 +90,32 @@ class LibraryScreen extends _$LibraryScreen {
return null; return null;
} }
Future<void> loadResume(ViewModel viewModel) async {}
Future<void> loadRecommendations(ViewModel viewModel) async { Future<void> loadRecommendations(ViewModel viewModel) async {
List<RecommendedModel> newRecommendations = []; List<RecommendedModel> newRecommendations = [];
final latest = await api.usersUserIdItemsLatestGet(
final resume = await api.usersUserIdItemsResumeGet(
parentId: viewModel.id, parentId: viewModel.id,
limit: 14, limit: 14,
isPlayed: false, enableUserData: true,
imageTypeLimit: 1, enableImageTypes: [
includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(), ImageType.primary,
ImageType.banner,
ImageType.screenshot,
],
mediaTypes: [MediaType.video],
enableTotalRecordCount: false,
); );
newRecommendations = [ newRecommendations = [
...newRecommendations, ...newRecommendations,
RecommendedModel( RecommendedModel(
name: const Latest(), name: const Resume(),
posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [], posters: resume.body?.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: null, type: null,
), ),
]; ];
if (viewModel.collectionType == CollectionType.movies) { if (viewModel.collectionType == CollectionType.movies) {
final response = await api.moviesRecommendationsGet( final response = await api.moviesRecommendationsGet(
parentId: viewModel.id, parentId: viewModel.id,
@ -146,6 +155,22 @@ class LibraryScreen extends _$LibraryScreen {
]; ];
} }
final latest = await api.usersUserIdItemsLatestGet(
parentId: viewModel.id,
limit: 14,
isPlayed: false,
imageTypeLimit: 1,
includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(),
);
newRecommendations = [
...newRecommendations,
RecommendedModel(
name: const Latest(),
posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: null,
),
];
state = state.copyWith( state = state.copyWith(
recommendations: newRecommendations, recommendations: newRecommendations,
); );

View file

@ -40,6 +40,9 @@ class LibraryScreen extends ConsumerStatefulWidget {
class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTickerProviderStateMixin { class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTickerProviderStateMixin {
final GlobalKey<RefreshIndicatorState>? refreshKey = GlobalKey(); final GlobalKey<RefreshIndicatorState>? refreshKey = GlobalKey();
bool refreshing = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final libraryScreenState = ref.watch(libraryScreenProvider); final libraryScreenState = ref.watch(libraryScreenProvider);
@ -60,152 +63,170 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTicker
body: PullToRefresh( body: PullToRefresh(
refreshOnStart: true, refreshOnStart: true,
refreshKey: refreshKey, refreshKey: refreshKey,
onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(), onRefresh: () async {
child: SizedBox.expand( if (refreshing) return;
child: CustomScrollView( setState(() => refreshing = true);
controller: AdaptiveLayout.scrollOf(context, HomeTabs.library), try {
physics: const AlwaysScrollableScrollPhysics(), await ref.read(libraryScreenProvider.notifier).fetchAllLibraries();
slivers: [ } finally {
const DefaultSliverTopBadding(), if (mounted) {
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) setState(() => refreshing = false);
NestedSliverAppBar( }
route: LibrarySearchRoute(), }
parent: context, },
), child: AnimatedOpacity(
if (views.isNotEmpty) opacity: refreshing ? 0.75 : 1.0,
SliverToBoxAdapter( duration: const Duration(milliseconds: 175),
child: LibraryRow( child: SizedBox.expand(
padding: padding, child: CustomScrollView(
views: views, controller: AdaptiveLayout.scrollOf(context, HomeTabs.library),
selectedView: libraryScreenState.selectedViewModel, physics: const AlwaysScrollableScrollPhysics(),
onSelected: (view) { slivers: [
ref.read(libraryScreenProvider.notifier).selectLibrary(view); const DefaultSliverTopBadding(),
refreshKey?.currentState?.show(); if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
}, NestedSliverAppBar(
route: LibrarySearchRoute(),
parent: context,
), ),
), if (views.isNotEmpty)
if (selectedView != null) SliverToBoxAdapter(
SliverToBoxAdapter( child: LibraryRow(
child: Padding( padding: padding,
padding: const EdgeInsets.only(top: 24, bottom: 16), views: views,
child: SizedBox( selectedView: libraryScreenState.selectedViewModel,
height: 40, onSelected: (view) {
child: ListView( if (refreshing) return;
padding: padding, ref.read(libraryScreenProvider.notifier).selectLibrary(view);
shrinkWrap: true, refreshKey?.currentState?.show();
scrollDirection: Axis.horizontal, },
children: [
FilledButton.tonalIcon(
onPressed: () => context.pushRoute(LibrarySearchRoute(viewModelId: selectedView.id)),
label: Text("${context.localized.search} ${selectedView.name}..."),
icon: const Icon(IconsaxPlusLinear.search_normal),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: VerticalDivider(),
),
ExpressiveButtonGroup(
multiSelection: true,
options: LibraryViewType.values
.map((element) => ButtonGroupOption(
value: element,
icon: Icon(element.icon),
selected: Icon(element.iconSelected),
child: Text(
element.label(context),
)))
.toList(),
selectedValues: viewTypes,
onSelected: (value) {
ref.read(libraryScreenProvider.notifier).setViewType(value);
},
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: VerticalDivider(),
),
ElevatedButton.icon(
onPressed: () => showRefreshPopup(context, selectedView.id, selectedView.name),
label: Text(context.localized.scanLibrary),
icon: const Icon(IconsaxPlusLinear.refresh),
),
],
),
), ),
), ),
), if (selectedView != null)
if (viewTypes.isEmpty)
SliverFillRemaining(
child: Center(child: Text(context.localized.noResults)),
),
if (viewTypes.contains(LibraryViewType.recommended)) ...[
if (recommendations.isNotEmpty)
...recommendations.where((element) => element.posters.isNotEmpty).map(
(element) => SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: PosterRow(
contentPadding: padding,
posters: element.posters,
label: element.type != null
? "${element.type?.label(context)} - ${element.name.label(context)}"
: element.name.label(context),
),
),
),
),
],
if (viewTypes.contains(LibraryViewType.favourites))
if (favourites.isNotEmpty)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.only(top: 24, bottom: 16),
child: PosterRow( child: SizedBox(
contentPadding: padding, height: 40,
onLabelClick: () => context.pushRoute( child: ListView(
LibrarySearchRoute( padding: padding,
viewModelId: libraryScreenState.selectedViewModel?.id ?? "", shrinkWrap: true,
).withFilter( scrollDirection: Axis.horizontal,
const LibraryFilterModel( children: [
favourites: true, FilledButton.tonalIcon(
recursive: true, onPressed: () => context.pushRoute(LibrarySearchRoute(viewModelId: selectedView.id)),
label: Text("${context.localized.search} ${selectedView.name}..."),
icon: const Icon(IconsaxPlusLinear.search_normal),
), ),
), const Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: VerticalDivider(),
),
ExpressiveButtonGroup(
multiSelection: true,
options: LibraryViewType.values
.map((element) => ButtonGroupOption(
value: element,
icon: Icon(element.icon),
selected: Icon(element.iconSelected),
child: Text(
element.label(context),
)))
.toList(),
selectedValues: viewTypes,
onSelected: (value) {
ref.read(libraryScreenProvider.notifier).setViewType(value);
},
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4.0),
child: VerticalDivider(),
),
ElevatedButton.icon(
onPressed: () => showRefreshPopup(context, selectedView.id, selectedView.name),
label: Text(context.localized.scanLibrary),
icon: const Icon(IconsaxPlusLinear.refresh),
),
],
), ),
posters: favourites,
label: context.localized.favorites,
), ),
), ),
), ),
if (viewTypes.contains(LibraryViewType.genres)) ...[ if (viewTypes.isEmpty)
if (genres.isNotEmpty) SliverFillRemaining(
...genres.where((element) => element.posters.isNotEmpty).map( child: Center(child: Text(context.localized.noResults)),
(element) => SliverToBoxAdapter( ),
if (viewTypes.contains(LibraryViewType.recommended)) ...[
if (recommendations.isNotEmpty)
...recommendations.where((element) => element.posters.isNotEmpty).map(
(element) {
return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: PosterRow( child: PosterRow(
contentPadding: padding, contentPadding: padding,
posters: element.posters, posters: element.posters,
onLabelClick: () => context.pushRoute( primaryPosters: element.name is Resume,
LibrarySearchRoute(
viewModelId: libraryScreenState.selectedViewModel?.id ?? "",
).withFilter(
LibraryFilterModel(
recursive: true,
genres: {(element.name as Other).customLabel: true},
),
),
),
label: element.type != null label: element.type != null
? "${element.type?.label(context)} - ${element.name.label(context)}" ? "${element.type?.label(context)} - ${element.name.label(context)}"
: element.name.label(context), : element.name.label(context),
), ),
), ),
);
},
),
],
if (viewTypes.contains(LibraryViewType.favourites))
if (favourites.isNotEmpty)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: PosterRow(
contentPadding: padding,
onLabelClick: () => context.pushRoute(
LibrarySearchRoute(
viewModelId: libraryScreenState.selectedViewModel?.id ?? "",
).withFilter(
const LibraryFilterModel(
favourites: true,
recursive: true,
),
),
),
posters: favourites,
label: context.localized.favorites,
), ),
) ),
),
if (viewTypes.contains(LibraryViewType.genres)) ...[
if (genres.isNotEmpty)
...genres.where((element) => element.posters.isNotEmpty).map(
(element) => SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: PosterRow(
contentPadding: padding,
posters: element.posters,
onLabelClick: () => context.pushRoute(
LibrarySearchRoute(
viewModelId: libraryScreenState.selectedViewModel?.id ?? "",
).withFilter(
LibraryFilterModel(
recursive: true,
genres: {(element.name as Other).customLabel: true},
),
),
),
label: element.type != null
? "${element.type?.label(context)} - ${element.name.label(context)}"
: element.name.label(context),
),
),
),
)
],
const DefautlSliverBottomPadding(),
], ],
const DefautlSliverBottomPadding(), ),
],
), ),
), ),
), ),
@ -297,12 +318,27 @@ class LibraryRow extends ConsumerWidget {
), ),
), ),
), ),
Text( Row(
view.name, spacing: 8,
style: Theme.of(context).textTheme.titleMedium, mainAxisAlignment: MainAxisAlignment.center,
maxLines: 2, children: [
overflow: TextOverflow.ellipsis, if (isSelected)
textAlign: TextAlign.start, Container(
height: 12,
width: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
),
Text(
view.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
)
],
) )
], ],
); );