import 'dart:async'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/recommended_model.dart'; import 'package:fladder/models/view_model.dart'; import 'package:fladder/providers/library_screen_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sliver_list_padding.dart'; import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; import 'package:fladder/widgets/shared/button_group.dart'; import 'package:fladder/widgets/shared/horizontal_list.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; @RoutePage() class LibraryScreen extends ConsumerStatefulWidget { const LibraryScreen({ super.key, }); @override ConsumerState createState() => _LibraryScreenState(); } class _LibraryScreenState extends ConsumerState with SingleTickerProviderStateMixin { final GlobalKey? refreshKey = GlobalKey(); @override Widget build(BuildContext context) { final libraryScreenState = ref.watch(libraryScreenProvider); final views = libraryScreenState.views; final recommendations = libraryScreenState.recommendations; final favourites = libraryScreenState.favourites; final selectedView = libraryScreenState.selectedViewModel; final viewTypes = libraryScreenState.viewType; final genres = libraryScreenState.genres; final padding = AdaptiveLayout.adaptivePadding(context); return NestedScaffold( background: BackgroundImage( items: [ ...recommendations.expand((e) => e.posters), ...favourites, ], ), body: PullToRefresh( refreshOnStart: true, refreshKey: refreshKey, onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(), child: SizedBox.expand( child: CustomScrollView( controller: AdaptiveLayout.scrollOf(context), 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(); }, ), ), 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 (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( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: PosterRow( contentPadding: padding, 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, label: element.type != null ? "${element.type?.label(context)} - ${element.name.label(context)}" : element.name.label(context), ), ), ), ) ], const DefautlSliverBottomPadding(), ], ), ), ), ); } } class LibraryRow extends ConsumerWidget { const LibraryRow({ super.key, required this.views, this.selectedView, required this.padding, this.onSelected, }); final List views; final ViewModel? selectedView; final EdgeInsets padding; final FutureOr Function(ViewModel selected)? onSelected; @override Widget build(BuildContext context, WidgetRef ref) { return HorizontalList( label: context.localized.library(views.length), items: views, startIndex: selectedView != null ? views.indexOf(selectedView!) : null, height: 165, contentPadding: padding, itemBuilder: (context, index) { final view = views[index]; final isSelected = selectedView == view; final List viewActions = [ ItemActionButton( label: Text(context.localized.search), icon: const Icon(IconsaxPlusLinear.search_normal), action: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), ), ItemActionButton( label: Text(context.localized.scanLibrary), icon: const Icon(IconsaxPlusLinear.refresh), action: () => showRefreshPopup(context, view.id, view.name), ) ]; return FlatButton( onTap: isSelected ? null : () => onSelected?.call(view), onLongPress: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); await showMenu( context: context, position: position, items: viewActions.popupMenuItems(useIcons: true), ); }, child: Card( color: isSelected ? Theme.of(context).colorScheme.primaryContainer : null, shadowColor: Colors.transparent, child: Padding( padding: const EdgeInsets.all(4.0), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, spacing: 4, children: [ SizedBox( width: 200, child: Card( child: AspectRatio( aspectRatio: 1.60, child: FladderImage( image: view.imageData?.primary, fit: BoxFit.cover, placeHolder: Center( child: Text( view.name, style: Theme.of(context).textTheme.titleMedium, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, ), ), ), ), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Row( spacing: 8, children: [ if (isSelected) Container( height: 12, width: 12, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary, shape: BoxShape.circle, ), ), Text( view.name, style: Theme.of(context).textTheme.titleMedium, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, ), ], ), ), ), ], ), ), ), ); }, ); } }