diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupPerformersPanel.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupPerformersPanel.tsx index 057b99f2a..3ec78084a 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupPerformersPanel.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupPerformersPanel.tsx @@ -1,7 +1,7 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; import { useGroupFilterHook } from "src/core/groups"; -import { PerformerList } from "src/components/Performers/PerformerList"; +import { FilteredPerformerList } from "src/components/Performers/PerformerList"; import { View } from "src/components/List/views"; interface IGroupPerformersPanel { @@ -18,7 +18,7 @@ export const GroupPerformersPanel: React.FC = ({ const filterHook = useGroupFilterHook(group, showChildGroupContent); return ( - = ({ ); }; -interface IListOperations { +export interface IListOperations { text: string; onClick: () => void; isDisplayed?: () => boolean; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/performerAppearsWithPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/performerAppearsWithPanel.tsx index 913d16625..b6973bf83 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/performerAppearsWithPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/performerAppearsWithPanel.tsx @@ -1,6 +1,6 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; -import { PerformerList } from "src/components/Performers/PerformerList"; +import { FilteredPerformerList } from "src/components/Performers/PerformerList"; import { usePerformerFilterHook } from "src/core/performers"; import { View } from "src/components/List/views"; import { PatchComponent } from "src/patch"; @@ -24,7 +24,7 @@ export const PerformerAppearsWithPanel: React.FC = const filterHook = usePerformerFilterHook(performer); return ( - { const intl = useIntl(); @@ -165,193 +184,296 @@ interface IPerformerList { extraOperations?: IItemListOperation[]; } -export const PerformerList: React.FC = PatchComponent( +const PerformerList: React.FC<{ + performers: GQL.PerformerDataFragment[]; + filter: ListFilterModel; + selectedIds: Set; + onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void; + extraCriteria?: IPerformerCardExtraCriteria; +}> = PatchComponent( "PerformerList", - ({ filterHook, view, alterQuery, extraCriteria, extraOperations = [] }) => { + ({ performers, filter, selectedIds, onSelectChange, extraCriteria }) => { + if (performers.length === 0) { + return null; + } + + if (filter.displayMode === DisplayMode.Grid) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.List) { + return ( + + ); + } + if (filter.displayMode === DisplayMode.Tagger) { + return ; + } + + return null; + } +); + +const PerformerFilterSidebarSections = PatchContainerComponent( + "FilteredPerformerList.SidebarSections" +); + +const SidebarContent: React.FC<{ + filter: ListFilterModel; + setFilter: (filter: ListFilterModel) => void; + filterHook?: (filter: ListFilterModel) => ListFilterModel; + view?: View; + sidebarOpen: boolean; + onClose?: () => void; + showEditFilter: (editingCriterion?: string) => void; + count?: number; + focus?: ReturnType; +}> = ({ + filter, + setFilter, + filterHook, + view, + showEditFilter, + sidebarOpen, + onClose, + count, + focus, +}) => { + const showResultsId = + count !== undefined ? "actions.show_count_results" : "actions.show_results"; + + // const hideStudios = view === View.StudioPerformers; + + const AgeCriterionOption = PerformerListFilterOptions.criterionOptions.find( + (c) => c.type === "age" + ); + + return ( + <> + + + + {/* {!hideStudios && ( + + )} */} + + + } + option={AgeCriterionOption!} + filter={filter} + setFilter={setFilter} + sectionID="age" + /> + + +
+ +
+ + ); +}; + +function useViewRandom(filter: ListFilterModel, count: number) { + const history = useHistory(); + + const viewRandom = useCallback(async () => { + // query for a random performer + if (count === 0) { + return; + } + + const index = Math.floor(Math.random() * count); + const filterCopy = cloneDeep(filter); + filterCopy.itemsPerPage = 1; + filterCopy.currentPage = index + 1; + const singleResult = await queryFindPerformers(filterCopy); + if (singleResult.data.findPerformers.performers.length === 1) { + const { id } = singleResult.data.findPerformers.performers[0]; + // navigate to the image player page + history.push(`/performers/${id}`); + } + }, [history, filter, count]); + + return viewRandom; +} + +function useAddKeybinds(filter: ListFilterModel, count: number) { + const viewRandom = useViewRandom(filter, count); + + useEffect(() => { + Mousetrap.bind("p r", () => { + viewRandom(); + }); + + return () => { + Mousetrap.unbind("p r"); + }; + }, [viewRandom]); +} + +export const FilteredPerformerList = PatchComponent( + "FilteredPerformerList", + (props: IPerformerList) => { const intl = useIntl(); const history = useHistory(); - const [mergePerformers, setMergePerformers] = useState< - GQL.SelectPerformerDataFragment[] | undefined - >(undefined); - const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [isExportAll, setIsExportAll] = useState(false); + const location = useLocation(); - const filterMode = GQL.FilterMode.Performers; + const searchFocus = useFocus(); - const otherOperations = [ - ...extraOperations, - { - text: intl.formatMessage({ id: "actions.open_random" }), - onClick: openRandom, - }, - { - text: `${intl.formatMessage({ id: "actions.merge" })}…`, - onClick: merge, - isDisplayed: showWhenSelected, - }, - { - text: intl.formatMessage({ id: "actions.export" }), - onClick: onExport, - isDisplayed: showWhenSelected, - }, - { - text: intl.formatMessage({ id: "actions.export_all" }), - onClick: onExportAll, - }, - ]; + const { + filterHook, + view, + alterQuery, + extraCriteria, + extraOperations = [], + } = props; - function addKeybinds( - result: GQL.FindPerformersQueryResult, - filter: ListFilterModel - ) { - Mousetrap.bind("p r", () => { - openRandom(result, filter); + // States + const { + showSidebar, + setShowSidebar, + sectionOpen, + setSectionOpen, + loading: sidebarStateLoading, + } = useSidebarState(view); + + const { filterState, queryResult, modalState, listSelect, showEditFilter } = + useFilteredItemList({ + filterStateProps: { + filterMode: GQL.FilterMode.Performers, + view, + useURL: alterQuery, + }, + queryResultProps: { + useResult: useFindPerformers, + getCount: (r) => r.data?.findPerformers.count ?? 0, + getItems: (r) => r.data?.findPerformers.performers ?? [], + filterHook, + }, + }); + + const { filter, setFilter } = filterState; + + const { effectiveFilter, result, cachedResult, items, totalCount } = + queryResult; + + const { + selectedIds, + selectedItems, + onSelectChange, + onSelectAll, + onSelectNone, + onInvertSelection, + hasSelection, + } = listSelect; + + const { modal, showModal, closeModal } = modalState; + + // Utility hooks + const { setPage, removeCriterion, clearAllCriteria } = useFilterOperations({ + filter, + setFilter, + }); + + useAddKeybinds(filter, totalCount); + useFilteredSidebarKeybinds({ + showSidebar, + setShowSidebar, + }); + + useEffect(() => { + Mousetrap.bind("e", () => { + if (hasSelection) { + onEdit?.(); + } + }); + + Mousetrap.bind("d d", () => { + if (hasSelection) { + onDelete?.(); + } }); return () => { - Mousetrap.unbind("p r"); + Mousetrap.unbind("e"); + Mousetrap.unbind("d d"); }; - } + }); - async function openRandom( - result: GQL.FindPerformersQueryResult, - filter: ListFilterModel - ) { - if (result.data?.findPerformers) { - const { count } = result.data.findPerformers; - const index = Math.floor(Math.random() * count); - const filterCopy = cloneDeep(filter); - filterCopy.itemsPerPage = 1; - filterCopy.currentPage = index + 1; - const singleResult = await queryFindPerformers(filterCopy); - if (singleResult.data.findPerformers.performers.length === 1) { - const { id } = singleResult.data.findPerformers.performers[0]!; - history.push(`/performers/${id}`); - } + const onCloseEditDelete = useCloseEditDelete({ + closeModal, + onSelectNone, + result, + }); + + function onCreateNew() { + let queryParam = new URLSearchParams(location.search).get("q"); + let newPath = "/performers/new"; + if (queryParam) { + newPath += "?q=" + encodeURIComponent(queryParam); } + history.push(newPath); } - async function merge( - result: GQL.FindPerformersQueryResult, - filter: ListFilterModel, - selectedIds: Set - ) { - const selected = - result.data?.findPerformers.performers.filter((p) => - selectedIds.has(p.id) - ) ?? []; - setMergePerformers(selected); - } + const viewRandom = useViewRandom(filter, totalCount); - async function onExport() { - setIsExportAll(false); - setIsExportDialogOpen(true); - } - - async function onExportAll() { - setIsExportAll(true); - setIsExportDialogOpen(true); - } - - function renderContent( - result: GQL.FindPerformersQueryResult, - filter: ListFilterModel, - selectedIds: Set, - onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void - ) { - function renderMergeDialog() { - if (mergePerformers) { - return ( - { - setMergePerformers(undefined); - if (mergedId) { - history.push(`/performers/${mergedId}`); - } - }} - show - /> - ); - } - } - - function maybeRenderPerformerExportDialog() { - if (isExportDialogOpen) { - return ( - <> - setIsExportDialogOpen(false)} - /> - - ); - } - } - - function renderPerformers() { - if (!result.data?.findPerformers) return; - - if (filter.displayMode === DisplayMode.Grid) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.List) { - return ( - - ); - } - if (filter.displayMode === DisplayMode.Tagger) { - return ( - - ); - } - } - - return ( - <> - {renderMergeDialog()} - {maybeRenderPerformerExportDialog()} - {renderPerformers()} - + function onExport(all: boolean) { + showModal( + closeModal()} + /> ); } - function renderEditDialog( - selectedPerformers: GQL.SlimPerformerDataFragment[], - onClose: (applied: boolean) => void - ) { - return ( - + function onEdit() { + showModal( + ); } - function renderDeleteDialog( - selectedPerformers: GQL.SlimPerformerDataFragment[], - onClose: (confirmed: boolean) => void - ) { - return ( + function onDelete() { + showModal( = PatchComponent( ); } - return ( - - { + closeModal(); + if (mergedId) { + history.push(`/performers/${mergedId}`); + } + }} + show /> - + ); + } + + const convertedExtraOperations: IListOperations[] = extraOperations.map( + (o) => ({ + ...o, + isDisplayed: o.isDisplayed + ? () => o.isDisplayed!(result, filter, selectedIds) + : undefined, + onClick: () => { + o.onClick(result, filter, selectedIds); + }, + }) + ); + + const otherOperations: IListOperations[] = [ + ...convertedExtraOperations, + { + text: intl.formatMessage({ id: "actions.select_all" }), + onClick: () => onSelectAll(), + isDisplayed: () => totalCount > 0, + }, + { + text: intl.formatMessage({ id: "actions.select_none" }), + onClick: () => onSelectNone(), + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.invert_selection" }), + onClick: () => onInvertSelection(), + isDisplayed: () => totalCount > 0, + }, + { + text: intl.formatMessage({ id: "actions.open_random" }), + onClick: viewRandom, + }, + { + text: `${intl.formatMessage({ id: "actions.merge" })}…`, + onClick: onMerge, + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.export" }), + onClick: () => onExport(false), + isDisplayed: () => hasSelection, + }, + { + text: intl.formatMessage({ id: "actions.export_all" }), + onClick: () => onExport(true), + }, + ]; + + // render + if (sidebarStateLoading) return null; + + const operations = ( + + ); + + return ( +
+ {modal} + + + + setShowSidebar(false)}> + setShowSidebar(false)} + count={cachedResult.loading ? undefined : totalCount} + focus={searchFocus} + /> + + setShowSidebar(!showSidebar)} + > + + + showEditFilter(c.criterionOption.type)} + onRemoveCriterion={removeCriterion} + onRemoveAll={clearAllCriteria} + /> + +
+ setFilter(filter.changePage(page))} + /> + +
+ + + + + + {totalCount > filter.itemsPerPage && ( +
+
+ +
+
+ )} +
+
+
+
); } ); diff --git a/ui/v2.5/src/components/Performers/Performers.tsx b/ui/v2.5/src/components/Performers/Performers.tsx index d240ce988..7b6e32b8f 100644 --- a/ui/v2.5/src/components/Performers/Performers.tsx +++ b/ui/v2.5/src/components/Performers/Performers.tsx @@ -4,11 +4,11 @@ import { Helmet } from "react-helmet"; import { useTitleProps } from "src/hooks/title"; import Performer from "./PerformerDetails/Performer"; import PerformerCreate from "./PerformerDetails/PerformerCreate"; -import { PerformerList } from "./PerformerList"; +import { FilteredPerformerList } from "./PerformerList"; import { View } from "../List/views"; const Performers: React.FC = () => { - return ; + return ; }; const PerformerRoutes: React.FC = () => { diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx index 329ac5bc8..551632266 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioPerformersPanel.tsx @@ -1,7 +1,7 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; import { useStudioFilterHook } from "src/core/studios"; -import { PerformerList } from "src/components/Performers/PerformerList"; +import { FilteredPerformerList } from "src/components/Performers/PerformerList"; import { StudiosCriterion } from "src/models/list-filter/criteria/studios"; import { View } from "src/components/List/views"; @@ -33,7 +33,7 @@ export const StudioPerformersPanel: React.FC = ({ const filterHook = useStudioFilterHook(studio, showChildStudioContent); return ( - = ({ }) => { const filterHook = useTagFilterHook(tag, showSubTagContent); return ( -