import 'dart:ui'; import 'package:collection/collection.dart'; import 'package:ficonsax/ficonsax.dart'; import 'package:fladder/models/boxset_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/playlist_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; import 'package:fladder/screens/shared/media/poster_grid.dart'; import 'package:fladder/screens/shared/media/poster_list_item.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:intl/intl.dart'; import 'package:page_transition/page_transition.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:sticky_headers/sticky_headers/widget.dart'; final libraryViewTypeProvider = StateProvider((ref) { return LibraryViewTypes.grid; }); enum LibraryViewTypes { grid(icon: IconsaxOutline.grid_2), list(icon: IconsaxOutline.grid_6), masonry(icon: IconsaxOutline.grid_3); const LibraryViewTypes({required this.icon}); String label(BuildContext context) => switch (this) { LibraryViewTypes.grid => context.localized.grid, LibraryViewTypes.list => context.localized.list, LibraryViewTypes.masonry => context.localized.masonry, }; final IconData icon; } class LibraryViews extends ConsumerWidget { final List items; final GroupBy groupByType; final Function(ItemBaseModel)? onPressed; final Set excludeActions = const {ItemActions.openParent}; const LibraryViews({required this.items, required this.groupByType, this.onPressed, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 4), sliver: SliverAnimatedSwitcher( duration: const Duration(milliseconds: 250), child: _getWidget(ref, context), ), ); } Widget _getWidget(WidgetRef ref, BuildContext context) { final selected = ref.watch(librarySearchProvider(key!).select((value) => value.selectedPosters)); final posterSizeMultiplier = ref.watch(clientSettingsProvider.select((value) => value.posterSize)); final libraryProvider = ref.read(librarySearchProvider(key!).notifier); final posterSize = MediaQuery.sizeOf(context).width / (AdaptiveLayout.poster(context).gridRatio * ref.watch(clientSettingsProvider.select((value) => value.posterSize))); final decimal = posterSize - posterSize.toInt(); final sortingOptions = ref.watch(librarySearchProvider(key!).select((value) => value.sortingOption)); List otherActions(ItemBaseModel item) { return [ if (ref.watch(librarySearchProvider(key!).select((value) => value.nestedCurrentItem is BoxSetModel))) ...{ ItemActionButton( label: Text(context.localized.removeFromCollection), icon: const Icon(IconsaxOutline.archive_slash), action: () async { await libraryProvider.removeFromCollection(items: [item]); if (context.mounted) { context.refreshData(); } }, ) }, if (ref.watch(librarySearchProvider(key!).select((value) => value.nestedCurrentItem is PlaylistModel))) ...{ ItemActionButton( label: Text(context.localized.removeFromPlaylist), icon: const Icon(IconsaxOutline.archive_minus), action: () async { await libraryProvider.removeFromPlaylist(items: [item]); if (context.mounted) { context.refreshData(); } }, ) } ]; } switch (ref.watch(libraryViewTypeProvider)) { case LibraryViewTypes.grid: if (groupByType != GroupBy.none) { final groupedItems = groupItemsBy(context, items, groupByType); return SliverList.builder( itemCount: groupedItems.length, itemBuilder: (context, index) { final name = groupedItems.keys.elementAt(index); final group = groupedItems[name]; if (group?.isEmpty ?? false || group == null) { return Text(context.localized.empty); } return PosterGrid( posters: group!, name: name, itemBuilder: (context, index) { final item = group[index]; return PosterWidget( key: Key(item.id), poster: group[index], maxLines: 2, heroTag: true, subTitle: item.subTitle(sortingOptions), excludeActions: excludeActions, otherActions: otherActions(item), selected: selected.contains(item), onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ); } else { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 8), sliver: SliverGrid.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: posterSize.toInt(), mainAxisSpacing: (8 * decimal) + 8, crossAxisSpacing: (8 * decimal) + 8, childAspectRatio: AdaptiveLayout.poster(context).ratio, ), itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return PosterWidget( key: Key(item.id), poster: item, maxLines: 2, heroTag: true, subTitle: item.subTitle(sortingOptions), excludeActions: excludeActions, otherActions: otherActions(item), selected: selected.contains(item), onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ), ); } case LibraryViewTypes.list: if (groupByType != GroupBy.none) { final groupedItems = groupItemsBy(context, items, groupByType); return SliverList.builder( itemCount: groupedItems.length, itemBuilder: (context, index) { final name = groupedItems.keys.elementAt(index); final group = groupedItems[name]; if (group?.isEmpty ?? false) { return Text(context.localized.empty); } return StickyHeader( header: Text(name, style: Theme.of(context).textTheme.headlineSmall), content: ListView.builder( shrinkWrap: true, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: group?.length, itemBuilder: (context, index) { final poster = group![index]; return PosterListItem( key: Key(poster.id), poster: poster, subTitle: poster.subTitle(sortingOptions), excludeActions: excludeActions, otherActions: otherActions(poster), selected: selected.contains(poster), onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ), ); }, ); } return SliverList.builder( itemCount: items.length, itemBuilder: (context, index) { final poster = items[index]; return PosterListItem( poster: poster, selected: selected.contains(poster), excludeActions: excludeActions, otherActions: otherActions(poster), subTitle: poster.subTitle(sortingOptions), onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ); case LibraryViewTypes.masonry: if (groupByType != GroupBy.none) { final groupedItems = groupItemsBy(context, items, groupByType); return SliverList.builder( itemCount: groupedItems.length, itemBuilder: (context, index) { final name = groupedItems.keys.elementAt(index); final group = groupedItems[name]; if (group?.isEmpty ?? false) { return Text(context.localized.empty); } return Padding( padding: EdgeInsets.only(top: index == 0 ? 0 : 64.0), child: StickyHeader( header: Text(name, style: Theme.of(context).textTheme.headlineMedium), overlapHeaders: true, content: Padding( padding: const EdgeInsets.only(top: 16.0), child: MasonryGridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: (8 * decimal) + 8, crossAxisSpacing: (8 * decimal) + 8, gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: (MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0)) .toDouble() * 20, ), itemCount: group!.length, itemBuilder: (context, index) { final item = group[index]; return PosterWidget( key: Key(item.id), poster: item, aspectRatio: item.primaryRatio, selected: selected.contains(item), inlineTitle: true, heroTag: true, subTitle: item.subTitle(sortingOptions), excludeActions: excludeActions, otherActions: otherActions(group[index]), onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ), )), ); }, ); } else { return SliverMasonryGrid.count( mainAxisSpacing: (8 * decimal) + 8, crossAxisSpacing: (8 * decimal) + 8, crossAxisCount: posterSize.toInt(), childCount: items.length, itemBuilder: (context, index) { final item = items[index]; return PosterWidget( poster: item, key: Key(item.id), aspectRatio: item.primaryRatio, selected: selected.contains(item), inlineTitle: true, heroTag: true, excludeActions: excludeActions, otherActions: otherActions(item), subTitle: item.subTitle(sortingOptions), onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ); } } } Map> groupItemsBy(BuildContext context, List list, GroupBy groupOption) { switch (groupOption) { case GroupBy.dateAdded: return groupBy( items, (poster) => DateFormat.yMMMMd().format(DateTime( poster.overview.dateAdded!.year, poster.overview.dateAdded!.month, poster.overview.dateAdded!.day))); case GroupBy.releaseDate: return groupBy(list, (poster) => poster.overview.yearAired?.toString() ?? context.localized.unknown); case GroupBy.rating: return groupBy(list, (poster) => poster.overview.parentalRating ?? context.localized.noRating); case GroupBy.tags: return groupByList(context, list, true); case GroupBy.genres: return groupByList(context, list, false); case GroupBy.name: return groupBy(list, (poster) => poster.name[0].capitalize()); case GroupBy.type: return groupBy(list, (poster) => poster.type.label(context)); case GroupBy.none: return {}; } } Future onItemPressed( Function() action, Key? key, ItemBaseModel item, WidgetRef ref, BuildContext context) async { final selectMode = ref.read(librarySearchProvider(key!).select((value) => value.selecteMode)); if (selectMode) { ref.read(librarySearchProvider(key).notifier).toggleSelection(item); return; } switch (item) { case PhotoModel _: final photoList = items.whereType().toList(); if (context.mounted) { await Navigator.of(context, rootNavigator: true).push( PageTransition( child: PhotoViewerScreen( items: photoList, loadingItems: ref.read(librarySearchProvider(key).notifier).fetchGallery(), indexOfSelected: photoList.indexWhere((element) => element.id == item.id), ), type: PageTransitionType.fade), ); } if (context.mounted) context.refreshData(); break; default: action.call(); break; } } } Map> groupByList(BuildContext context, List items, bool tags) { Map tagsCount = {}; for (var item in items) { for (var tag in (tags ? item.overview.tags : item.overview.genres)) { tagsCount[tag] = (tagsCount[tag] ?? 0) + 1; } } List sortedTags = tagsCount.keys.toList()..sort((a, b) => tagsCount[a]!.compareTo(tagsCount[b]!)); Map> groupedItems = {}; for (var item in items) { List itemTags = (tags ? item.overview.tags : item.overview.genres); itemTags.sort((a, b) => sortedTags.indexOf(a).compareTo(sortedTags.indexOf(b))); String key = itemTags.take(2).join(', '); key = key.isNotEmpty ? key : context.localized.none; groupedItems[key] = [...(groupedItems[key] ?? []), item]; } return groupedItems; }