mirror of
https://github.com/stashapp/stash.git
synced 2026-02-28 18:22:59 +01:00
Add sidebar to images list (#6607)
* Use effective filter for keybinds/view random * Refactor ImageList to use sidebar * Add performer age filter to gallery sidebar * Port metadata info changes * Fix incorrect patch component parameter * Update plugin doc and types
This commit is contained in:
parent
c522e54805
commit
ed58d18334
20 changed files with 645 additions and 287 deletions
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
import { FilteredImageList } from "src/components/Images/ImageList";
|
||||
import { showWhenSelected } from "src/components/List/ItemList";
|
||||
import { mutateAddGalleryImages } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
|
|
@ -100,7 +100,7 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = PatchComponent(
|
|||
];
|
||||
|
||||
return (
|
||||
<ImageList
|
||||
<FilteredImageList
|
||||
filterHook={filterHook}
|
||||
extraOperations={otherOperations}
|
||||
alterQuery={active}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { GalleriesCriterion } from "src/models/list-filter/criteria/galleries";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
import { FilteredImageList } from "src/components/Images/ImageList";
|
||||
import {
|
||||
mutateRemoveGalleryImages,
|
||||
mutateSetGalleryCover,
|
||||
|
|
@ -142,7 +142,7 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> =
|
|||
];
|
||||
|
||||
return (
|
||||
<ImageList
|
||||
<FilteredImageList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
extraOperations={otherOperations}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ import {
|
|||
IItemListOperation,
|
||||
} from "../List/FilteredListToolbar";
|
||||
import { FilterTags } from "../List/FilterTags";
|
||||
import { SidebarAgeFilter } from "../List/Filters/SidebarAgeFilter";
|
||||
import { PerformerAgeCriterionOption } from "src/models/list-filter/galleries";
|
||||
|
||||
const GalleryList: React.FC<{
|
||||
galleries: GQL.SlimGalleryDataFragment[];
|
||||
|
|
@ -169,6 +171,14 @@ const SidebarContent: React.FC<{
|
|||
option={OrganizedCriterionOption}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
sectionID="organized"
|
||||
/>
|
||||
<SidebarAgeFilter
|
||||
title={<FormattedMessage id="performer_age" />}
|
||||
option={PerformerAgeCriterionOption}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
sectionID="performer_age"
|
||||
/>
|
||||
</GalleryFilterSidebarSections>
|
||||
|
||||
|
|
@ -282,7 +292,7 @@ export const FilteredGalleryList = PatchComponent(
|
|||
setFilter,
|
||||
});
|
||||
|
||||
useAddKeybinds(filter, totalCount);
|
||||
useAddKeybinds(effectiveFilter, totalCount);
|
||||
useFilteredSidebarKeybinds({
|
||||
showSidebar,
|
||||
setShowSidebar,
|
||||
|
|
@ -313,7 +323,7 @@ export const FilteredGalleryList = PatchComponent(
|
|||
result,
|
||||
});
|
||||
|
||||
const viewRandom = useViewRandom(filter, totalCount);
|
||||
const viewRandom = useViewRandom(effectiveFilter, totalCount);
|
||||
|
||||
function onExport(all: boolean) {
|
||||
showModal(
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ export const FilteredGroupList = PatchComponent(
|
|||
setFilter,
|
||||
});
|
||||
|
||||
useAddKeybinds(filter, totalCount);
|
||||
useAddKeybinds(effectiveFilter, totalCount);
|
||||
useFilteredSidebarKeybinds({
|
||||
showSidebar,
|
||||
setShowSidebar,
|
||||
|
|
@ -296,7 +296,7 @@ export const FilteredGroupList = PatchComponent(
|
|||
result,
|
||||
});
|
||||
|
||||
const viewRandom = useViewRandom(filter, totalCount);
|
||||
const viewRandom = useViewRandom(effectiveFilter, totalCount);
|
||||
|
||||
function onExport(all: boolean) {
|
||||
showModal(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import React, { useCallback, useState, useMemo, MouseEvent } from "react";
|
||||
import { FormattedNumber, useIntl } from "react-intl";
|
||||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
useMemo,
|
||||
MouseEvent,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
|
@ -9,11 +15,10 @@ import {
|
|||
useFindImages,
|
||||
useFindImagesMetadata,
|
||||
} from "src/core/StashService";
|
||||
import { ItemList, ItemListContext, showWhenSelected } from "../List/ItemList";
|
||||
import { useFilteredItemList } from "../List/ItemList";
|
||||
import { useLightbox } from "src/hooks/Lightbox/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
|
||||
import { ImageWallItem } from "./ImageWallItem";
|
||||
import { EditImagesDialog } from "./EditImagesDialog";
|
||||
import { DeleteImagesDialog } from "./DeleteImagesDialog";
|
||||
|
|
@ -24,11 +29,43 @@ import { objectTitle } from "src/core/files";
|
|||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { ImageCardGrid } from "./ImageCardGrid";
|
||||
import { View } from "../List/views";
|
||||
import { IItemListOperation } from "../List/FilteredListToolbar";
|
||||
import {
|
||||
FilteredListToolbar,
|
||||
IItemListOperation,
|
||||
} from "../List/FilteredListToolbar";
|
||||
import { FileSize } from "../Shared/FileSize";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { PatchComponent, PatchContainerComponent } from "src/patch";
|
||||
import { GenerateDialog } from "../Dialogs/GenerateDialog";
|
||||
import { useModal } from "src/hooks/modal";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarPane,
|
||||
SidebarPaneContent,
|
||||
SidebarStateContext,
|
||||
useSidebarState,
|
||||
} from "../Shared/Sidebar";
|
||||
import { useCloseEditDelete, useFilterOperations } from "../List/util";
|
||||
import {
|
||||
FilteredSidebarHeader,
|
||||
useFilteredSidebarKeybinds,
|
||||
} from "../List/Filters/FilterSidebar";
|
||||
import {
|
||||
IListFilterOperation,
|
||||
ListOperations,
|
||||
} from "../List/ListOperationButtons";
|
||||
import { FilterTags } from "../List/FilterTags";
|
||||
import { Pagination, PaginationIndex } from "../List/Pagination";
|
||||
import { LoadedContent } from "../List/PagedList";
|
||||
import useFocus from "src/utils/focus";
|
||||
import cx from "classnames";
|
||||
import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter";
|
||||
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
|
||||
import { SidebarTagsFilter } from "../List/Filters/TagsFilter";
|
||||
import { SidebarRatingFilter } from "../List/Filters/RatingFilter";
|
||||
import { SidebarBooleanFilter } from "../List/Filters/BooleanFilter";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { OrganizedCriterionOption } from "src/models/list-filter/criteria/organized";
|
||||
import { SidebarAgeFilter } from "../List/Filters/SidebarAgeFilter";
|
||||
import { PerformerAgeCriterionOption } from "src/models/list-filter/images";
|
||||
|
||||
interface IImageWallProps {
|
||||
images: GQL.SlimImageDataFragment[];
|
||||
|
|
@ -180,131 +217,125 @@ interface IImageListImages {
|
|||
chapters?: GQL.GalleryChapterDataFragment[];
|
||||
}
|
||||
|
||||
const ImageListImages: React.FC<IImageListImages> = ({
|
||||
images,
|
||||
filter,
|
||||
selectedIds,
|
||||
onChangePage,
|
||||
pageCount,
|
||||
onSelectChange,
|
||||
slideshowRunning,
|
||||
setSlideshowRunning,
|
||||
chapters = [],
|
||||
}) => {
|
||||
const handleLightBoxPage = useCallback(
|
||||
(props: { direction?: number; page?: number }) => {
|
||||
const { direction, page: newPage } = props;
|
||||
|
||||
if (direction !== undefined) {
|
||||
if (direction < 0) {
|
||||
if (filter.currentPage === 1) {
|
||||
onChangePage(pageCount);
|
||||
} else {
|
||||
onChangePage(filter.currentPage + direction);
|
||||
}
|
||||
} else if (direction > 0) {
|
||||
if (filter.currentPage === pageCount) {
|
||||
// return to the first page
|
||||
onChangePage(1);
|
||||
} else {
|
||||
onChangePage(filter.currentPage + direction);
|
||||
}
|
||||
}
|
||||
} else if (newPage !== undefined) {
|
||||
onChangePage(newPage);
|
||||
}
|
||||
},
|
||||
[onChangePage, filter.currentPage, pageCount]
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSlideshowRunning(false);
|
||||
}, [setSlideshowRunning]);
|
||||
|
||||
const lightboxState = useMemo(() => {
|
||||
return {
|
||||
images,
|
||||
showNavigation: false,
|
||||
pageCallback: pageCount > 1 ? handleLightBoxPage : undefined,
|
||||
page: filter.currentPage,
|
||||
pages: pageCount,
|
||||
pageSize: filter.itemsPerPage,
|
||||
slideshowEnabled: slideshowRunning,
|
||||
onClose: handleClose,
|
||||
};
|
||||
}, [
|
||||
const ImageList: React.FC<IImageListImages> = PatchComponent(
|
||||
"ImageList",
|
||||
({
|
||||
images,
|
||||
filter,
|
||||
selectedIds,
|
||||
onChangePage,
|
||||
pageCount,
|
||||
filter.currentPage,
|
||||
filter.itemsPerPage,
|
||||
onSelectChange,
|
||||
slideshowRunning,
|
||||
handleClose,
|
||||
handleLightBoxPage,
|
||||
]);
|
||||
setSlideshowRunning,
|
||||
chapters = [],
|
||||
}) => {
|
||||
const handleLightBoxPage = useCallback(
|
||||
(props: { direction?: number; page?: number }) => {
|
||||
const { direction, page: newPage } = props;
|
||||
|
||||
const showLightbox = useLightbox(
|
||||
lightboxState,
|
||||
filter.sortBy === "path" &&
|
||||
filter.sortDirection === GQL.SortDirectionEnum.Asc
|
||||
? chapters
|
||||
: []
|
||||
);
|
||||
|
||||
const handleImageOpen = useCallback(
|
||||
(index) => {
|
||||
setSlideshowRunning(true);
|
||||
showLightbox({ initialIndex: index, slideshowEnabled: true });
|
||||
},
|
||||
[showLightbox, setSlideshowRunning]
|
||||
);
|
||||
|
||||
function onPreview(index: number, ev: MouseEvent) {
|
||||
handleImageOpen(index);
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<ImageCardGrid
|
||||
images={images}
|
||||
selectedIds={selectedIds}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
onSelectChange={onSelectChange}
|
||||
onPreview={onPreview}
|
||||
/>
|
||||
if (direction !== undefined) {
|
||||
if (direction < 0) {
|
||||
if (filter.currentPage === 1) {
|
||||
onChangePage(pageCount);
|
||||
} else {
|
||||
onChangePage(filter.currentPage + direction);
|
||||
}
|
||||
} else if (direction > 0) {
|
||||
if (filter.currentPage === pageCount) {
|
||||
// return to the first page
|
||||
onChangePage(1);
|
||||
} else {
|
||||
onChangePage(filter.currentPage + direction);
|
||||
}
|
||||
}
|
||||
} else if (newPage !== undefined) {
|
||||
onChangePage(newPage);
|
||||
}
|
||||
},
|
||||
[onChangePage, filter.currentPage, pageCount]
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return (
|
||||
<ImageWall
|
||||
images={images}
|
||||
onChangePage={onChangePage}
|
||||
currentPage={filter.currentPage}
|
||||
pageCount={pageCount}
|
||||
handleImageOpen={handleImageOpen}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
selecting={!!selectedIds && selectedIds.size > 0}
|
||||
/>
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSlideshowRunning(false);
|
||||
}, [setSlideshowRunning]);
|
||||
|
||||
const lightboxState = useMemo(() => {
|
||||
return {
|
||||
images,
|
||||
showNavigation: false,
|
||||
pageCallback: pageCount > 1 ? handleLightBoxPage : undefined,
|
||||
page: filter.currentPage,
|
||||
pages: pageCount,
|
||||
pageSize: filter.itemsPerPage,
|
||||
slideshowEnabled: slideshowRunning,
|
||||
onClose: handleClose,
|
||||
};
|
||||
}, [
|
||||
images,
|
||||
pageCount,
|
||||
filter.currentPage,
|
||||
filter.itemsPerPage,
|
||||
slideshowRunning,
|
||||
handleClose,
|
||||
handleLightBoxPage,
|
||||
]);
|
||||
|
||||
const showLightbox = useLightbox(
|
||||
lightboxState,
|
||||
filter.sortBy === "path" &&
|
||||
filter.sortDirection === GQL.SortDirectionEnum.Asc
|
||||
? chapters
|
||||
: []
|
||||
);
|
||||
|
||||
const handleImageOpen = useCallback(
|
||||
(index) => {
|
||||
setSlideshowRunning(true);
|
||||
showLightbox({ initialIndex: index, slideshowEnabled: true });
|
||||
},
|
||||
[showLightbox, setSlideshowRunning]
|
||||
);
|
||||
|
||||
function onPreview(index: number, ev: MouseEvent) {
|
||||
handleImageOpen(index);
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<ImageCardGrid
|
||||
images={images}
|
||||
selectedIds={selectedIds}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
onSelectChange={onSelectChange}
|
||||
onPreview={onPreview}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return (
|
||||
<ImageWall
|
||||
images={images}
|
||||
onChangePage={onChangePage}
|
||||
currentPage={filter.currentPage}
|
||||
pageCount={pageCount}
|
||||
handleImageOpen={handleImageOpen}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
selecting={!!selectedIds && selectedIds.size > 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// should not happen
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// should not happen
|
||||
return <></>;
|
||||
};
|
||||
|
||||
function getItems(result: GQL.FindImagesQueryResult) {
|
||||
return result?.data?.findImages?.images ?? [];
|
||||
}
|
||||
|
||||
function getCount(result: GQL.FindImagesQueryResult) {
|
||||
return result?.data?.findImages?.count ?? 0;
|
||||
}
|
||||
);
|
||||
|
||||
function renderMetadataByline(
|
||||
result: GQL.FindImagesQueryResult,
|
||||
metadataInfo?: GQL.FindImagesMetadataQueryResult
|
||||
metadataInfo: GQL.FindImagesMetadataQueryResult | undefined
|
||||
) {
|
||||
const megapixels = metadataInfo?.data?.findImages?.megapixels;
|
||||
const size = metadataInfo?.data?.findImages?.filesize;
|
||||
|
|
@ -339,6 +370,130 @@ function renderMetadataByline(
|
|||
);
|
||||
}
|
||||
|
||||
const ImageFilterSidebarSections = PatchContainerComponent(
|
||||
"FilteredImageList.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<typeof useFocus>;
|
||||
}> = ({
|
||||
filter,
|
||||
setFilter,
|
||||
filterHook,
|
||||
view,
|
||||
showEditFilter,
|
||||
sidebarOpen,
|
||||
onClose,
|
||||
count,
|
||||
focus,
|
||||
}) => {
|
||||
const showResultsId =
|
||||
count !== undefined ? "actions.show_count_results" : "actions.show_results";
|
||||
|
||||
const hideStudios = view === View.StudioScenes;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilteredSidebarHeader
|
||||
sidebarOpen={sidebarOpen}
|
||||
showEditFilter={showEditFilter}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
view={view}
|
||||
focus={focus}
|
||||
/>
|
||||
|
||||
<ImageFilterSidebarSections>
|
||||
{!hideStudios && (
|
||||
<SidebarStudiosFilter
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
filterHook={filterHook}
|
||||
/>
|
||||
)}
|
||||
<SidebarPerformersFilter
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
filterHook={filterHook}
|
||||
/>
|
||||
<SidebarTagsFilter
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
filterHook={filterHook}
|
||||
/>
|
||||
<SidebarRatingFilter filter={filter} setFilter={setFilter} />
|
||||
<SidebarBooleanFilter
|
||||
title={<FormattedMessage id="organized" />}
|
||||
data-type={OrganizedCriterionOption.type}
|
||||
option={OrganizedCriterionOption}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
sectionID="organized"
|
||||
/>
|
||||
<SidebarAgeFilter
|
||||
title={<FormattedMessage id="performer_age" />}
|
||||
option={PerformerAgeCriterionOption}
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
sectionID="performer_age"
|
||||
/>
|
||||
</ImageFilterSidebarSections>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<Button className="sidebar-close-button" onClick={onClose}>
|
||||
<FormattedMessage id={showResultsId} values={{ count }} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function useViewRandom(filter: ListFilterModel, count: number) {
|
||||
const history = useHistory();
|
||||
|
||||
const viewRandom = useCallback(async () => {
|
||||
// query for a random image
|
||||
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 queryFindImages(filterCopy);
|
||||
if (singleResult.data.findImages.images.length === 1) {
|
||||
const { id } = singleResult.data.findImages.images[0];
|
||||
// navigate to the image player page
|
||||
history.push(`/images/${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]);
|
||||
}
|
||||
|
||||
interface IImageList {
|
||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||
view?: View;
|
||||
|
|
@ -347,28 +502,185 @@ interface IImageList {
|
|||
chapters?: GQL.GalleryChapterDataFragment[];
|
||||
}
|
||||
|
||||
export const ImageList: React.FC<IImageList> = PatchComponent(
|
||||
"ImageList",
|
||||
({ filterHook, view, alterQuery, extraOperations = [], chapters = [] }) => {
|
||||
export const FilteredImageList = PatchComponent(
|
||||
"FilteredImageList",
|
||||
(props: IImageList) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
|
||||
const [slideshowRunning, setSlideshowRunning] = useState<boolean>(false);
|
||||
|
||||
const filterMode = GQL.FilterMode.Images;
|
||||
const searchFocus = useFocus();
|
||||
|
||||
const { modal, showModal, closeModal } = useModal();
|
||||
const withSidebar = props.view !== View.GalleryImages;
|
||||
|
||||
const otherOperations: IItemListOperation<GQL.FindImagesQueryResult>[] = [
|
||||
...extraOperations,
|
||||
const {
|
||||
filterHook,
|
||||
view,
|
||||
alterQuery,
|
||||
extraOperations: providedOperations = [],
|
||||
chapters,
|
||||
} = props;
|
||||
|
||||
// States
|
||||
const {
|
||||
showSidebar,
|
||||
setShowSidebar,
|
||||
sectionOpen,
|
||||
setSectionOpen,
|
||||
loading: sidebarStateLoading,
|
||||
} = useSidebarState(view);
|
||||
|
||||
const {
|
||||
filterState,
|
||||
queryResult,
|
||||
metadataInfo,
|
||||
modalState,
|
||||
listSelect,
|
||||
showEditFilter,
|
||||
} = useFilteredItemList({
|
||||
filterStateProps: {
|
||||
filterMode: GQL.FilterMode.Images,
|
||||
view,
|
||||
useURL: alterQuery,
|
||||
},
|
||||
queryResultProps: {
|
||||
useResult: useFindImages,
|
||||
useMetadataInfo: useFindImagesMetadata,
|
||||
getCount: (r) => r.data?.findImages.count ?? 0,
|
||||
getItems: (r) => r.data?.findImages.images ?? [],
|
||||
filterHook,
|
||||
},
|
||||
});
|
||||
|
||||
const { filter, setFilter } = filterState;
|
||||
|
||||
const { effectiveFilter, result, cachedResult, items, totalCount } =
|
||||
queryResult;
|
||||
|
||||
const metadataByline = useMemo(() => {
|
||||
if (cachedResult.loading) return null;
|
||||
|
||||
return renderMetadataByline(metadataInfo) ?? null;
|
||||
}, [cachedResult.loading, metadataInfo]);
|
||||
|
||||
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("e");
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
});
|
||||
|
||||
const onCloseEditDelete = useCloseEditDelete({
|
||||
closeModal,
|
||||
onSelectNone,
|
||||
result,
|
||||
});
|
||||
|
||||
const viewRandom = useViewRandom(effectiveFilter, totalCount);
|
||||
|
||||
function onExport(all: boolean) {
|
||||
showModal(
|
||||
<ExportDialog
|
||||
exportInput={{
|
||||
groups: {
|
||||
ids: Array.from(selectedIds.values()),
|
||||
all: all,
|
||||
},
|
||||
}}
|
||||
onClose={() => closeModal()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function onEdit() {
|
||||
showModal(
|
||||
<EditImagesDialog
|
||||
selected={selectedItems}
|
||||
onClose={onCloseEditDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function onDelete() {
|
||||
showModal(
|
||||
<DeleteImagesDialog
|
||||
selected={selectedItems}
|
||||
onClose={onCloseEditDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const convertedExtraOperations: IListFilterOperation[] =
|
||||
providedOperations.map((o) => ({
|
||||
...o,
|
||||
isDisplayed: o.isDisplayed
|
||||
? () => o.isDisplayed!(result, filter, selectedIds)
|
||||
: undefined,
|
||||
onClick: () => {
|
||||
o.onClick(result, filter, selectedIds);
|
||||
},
|
||||
}));
|
||||
|
||||
const otherOperations: IListFilterOperation[] = [
|
||||
...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.view_random" }),
|
||||
onClick: viewRandom,
|
||||
},
|
||||
{
|
||||
text: `${intl.formatMessage({ id: "actions.generate" })}…`,
|
||||
onClick: (result, filter, selectedIds) => {
|
||||
onClick: () => {
|
||||
showModal(
|
||||
<GenerateDialog
|
||||
type="image"
|
||||
|
|
@ -376,101 +688,78 @@ export const ImageList: React.FC<IImageList> = PatchComponent(
|
|||
onClose={() => closeModal()}
|
||||
/>
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
isDisplayed: showWhenSelected,
|
||||
isDisplayed: () => hasSelection,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
onClick: onExport,
|
||||
isDisplayed: showWhenSelected,
|
||||
onClick: () => onExport(false),
|
||||
isDisplayed: () => hasSelection,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||
onClick: onExportAll,
|
||||
onClick: () => onExport(true),
|
||||
},
|
||||
];
|
||||
|
||||
function addKeybinds(
|
||||
result: GQL.FindImagesQueryResult,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
Mousetrap.bind("p r", () => {
|
||||
viewRandom(result, filter);
|
||||
});
|
||||
// render
|
||||
if (sidebarStateLoading) return null;
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("p r");
|
||||
};
|
||||
}
|
||||
const operations = (
|
||||
<ListOperations
|
||||
items={items.length}
|
||||
hasSelection={hasSelection}
|
||||
operations={otherOperations}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
operationsMenuClassName="image-list-operations-dropdown"
|
||||
/>
|
||||
);
|
||||
|
||||
async function viewRandom(
|
||||
result: GQL.FindImagesQueryResult,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
// query for a random image
|
||||
if (result.data?.findImages) {
|
||||
const { count } = result.data.findImages;
|
||||
const pageCount = Math.ceil(totalCount / filter.itemsPerPage);
|
||||
|
||||
const index = Math.floor(Math.random() * count);
|
||||
const filterCopy = cloneDeep(filter);
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await queryFindImages(filterCopy);
|
||||
if (singleResult.data.findImages.images.length === 1) {
|
||||
const { id } = singleResult.data.findImages.images[0];
|
||||
// navigate to the image player page
|
||||
history.push(`/images/${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const content = (
|
||||
<>
|
||||
<FilteredListToolbar
|
||||
filter={filter}
|
||||
listSelect={listSelect}
|
||||
setFilter={setFilter}
|
||||
showEditFilter={showEditFilter}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
operationComponent={operations}
|
||||
view={view}
|
||||
zoomable
|
||||
/>
|
||||
|
||||
async function onExport() {
|
||||
setIsExportAll(false);
|
||||
setIsExportDialogOpen(true);
|
||||
}
|
||||
<FilterTags
|
||||
criteria={filter.criteria}
|
||||
onEditCriterion={(c) => showEditFilter(c.criterionOption.type)}
|
||||
onRemoveCriterion={removeCriterion}
|
||||
onRemoveAll={clearAllCriteria}
|
||||
/>
|
||||
|
||||
async function onExportAll() {
|
||||
setIsExportAll(true);
|
||||
setIsExportDialogOpen(true);
|
||||
}
|
||||
<div className="pagination-index-container">
|
||||
<Pagination
|
||||
currentPage={filter.currentPage}
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
totalItems={totalCount}
|
||||
onChangePage={(page) => setFilter(filter.changePage(page))}
|
||||
/>
|
||||
<PaginationIndex
|
||||
loading={cachedResult.loading}
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
metadataByline={metadataByline}
|
||||
/>
|
||||
</div>
|
||||
|
||||
function renderContent(
|
||||
result: GQL.FindImagesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>,
|
||||
onSelectChange: (
|
||||
id: string,
|
||||
selected: boolean,
|
||||
shiftKey: boolean
|
||||
) => void,
|
||||
onChangePage: (page: number) => void,
|
||||
pageCount: number
|
||||
) {
|
||||
function maybeRenderImageExportDialog() {
|
||||
if (isExportDialogOpen) {
|
||||
return (
|
||||
<ExportDialog
|
||||
exportInput={{
|
||||
images: {
|
||||
ids: Array.from(selectedIds.values()),
|
||||
all: isExportAll,
|
||||
},
|
||||
}}
|
||||
onClose={() => setIsExportDialogOpen(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderImages() {
|
||||
if (!result.data?.findImages) return;
|
||||
|
||||
return (
|
||||
<ImageListImages
|
||||
<LoadedContent loading={result.loading} error={result.error}>
|
||||
<ImageList
|
||||
filter={filter}
|
||||
images={result.data.findImages.images}
|
||||
onChangePage={onChangePage}
|
||||
images={items}
|
||||
onChangePage={(page) => setFilter(filter.changePage(page))}
|
||||
onSelectChange={onSelectChange}
|
||||
pageCount={pageCount}
|
||||
selectedIds={selectedIds}
|
||||
|
|
@ -478,54 +767,60 @@ export const ImageList: React.FC<IImageList> = PatchComponent(
|
|||
setSlideshowRunning={setSlideshowRunning}
|
||||
chapters={chapters}
|
||||
/>
|
||||
);
|
||||
}
|
||||
</LoadedContent>
|
||||
|
||||
return (
|
||||
<>
|
||||
{maybeRenderImageExportDialog()}
|
||||
{renderImages()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
{totalCount > filter.itemsPerPage && (
|
||||
<div className="pagination-footer-container">
|
||||
<div className="pagination-footer">
|
||||
<Pagination
|
||||
itemsPerPage={filter.itemsPerPage}
|
||||
currentPage={filter.currentPage}
|
||||
totalItems={totalCount}
|
||||
onChangePage={setPage}
|
||||
pagePopupPlacement="top"
|
||||
metadataByline={metadataByline}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
function renderEditDialog(
|
||||
selectedImages: GQL.SlimImageDataFragment[],
|
||||
onClose: (applied: boolean) => void
|
||||
) {
|
||||
return <EditImagesDialog selected={selectedImages} onClose={onClose} />;
|
||||
}
|
||||
|
||||
function renderDeleteDialog(
|
||||
selectedImages: GQL.SlimImageDataFragment[],
|
||||
onClose: (confirmed: boolean) => void
|
||||
) {
|
||||
return <DeleteImagesDialog selected={selectedImages} onClose={onClose} />;
|
||||
if (!withSidebar) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemListContext
|
||||
filterMode={filterMode}
|
||||
useResult={useFindImages}
|
||||
useMetadataInfo={useFindImagesMetadata}
|
||||
getItems={getItems}
|
||||
getCount={getCount}
|
||||
alterQuery={alterQuery}
|
||||
filterHook={filterHook}
|
||||
view={view}
|
||||
selectable
|
||||
<div
|
||||
className={cx("item-list-container image-list", {
|
||||
"hide-sidebar": !showSidebar,
|
||||
})}
|
||||
>
|
||||
{modal}
|
||||
<ItemList
|
||||
view={view}
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
renderContent={renderContent}
|
||||
renderEditDialog={renderEditDialog}
|
||||
renderDeleteDialog={renderDeleteDialog}
|
||||
renderMetadataByline={renderMetadataByline}
|
||||
/>
|
||||
</ItemListContext>
|
||||
|
||||
<SidebarStateContext.Provider value={{ sectionOpen, setSectionOpen }}>
|
||||
<SidebarPane hideSidebar={!showSidebar}>
|
||||
<Sidebar hide={!showSidebar} onHide={() => setShowSidebar(false)}>
|
||||
<SidebarContent
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
filterHook={filterHook}
|
||||
showEditFilter={showEditFilter}
|
||||
view={view}
|
||||
sidebarOpen={showSidebar}
|
||||
onClose={() => setShowSidebar(false)}
|
||||
count={cachedResult.loading ? undefined : totalCount}
|
||||
focus={searchFocus}
|
||||
/>
|
||||
</Sidebar>
|
||||
<SidebarPaneContent
|
||||
onSidebarToggle={() => setShowSidebar(!showSidebar)}
|
||||
>
|
||||
{content}
|
||||
</SidebarPaneContent>
|
||||
</SidebarPane>
|
||||
</SidebarStateContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { Route, Switch } from "react-router-dom";
|
|||
import { Helmet } from "react-helmet";
|
||||
import { useTitleProps } from "src/hooks/title";
|
||||
import Image from "./ImageDetails/Image";
|
||||
import { ImageList } from "./ImageList";
|
||||
import { FilteredImageList } from "./ImageList";
|
||||
import { View } from "../List/views";
|
||||
|
||||
const Images: React.FC = () => {
|
||||
return <ImageList view={View.Images} />;
|
||||
return <FilteredImageList view={View.Images} />;
|
||||
};
|
||||
|
||||
const ImageRoutes: React.FC = () => {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
FilterMode,
|
||||
GalleryFilterType,
|
||||
GroupFilterType,
|
||||
ImageFilterType,
|
||||
InputMaybe,
|
||||
IntCriterionInput,
|
||||
PerformerFilterType,
|
||||
|
|
@ -524,6 +525,8 @@ interface IFilterType {
|
|||
performer_count?: InputMaybe<IntCriterionInput>;
|
||||
galleries_filter?: InputMaybe<GalleryFilterType>;
|
||||
gallery_count?: InputMaybe<IntCriterionInput>;
|
||||
images_filter?: InputMaybe<ImageFilterType>;
|
||||
image_count?: InputMaybe<IntCriterionInput>;
|
||||
groups_filter?: InputMaybe<GroupFilterType>;
|
||||
group_count?: InputMaybe<IntCriterionInput>;
|
||||
studios_filter?: InputMaybe<StudioFilterType>;
|
||||
|
|
@ -578,6 +581,17 @@ export function setObjectFilter(
|
|||
}
|
||||
out.galleries_filter = relatedFilterOutput as GalleryFilterType;
|
||||
break;
|
||||
case FilterMode.Images:
|
||||
// if empty, only get objects with galleries
|
||||
if (empty) {
|
||||
out.image_count = {
|
||||
modifier: CriterionModifier.GreaterThan,
|
||||
value: 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
out.images_filter = relatedFilterOutput as ImageFilterType;
|
||||
break;
|
||||
case FilterMode.Groups:
|
||||
// if empty, only get objects with groups
|
||||
if (empty) {
|
||||
|
|
|
|||
|
|
@ -46,16 +46,21 @@ import { useConfigurationContext } from "src/hooks/Config";
|
|||
import { useZoomKeybinds } from "./ZoomSlider";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
|
||||
interface IFilteredItemList<T extends QueryResult, E extends IHasID = IHasID> {
|
||||
interface IFilteredItemList<
|
||||
T extends QueryResult,
|
||||
E extends IHasID = IHasID,
|
||||
M = unknown
|
||||
> {
|
||||
filterStateProps: IFilterStateHook;
|
||||
queryResultProps: IQueryResultHook<T, E>;
|
||||
queryResultProps: IQueryResultHook<T, E, M>;
|
||||
}
|
||||
|
||||
// Provides the common state and behaviour for filtered item list components
|
||||
export function useFilteredItemList<
|
||||
T extends QueryResult,
|
||||
E extends IHasID = IHasID
|
||||
>(props: IFilteredItemList<T, E>) {
|
||||
E extends IHasID = IHasID,
|
||||
M = unknown
|
||||
>(props: IFilteredItemList<T, E, M>) {
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
// States
|
||||
|
|
@ -70,7 +75,7 @@ export function useFilteredItemList<
|
|||
filter,
|
||||
...props.queryResultProps,
|
||||
});
|
||||
const { result, items, totalCount, pages } = queryResult;
|
||||
const { result, items, totalCount, pages, metadataInfo } = queryResult;
|
||||
|
||||
const listSelect = useListSelect(items);
|
||||
const { onSelectAll, onSelectNone, onInvertSelection } = listSelect;
|
||||
|
|
@ -107,6 +112,7 @@ export function useFilteredItemList<
|
|||
return {
|
||||
filterState,
|
||||
queryResult,
|
||||
metadataInfo,
|
||||
listSelect,
|
||||
modalState,
|
||||
showEditFilter,
|
||||
|
|
|
|||
|
|
@ -509,23 +509,27 @@ export function useCachedQueryResult<T extends QueryResult>(
|
|||
|
||||
export interface IQueryResultHook<
|
||||
T extends QueryResult,
|
||||
E extends IHasID = IHasID
|
||||
E extends IHasID = IHasID,
|
||||
M = unknown
|
||||
> {
|
||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||
useResult: (filter: ListFilterModel) => T;
|
||||
useMetadataInfo?: (filter: ListFilterModel) => M;
|
||||
getCount: (data: T) => number;
|
||||
getItems: (data: T) => E[];
|
||||
}
|
||||
|
||||
export function useQueryResult<
|
||||
T extends QueryResult,
|
||||
E extends IHasID = IHasID
|
||||
E extends IHasID = IHasID,
|
||||
M = unknown
|
||||
>(
|
||||
props: IQueryResultHook<T, E> & {
|
||||
props: IQueryResultHook<T, E, M> & {
|
||||
filter: ListFilterModel;
|
||||
}
|
||||
) {
|
||||
const { filter, filterHook, useResult, getItems, getCount } = props;
|
||||
const { filter, filterHook, useResult, useMetadataInfo, getItems, getCount } =
|
||||
props;
|
||||
|
||||
const effectiveFilter = useMemo(() => {
|
||||
if (filterHook) {
|
||||
|
|
@ -534,7 +538,14 @@ export function useQueryResult<
|
|||
return filter;
|
||||
}, [filter, filterHook]);
|
||||
|
||||
// metadata filter is the effective filter with the sort, page size and page number removed
|
||||
const metadataFilter = useMemo(
|
||||
() => effectiveFilter.metadataInfo(),
|
||||
[effectiveFilter]
|
||||
);
|
||||
|
||||
const result = useResult(effectiveFilter);
|
||||
const metadataInfo = useMetadataInfo?.(metadataFilter);
|
||||
|
||||
// use cached query result for pagination and metadata rendering
|
||||
const cachedResult = useCachedQueryResult(effectiveFilter, result);
|
||||
|
|
@ -549,6 +560,7 @@ export function useQueryResult<
|
|||
|
||||
return {
|
||||
effectiveFilter,
|
||||
metadataInfo,
|
||||
result,
|
||||
cachedResult,
|
||||
items,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
import { FilteredImageList } from "src/components/Images/ImageList";
|
||||
import { usePerformerFilterHook } from "src/core/performers";
|
||||
import { View } from "src/components/List/views";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
|
@ -14,7 +14,7 @@ export const PerformerImagesPanel: React.FC<IPerformerImagesPanel> =
|
|||
PatchComponent("PerformerImagesPanel", ({ active, performer }) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<ImageList
|
||||
<FilteredImageList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerImages}
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@ export const FilteredPerformerList = PatchComponent(
|
|||
setFilter,
|
||||
});
|
||||
|
||||
useAddKeybinds(filter, totalCount);
|
||||
useAddKeybinds(effectiveFilter, totalCount);
|
||||
useFilteredSidebarKeybinds({
|
||||
showSidebar,
|
||||
setShowSidebar,
|
||||
|
|
@ -454,7 +454,7 @@ export const FilteredPerformerList = PatchComponent(
|
|||
result,
|
||||
});
|
||||
|
||||
const viewRandom = useViewRandom(filter, totalCount);
|
||||
const viewRandom = useViewRandom(effectiveFilter, totalCount);
|
||||
|
||||
function onExport(all: boolean) {
|
||||
showModal(
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ export const FilteredSceneList = PatchComponent(
|
|||
setFilter,
|
||||
});
|
||||
|
||||
useAddKeybinds(filter, totalCount);
|
||||
useAddKeybinds(effectiveFilter, totalCount);
|
||||
useFilteredSidebarKeybinds({
|
||||
showSidebar,
|
||||
setShowSidebar,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const SceneMarkerList: React.FC<{
|
|||
selectedIds: Set<string>;
|
||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||
}> = PatchComponent(
|
||||
"SceneList",
|
||||
"SceneMarkerList",
|
||||
({ markers, filter, selectedIds, onSelectChange }) => {
|
||||
if (markers.length === 0) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useStudioFilterHook } from "src/core/studios";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
import { FilteredImageList } from "src/components/Images/ImageList";
|
||||
import { View } from "src/components/List/views";
|
||||
|
||||
interface IStudioImagesPanel {
|
||||
|
|
@ -17,7 +17,7 @@ export const StudioImagesPanel: React.FC<IStudioImagesPanel> = ({
|
|||
}) => {
|
||||
const filterHook = useStudioFilterHook(studio, showChildStudioContent);
|
||||
return (
|
||||
<ImageList
|
||||
<FilteredImageList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.StudioImages}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export const FilteredStudioList = PatchComponent(
|
|||
setFilter,
|
||||
});
|
||||
|
||||
useAddKeybinds(filter, totalCount);
|
||||
useAddKeybinds(effectiveFilter, totalCount);
|
||||
useFilteredSidebarKeybinds({
|
||||
showSidebar,
|
||||
setShowSidebar,
|
||||
|
|
@ -282,7 +282,7 @@ export const FilteredStudioList = PatchComponent(
|
|||
result,
|
||||
});
|
||||
|
||||
const viewRandom = useViewRandom(filter, totalCount);
|
||||
const viewRandom = useViewRandom(effectiveFilter, totalCount);
|
||||
|
||||
function onExport(all: boolean) {
|
||||
showModal(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useTagFilterHook } from "src/core/tags";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
import { FilteredImageList } from "src/components/Images/ImageList";
|
||||
import { View } from "src/components/List/views";
|
||||
|
||||
interface ITagImagesPanel {
|
||||
|
|
@ -17,7 +17,7 @@ export const TagImagesPanel: React.FC<ITagImagesPanel> = ({
|
|||
}) => {
|
||||
const filterHook = useTagFilterHook(tag, showSubTagContent);
|
||||
return (
|
||||
<ImageList
|
||||
<FilteredImageList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.TagImages}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,12 @@ Returns `void`.
|
|||
- `ExternalLinkButtons`
|
||||
- `ExternalLinksButton`
|
||||
- `FilteredGalleryList`
|
||||
- `FilteredGroupList`
|
||||
- `FilteredImageList`
|
||||
- `FilteredPerformerList`
|
||||
- `FilteredSceneList`
|
||||
- `FilteredSceneMarkerList`
|
||||
- `FilteredStudioList`
|
||||
- `FolderSelect`
|
||||
- `FrontPage`
|
||||
- `GalleryCard`
|
||||
|
|
@ -248,6 +253,7 @@ Returns `void`.
|
|||
- `GroupCard`
|
||||
- `GroupCardGrid`
|
||||
- `GroupIDSelect`
|
||||
- `GroupList`
|
||||
- `GroupRecommendationRow`
|
||||
- `GroupSelect`
|
||||
- `GroupSelect.sort`
|
||||
|
|
@ -262,6 +268,7 @@ Returns `void`.
|
|||
- `ImageDetailPanel`
|
||||
- `ImageGridCard`
|
||||
- `ImageInput`
|
||||
- `ImageList`
|
||||
- `ImageRecommendationRow`
|
||||
- `LightboxLink`
|
||||
- `LoadingIndicator`
|
||||
|
|
@ -286,6 +293,7 @@ Returns `void`.
|
|||
- `PerformerHeaderImage`
|
||||
- `PerformerIDSelect`
|
||||
- `PerformerImagesPanel`
|
||||
- `PerformerList`
|
||||
- `PerformerPage`
|
||||
- `PerformerRecommendationRow`
|
||||
- `PerformerScenesPanel`
|
||||
|
|
@ -310,6 +318,7 @@ Returns `void`.
|
|||
- `SceneMarkerCard.Image`
|
||||
- `SceneMarkerCard.Popovers`
|
||||
- `SceneMarkerCardsGrid`
|
||||
- `SceneMarkerList`
|
||||
- `SceneMarkerRecommendationRow`
|
||||
- `SceneList`
|
||||
- `ScenePage`
|
||||
|
|
@ -329,6 +338,7 @@ Returns `void`.
|
|||
- `StudioCardGrid`
|
||||
- `StudioDetailsPanel`
|
||||
- `StudioIDSelect`
|
||||
- `StudioList`
|
||||
- `StudioRecommendationRow`
|
||||
- `StudioSelect`
|
||||
- `StudioSelect.sort`
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ const displayModeOptions = [
|
|||
DisplayMode.Wall,
|
||||
];
|
||||
|
||||
export const PerformerAgeCriterionOption =
|
||||
createMandatoryNumberCriterionOption("performer_age");
|
||||
|
||||
const criterionOptions = [
|
||||
createStringCriterionOption("title"),
|
||||
createStringCriterionOption("code", "scene_code"),
|
||||
|
|
@ -61,7 +64,7 @@ const criterionOptions = [
|
|||
PerformerTagsCriterionOption,
|
||||
PerformersCriterionOption,
|
||||
createMandatoryNumberCriterionOption("performer_count"),
|
||||
createMandatoryNumberCriterionOption("performer_age"),
|
||||
PerformerAgeCriterionOption,
|
||||
PerformerFavoriteCriterionOption,
|
||||
createMandatoryNumberCriterionOption("image_count"),
|
||||
// StudioTagsCriterionOption,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { OrientationCriterionOption } from "./criteria/orientation";
|
|||
import { StudiosCriterionOption } from "./criteria/studios";
|
||||
import {
|
||||
PerformerTagsCriterionOption,
|
||||
// StudioTagsCriterionOption,
|
||||
TagsCriterionOption,
|
||||
} from "./criteria/tags";
|
||||
import { ListFilterOptions, MediaSortByOptions } from "./filter-options";
|
||||
|
|
@ -43,6 +42,10 @@ const sortByOptions = [
|
|||
},
|
||||
]);
|
||||
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
||||
|
||||
export const PerformerAgeCriterionOption =
|
||||
createMandatoryNumberCriterionOption("performer_age");
|
||||
|
||||
const criterionOptions = [
|
||||
createStringCriterionOption("title"),
|
||||
createStringCriterionOption("code", "scene_code"),
|
||||
|
|
@ -65,7 +68,7 @@ const criterionOptions = [
|
|||
PerformerTagsCriterionOption,
|
||||
PerformersCriterionOption,
|
||||
createMandatoryNumberCriterionOption("performer_count"),
|
||||
createMandatoryNumberCriterionOption("performer_age"),
|
||||
PerformerAgeCriterionOption,
|
||||
PerformerFavoriteCriterionOption,
|
||||
// StudioTagsCriterionOption,
|
||||
StudiosCriterionOption,
|
||||
|
|
|
|||
5
ui/v2.5/src/pluginApi.d.ts
vendored
5
ui/v2.5/src/pluginApi.d.ts
vendored
|
|
@ -667,7 +667,12 @@ declare namespace PluginApi {
|
|||
ExternalLinkButtons: React.FC<any>;
|
||||
ExternalLinksButton: React.FC<any>;
|
||||
FilteredGalleryList: React.FC<any>;
|
||||
FilteredGroupList: React.FC<any>;
|
||||
FilteredImageList: React.FC<any>;
|
||||
FilteredPerformerList: React.FC<any>;
|
||||
FilteredSceneList: React.FC<any>;
|
||||
FilteredSceneMarkerList: React.FC<any>;
|
||||
FilteredStudioList: React.FC<any>;
|
||||
FolderSelect: React.FC<any>;
|
||||
FrontPage: React.FC<any>;
|
||||
GalleryCard: React.FC<any>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue