mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 13:38:13 -08:00
387 lines
17 KiB
Dart
387 lines
17 KiB
Dart
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<LibraryViewTypes>((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<ItemBaseModel> items;
|
|
final GroupBy groupByType;
|
|
final Function(ItemBaseModel)? onPressed;
|
|
final Set<ItemActions> 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<ItemAction> 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<String, List<ItemBaseModel>> groupItemsBy(BuildContext context, List<ItemBaseModel> 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<void> 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<PhotoModel>().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<String, List<ItemBaseModel>> groupByList(BuildContext context, List<ItemBaseModel> items, bool tags) {
|
|
Map<String, int> tagsCount = {};
|
|
for (var item in items) {
|
|
for (var tag in (tags ? item.overview.tags : item.overview.genres)) {
|
|
tagsCount[tag] = (tagsCount[tag] ?? 0) + 1;
|
|
}
|
|
}
|
|
|
|
List<String> sortedTags = tagsCount.keys.toList()..sort((a, b) => tagsCount[a]!.compareTo(tagsCount[b]!));
|
|
|
|
Map<String, List<ItemBaseModel>> groupedItems = {};
|
|
|
|
for (var item in items) {
|
|
List<String> 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;
|
|
}
|