mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07:00
feat: Improve library search screen (#477)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
571b682b80
commit
d22d340181
41 changed files with 2881 additions and 2026 deletions
|
|
@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/collection_types.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/models/settings/home_settings_model.dart';
|
||||
import 'package:fladder/providers/dashboard_provider.dart';
|
||||
|
|
@ -177,19 +178,26 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
contentPadding: padding,
|
||||
label: context.localized.dashboardRecentlyAdded(view.name),
|
||||
collectionAspectRatio: view.collectionType.aspectRatio,
|
||||
onLabelClick: () => context.router.push(LibrarySearchRoute(
|
||||
viewModelId: view.id,
|
||||
sortingOptions: switch (view.collectionType) {
|
||||
CollectionType.tvshows ||
|
||||
CollectionType.books ||
|
||||
CollectionType.boxsets ||
|
||||
CollectionType.folders ||
|
||||
CollectionType.music =>
|
||||
SortingOptions.dateLastContentAdded,
|
||||
_ => SortingOptions.dateAdded,
|
||||
},
|
||||
sortOrder: SortingOrder.descending,
|
||||
)),
|
||||
onLabelClick: () => context.router.push(
|
||||
LibrarySearchRoute(
|
||||
viewModelId: view.id,
|
||||
types: switch (view.collectionType) {
|
||||
CollectionType.tvshows => {
|
||||
FladderItemType.episode: true,
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
sortingOptions: switch (view.collectionType) {
|
||||
CollectionType.books ||
|
||||
CollectionType.boxsets ||
|
||||
CollectionType.folders ||
|
||||
CollectionType.music =>
|
||||
SortingOptions.dateLastContentAdded,
|
||||
_ => SortingOptions.dateAdded,
|
||||
},
|
||||
sortOrder: SortingOrder.descending,
|
||||
),
|
||||
),
|
||||
posters: view.recentlyAdded,
|
||||
),
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/library_filter_model.dart';
|
||||
import 'package:fladder/providers/favourites_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/routes/auto_router.gr.dart';
|
||||
|
|
@ -56,6 +57,15 @@ class FavouritesScreen extends ConsumerWidget {
|
|||
(e) => SliverToBoxAdapter(
|
||||
child: PosterRow(
|
||||
contentPadding: padding,
|
||||
onLabelClick: () => context.pushRoute(
|
||||
LibrarySearchRoute().withFilter(
|
||||
LibraryFilterModel(
|
||||
favourites: true,
|
||||
types: {e.key: true},
|
||||
recursive: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
label: e.key.label(context),
|
||||
posters: e.value,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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/library_filter_model.dart';
|
||||
import 'package:fladder/models/recommended_model.dart';
|
||||
import 'package:fladder/models/view_model.dart';
|
||||
import 'package:fladder/providers/library_screen_provider.dart';
|
||||
|
|
@ -15,6 +16,7 @@ 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/theme.dart';
|
||||
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
||||
import 'package:fladder/util/fladder_image.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
|
@ -159,6 +161,16 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTicker
|
|||
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,
|
||||
),
|
||||
|
|
@ -173,6 +185,16 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen> with SingleTicker
|
|||
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),
|
||||
|
|
@ -210,7 +232,7 @@ class LibraryRow extends ConsumerWidget {
|
|||
label: context.localized.library(views.length),
|
||||
items: views,
|
||||
startIndex: selectedView != null ? views.indexOf(selectedView!) : null,
|
||||
height: 165,
|
||||
height: 125,
|
||||
contentPadding: padding,
|
||||
itemBuilder: (context, index) {
|
||||
final view = views[index];
|
||||
|
|
@ -240,52 +262,51 @@ class LibraryRow extends ConsumerWidget {
|
|||
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,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
border: Border.all(
|
||||
width: isSelected ? 4 : 0,
|
||||
color: isSelected ? Theme.of(context).colorScheme.primary : Colors.transparent,
|
||||
),
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
width: 200,
|
||||
child: ClipRRect(
|
||||
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||
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(
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
opacity: isSelected ? 0 : 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
padding: const EdgeInsets.all(6),
|
||||
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,
|
||||
|
|
@ -297,9 +318,9 @@ class LibraryRow extends ConsumerWidget {
|
|||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:iconsax_plus/iconsax_plus.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_filter_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_model.dart';
|
||||
import 'package:fladder/models/library_search/library_search_options.dart';
|
||||
import 'package:fladder/models/playlist_model.dart';
|
||||
|
|
@ -44,7 +45,6 @@ import 'package:fladder/widgets/shared/pinch_poster_zoom.dart';
|
|||
import 'package:fladder/widgets/shared/poster_size_slider.dart';
|
||||
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
|
||||
import 'package:fladder/widgets/shared/scroll_position.dart';
|
||||
import 'package:fladder/widgets/shared/shapes.dart';
|
||||
|
||||
@RoutePage()
|
||||
class LibrarySearchScreen extends ConsumerStatefulWidget {
|
||||
|
|
@ -53,6 +53,9 @@ class LibrarySearchScreen extends ConsumerStatefulWidget {
|
|||
final List<String>? folderId;
|
||||
final SortingOrder? sortOrder;
|
||||
final SortingOptions? sortingOptions;
|
||||
final Map<FladderItemType, bool>? types;
|
||||
final Map<String, bool>? genres;
|
||||
final bool recursive;
|
||||
final PhotoModel? photoToView;
|
||||
const LibrarySearchScreen({
|
||||
@QueryParam("parentId") this.viewModelId,
|
||||
|
|
@ -60,6 +63,9 @@ class LibrarySearchScreen extends ConsumerStatefulWidget {
|
|||
@QueryParam("favourites") this.favourites,
|
||||
@QueryParam("sortOrder") this.sortOrder,
|
||||
@QueryParam("sortOptions") this.sortingOptions,
|
||||
@QueryParam("itemTypes") this.types,
|
||||
@QueryParam("genres") this.genres,
|
||||
@QueryParam("recursive") this.recursive = true,
|
||||
this.photoToView,
|
||||
super.key,
|
||||
});
|
||||
|
|
@ -69,7 +75,6 @@ class LibrarySearchScreen extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
||||
final SearchController searchController = SearchController();
|
||||
final Debouncer debouncer = Debouncer(const Duration(seconds: 1));
|
||||
final GlobalKey<RefreshIndicatorState> refreshKey = GlobalKey<RefreshIndicatorState>();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
|
@ -93,32 +98,24 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initLibrary();
|
||||
WidgetsBinding.instance.addPostFrameCallback((value) {
|
||||
initLibrary();
|
||||
});
|
||||
}
|
||||
|
||||
void initLibrary() {
|
||||
searchController.addListener(() {
|
||||
debouncer.run(() {
|
||||
ref.read(providerKey.notifier).setSearch(searchController.text);
|
||||
});
|
||||
});
|
||||
|
||||
Future.microtask(
|
||||
() async {
|
||||
await refreshKey.currentState?.show();
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: [],
|
||||
);
|
||||
|
||||
if (context.mounted && widget.photoToView != null) {
|
||||
libraryProvider.viewGallery(context, selected: widget.photoToView);
|
||||
}
|
||||
scrollController.addListener(() {
|
||||
scrollPosition();
|
||||
});
|
||||
},
|
||||
Future<void> initLibrary() async {
|
||||
await refreshKey.currentState?.show();
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
overlays: [],
|
||||
);
|
||||
|
||||
if (context.mounted && widget.photoToView != null) {
|
||||
libraryProvider.viewGallery(context, selected: widget.photoToView);
|
||||
}
|
||||
scrollController.addListener(() {
|
||||
scrollPosition();
|
||||
});
|
||||
}
|
||||
|
||||
void scrollPosition() {
|
||||
|
|
@ -127,19 +124,27 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> refreshSearch() async {
|
||||
await refreshKey.currentState?.show();
|
||||
scrollController.jumpTo(0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final surfaceColor = Theme.of(context).colorScheme.surface;
|
||||
|
||||
final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null;
|
||||
final librarySearchResults = ref.watch(providerKey);
|
||||
final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows);
|
||||
final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.filters.hideEmptyShows);
|
||||
final libraryViewType = ref.watch(libraryViewTypeProvider);
|
||||
|
||||
final floatingAppBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single;
|
||||
|
||||
ref.listen(
|
||||
providerKey,
|
||||
(previous, next) {
|
||||
if (previous != next) {
|
||||
refreshKey.currentState?.show();
|
||||
scrollController.jumpTo(0);
|
||||
if (previous?.filters != next.filters) {
|
||||
refreshSearch();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -153,20 +158,17 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
}
|
||||
},
|
||||
child: NestedScaffold(
|
||||
background: BackgroundImage(items: librarySearchResults.activePosters),
|
||||
background: BackgroundImage(images: postersList.map((e) => e.images).nonNulls.toList()),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth),
|
||||
child: Scaffold(
|
||||
extendBody: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
extendBodyBehindAppBar: true,
|
||||
floatingActionButton: HideOnScroll(
|
||||
controller: scrollController,
|
||||
visibleBuilder: (visible) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (librarySearchResults.activePosters.isNotEmpty)
|
||||
FloatingActionButtonAnimated(
|
||||
visibleBuilder: (visible) => librarySearchResults.activePosters.isNotEmpty
|
||||
? FloatingActionButtonAnimated(
|
||||
key: Key(context.localized.playLabel),
|
||||
isExtended: visible,
|
||||
tooltip: context.localized.playVideos,
|
||||
|
|
@ -192,12 +194,12 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
},
|
||||
label: Text(context.localized.playLabel),
|
||||
icon: const Icon(IconsaxPlusBold.play),
|
||||
),
|
||||
].addInBetween(const SizedBox(height: 10)),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
bottomNavigationBar: HideOnScroll(
|
||||
controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController,
|
||||
controller: scrollController,
|
||||
canHide: !floatingAppBar,
|
||||
child: IgnorePointer(
|
||||
ignoring: librarySearchResults.fetchingItems,
|
||||
child: _LibrarySearchBottomBar(
|
||||
|
|
@ -209,319 +211,306 @@ class _LibrarySearchScreenState extends ConsumerState<LibrarySearchScreen> {
|
|||
),
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: PinchPosterZoom(
|
||||
scaleDifference: (difference) =>
|
||||
ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
|
||||
child: ClipRRect(
|
||||
borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop
|
||||
? BorderRadius.circular(15)
|
||||
: BorderRadius.circular(0),
|
||||
child: FladderScrollbar(
|
||||
visible: AdaptiveLayout.of(context).inputDevice != InputDevice.pointer,
|
||||
controller: scrollController,
|
||||
child: PullToRefresh(
|
||||
refreshKey: refreshKey,
|
||||
autoFocus: false,
|
||||
contextRefresh: false,
|
||||
onRefresh: () async {
|
||||
if (libraryProvider.mounted) {
|
||||
return libraryProvider.initRefresh(
|
||||
widget.folderId,
|
||||
widget.viewModelId,
|
||||
widget.favourites,
|
||||
widget.sortOrder,
|
||||
widget.sortingOptions,
|
||||
);
|
||||
}
|
||||
},
|
||||
refreshOnStart: false,
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
floating: !AdaptiveLayout.of(context).isDesktop,
|
||||
collapsedHeight: 80,
|
||||
automaticallyImplyLeading: false,
|
||||
pinned: AdaptiveLayout.of(context).isDesktop,
|
||||
primary: true,
|
||||
elevation: 5,
|
||||
leading: context.router.backButton(),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
shape: AppBarShape(),
|
||||
titleSpacing: 4,
|
||||
leadingWidth: 48,
|
||||
actions: [
|
||||
const SizedBox(width: 4),
|
||||
Builder(builder: (context) {
|
||||
final isFavorite =
|
||||
librarySearchResults.nestedCurrentItem?.userData.isFavourite == true;
|
||||
final itemActions = librarySearchResults.nestedCurrentItem?.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: {
|
||||
ItemActions.details,
|
||||
ItemActions.markPlayed,
|
||||
ItemActions.markUnplayed,
|
||||
},
|
||||
onItemUpdated: (item) {
|
||||
libraryProvider.updateParentItem(item);
|
||||
},
|
||||
onUserDataChanged: (userData) {
|
||||
libraryProvider.updateUserDataMain(userData);
|
||||
},
|
||||
) ??
|
||||
[];
|
||||
final itemCountWidget = ItemActionButton(
|
||||
label: Text(context.localized.itemCount(librarySearchResults.totalItemCount)),
|
||||
icon: const Icon(IconsaxPlusBold.document_1),
|
||||
);
|
||||
final refreshAction = ItemActionButton(
|
||||
label: Text(context.localized.forceRefresh),
|
||||
action: () => refreshKey.currentState?.show(),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
);
|
||||
final showSavedFiltersDialogue = ItemActionButton(
|
||||
label: Text(context.localized.filter(2)),
|
||||
action: () => showSavedFilters(context, librarySearchResults, libraryProvider),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
);
|
||||
final itemViewAction = ItemActionButton(
|
||||
label: Text(context.localized.selectViewType),
|
||||
icon: Icon(libraryViewType.icon),
|
||||
action: () {
|
||||
showAdaptiveDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final currentType = ref.watch(libraryViewTypeProvider);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(context.localized.selectViewType,
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
...LibraryViewTypes.values
|
||||
.map(
|
||||
(e) => FilledButton.tonal(
|
||||
style: FilledButtonTheme.of(context).style?.copyWith(
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 24)),
|
||||
backgroundColor: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
if (e != currentType) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(libraryViewTypeProvider.notifier).state = e;
|
||||
body: PinchPosterZoom(
|
||||
scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference),
|
||||
child: FladderScrollbar(
|
||||
visible: AdaptiveLayout.of(context).inputDevice != InputDevice.pointer,
|
||||
controller: scrollController,
|
||||
child: PullToRefresh(
|
||||
refreshKey: refreshKey,
|
||||
autoFocus: false,
|
||||
contextRefresh: false,
|
||||
onRefresh: () async {
|
||||
final defaultFilter = const LibraryFilterModel();
|
||||
if (libraryProvider.mounted) {
|
||||
return libraryProvider.initRefresh(
|
||||
widget.folderId,
|
||||
widget.viewModelId,
|
||||
defaultFilter.copyWith(
|
||||
favourites: widget.favourites ?? defaultFilter.favourites,
|
||||
sortOrder: widget.sortOrder ?? defaultFilter.sortOrder,
|
||||
sortingOption: widget.sortingOptions ?? defaultFilter.sortingOption,
|
||||
types: widget.types ?? {},
|
||||
genres: widget.genres ?? {},
|
||||
recursive: widget.recursive,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
refreshOnStart: false,
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
floating: !floatingAppBar,
|
||||
collapsedHeight: 80,
|
||||
automaticallyImplyLeading: false,
|
||||
primary: true,
|
||||
pinned: floatingAppBar,
|
||||
elevation: 5,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent,
|
||||
titleSpacing: 4,
|
||||
flexibleSpace: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
surfaceColor.withValues(alpha: 0.8),
|
||||
surfaceColor.withValues(alpha: 0.75),
|
||||
surfaceColor.withValues(alpha: 0.5),
|
||||
surfaceColor.withValues(alpha: 0),
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
)),
|
||||
),
|
||||
actions: [
|
||||
Builder(builder: (context) {
|
||||
final isFavorite = librarySearchResults.nestedCurrentItem?.userData.isFavourite == true;
|
||||
final itemActions = librarySearchResults.nestedCurrentItem?.generateActions(
|
||||
context,
|
||||
ref,
|
||||
exclude: {
|
||||
ItemActions.details,
|
||||
ItemActions.markPlayed,
|
||||
ItemActions.markUnplayed,
|
||||
},
|
||||
onItemUpdated: (item) {
|
||||
libraryProvider.updateParentItem(item);
|
||||
},
|
||||
onUserDataChanged: (userData) {
|
||||
libraryProvider.updateUserDataMain(userData);
|
||||
},
|
||||
) ??
|
||||
[];
|
||||
final itemCountWidget = ItemActionButton(
|
||||
label: Text(context.localized.itemCount(librarySearchResults.totalItemCount)),
|
||||
icon: const Icon(IconsaxPlusBold.document_1),
|
||||
);
|
||||
final refreshAction = ItemActionButton(
|
||||
label: Text(context.localized.forceRefresh),
|
||||
action: () => refreshKey.currentState?.show(),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
);
|
||||
final showSavedFiltersDialogue = ItemActionButton(
|
||||
label: Text(context.localized.filter(2)),
|
||||
action: () => showSavedFilters(context, uniqueKey),
|
||||
icon: const Icon(IconsaxPlusLinear.refresh),
|
||||
);
|
||||
final itemViewAction = ItemActionButton(
|
||||
label: Text(context.localized.selectViewType),
|
||||
icon: Icon(libraryViewType.icon),
|
||||
action: () {
|
||||
showAdaptiveDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final currentType = ref.watch(libraryViewTypeProvider);
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(context.localized.selectViewType,
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 12),
|
||||
...LibraryViewTypes.values
|
||||
.map(
|
||||
(e) => FilledButton.tonal(
|
||||
style: FilledButtonTheme.of(context).style?.copyWith(
|
||||
padding: const WidgetStatePropertyAll(
|
||||
EdgeInsets.symmetric(horizontal: 12, vertical: 24)),
|
||||
backgroundColor: WidgetStateProperty.resolveWith(
|
||||
(states) {
|
||||
if (e != currentType) {
|
||||
return Colors.transparent;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(e.icon),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
e.label(context),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
ref.read(libraryViewTypeProvider.notifier).state = e;
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(e.icon),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
e.label(context),
|
||||
)
|
||||
.toList()
|
||||
.addInBetween(const SizedBox(height: 12)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
.addInBetween(const SizedBox(height: 12)),
|
||||
],
|
||||
);
|
||||
});
|
||||
return Card(
|
||||
elevation: 0,
|
||||
child: Tooltip(
|
||||
message: librarySearchResults.nestedCurrentItem?.type.label(context) ??
|
||||
context.localized.library(1),
|
||||
child: InkWell(
|
||||
onTapUp: (details) async {
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
|
||||
double left = details.globalPosition.dx;
|
||||
double top = details.globalPosition.dy;
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(left, top, 40, 100),
|
||||
items: <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
child: Text(
|
||||
librarySearchResults.nestedCurrentItem?.type.label(context) ??
|
||||
context.localized.library(0))),
|
||||
itemCountWidget.toPopupMenuItem(useIcons: true),
|
||||
refreshAction.toPopupMenuItem(useIcons: true),
|
||||
itemViewAction.toPopupMenuItem(useIcons: true),
|
||||
if (librarySearchResults.views.hasEnabled == true)
|
||||
showSavedFiltersDialogue.toPopupMenuItem(useIcons: true),
|
||||
if (itemActions.isNotEmpty) ItemActionDivider().toPopupMenuItem(),
|
||||
...itemActions.popupMenuItems(useIcons: true),
|
||||
],
|
||||
elevation: 8.0,
|
||||
);
|
||||
} else {
|
||||
await showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: [
|
||||
itemCountWidget.toListItem(context, useIcons: true),
|
||||
refreshAction.toListItem(context, useIcons: true),
|
||||
itemViewAction.toListItem(context, useIcons: true),
|
||||
if (librarySearchResults.views.hasEnabled == true)
|
||||
showSavedFiltersDialogue.toPopupMenuItem(useIcons: true),
|
||||
if (itemActions.isNotEmpty) ItemActionDivider().toListItem(context),
|
||||
...itemActions.listTileItems(context, useIcons: true),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Icon(
|
||||
isFavorite
|
||||
? librarySearchResults.nestedCurrentItem?.type.selectedicon
|
||||
: librarySearchResults.nestedCurrentItem?.type.icon ??
|
||||
IconsaxPlusLinear.document,
|
||||
color: isFavorite ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) ...[
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox.square(dimension: 46, child: SettingsUserIcon()),
|
||||
],
|
||||
const SizedBox(width: 12)
|
||||
],
|
||||
title: Hero(
|
||||
tag: "PrimarySearch",
|
||||
child: SuggestionSearchBar(
|
||||
autoFocus: isEmptySearchScreen,
|
||||
key: uniqueKey,
|
||||
title: librarySearchResults.searchBarTitle(context),
|
||||
debounceDuration: const Duration(seconds: 1),
|
||||
onItem: (value) async {
|
||||
await value.navigateTo(context);
|
||||
refreshKey.currentState?.show();
|
||||
},
|
||||
onSubmited: (value) async {
|
||||
if (librarySearchResults.searchQuery != value) {
|
||||
libraryProvider.setSearch(value);
|
||||
refreshKey.currentState?.show();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size(0, 50),
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, AdaptiveLayout.of(context).isDesktop ? -20 : -15),
|
||||
child: IgnorePointer(
|
||||
ignoring: librarySearchResults.loading,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: librarySearchResults.loading ? 0.5 : 1,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: LibraryFilterChips(
|
||||
key: uniqueKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
return Card(
|
||||
elevation: 0,
|
||||
child: Tooltip(
|
||||
message: librarySearchResults.nestedCurrentItem?.type.label(context) ??
|
||||
context.localized.library(1),
|
||||
child: InkWell(
|
||||
onTapUp: (details) async {
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) {
|
||||
double left = details.globalPosition.dx;
|
||||
double top = details.globalPosition.dy;
|
||||
await showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromLTRB(left, top, 40, 100),
|
||||
items: <PopupMenuEntry>[
|
||||
PopupMenuItem(
|
||||
child: Text(librarySearchResults.nestedCurrentItem?.type.label(context) ??
|
||||
context.localized.library(0))),
|
||||
itemCountWidget.toPopupMenuItem(useIcons: true),
|
||||
refreshAction.toPopupMenuItem(useIcons: true),
|
||||
itemViewAction.toPopupMenuItem(useIcons: true),
|
||||
if (librarySearchResults.views.hasEnabled == true)
|
||||
showSavedFiltersDialogue.toPopupMenuItem(useIcons: true),
|
||||
if (itemActions.isNotEmpty) ItemActionDivider().toPopupMenuItem(),
|
||||
...itemActions.popupMenuItems(useIcons: true),
|
||||
],
|
||||
),
|
||||
elevation: 8.0,
|
||||
);
|
||||
} else {
|
||||
await showBottomSheetPill(
|
||||
context: context,
|
||||
content: (context, scrollController) => ListView(
|
||||
shrinkWrap: true,
|
||||
controller: scrollController,
|
||||
children: [
|
||||
itemCountWidget.toListItem(context, useIcons: true),
|
||||
refreshAction.toListItem(context, useIcons: true),
|
||||
itemViewAction.toListItem(context, useIcons: true),
|
||||
if (librarySearchResults.views.hasEnabled == true)
|
||||
showSavedFiltersDialogue.toPopupMenuItem(useIcons: true),
|
||||
if (itemActions.isNotEmpty) ItemActionDivider().toListItem(context),
|
||||
...itemActions.listTileItems(context, useIcons: true),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Icon(
|
||||
isFavorite
|
||||
? librarySearchResults.nestedCurrentItem?.type.selectedicon
|
||||
: librarySearchResults.nestedCurrentItem?.type.icon ??
|
||||
IconsaxPlusLinear.document,
|
||||
color: isFavorite ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.of(context).isDesktop)
|
||||
const SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
PosterSizeWidget(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) ...[
|
||||
const SizedBox(width: 6),
|
||||
const SizedBox.square(dimension: 46, child: SettingsUserIcon()),
|
||||
],
|
||||
const SizedBox(width: 12)
|
||||
],
|
||||
title: Row(
|
||||
spacing: 2,
|
||||
children: [
|
||||
const SizedBox(width: 2),
|
||||
Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 47,
|
||||
child: Card(
|
||||
child: context.router.backButton(),
|
||||
),
|
||||
if (postersList.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: MediaQuery.of(context).padding.left,
|
||||
right: MediaQuery.of(context).padding.right),
|
||||
sliver: LibraryViews(
|
||||
key: uniqueKey,
|
||||
items: postersList,
|
||||
groupByType: librarySearchResults.groupBy,
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Text(context.localized.noItemsToShow),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Hero(
|
||||
tag: "PrimarySearch",
|
||||
child: SuggestionSearchBar(
|
||||
autoFocus: isEmptySearchScreen,
|
||||
key: uniqueKey,
|
||||
title: librarySearchResults.searchBarTitle(context),
|
||||
debounceDuration: const Duration(seconds: 1),
|
||||
onItem: (value) async {
|
||||
await value.navigateTo(context);
|
||||
refreshKey.currentState?.show();
|
||||
},
|
||||
onSubmited: (value) async {
|
||||
if (librarySearchResults.searchQuery != value) {
|
||||
libraryProvider.setSearch(value);
|
||||
refreshKey.currentState?.show();
|
||||
}
|
||||
},
|
||||
),
|
||||
SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size(0, 50),
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, AdaptiveLayout.of(context).isDesktop ? -20 : -15),
|
||||
child: IgnorePointer(
|
||||
ignoring: librarySearchResults.loading,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: librarySearchResults.loading ? 0.5 : 1,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: LibraryFilterChips(
|
||||
key: uniqueKey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (AdaptiveLayout.of(context).isDesktop)
|
||||
const SliverToBoxAdapter(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
PosterSizeWidget(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (postersList.isNotEmpty)
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.only(
|
||||
left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right),
|
||||
sliver: LibraryViews(
|
||||
key: uniqueKey,
|
||||
items: postersList,
|
||||
groupByType: librarySearchResults.filters.groupBy,
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Text(context.localized.noItemsToShow),
|
||||
),
|
||||
),
|
||||
SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20))
|
||||
],
|
||||
),
|
||||
),
|
||||
if (librarySearchResults.fetchingItems) ...[
|
||||
Container(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator.adaptive(),
|
||||
Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium),
|
||||
IconButton(
|
||||
onPressed: () => libraryProvider.cancelFetch(),
|
||||
icon: const Icon(IconsaxPlusLinear.close_square),
|
||||
)
|
||||
].addInBetween(const SizedBox(width: 16)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -663,7 +652,7 @@ class _LibrarySearchBottomBar extends ConsumerWidget {
|
|||
context,
|
||||
libraryProvider: libraryProvider,
|
||||
uniqueKey: uniqueKey,
|
||||
options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder),
|
||||
options: (librarySearchResults.filters.sortingOption, librarySearchResults.filters.sortOrder),
|
||||
);
|
||||
if (newOptions != null) {
|
||||
if (newOptions.$1 != null) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
|
|
@ -11,7 +12,9 @@ import 'package:fladder/providers/library_search_provider.dart';
|
|||
import 'package:fladder/screens/shared/chips/category_chip.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
import 'package:fladder/util/position_provider.dart';
|
||||
import 'package:fladder/util/refresh_state.dart';
|
||||
import 'package:fladder/widgets/shared/button_group.dart';
|
||||
|
||||
class LibraryFilterChips extends ConsumerStatefulWidget {
|
||||
const LibraryFilterChips({super.key});
|
||||
|
|
@ -25,133 +28,137 @@ class _LibraryFilterChipsState extends ConsumerState<LibraryFilterChips> {
|
|||
Widget build(BuildContext context) {
|
||||
final uniqueKey = widget.key ?? UniqueKey();
|
||||
final libraryProvider = ref.watch(librarySearchProvider(uniqueKey).notifier);
|
||||
final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
|
||||
final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.favourites));
|
||||
final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.recursive));
|
||||
final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.hideEmptyShows));
|
||||
final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.groupBy));
|
||||
final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.favourites));
|
||||
final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.recursive));
|
||||
final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.hideEmptyShows));
|
||||
final librarySearchResults = ref.watch(librarySearchProvider(uniqueKey));
|
||||
|
||||
final chips = [
|
||||
if (librarySearchResults.folderOverwrite.isEmpty)
|
||||
CategoryChip(
|
||||
label: Text(context.localized.library(2)),
|
||||
items: librarySearchResults.views.sortByKey((value) => value.name),
|
||||
labelBuilder: (item) => Text(item.name),
|
||||
onSave: (value) => libraryProvider.setViews(value),
|
||||
onCancel: () => libraryProvider.setViews(librarySearchResults.views),
|
||||
onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)),
|
||||
),
|
||||
CategoryChip<FladderItemType>(
|
||||
label: Text(context.localized.type(librarySearchResults.filters.types.length)),
|
||||
items: librarySearchResults.filters.types.sortByKey((value) => value.label(context)),
|
||||
activeIcon: IconsaxPlusBold.filter_tick,
|
||||
labelBuilder: (item) => Row(
|
||||
children: [
|
||||
Icon(item.icon),
|
||||
const SizedBox(width: 12),
|
||||
Text(item.label(context)),
|
||||
],
|
||||
),
|
||||
onSave: (value) => libraryProvider.setTypes(value),
|
||||
onClear: () => libraryProvider.setTypes(librarySearchResults.filters.types.setAll(false)),
|
||||
),
|
||||
ExpressiveButton(
|
||||
isSelected: favourites,
|
||||
icon: favourites ? const Icon(IconsaxPlusBold.heart) : null,
|
||||
label: Text(context.localized.favorites),
|
||||
onPressed: () {
|
||||
libraryProvider.toggleFavourite();
|
||||
context.refreshData();
|
||||
},
|
||||
),
|
||||
ExpressiveButton(
|
||||
isSelected: recursive,
|
||||
icon: recursive ? const Icon(IconsaxPlusBold.tick_circle) : null,
|
||||
label: Text(context.localized.recursive),
|
||||
onPressed: () {
|
||||
libraryProvider.toggleRecursive();
|
||||
context.refreshData();
|
||||
},
|
||||
),
|
||||
if (librarySearchResults.filters.genres.isNotEmpty)
|
||||
CategoryChip<String>(
|
||||
label: Text(context.localized.genre(librarySearchResults.filters.genres.length)),
|
||||
activeIcon: IconsaxPlusBold.hierarchy_2,
|
||||
items: librarySearchResults.filters.genres,
|
||||
labelBuilder: (item) => Text(item),
|
||||
onSave: (value) => libraryProvider.setGenres(value),
|
||||
onCancel: () => libraryProvider.setGenres(librarySearchResults.filters.genres),
|
||||
onClear: () => libraryProvider.setGenres(librarySearchResults.filters.genres.setAll(false)),
|
||||
),
|
||||
if (librarySearchResults.filters.studios.isNotEmpty)
|
||||
CategoryChip<Studio>(
|
||||
label: Text(context.localized.studio(librarySearchResults.filters.studios.length)),
|
||||
activeIcon: IconsaxPlusBold.airdrop,
|
||||
items: librarySearchResults.filters.studios,
|
||||
labelBuilder: (item) => Text(item.name),
|
||||
onSave: (value) => libraryProvider.setStudios(value),
|
||||
onCancel: () => libraryProvider.setStudios(librarySearchResults.filters.studios),
|
||||
onClear: () => libraryProvider.setStudios(librarySearchResults.filters.studios.setAll(false)),
|
||||
),
|
||||
if (librarySearchResults.filters.tags.isNotEmpty)
|
||||
CategoryChip<String>(
|
||||
label: Text(context.localized.label(librarySearchResults.filters.tags.length)),
|
||||
activeIcon: Icons.label_rounded,
|
||||
items: librarySearchResults.filters.tags,
|
||||
labelBuilder: (item) => Text(item),
|
||||
onSave: (value) => libraryProvider.setTags(value),
|
||||
onCancel: () => libraryProvider.setTags(librarySearchResults.filters.tags),
|
||||
onClear: () => libraryProvider.setTags(librarySearchResults.filters.tags.setAll(false)),
|
||||
),
|
||||
ExpressiveButton(
|
||||
isSelected: groupBy != GroupBy.none,
|
||||
icon: groupBy != GroupBy.none ? const Icon(IconsaxPlusBold.bag_tick) : null,
|
||||
label: Text(context.localized.group),
|
||||
onPressed: () {
|
||||
_openGroupDialogue(context, ref, libraryProvider, uniqueKey);
|
||||
},
|
||||
),
|
||||
CategoryChip<ItemFilter>(
|
||||
label: Text(context.localized.filter(librarySearchResults.filters.itemFilters.length)),
|
||||
items: librarySearchResults.filters.itemFilters,
|
||||
labelBuilder: (item) => Text(item.label(context)),
|
||||
onSave: (value) => libraryProvider.setFilters(value),
|
||||
onClear: () => libraryProvider.setFilters(librarySearchResults.filters.itemFilters.setAll(false)),
|
||||
),
|
||||
if (librarySearchResults.filters.types[FladderItemType.series] == true)
|
||||
ExpressiveButton(
|
||||
isSelected: !hideEmpty,
|
||||
icon: !hideEmpty ? const Icon(IconsaxPlusBold.ghost) : null,
|
||||
label: Text(!hideEmpty ? context.localized.hideEmpty : context.localized.showEmpty),
|
||||
onPressed: libraryProvider.toggleEmptyShows,
|
||||
),
|
||||
if (librarySearchResults.filters.officialRatings.isNotEmpty)
|
||||
CategoryChip<String>(
|
||||
label: Text(context.localized.rating(librarySearchResults.filters.officialRatings.length)),
|
||||
activeIcon: Icons.star_rate_rounded,
|
||||
items: librarySearchResults.filters.officialRatings,
|
||||
labelBuilder: (item) => Text(item),
|
||||
onSave: (value) => libraryProvider.setRatings(value),
|
||||
onCancel: () => libraryProvider.setRatings(librarySearchResults.filters.officialRatings),
|
||||
onClear: () => libraryProvider.setRatings(librarySearchResults.filters.officialRatings.setAll(false)),
|
||||
),
|
||||
if (librarySearchResults.filters.years.isNotEmpty)
|
||||
CategoryChip<int>(
|
||||
label: Text(context.localized.year(librarySearchResults.filters.years.length)),
|
||||
items: librarySearchResults.filters.years,
|
||||
labelBuilder: (item) => Text(item.toString()),
|
||||
onSave: (value) => libraryProvider.setYears(value),
|
||||
onCancel: () => libraryProvider.setYears(librarySearchResults.filters.years),
|
||||
onClear: () => libraryProvider.setYears(librarySearchResults.filters.years.setAll(false)),
|
||||
),
|
||||
];
|
||||
|
||||
return Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (librarySearchResults.folderOverwrite.isEmpty)
|
||||
CategoryChip(
|
||||
label: Text(context.localized.library(2)),
|
||||
items: librarySearchResults.views,
|
||||
labelBuilder: (item) => Text(item.name),
|
||||
onSave: (value) => libraryProvider.setViews(value),
|
||||
onCancel: () => libraryProvider.setViews(librarySearchResults.views),
|
||||
onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)),
|
||||
),
|
||||
CategoryChip<FladderItemType>(
|
||||
label: Text(context.localized.type(librarySearchResults.types.length)),
|
||||
items: librarySearchResults.types,
|
||||
labelBuilder: (item) => Row(
|
||||
children: [
|
||||
Icon(item.icon),
|
||||
const SizedBox(width: 12),
|
||||
Text(item.label(context)),
|
||||
],
|
||||
),
|
||||
onSave: (value) => libraryProvider.setTypes(value),
|
||||
onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.localized.favorites),
|
||||
avatar: Icon(
|
||||
favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
selected: favourites,
|
||||
showCheckmark: false,
|
||||
onSelected: (_) {
|
||||
libraryProvider.toggleFavourite();
|
||||
context.refreshData();
|
||||
},
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.localized.recursive),
|
||||
selected: recursive,
|
||||
onSelected: (_) {
|
||||
libraryProvider.toggleRecursive();
|
||||
context.refreshData();
|
||||
},
|
||||
),
|
||||
if (librarySearchResults.genres.isNotEmpty)
|
||||
CategoryChip<String>(
|
||||
label: Text(context.localized.genre(librarySearchResults.genres.length)),
|
||||
activeIcon: IconsaxPlusBold.hierarchy_2,
|
||||
items: librarySearchResults.genres,
|
||||
labelBuilder: (item) => Text(item),
|
||||
onSave: (value) => libraryProvider.setGenres(value),
|
||||
onCancel: () => libraryProvider.setGenres(librarySearchResults.genres),
|
||||
onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)),
|
||||
),
|
||||
if (librarySearchResults.studios.isNotEmpty)
|
||||
CategoryChip<Studio>(
|
||||
label: Text(context.localized.studio(librarySearchResults.studios.length)),
|
||||
activeIcon: IconsaxPlusBold.airdrop,
|
||||
items: librarySearchResults.studios,
|
||||
labelBuilder: (item) => Text(item.name),
|
||||
onSave: (value) => libraryProvider.setStudios(value),
|
||||
onCancel: () => libraryProvider.setStudios(librarySearchResults.studios),
|
||||
onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)),
|
||||
),
|
||||
if (librarySearchResults.tags.isNotEmpty)
|
||||
CategoryChip<String>(
|
||||
label: Text(context.localized.label(librarySearchResults.tags.length)),
|
||||
activeIcon: Icons.label_rounded,
|
||||
items: librarySearchResults.tags,
|
||||
labelBuilder: (item) => Text(item),
|
||||
onSave: (value) => libraryProvider.setTags(value),
|
||||
onCancel: () => libraryProvider.setTags(librarySearchResults.tags),
|
||||
onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.localized.group),
|
||||
selected: groupBy != GroupBy.none,
|
||||
onSelected: (_) {
|
||||
_openGroupDialogue(context, ref, libraryProvider, uniqueKey);
|
||||
},
|
||||
),
|
||||
CategoryChip<ItemFilter>(
|
||||
label: Text(context.localized.filter(librarySearchResults.filters.length)),
|
||||
items: librarySearchResults.filters,
|
||||
labelBuilder: (item) => Text(item.label(context)),
|
||||
onSave: (value) => libraryProvider.setFilters(value),
|
||||
onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)),
|
||||
),
|
||||
if (librarySearchResults.types[FladderItemType.series] == true)
|
||||
FilterChip(
|
||||
avatar: Icon(
|
||||
hideEmpty ? Icons.visibility_off_rounded : Icons.visibility_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
selected: hideEmpty,
|
||||
showCheckmark: false,
|
||||
label: Text(context.localized.hideEmpty),
|
||||
onSelected: libraryProvider.setHideEmpty,
|
||||
),
|
||||
if (librarySearchResults.officialRatings.isNotEmpty)
|
||||
CategoryChip<String>(
|
||||
label: Text(context.localized.rating(librarySearchResults.officialRatings.length)),
|
||||
activeIcon: Icons.star_rate_rounded,
|
||||
items: librarySearchResults.officialRatings,
|
||||
labelBuilder: (item) => Text(item),
|
||||
onSave: (value) => libraryProvider.setRatings(value),
|
||||
onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings),
|
||||
onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)),
|
||||
),
|
||||
if (librarySearchResults.years.isNotEmpty)
|
||||
CategoryChip<int>(
|
||||
label: Text(context.localized.year(librarySearchResults.years.length)),
|
||||
items: librarySearchResults.years,
|
||||
labelBuilder: (item) => Text(item.toString()),
|
||||
onSave: (value) => libraryProvider.setYears(value),
|
||||
onCancel: () => libraryProvider.setYears(librarySearchResults.years),
|
||||
onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)),
|
||||
),
|
||||
],
|
||||
spacing: 4,
|
||||
children: chips.mapIndexed(
|
||||
(index, element) {
|
||||
final position = index == 0
|
||||
? PositionContext.first
|
||||
: (index == chips.length - 1 ? PositionContext.last : PositionContext.middle);
|
||||
return PositionProvider(position: position, child: element);
|
||||
},
|
||||
).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +171,7 @@ class _LibraryFilterChipsState extends ConsumerState<LibraryFilterChips> {
|
|||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy));
|
||||
final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.filters.groupBy));
|
||||
return AlertDialog(
|
||||
content: SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.65,
|
||||
|
|
|
|||
|
|
@ -3,41 +3,40 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/library_search/library_search_model.dart';
|
||||
import 'package:fladder/providers/library_search_provider.dart';
|
||||
import 'package:fladder/screens/shared/default_alert_dialog.dart';
|
||||
import 'package:fladder/screens/shared/outlined_text_field.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
Future<void> showSavedFilters(
|
||||
BuildContext context,
|
||||
LibrarySearchModel model,
|
||||
LibrarySearchNotifier provider,
|
||||
Key providerKey,
|
||||
) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (context) => LibrarySavedFiltersDialogue(
|
||||
searchModel: model,
|
||||
provider: provider,
|
||||
providerKey: providerKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class LibrarySavedFiltersDialogue extends ConsumerWidget {
|
||||
final LibrarySearchModel searchModel;
|
||||
final LibrarySearchNotifier provider;
|
||||
final Key providerKey;
|
||||
|
||||
const LibrarySavedFiltersDialogue({
|
||||
required this.searchModel,
|
||||
required this.provider,
|
||||
super.key,
|
||||
required this.providerKey,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final controller = TextEditingController();
|
||||
final provider = ref.watch(librarySearchProvider(providerKey).notifier);
|
||||
final currentFilters = ref.watch(librarySearchProvider(providerKey).select((value) => value.filters));
|
||||
final filters = ref.watch(provider.filterProvider);
|
||||
final filterProvider = ref.watch(provider.filterProvider.notifier);
|
||||
final anyFilterSelected = filters.any((element) => element.filter == currentFilters);
|
||||
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
|
|
@ -57,68 +56,75 @@ class LibrarySavedFiltersDialogue extends ConsumerWidget {
|
|||
children: [
|
||||
...filters.map(
|
||||
(filter) {
|
||||
final isCurrentFilter = filter.filter == currentFilters;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Card(
|
||||
child: InkWell(
|
||||
onTap: () => provider.loadModel(filter),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(filter.name)),
|
||||
IconButton.filledTonal(
|
||||
tooltip: context.localized.defaultFilterForLibrary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
filter.isFavourite
|
||||
? Colors.yellowAccent.shade700.withValues(alpha: 0.5)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
onPressed: () =>
|
||||
filterProvider.saveFilter(filter.copyWith(isFavourite: !filter.isFavourite)),
|
||||
icon: Icon(
|
||||
color: filter.isFavourite ? Colors.yellowAccent : null,
|
||||
filter.isFavourite ? IconsaxPlusBold.star_1 : IconsaxPlusLinear.star,
|
||||
),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: context.localized.updateFilterForLibrary,
|
||||
onPressed: () => provider.updateFilter(filter),
|
||||
icon: const Icon(IconsaxPlusBold.refresh),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: context.localized.delete,
|
||||
onPressed: () {
|
||||
showDefaultAlertDialog(
|
||||
context,
|
||||
context.localized.removeFilterForLibrary(filter.name),
|
||||
context.localized.deleteFilterConfirmation,
|
||||
(context) {
|
||||
filterProvider.removeFilter(filter);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
context.localized.delete,
|
||||
(context) {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
context.localized.cancel,
|
||||
);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
|
||||
iconColor:
|
||||
WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
foregroundColor:
|
||||
WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
),
|
||||
icon: const Icon(IconsaxPlusLinear.trash),
|
||||
),
|
||||
].addInBetween(const SizedBox(width: 8)),
|
||||
color: isCurrentFilter
|
||||
? Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.75)
|
||||
: null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Row(spacing: 8, children: [
|
||||
Expanded(
|
||||
child: OutlinedTextField(
|
||||
fillColor: Colors.transparent,
|
||||
controller: TextEditingController(text: filter.name),
|
||||
onSubmitted: (value) => provider.updateFilter(filter.copyWith(name: value)),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
onPressed: isCurrentFilter ? null : () => provider.loadModel(filter.filter),
|
||||
icon: const Icon(IconsaxPlusBold.filter_add),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: context.localized.defaultFilterForLibrary,
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
filter.isFavourite ? Colors.yellowAccent.shade700.withValues(alpha: 0.5) : null,
|
||||
),
|
||||
),
|
||||
onPressed: () =>
|
||||
filterProvider.saveFilter(filter.copyWith(isFavourite: !filter.isFavourite)),
|
||||
icon: Icon(
|
||||
color: filter.isFavourite ? Colors.yellowAccent : null,
|
||||
filter.isFavourite ? IconsaxPlusBold.star_1 : IconsaxPlusLinear.star_1,
|
||||
),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: context.localized.updateFilterForLibrary,
|
||||
onPressed:
|
||||
isCurrentFilter || anyFilterSelected ? null : () => provider.updateFilter(filter),
|
||||
icon: const Icon(IconsaxPlusBold.refresh),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: context.localized.delete,
|
||||
onPressed: () {
|
||||
showDefaultAlertDialog(
|
||||
context,
|
||||
context.localized.removeFilterForLibrary(filter.name),
|
||||
context.localized.deleteFilterConfirmation,
|
||||
(context) {
|
||||
filterProvider.removeFilter(filter);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
context.localized.delete,
|
||||
(context) {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
context.localized.cancel,
|
||||
);
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor:
|
||||
WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer),
|
||||
iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
foregroundColor:
|
||||
WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer),
|
||||
),
|
||||
icon: const Icon(IconsaxPlusLinear.trash),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -129,31 +135,42 @@ class LibrarySavedFiltersDialogue extends ConsumerWidget {
|
|||
),
|
||||
const Divider(),
|
||||
],
|
||||
if (filters.length < 10)
|
||||
if (filters.length < 10 && !anyFilterSelected)
|
||||
StatefulBuilder(builder: (context, setState) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: OutlinedTextField(
|
||||
controller: controller,
|
||||
label: context.localized.name,
|
||||
onChanged: (value) => setState(() {}),
|
||||
onSubmitted: (value) => provider.saveFiltersNew(value),
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedTextField(
|
||||
controller: controller,
|
||||
label: context.localized.name,
|
||||
onChanged: (value) => setState(() {}),
|
||||
onSubmitted: (value) => provider.saveFiltersNew(value),
|
||||
),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: controller.text.isEmpty
|
||||
? null
|
||||
: () {
|
||||
provider.saveFiltersNew(controller.text);
|
||||
},
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [Text(context.localized.save), const Icon(IconsaxPlusLinear.save_2)],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
FilledButton.tonal(
|
||||
onPressed: controller.text.isEmpty
|
||||
? null
|
||||
: () {
|
||||
provider.saveFiltersNew(controller.text);
|
||||
},
|
||||
child: const Icon(IconsaxPlusLinear.save_2),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
})
|
||||
else
|
||||
else if (filters.length >= 10)
|
||||
Text(context.localized.libraryFiltersLimitReached),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:sliver_tools/sliver_tools.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_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';
|
||||
|
|
@ -76,7 +77,7 @@ class LibraryViews extends ConsumerWidget {
|
|||
ref.watch(clientSettingsProvider.select((value) => value.posterSize)));
|
||||
final decimal = posterSize - posterSize.toInt();
|
||||
|
||||
final sortingOptions = ref.watch(librarySearchProvider(key!).select((value) => value.sortingOption));
|
||||
final sortingOptions = ref.watch(librarySearchProvider(key!).select((value) => value.filters.sortingOption));
|
||||
|
||||
List<ItemAction> otherActions(ItemBaseModel item) {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
|
|||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: FladderTheme.largeShape.borderRadius,
|
||||
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||
),
|
||||
shadowColor: Colors.transparent,
|
||||
child: TypeAheadField<ItemBaseModel>(
|
||||
|
|
@ -83,7 +83,7 @@ class _SearchBarState extends ConsumerState<SuggestionSearchBar> {
|
|||
decorationBuilder: (context, child) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: FladderTheme.largeShape.borderRadius,
|
||||
borderRadius: FladderTheme.smallShape.borderRadius,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
|
|||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/map_bool_helper.dart';
|
||||
import 'package:fladder/widgets/shared/button_group.dart';
|
||||
import 'package:fladder/widgets/shared/modal_bottom_sheet.dart';
|
||||
import 'package:fladder/widgets/shared/modal_side_sheet.dart';
|
||||
|
||||
|
|
@ -20,7 +21,6 @@ class CategoryChip<T> extends StatelessWidget {
|
|||
final VoidCallback? onCancel;
|
||||
final VoidCallback? onClear;
|
||||
final VoidCallback? onDismiss;
|
||||
|
||||
const CategoryChip({
|
||||
required this.label,
|
||||
this.dialogueTitle,
|
||||
|
|
@ -37,37 +37,21 @@ class CategoryChip<T> extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var selection = items.included.isNotEmpty;
|
||||
return FilterChip(
|
||||
selected: selection,
|
||||
showCheckmark: activeIcon == null,
|
||||
return ExpressiveButton(
|
||||
isSelected: selection,
|
||||
icon: selection ? Icon(activeIcon ?? IconsaxPlusBold.archive_tick) : null,
|
||||
label: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 6,
|
||||
children: [
|
||||
if (activeIcon != null)
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: selection
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Icon(
|
||||
activeIcon!,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
label,
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.arrow_drop_down_rounded,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const Icon(
|
||||
IconsaxPlusLinear.arrow_down,
|
||||
size: 16,
|
||||
)
|
||||
],
|
||||
),
|
||||
onSelected: items.isNotEmpty
|
||||
? (_) async {
|
||||
onPressed: items.isNotEmpty
|
||||
? () async {
|
||||
final newEntry = await openActionSheet(context);
|
||||
if (newEntry != null) {
|
||||
onSave?.call(newEntry);
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
|
|||
onPressed: () => context.router.popBack(),
|
||||
icon: Padding(
|
||||
padding: EdgeInsets.all(AdaptiveLayout.of(context).inputDevice == InputDevice.pointer ? 0 : 4),
|
||||
child: const Icon(IconsaxPlusLinear.arrow_left_2),
|
||||
child: const BackButtonIcon(),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
await widget.poster.navigateTo(context);
|
||||
}
|
||||
|
||||
final posterRadius = FladderTheme.smallShape.borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final poster = widget.poster;
|
||||
|
|
@ -101,7 +103,7 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
width: 1.0,
|
||||
color: Colors.white.withValues(alpha: 0.10),
|
||||
),
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
borderRadius: posterRadius,
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
|
|
@ -135,7 +137,7 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
borderRadius: posterRadius,
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Stack(
|
||||
|
|
@ -201,6 +203,102 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (widget.poster.unWatched)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: StatusCard(
|
||||
color: Colors.amber,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.amber,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.inlineTitle)
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
widget.poster.title.maxLength(limitTo: 25),
|
||||
style:
|
||||
Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel)
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: StatusCard(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
useFittedBox: widget.poster.unPlayedItemCount != 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: widget.poster.unPlayedItemCount != 0
|
||||
? Container(
|
||||
constraints: const BoxConstraints(minWidth: 16),
|
||||
child: Text(
|
||||
widget.poster.userData.unPlayedItemCount.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.visible,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_rounded,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.poster.overview.runTime != null &&
|
||||
((widget.poster is PhotoModel) &&
|
||||
(widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
elevation: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.poster.overview.runTime.humanizeSmall ?? "",
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
//Desktop overlay
|
||||
if (AdaptiveLayout.of(context).inputDevice != InputDevice.touch &&
|
||||
widget.poster.type != FladderItemType.person)
|
||||
|
|
@ -215,7 +313,7 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.55),
|
||||
border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary),
|
||||
borderRadius: FladderTheme.defaultShape.borderRadius,
|
||||
borderRadius: posterRadius,
|
||||
)),
|
||||
//Poster Button
|
||||
Focus(
|
||||
|
|
@ -317,102 +415,6 @@ class _PosterImageState extends ConsumerState<PosterImage> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (widget.poster.unWatched)
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: StatusCard(
|
||||
color: Colors.amber,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.amber,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.inlineTitle)
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
widget.poster.title.maxLength(limitTo: 25),
|
||||
style:
|
||||
Theme.of(context).textTheme.labelLarge?.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.poster.unPlayedItemCount != null && widget.poster is SeriesModel)
|
||||
IgnorePointer(
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: StatusCard(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
useFittedBox: widget.poster.unPlayedItemCount != 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: widget.poster.unPlayedItemCount != 0
|
||||
? Container(
|
||||
constraints: const BoxConstraints(minWidth: 16),
|
||||
child: Text(
|
||||
widget.poster.userData.unPlayedItemCount.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.visible,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.check_rounded,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.poster.overview.runTime != null &&
|
||||
((widget.poster is PhotoModel) &&
|
||||
(widget.poster as PhotoModel).internalType == FladderItemType.video)) ...{
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: Card(
|
||||
elevation: 5,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
widget.poster.overview.runTime.humanizeSmall ?? "",
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Icon(
|
||||
Icons.play_arrow_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class NestedScaffold extends ConsumerWidget {
|
|||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: backgroundOpacity),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: backgroundOpacity - 0.15),
|
||||
Theme.of(context).colorScheme.surface.withValues(alpha: backgroundOpacity / 1.5),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/screens/shared/animated_fade_size.dart';
|
||||
import 'package:fladder/theme.dart';
|
||||
|
||||
class OutlinedTextField extends ConsumerStatefulWidget {
|
||||
final String? label;
|
||||
final FocusNode? focusNode;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue