mirror of
https://github.com/stashapp/stash.git
synced 2026-02-07 16:05:47 +01:00
Revamp gallery list with sidebar (#6157)
* Make list operation utility component * Add defaults for sidebar filters * Refactor gallery list for sidebar * Fix gallery styling * Fix sidebar state issues * Auto-populate query string into name on create * Remove new gallery button from navbar * Make components patchable
This commit is contained in:
parent
88eb46380c
commit
b5de30a295
19 changed files with 1084 additions and 745 deletions
|
|
@ -4,7 +4,7 @@ import { Helmet } from "react-helmet";
|
|||
import { useTitleProps } from "src/hooks/title";
|
||||
import Gallery from "./GalleryDetails/Gallery";
|
||||
import GalleryCreate from "./GalleryDetails/GalleryCreate";
|
||||
import { GalleryList } from "./GalleryList";
|
||||
import { FilteredGalleryList } from "./GalleryList";
|
||||
import { View } from "../List/views";
|
||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { ErrorMessage } from "../Shared/ErrorMessage";
|
||||
|
|
@ -40,7 +40,7 @@ const GalleryImage: React.FC<RouteComponentProps<IGalleryImageParams>> = ({
|
|||
};
|
||||
|
||||
const Galleries: React.FC = () => {
|
||||
return <GalleryList view={View.Galleries} />;
|
||||
return <FilteredGalleryList view={View.Galleries} />;
|
||||
};
|
||||
|
||||
const GalleryRoutes: React.FC = () => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ItemList, ItemListContext, showWhenSelected } from "../List/ItemList";
|
||||
import { useFilteredItemList } from "../List/ItemList";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { queryFindGalleries, useFindGalleries } from "src/core/StashService";
|
||||
|
|
@ -16,17 +16,167 @@ import { GenerateDialog } from "../Dialogs/GenerateDialog";
|
|||
import { GalleryListTable } from "./GalleryListTable";
|
||||
import { GalleryCardGrid } from "./GalleryCardGrid";
|
||||
import { View } from "../List/views";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { IItemListOperation } from "../List/FilteredListToolbar";
|
||||
import { useModal } from "src/hooks/modal";
|
||||
import useFocus from "src/utils/focus";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarPane,
|
||||
SidebarPaneContent,
|
||||
SidebarStateContext,
|
||||
useSidebarState,
|
||||
} from "../Shared/Sidebar";
|
||||
import { useCloseEditDelete, useFilterOperations } from "../List/util";
|
||||
import {
|
||||
FilteredSidebarHeader,
|
||||
useFilteredSidebarKeybinds,
|
||||
} from "../List/Filters/FilterSidebar";
|
||||
import cx from "classnames";
|
||||
import { LoadedContent } from "../List/PagedList";
|
||||
import { Pagination, PaginationIndex } from "../List/Pagination";
|
||||
import { PatchComponent, PatchContainerComponent } from "src/patch";
|
||||
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 { OrganizedCriterionOption } from "src/models/list-filter/criteria/organized";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { ListOperations } from "../List/ListOperationButtons";
|
||||
import {
|
||||
FilteredListToolbar,
|
||||
IItemListOperation,
|
||||
} from "../List/FilteredListToolbar";
|
||||
import { FilterTags } from "../List/FilterTags";
|
||||
|
||||
function getItems(result: GQL.FindGalleriesQueryResult) {
|
||||
return result?.data?.findGalleries?.galleries ?? [];
|
||||
}
|
||||
const GalleryList: React.FC<{
|
||||
galleries: GQL.SlimGalleryDataFragment[];
|
||||
filter: ListFilterModel;
|
||||
selectedIds: Set<string>;
|
||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void;
|
||||
}> = PatchComponent(
|
||||
"GalleryList",
|
||||
({ galleries, filter, selectedIds, onSelectChange }) => {
|
||||
if (galleries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCount(result: GQL.FindGalleriesQueryResult) {
|
||||
return result?.data?.findGalleries?.count ?? 0;
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<GalleryCardGrid
|
||||
galleries={galleries}
|
||||
selectedIds={selectedIds}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
return (
|
||||
<GalleryListTable
|
||||
galleries={galleries}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return (
|
||||
<div className={`GalleryWall zoom-${filter.zoomIndex}`}>
|
||||
{galleries.map((gallery) => (
|
||||
<GalleryWallCard
|
||||
key={gallery.id}
|
||||
gallery={gallery}
|
||||
selected={selectedIds.has(gallery.id)}
|
||||
onSelectedChanged={(selected, shiftKey) =>
|
||||
onSelectChange(gallery.id, selected, shiftKey)
|
||||
}
|
||||
selecting={selectedIds.size > 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
const GalleryFilterSidebarSections = PatchContainerComponent(
|
||||
"FilteredGalleryList.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}
|
||||
/>
|
||||
|
||||
<GalleryFilterSidebarSections>
|
||||
{!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}
|
||||
/>
|
||||
</GalleryFilterSidebarSections>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<Button className="sidebar-close-button" onClick={onClose}>
|
||||
<FormattedMessage id={showResultsId} values={{ count }} />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IGalleryList {
|
||||
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||
|
|
@ -35,208 +185,324 @@ interface IGalleryList {
|
|||
extraOperations?: IItemListOperation<GQL.FindGalleriesQueryResult>[];
|
||||
}
|
||||
|
||||
export const GalleryList: React.FC<IGalleryList> = PatchComponent(
|
||||
"GalleryList",
|
||||
({ filterHook, view, alterQuery, extraOperations = [] }) => {
|
||||
function useViewRandom(filter: ListFilterModel, count: number) {
|
||||
const history = useHistory();
|
||||
|
||||
const viewRandom = useCallback(async () => {
|
||||
// query for a random scene
|
||||
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 queryFindGalleries(filterCopy);
|
||||
if (singleResult.data.findGalleries.galleries.length === 1) {
|
||||
const { id } = singleResult.data.findGalleries.galleries[0];
|
||||
// navigate to the image player page
|
||||
history.push(`/galleries/${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 FilteredGalleryList = PatchComponent(
|
||||
"FilteredGalleryList",
|
||||
(props: IGalleryList) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
const { modal, showModal, closeModal } = useModal();
|
||||
const location = useLocation();
|
||||
|
||||
const filterMode = GQL.FilterMode.Galleries;
|
||||
const searchFocus = useFocus();
|
||||
|
||||
const { filterHook, view, alterQuery } = props;
|
||||
|
||||
// States
|
||||
const {
|
||||
showSidebar,
|
||||
setShowSidebar,
|
||||
sectionOpen,
|
||||
setSectionOpen,
|
||||
loading: sidebarStateLoading,
|
||||
} = useSidebarState(view);
|
||||
|
||||
const { filterState, queryResult, modalState, listSelect, showEditFilter } =
|
||||
useFilteredItemList({
|
||||
filterStateProps: {
|
||||
filterMode: GQL.FilterMode.Galleries,
|
||||
view,
|
||||
useURL: alterQuery,
|
||||
},
|
||||
queryResultProps: {
|
||||
useResult: useFindGalleries,
|
||||
getCount: (r) => r.data?.findGalleries.count ?? 0,
|
||||
getItems: (r) => r.data?.findGalleries.galleries ?? [],
|
||||
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("e");
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
});
|
||||
|
||||
const onCloseEditDelete = useCloseEditDelete({
|
||||
closeModal,
|
||||
onSelectNone,
|
||||
result,
|
||||
});
|
||||
|
||||
function onCreateNew() {
|
||||
let queryParam = new URLSearchParams(location.search).get("q");
|
||||
let newPath = "/galleries/new";
|
||||
if (queryParam) {
|
||||
newPath += "?q=" + encodeURIComponent(queryParam);
|
||||
}
|
||||
history.push(newPath);
|
||||
}
|
||||
|
||||
const viewRandom = useViewRandom(filter, totalCount);
|
||||
|
||||
function onExport(all: boolean) {
|
||||
showModal(
|
||||
<ExportDialog
|
||||
exportInput={{
|
||||
galleries: {
|
||||
ids: Array.from(selectedIds.values()),
|
||||
all: all,
|
||||
},
|
||||
}}
|
||||
onClose={() => closeModal()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function onEdit() {
|
||||
showModal(
|
||||
<EditGalleriesDialog
|
||||
selected={selectedItems}
|
||||
onClose={onCloseEditDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function onDelete() {
|
||||
showModal(
|
||||
<DeleteGalleriesDialog
|
||||
selected={selectedItems}
|
||||
onClose={onCloseEditDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function onGenerate() {
|
||||
showModal(
|
||||
<GenerateDialog
|
||||
type="gallery"
|
||||
selectedIds={Array.from(selectedIds.values())}
|
||||
onClose={() => closeModal()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const otherOperations = [
|
||||
...extraOperations,
|
||||
{
|
||||
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: GQL.FindGalleriesQueryResult,
|
||||
_filter: ListFilterModel,
|
||||
selectedIds: Set<string>
|
||||
) => {
|
||||
showModal(
|
||||
<GenerateDialog
|
||||
type="gallery"
|
||||
selectedIds={Array.from(selectedIds.values())}
|
||||
onClose={() => closeModal()}
|
||||
/>
|
||||
);
|
||||
return Promise.resolve();
|
||||
},
|
||||
isDisplayed: showWhenSelected,
|
||||
onClick: onGenerate,
|
||||
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.FindGalleriesQueryResult,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
Mousetrap.bind("p r", () => {
|
||||
viewRandom(result, filter);
|
||||
});
|
||||
// render
|
||||
if (sidebarStateLoading) return null;
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("p r");
|
||||
};
|
||||
}
|
||||
|
||||
async function viewRandom(
|
||||
result: GQL.FindGalleriesQueryResult,
|
||||
filter: ListFilterModel
|
||||
) {
|
||||
// query for a random image
|
||||
if (result.data?.findGalleries) {
|
||||
const { count } = result.data.findGalleries;
|
||||
|
||||
const index = Math.floor(Math.random() * count);
|
||||
const filterCopy = cloneDeep(filter);
|
||||
filterCopy.itemsPerPage = 1;
|
||||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await queryFindGalleries(filterCopy);
|
||||
if (singleResult.data.findGalleries.galleries.length === 1) {
|
||||
const { id } = singleResult.data.findGalleries.galleries[0];
|
||||
// navigate to the image player page
|
||||
history.push(`/galleries/${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onExport() {
|
||||
setIsExportAll(false);
|
||||
setIsExportDialogOpen(true);
|
||||
}
|
||||
|
||||
async function onExportAll() {
|
||||
setIsExportAll(true);
|
||||
setIsExportDialogOpen(true);
|
||||
}
|
||||
|
||||
function renderContent(
|
||||
result: GQL.FindGalleriesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>,
|
||||
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
|
||||
) {
|
||||
function maybeRenderGalleryExportDialog() {
|
||||
if (isExportDialogOpen) {
|
||||
return (
|
||||
<ExportDialog
|
||||
exportInput={{
|
||||
galleries: {
|
||||
ids: Array.from(selectedIds.values()),
|
||||
all: isExportAll,
|
||||
},
|
||||
}}
|
||||
onClose={() => setIsExportDialogOpen(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderGalleries() {
|
||||
if (!result.data?.findGalleries) return;
|
||||
|
||||
if (filter.displayMode === DisplayMode.Grid) {
|
||||
return (
|
||||
<GalleryCardGrid
|
||||
galleries={result.data.findGalleries.galleries}
|
||||
selectedIds={selectedIds}
|
||||
zoomIndex={filter.zoomIndex}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.List) {
|
||||
return (
|
||||
<GalleryListTable
|
||||
galleries={result.data.findGalleries.galleries}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (filter.displayMode === DisplayMode.Wall) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className={`GalleryWall zoom-${filter.zoomIndex}`}>
|
||||
{result.data.findGalleries.galleries.map((gallery) => (
|
||||
<GalleryWallCard
|
||||
key={gallery.id}
|
||||
gallery={gallery}
|
||||
selected={selectedIds.has(gallery.id)}
|
||||
onSelectedChanged={(selected, shiftKey) =>
|
||||
onSelectChange(gallery.id, selected, shiftKey)
|
||||
}
|
||||
selecting={selectedIds.size > 0}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{maybeRenderGalleryExportDialog()}
|
||||
{modal}
|
||||
{renderGalleries()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderEditDialog(
|
||||
selectedImages: GQL.SlimGalleryDataFragment[],
|
||||
onClose: (applied: boolean) => void
|
||||
) {
|
||||
return (
|
||||
<EditGalleriesDialog selected={selectedImages} onClose={onClose} />
|
||||
);
|
||||
}
|
||||
|
||||
function renderDeleteDialog(
|
||||
selectedImages: GQL.SlimGalleryDataFragment[],
|
||||
onClose: (confirmed: boolean) => void
|
||||
) {
|
||||
return (
|
||||
<DeleteGalleriesDialog selected={selectedImages} onClose={onClose} />
|
||||
);
|
||||
}
|
||||
const operations = (
|
||||
<ListOperations
|
||||
items={items.length}
|
||||
hasSelection={hasSelection}
|
||||
operations={otherOperations}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onCreateNew={onCreateNew}
|
||||
entityType={intl.formatMessage({ id: "gallery" })}
|
||||
operationsMenuClassName="gallery-list-operations-dropdown"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ItemListContext
|
||||
filterMode={filterMode}
|
||||
useResult={useFindGalleries}
|
||||
getItems={getItems}
|
||||
getCount={getCount}
|
||||
alterQuery={alterQuery}
|
||||
filterHook={filterHook}
|
||||
view={view}
|
||||
selectable
|
||||
<div
|
||||
className={cx("item-list-container gallery-list", {
|
||||
"hide-sidebar": !showSidebar,
|
||||
})}
|
||||
>
|
||||
<ItemList
|
||||
view={view}
|
||||
otherOperations={otherOperations}
|
||||
addKeybinds={addKeybinds}
|
||||
renderContent={renderContent}
|
||||
renderEditDialog={renderEditDialog}
|
||||
renderDeleteDialog={renderDeleteDialog}
|
||||
/>
|
||||
</ItemListContext>
|
||||
{modal}
|
||||
|
||||
<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)}
|
||||
>
|
||||
<FilteredListToolbar
|
||||
filter={filter}
|
||||
listSelect={listSelect}
|
||||
setFilter={setFilter}
|
||||
showEditFilter={showEditFilter}
|
||||
onDelete={onDelete}
|
||||
onEdit={onEdit}
|
||||
operationComponent={operations}
|
||||
view={view}
|
||||
zoomable
|
||||
/>
|
||||
|
||||
<FilterTags
|
||||
criteria={filter.criteria}
|
||||
onEditCriterion={(c) => showEditFilter(c.criterionOption.type)}
|
||||
onRemoveCriterion={removeCriterion}
|
||||
onRemoveAll={clearAllCriteria}
|
||||
/>
|
||||
|
||||
<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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LoadedContent loading={result.loading} error={result.error}>
|
||||
<GalleryList
|
||||
filter={effectiveFilter}
|
||||
galleries={items}
|
||||
selectedIds={selectedIds}
|
||||
onSelectChange={onSelectChange}
|
||||
/>
|
||||
</LoadedContent>
|
||||
|
||||
{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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SidebarPaneContent>
|
||||
</SidebarPane>
|
||||
</SidebarStateContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -229,7 +229,6 @@ div.GalleryWall {
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
width: 96vw;
|
||||
|
||||
/* Prevents last row from consuming all space and stretching images to oblivion */
|
||||
&::after {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { Option } from "./SidebarListFilter";
|
|||
import {
|
||||
CriterionModifier,
|
||||
FilterMode,
|
||||
GalleryFilterType,
|
||||
InputMaybe,
|
||||
IntCriterionInput,
|
||||
SceneFilterType,
|
||||
|
|
@ -515,12 +516,14 @@ export function makeQueryVariables(query: string, extraProps: {}) {
|
|||
interface IFilterType {
|
||||
scenes_filter?: InputMaybe<SceneFilterType>;
|
||||
scene_count?: InputMaybe<IntCriterionInput>;
|
||||
galleries_filter?: InputMaybe<GalleryFilterType>;
|
||||
gallery_count?: InputMaybe<IntCriterionInput>;
|
||||
}
|
||||
|
||||
export function setObjectFilter(
|
||||
out: IFilterType,
|
||||
mode: FilterMode,
|
||||
relatedFilterOutput: SceneFilterType
|
||||
relatedFilterOutput: SceneFilterType | GalleryFilterType
|
||||
) {
|
||||
const empty = Object.keys(relatedFilterOutput).length === 0;
|
||||
|
||||
|
|
@ -535,5 +538,17 @@ export function setObjectFilter(
|
|||
}
|
||||
out.scenes_filter = relatedFilterOutput;
|
||||
break;
|
||||
case FilterMode.Galleries:
|
||||
// if empty, only get objects with galleries
|
||||
if (empty) {
|
||||
out.gallery_count = {
|
||||
modifier: CriterionModifier.GreaterThan,
|
||||
value: 0,
|
||||
};
|
||||
}
|
||||
out.galleries_filter = relatedFilterOutput;
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid filter mode");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import React, { ReactNode, useMemo } from "react";
|
||||
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||
import {
|
||||
PerformersCriterion,
|
||||
PerformersCriterionOption,
|
||||
} from "src/models/list-filter/criteria/performers";
|
||||
import {
|
||||
CriterionModifier,
|
||||
FindPerformersForSelectQueryVariables,
|
||||
|
|
@ -18,6 +21,7 @@ import {
|
|||
useLabeledIdFilterState,
|
||||
} from "./LabeledIdFilter";
|
||||
import { SidebarListFilter } from "./SidebarListFilter";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface IPerformersFilter {
|
||||
criterion: PerformersCriterion;
|
||||
|
|
@ -106,12 +110,19 @@ const PerformersFilter: React.FC<IPerformersFilter> = ({
|
|||
|
||||
export const SidebarPerformersFilter: React.FC<{
|
||||
title?: ReactNode;
|
||||
option: CriterionOption;
|
||||
option?: CriterionOption;
|
||||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||
sectionID?: string;
|
||||
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||
}> = ({
|
||||
title = <FormattedMessage id="performers" />,
|
||||
option = PerformersCriterionOption,
|
||||
filter,
|
||||
setFilter,
|
||||
filterHook,
|
||||
sectionID = "performers",
|
||||
}) => {
|
||||
const state = useLabeledIdFilterState({
|
||||
filter,
|
||||
setFilter,
|
||||
|
|
@ -120,7 +131,14 @@ export const SidebarPerformersFilter: React.FC<{
|
|||
useQuery: usePerformerQueryFilter,
|
||||
});
|
||||
|
||||
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||
return (
|
||||
<SidebarListFilter
|
||||
{...state}
|
||||
data-type={option.type}
|
||||
title={title}
|
||||
sectionID={sectionID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PerformersFilter;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ import {
|
|||
defaultRatingSystemOptions,
|
||||
} from "src/utils/rating";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { RatingCriterion } from "src/models/list-filter/criteria/rating";
|
||||
import {
|
||||
RatingCriterion,
|
||||
RatingCriterionOption,
|
||||
} from "src/models/list-filter/criteria/rating";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { Option, SidebarListFilter } from "./SidebarListFilter";
|
||||
|
||||
|
|
@ -74,7 +77,7 @@ export const RatingFilter: React.FC<IRatingFilterProps> = ({
|
|||
|
||||
interface ISidebarFilter {
|
||||
title?: React.ReactNode;
|
||||
option: CriterionOption;
|
||||
option?: CriterionOption;
|
||||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
sectionID?: string;
|
||||
|
|
@ -84,11 +87,11 @@ const any = "any";
|
|||
const none = "none";
|
||||
|
||||
export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
||||
title,
|
||||
option,
|
||||
title = <FormattedMessage id="rating" />,
|
||||
option = RatingCriterionOption,
|
||||
filter,
|
||||
setFilter,
|
||||
sectionID,
|
||||
sectionID = "rating",
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
@ -193,6 +196,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
|||
return (
|
||||
<>
|
||||
<SidebarListFilter
|
||||
data-type={option.type}
|
||||
title={title}
|
||||
candidates={options}
|
||||
onSelect={onSelect}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ import { Option, SidebarListFilter } from "./SidebarListFilter";
|
|||
import TextUtils from "src/utils/text";
|
||||
import { DoubleRangeInput } from "src/components/Shared/DoubleRangeInput";
|
||||
import { useDebounce } from "src/hooks/debounce";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { DurationCriterionOption } from "src/models/list-filter/scenes";
|
||||
|
||||
interface ISidebarFilter {
|
||||
title?: React.ReactNode;
|
||||
option: CriterionOption;
|
||||
option?: CriterionOption;
|
||||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
sectionID?: string;
|
||||
|
|
@ -55,11 +57,11 @@ function snapToStep(value: number): number {
|
|||
}
|
||||
|
||||
export const SidebarDurationFilter: React.FC<ISidebarFilter> = ({
|
||||
title,
|
||||
option,
|
||||
title = <FormattedMessage id="duration" />,
|
||||
option = DurationCriterionOption,
|
||||
filter,
|
||||
setFilter,
|
||||
sectionID,
|
||||
sectionID = "duration",
|
||||
}) => {
|
||||
const criteria = filter.criteriaFor(option.type) as DurationCriterion[];
|
||||
const criterion = criteria.length > 0 ? criteria[0] : null;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import {
|
|||
useFindStudiosForSelectQuery,
|
||||
} from "src/core/generated-graphql";
|
||||
import { HierarchicalObjectsFilter } from "./SelectableFilter";
|
||||
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||
import {
|
||||
StudiosCriterion,
|
||||
StudiosCriterionOption,
|
||||
} from "src/models/list-filter/criteria/studios";
|
||||
import { sortByRelevance } from "src/utils/query";
|
||||
import { CriterionOption } from "src/models/list-filter/criteria/criterion";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
|
@ -16,6 +19,7 @@ import {
|
|||
useLabeledIdFilterState,
|
||||
} from "./LabeledIdFilter";
|
||||
import { SidebarListFilter } from "./SidebarListFilter";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface IStudiosFilter {
|
||||
criterion: StudiosCriterion;
|
||||
|
|
@ -94,12 +98,19 @@ const StudiosFilter: React.FC<IStudiosFilter> = ({
|
|||
|
||||
export const SidebarStudiosFilter: React.FC<{
|
||||
title?: ReactNode;
|
||||
option: CriterionOption;
|
||||
option?: CriterionOption;
|
||||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||
sectionID?: string;
|
||||
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||
}> = ({
|
||||
title = <FormattedMessage id="studios" />,
|
||||
option = StudiosCriterionOption,
|
||||
filter,
|
||||
setFilter,
|
||||
filterHook,
|
||||
sectionID = "studios",
|
||||
}) => {
|
||||
const state = useLabeledIdFilterState({
|
||||
filter,
|
||||
setFilter,
|
||||
|
|
@ -111,7 +122,14 @@ export const SidebarStudiosFilter: React.FC<{
|
|||
includeSubMessageID: "subsidiary_studios",
|
||||
});
|
||||
|
||||
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||
return (
|
||||
<SidebarListFilter
|
||||
{...state}
|
||||
data-type={option.type}
|
||||
title={title}
|
||||
sectionID={sectionID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default StudiosFilter;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ import {
|
|||
useLabeledIdFilterState,
|
||||
} from "./LabeledIdFilter";
|
||||
import { SidebarListFilter } from "./SidebarListFilter";
|
||||
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||
import {
|
||||
TagsCriterion,
|
||||
TagsCriterionOption,
|
||||
} from "src/models/list-filter/criteria/tags";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface ITagsFilter {
|
||||
criterion: TagsCriterion;
|
||||
|
|
@ -99,12 +103,19 @@ const TagsFilter: React.FC<ITagsFilter> = ({ criterion, setCriterion }) => {
|
|||
|
||||
export const SidebarTagsFilter: React.FC<{
|
||||
title?: ReactNode;
|
||||
option: CriterionOption;
|
||||
option?: CriterionOption;
|
||||
filter: ListFilterModel;
|
||||
setFilter: (f: ListFilterModel) => void;
|
||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||
sectionID?: string;
|
||||
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||
}> = ({
|
||||
title = <FormattedMessage id="tags" />,
|
||||
option = TagsCriterionOption,
|
||||
filter,
|
||||
setFilter,
|
||||
filterHook,
|
||||
sectionID = "tags",
|
||||
}) => {
|
||||
const state = useLabeledIdFilterState({
|
||||
filter,
|
||||
setFilter,
|
||||
|
|
@ -115,7 +126,14 @@ export const SidebarTagsFilter: React.FC<{
|
|||
includeSubMessageID: "sub_tags",
|
||||
});
|
||||
|
||||
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||
return (
|
||||
<SidebarListFilter
|
||||
{...state}
|
||||
data-type={option.type}
|
||||
title={title}
|
||||
sectionID={sectionID}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsFilter;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
|||
import { Icon } from "../Shared/Icon";
|
||||
import {
|
||||
faEllipsisH,
|
||||
faPencil,
|
||||
faPencilAlt,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faTrash,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import cx from "classnames";
|
||||
|
|
@ -264,3 +267,108 @@ export const ListOperationButtons: React.FC<IListOperationButtonsProps> = ({
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IListOperations {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
isDisplayed?: () => boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ListOperations: React.FC<{
|
||||
items: number;
|
||||
hasSelection?: boolean;
|
||||
operations?: IListOperations[];
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
onPlay?: () => void;
|
||||
onCreateNew?: () => void;
|
||||
entityType?: string;
|
||||
operationsClassName?: string;
|
||||
operationsMenuClassName?: string;
|
||||
}> = ({
|
||||
items,
|
||||
hasSelection = false,
|
||||
operations = [],
|
||||
onEdit,
|
||||
onDelete,
|
||||
onPlay,
|
||||
onCreateNew,
|
||||
entityType,
|
||||
operationsClassName = "list-operations",
|
||||
operationsMenuClassName,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className="list-operations">
|
||||
<ButtonGroup>
|
||||
{!!items && onPlay && (
|
||||
<Button
|
||||
className="play-button"
|
||||
variant="secondary"
|
||||
onClick={() => onPlay()}
|
||||
title={intl.formatMessage({ id: "actions.play" })}
|
||||
>
|
||||
<Icon icon={faPlay} />
|
||||
</Button>
|
||||
)}
|
||||
{!hasSelection && onCreateNew && (
|
||||
<Button
|
||||
className="create-new-button"
|
||||
variant="secondary"
|
||||
onClick={() => onCreateNew()}
|
||||
title={intl.formatMessage(
|
||||
{ id: "actions.create_entity" },
|
||||
{ entityType }
|
||||
)}
|
||||
>
|
||||
<Icon icon={faPlus} />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{hasSelection && (onEdit || onDelete) && (
|
||||
<>
|
||||
{onEdit && (
|
||||
<Button variant="secondary" onClick={() => onEdit()}>
|
||||
<Icon icon={faPencil} />
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<Button
|
||||
variant="danger"
|
||||
className="btn-danger-minimal"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
<Icon icon={faTrash} />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{operations.length > 0 && (
|
||||
<OperationDropdown
|
||||
className={operationsClassName}
|
||||
menuClassName={operationsMenuClassName}
|
||||
menuPortalTarget={document.body}
|
||||
>
|
||||
{operations.map((o) => {
|
||||
if (o.isDisplayed && !o.isDisplayed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OperationDropdownItem
|
||||
key={o.text}
|
||||
onClick={o.onClick}
|
||||
text={o.text}
|
||||
className={o.className}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</OperationDropdown>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1120,7 +1120,8 @@ input[type="range"].zoom-slider {
|
|||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.scene-list-toolbar .selected-items-info {
|
||||
.scene-list-toolbar .selected-items-info,
|
||||
.gallery-list-toolbar .selected-items-info {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ const allMenuItems: IMenuItem[] = [
|
|||
href: "/galleries",
|
||||
icon: faImages,
|
||||
hotkey: "g l",
|
||||
userCreatable: true,
|
||||
},
|
||||
{
|
||||
name: "performers",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { GalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { FilteredGalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { usePerformerFilterHook } from "src/core/performers";
|
||||
import { View } from "src/components/List/views";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
|
@ -14,7 +14,7 @@ export const PerformerGalleriesPanel: React.FC<IPerformerDetailsProps> =
|
|||
PatchComponent("PerformerGalleriesPanel", ({ active, performer }) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<GalleryList
|
||||
<FilteredGalleryList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerGalleries}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { GalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { FilteredGalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { useStudioFilterHook } from "src/core/studios";
|
||||
import { View } from "src/components/List/views";
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ export const StudioGalleriesPanel: React.FC<IStudioGalleriesPanel> = ({
|
|||
}) => {
|
||||
const filterHook = useStudioFilterHook(studio, showChildStudioContent);
|
||||
return (
|
||||
<GalleryList
|
||||
<FilteredGalleryList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.StudioGalleries}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useTagFilterHook } from "src/core/tags";
|
||||
import { GalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { FilteredGalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { View } from "src/components/List/views";
|
||||
|
||||
interface ITagGalleriesPanel {
|
||||
|
|
@ -17,7 +17,7 @@ export const TagGalleriesPanel: React.FC<ITagGalleriesPanel> = ({
|
|||
}) => {
|
||||
const filterHook = useTagFilterHook(tag, showSubTagContent);
|
||||
return (
|
||||
<GalleryList
|
||||
<FilteredGalleryList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.TagGalleries}
|
||||
|
|
|
|||
|
|
@ -229,6 +229,8 @@ Returns `void`.
|
|||
- `DetailImage`
|
||||
- `ExternalLinkButtons`
|
||||
- `ExternalLinksButton`
|
||||
- `FilteredGalleryList`
|
||||
- `FilteredSceneList`
|
||||
- `FolderSelect`
|
||||
- `FrontPage`
|
||||
- `GalleryCard`
|
||||
|
|
@ -238,6 +240,7 @@ Returns `void`.
|
|||
- `GalleryCard.Popovers`
|
||||
- `GalleryCardGrid`
|
||||
- `GalleryIDSelect`
|
||||
- `GalleryList`
|
||||
- `GalleryRecommendationRow`
|
||||
- `GallerySelect`
|
||||
- `GallerySelect.sort`
|
||||
|
|
@ -308,6 +311,7 @@ Returns `void`.
|
|||
- `SceneMarkerCard.Popovers`
|
||||
- `SceneMarkerCardsGrid`
|
||||
- `SceneMarkerRecommendationRow`
|
||||
- `SceneList`
|
||||
- `ScenePage`
|
||||
- `ScenePage.TabContent`
|
||||
- `ScenePage.Tabs`
|
||||
|
|
|
|||
|
|
@ -526,8 +526,6 @@ textarea.text-input {
|
|||
}
|
||||
|
||||
.zoom-1 {
|
||||
width: 320px;
|
||||
|
||||
.gallery-card-image,
|
||||
.tag-card-image {
|
||||
height: 240px;
|
||||
|
|
|
|||
2
ui/v2.5/src/pluginApi.d.ts
vendored
2
ui/v2.5/src/pluginApi.d.ts
vendored
|
|
@ -666,6 +666,8 @@ declare namespace PluginApi {
|
|||
DetailImage: React.FC<any>;
|
||||
ExternalLinkButtons: React.FC<any>;
|
||||
ExternalLinksButton: React.FC<any>;
|
||||
FilteredGalleryList: React.FC<any>;
|
||||
FilteredSceneList: React.FC<any>;
|
||||
FolderSelect: React.FC<any>;
|
||||
FrontPage: React.FC<any>;
|
||||
GalleryCard: React.FC<any>;
|
||||
|
|
|
|||
Loading…
Reference in a new issue