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);
}
class Resume extends NameSwitch {
const Resume();
@override
String label(BuildContext context) => context.localized.dashboardContinue;
}
class NextUp extends NameSwitch {
const NextUp();

View file

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

View file

@ -40,6 +40,9 @@ class LibraryScreen extends ConsumerStatefulWidget {
class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTickerProviderStateMixin {
final GlobalKey<RefreshIndicatorState>? refreshKey = GlobalKey();
bool refreshing = false;
@override
Widget build(BuildContext context) {
final libraryScreenState = ref.watch(libraryScreenProvider);
@ -60,152 +63,170 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTicker
body: PullToRefresh(
refreshOnStart: true,
refreshKey: refreshKey,
onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(),
child: SizedBox.expand(
child: CustomScrollView(
controller: AdaptiveLayout.scrollOf(context, HomeTabs.library),
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
const DefaultSliverTopBadding(),
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar(
route: LibrarySearchRoute(),
parent: context,
),
if (views.isNotEmpty)
SliverToBoxAdapter(
child: LibraryRow(
padding: padding,
views: views,
selectedView: libraryScreenState.selectedViewModel,
onSelected: (view) {
ref.read(libraryScreenProvider.notifier).selectLibrary(view);
refreshKey?.currentState?.show();
},
onRefresh: () async {
if (refreshing) return;
setState(() => refreshing = true);
try {
await ref.read(libraryScreenProvider.notifier).fetchAllLibraries();
} finally {
if (mounted) {
setState(() => refreshing = false);
}
}
},
child: AnimatedOpacity(
opacity: refreshing ? 0.75 : 1.0,
duration: const Duration(milliseconds: 175),
child: SizedBox.expand(
child: CustomScrollView(
controller: AdaptiveLayout.scrollOf(context, HomeTabs.library),
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
const DefaultSliverTopBadding(),
if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone)
NestedSliverAppBar(
route: LibrarySearchRoute(),
parent: context,
),
),
if (selectedView != null)
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 24, bottom: 16),
child: SizedBox(
height: 40,
child: ListView(
padding: padding,
shrinkWrap: true,
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 (views.isNotEmpty)
SliverToBoxAdapter(
child: LibraryRow(
padding: padding,
views: views,
selectedView: libraryScreenState.selectedViewModel,
onSelected: (view) {
if (refreshing) return;
ref.read(libraryScreenProvider.notifier).selectLibrary(view);
refreshKey?.currentState?.show();
},
),
),
),
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)
if (selectedView != null)
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,
padding: const EdgeInsets.only(top: 24, bottom: 16),
child: SizedBox(
height: 40,
child: ListView(
padding: padding,
shrinkWrap: true,
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),
),
],
),
posters: favourites,
label: context.localized.favorites,
),
),
),
if (viewTypes.contains(LibraryViewType.genres)) ...[
if (genres.isNotEmpty)
...genres.where((element) => element.posters.isNotEmpty).map(
(element) => SliverToBoxAdapter(
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) {
return 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},
),
),
),
primaryPosters: element.name is Resume,
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(
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(
view.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.start,
Row(
spacing: 8,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (isSelected)
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,
)
],
)
],
);