mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
fix: Small improvement to library screen
This commit is contained in:
parent
b2657f6408
commit
16bf5e8a32
3 changed files with 204 additions and 136 deletions
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue