Add more patchable components (#6404)

This commit is contained in:
WithoutPants
2025-12-15 07:28:58 +11:00
committed by GitHub
parent 67b1dd8dd0
commit 62babfb332
12 changed files with 1570 additions and 1505 deletions

View File

@@ -9,98 +9,102 @@ import { useToast } from "src/hooks/Toast";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { galleryTitle } from "src/core/galleries"; import { galleryTitle } from "src/core/galleries";
import { IItemListOperation } from "src/components/List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
interface IGalleryAddProps { interface IGalleryAddProps {
active: boolean; active: boolean;
gallery: GQL.GalleryDataFragment; gallery: GQL.GalleryDataFragment;
extraOperations?: IItemListOperation<GQL.FindImagesQueryResult>[];
} }
export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ export const GalleryAddPanel: React.FC<IGalleryAddProps> = PatchComponent(
active, "GalleryAddPanel",
gallery, ({ active, gallery, extraOperations = [] }) => {
}) => { const Toast = useToast();
const Toast = useToast(); const intl = useIntl();
const intl = useIntl();
function filterHook(filter: ListFilterModel) { function filterHook(filter: ListFilterModel) {
const galleryValue = { const galleryValue = {
id: gallery.id, id: gallery.id,
label: galleryTitle(gallery), label: galleryTitle(gallery),
}; };
// if galleries is already present, then we modify it, otherwise add // if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => { let galleryCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "galleries"; return c.criterionOption.type === "galleries";
}) as GalleriesCriterion | undefined; }) as GalleriesCriterion | undefined;
if (
galleryCriterion &&
galleryCriterion.modifier === GQL.CriterionModifier.Excludes
) {
// add the gallery if not present
if ( if (
!galleryCriterion.value.find((p) => { galleryCriterion &&
return p.id === gallery.id; galleryCriterion.modifier === GQL.CriterionModifier.Excludes
})
) { ) {
galleryCriterion.value.push(galleryValue); // add the gallery if not present
if (
!galleryCriterion.value.find((p) => {
return p.id === gallery.id;
})
) {
galleryCriterion.value.push(galleryValue);
}
galleryCriterion.modifier = GQL.CriterionModifier.Excludes;
} else {
// overwrite
galleryCriterion = new GalleriesCriterion();
galleryCriterion.modifier = GQL.CriterionModifier.Excludes;
galleryCriterion.value = [galleryValue];
filter.criteria.push(galleryCriterion);
} }
galleryCriterion.modifier = GQL.CriterionModifier.Excludes; return filter;
} else {
// overwrite
galleryCriterion = new GalleriesCriterion();
galleryCriterion.modifier = GQL.CriterionModifier.Excludes;
galleryCriterion.value = [galleryValue];
filter.criteria.push(galleryCriterion);
} }
return filter; async function addImages(
} result: GQL.FindImagesQueryResult,
filter: ListFilterModel,
async function addImages( selectedIds: Set<string>
result: GQL.FindImagesQueryResult, ) {
filter: ListFilterModel, try {
selectedIds: Set<string> await mutateAddGalleryImages({
) { gallery_id: gallery.id!,
try { image_ids: Array.from(selectedIds.values()),
await mutateAddGalleryImages({ });
gallery_id: gallery.id!, const imageCount = selectedIds.size;
image_ids: Array.from(selectedIds.values()), Toast.success(
}); intl.formatMessage(
const imageCount = selectedIds.size; { id: "toast.added_entity" },
Toast.success( {
intl.formatMessage( count: imageCount,
{ id: "toast.added_entity" }, singularEntity: intl.formatMessage({ id: "image" }),
{ pluralEntity: intl.formatMessage({ id: "images" }),
count: imageCount, }
singularEntity: intl.formatMessage({ id: "image" }), )
pluralEntity: intl.formatMessage({ id: "images" }), );
} } catch (e) {
) Toast.error(e);
); }
} catch (e) {
Toast.error(e);
} }
const otherOperations = [
...extraOperations,
{
text: intl.formatMessage(
{ id: "actions.add_to_entity" },
{ entityType: intl.formatMessage({ id: "gallery" }) }
),
onClick: addImages,
isDisplayed: showWhenSelected,
postRefetch: true,
icon: faPlus,
},
];
return (
<ImageList
filterHook={filterHook}
extraOperations={otherOperations}
alterQuery={active}
/>
);
} }
);
const otherOperations = [
{
text: intl.formatMessage(
{ id: "actions.add_to_entity" },
{ entityType: intl.formatMessage({ id: "gallery" }) }
),
onClick: addImages,
isDisplayed: showWhenSelected,
postRefetch: true,
icon: faPlus,
},
];
return (
<ImageList
filterHook={filterHook}
extraOperations={otherOperations}
alterQuery={active}
/>
);
};

View File

@@ -16,132 +16,139 @@ import { useIntl } from "react-intl";
import { faMinus } from "@fortawesome/free-solid-svg-icons"; import { faMinus } from "@fortawesome/free-solid-svg-icons";
import { galleryTitle } from "src/core/galleries"; import { galleryTitle } from "src/core/galleries";
import { View } from "src/components/List/views"; import { View } from "src/components/List/views";
import { PatchComponent } from "src/patch";
import { IItemListOperation } from "src/components/List/FilteredListToolbar";
interface IGalleryDetailsProps { interface IGalleryDetailsProps {
active: boolean; active: boolean;
gallery: GQL.GalleryDataFragment; gallery: GQL.GalleryDataFragment;
extraOperations?: IItemListOperation<GQL.FindImagesQueryResult>[];
} }
export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> =
active, PatchComponent(
gallery, "GalleryImagesPanel",
}) => { ({ active, gallery, extraOperations = [] }) => {
const intl = useIntl(); const intl = useIntl();
const Toast = useToast(); const Toast = useToast();
function filterHook(filter: ListFilterModel) { function filterHook(filter: ListFilterModel) {
const galleryValue = { const galleryValue = {
id: gallery.id!, id: gallery.id!,
label: galleryTitle(gallery), label: galleryTitle(gallery),
}; };
// if galleries is already present, then we modify it, otherwise add // if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => { let galleryCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "galleries"; return c.criterionOption.type === "galleries";
}) as GalleriesCriterion | undefined; }) as GalleriesCriterion | undefined;
if ( if (
galleryCriterion && galleryCriterion &&
(galleryCriterion.modifier === GQL.CriterionModifier.IncludesAll || (galleryCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
galleryCriterion.modifier === GQL.CriterionModifier.Includes) galleryCriterion.modifier === GQL.CriterionModifier.Includes)
) { ) {
// add the gallery if not present // add the gallery if not present
if ( if (
!galleryCriterion.value.find((p) => { !galleryCriterion.value.find((p) => {
return p.id === gallery.id; return p.id === gallery.id;
}) })
) { ) {
galleryCriterion.value.push(galleryValue); galleryCriterion.value.push(galleryValue);
}
galleryCriterion.modifier = GQL.CriterionModifier.IncludesAll;
} else {
// overwrite
galleryCriterion = new GalleriesCriterion();
galleryCriterion.value = [galleryValue];
filter.criteria.push(galleryCriterion);
}
return filter;
} }
galleryCriterion.modifier = GQL.CriterionModifier.IncludesAll; async function setCover(
} else { result: GQL.FindImagesQueryResult,
// overwrite filter: ListFilterModel,
galleryCriterion = new GalleriesCriterion(); selectedIds: Set<string>
galleryCriterion.value = [galleryValue]; ) {
filter.criteria.push(galleryCriterion); const coverImageID = selectedIds.values().next();
} if (coverImageID.done) {
// operation should only be displayed when exactly one image is selected
return;
}
try {
await mutateSetGalleryCover({
gallery_id: gallery.id!,
cover_image_id: coverImageID.value,
});
return filter; Toast.success(
} intl.formatMessage(
{ id: "toast.updated_entity" },
{
entity: intl
.formatMessage({ id: "gallery" })
.toLocaleLowerCase(),
}
)
);
} catch (e) {
Toast.error(e);
}
}
async function setCover( async function removeImages(
result: GQL.FindImagesQueryResult, result: GQL.FindImagesQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string> selectedIds: Set<string>
) { ) {
const coverImageID = selectedIds.values().next(); try {
if (coverImageID.done) { await mutateRemoveGalleryImages({
// operation should only be displayed when exactly one image is selected gallery_id: gallery.id!,
return; image_ids: Array.from(selectedIds.values()),
} });
try {
await mutateSetGalleryCover({
gallery_id: gallery.id!,
cover_image_id: coverImageID.value,
});
Toast.success( Toast.success(
intl.formatMessage( intl.formatMessage(
{ id: "toast.updated_entity" }, { id: "toast.removed_entity" },
{ {
entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase(), count: selectedIds.size,
} singularEntity: intl.formatMessage({ id: "image" }),
) pluralEntity: intl.formatMessage({ id: "images" }),
}
)
);
} catch (e) {
Toast.error(e);
}
}
const otherOperations = [
...extraOperations,
{
text: intl.formatMessage({ id: "actions.set_cover" }),
onClick: setCover,
isDisplayed: showWhenSingleSelection,
},
{
text: intl.formatMessage({ id: "actions.remove_from_gallery" }),
onClick: removeImages,
isDisplayed: showWhenSelected,
postRefetch: true,
icon: faMinus,
buttonVariant: "danger",
},
];
return (
<ImageList
filterHook={filterHook}
alterQuery={active}
extraOperations={otherOperations}
view={View.GalleryImages}
chapters={gallery.chapters}
/>
); );
} catch (e) {
Toast.error(e);
} }
}
async function removeImages(
result: GQL.FindImagesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>
) {
try {
await mutateRemoveGalleryImages({
gallery_id: gallery.id!,
image_ids: Array.from(selectedIds.values()),
});
Toast.success(
intl.formatMessage(
{ id: "toast.removed_entity" },
{
count: selectedIds.size,
singularEntity: intl.formatMessage({ id: "image" }),
pluralEntity: intl.formatMessage({ id: "images" }),
}
)
);
} catch (e) {
Toast.error(e);
}
}
const otherOperations = [
{
text: intl.formatMessage({ id: "actions.set_cover" }),
onClick: setCover,
isDisplayed: showWhenSingleSelection,
},
{
text: intl.formatMessage({ id: "actions.remove_from_gallery" }),
onClick: removeImages,
isDisplayed: showWhenSelected,
postRefetch: true,
icon: faMinus,
buttonVariant: "danger",
},
];
return (
<ImageList
filterHook={filterHook}
alterQuery={active}
extraOperations={otherOperations}
view={View.GalleryImages}
chapters={gallery.chapters}
/>
); );
};

View File

@@ -15,6 +15,8 @@ import { ExportDialog } from "../Shared/ExportDialog";
import { GalleryListTable } from "./GalleryListTable"; import { GalleryListTable } from "./GalleryListTable";
import { GalleryCardGrid } from "./GalleryGridCard"; import { GalleryCardGrid } from "./GalleryGridCard";
import { View } from "../List/views"; import { View } from "../List/views";
import { PatchComponent } from "src/patch";
import { IItemListOperation } from "../List/FilteredListToolbar";
function getItems(result: GQL.FindGalleriesQueryResult) { function getItems(result: GQL.FindGalleriesQueryResult) {
return result?.data?.findGalleries?.galleries ?? []; return result?.data?.findGalleries?.galleries ?? [];
@@ -28,180 +30,183 @@ interface IGalleryList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View; view?: View;
alterQuery?: boolean; alterQuery?: boolean;
extraOperations?: IItemListOperation<GQL.FindGalleriesQueryResult>[];
} }
export const GalleryList: React.FC<IGalleryList> = ({ export const GalleryList: React.FC<IGalleryList> = PatchComponent(
filterHook, "GalleryList",
view, ({ filterHook, view, alterQuery, extraOperations = [] }) => {
alterQuery, const intl = useIntl();
}) => { const history = useHistory();
const intl = useIntl(); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const history = useHistory(); const [isExportAll, setIsExportAll] = useState(false);
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const filterMode = GQL.FilterMode.Galleries; const filterMode = GQL.FilterMode.Galleries;
const otherOperations = [ const otherOperations = [
{ ...extraOperations,
text: intl.formatMessage({ id: "actions.view_random" }), {
onClick: viewRandom, text: intl.formatMessage({ id: "actions.view_random" }),
}, onClick: viewRandom,
{ },
text: intl.formatMessage({ id: "actions.export" }), {
onClick: onExport, text: intl.formatMessage({ id: "actions.export" }),
isDisplayed: showWhenSelected, onClick: onExport,
}, isDisplayed: showWhenSelected,
{ },
text: intl.formatMessage({ id: "actions.export_all" }), {
onClick: onExportAll, text: intl.formatMessage({ id: "actions.export_all" }),
}, onClick: onExportAll,
]; },
];
function addKeybinds( function addKeybinds(
result: GQL.FindGalleriesQueryResult, result: GQL.FindGalleriesQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
Mousetrap.bind("p r", () => { Mousetrap.bind("p r", () => {
viewRandom(result, filter); viewRandom(result, filter);
}); });
return () => { return () => {
Mousetrap.unbind("p r"); 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() { async function viewRandom(
setIsExportAll(false); result: GQL.FindGalleriesQueryResult,
setIsExportDialogOpen(true); filter: ListFilterModel
} ) {
// query for a random image
if (result.data?.findGalleries) {
const { count } = result.data.findGalleries;
async function onExportAll() { const index = Math.floor(Math.random() * count);
setIsExportAll(true); const filterCopy = cloneDeep(filter);
setIsExportDialogOpen(true); filterCopy.itemsPerPage = 1;
} filterCopy.currentPage = index + 1;
const singleResult = await queryFindGalleries(filterCopy);
function renderContent( if (singleResult.data.findGalleries.galleries.length === 1) {
result: GQL.FindGalleriesQueryResult, const { id } = singleResult.data.findGalleries.galleries[0];
filter: ListFilterModel, // navigate to the image player page
selectedIds: Set<string>, history.push(`/galleries/${id}`);
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() { async function onExport() {
if (!result.data?.findGalleries) return; setIsExportAll(false);
setIsExportDialogOpen(true);
}
if (filter.displayMode === DisplayMode.Grid) { async function onExportAll() {
return ( setIsExportAll(true);
<GalleryCardGrid setIsExportDialogOpen(true);
galleries={result.data.findGalleries.galleries} }
selectedIds={selectedIds}
zoomIndex={filter.zoomIndex} function renderContent(
onSelectChange={onSelectChange} 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)}
/>
);
}
} }
if (filter.displayMode === DisplayMode.List) {
return ( function renderGalleries() {
<GalleryListTable if (!result.data?.findGalleries) return;
galleries={result.data.findGalleries.galleries}
selectedIds={selectedIds} if (filter.displayMode === DisplayMode.Grid) {
onSelectChange={onSelectChange} return (
/> <GalleryCardGrid
); galleries={result.data.findGalleries.galleries}
} selectedIds={selectedIds}
if (filter.displayMode === DisplayMode.Wall) { zoomIndex={filter.zoomIndex}
return ( onSelectChange={onSelectChange}
<div className="row"> />
<div className={`GalleryWall zoom-${filter.zoomIndex}`}> );
{result.data.findGalleries.galleries.map((gallery) => ( }
<GalleryWallCard key={gallery.id} gallery={gallery} /> 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} />
))}
</div>
</div> </div>
</div> );
); }
} }
return (
<>
{maybeRenderGalleryExportDialog()}
{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} />
);
} }
return ( return (
<> <ItemListContext
{maybeRenderGalleryExportDialog()} filterMode={filterMode}
{renderGalleries()} useResult={useFindGalleries}
</> getItems={getItems}
); getCount={getCount}
} alterQuery={alterQuery}
filterHook={filterHook}
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} />
);
}
return (
<ItemListContext
filterMode={filterMode}
useResult={useFindGalleries}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
view={view} view={view}
otherOperations={otherOperations} selectable
addKeybinds={addKeybinds} >
renderContent={renderContent} <ItemList
renderEditDialog={renderEditDialog} view={view}
renderDeleteDialog={renderDeleteDialog} otherOperations={otherOperations}
/> addKeybinds={addKeybinds}
</ItemListContext> renderContent={renderContent}
); renderEditDialog={renderEditDialog}
}; renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
}
);

View File

@@ -18,7 +18,10 @@ import {
SearchTermInput, SearchTermInput,
} from "src/components/List/ListFilter"; } from "src/components/List/ListFilter";
import { useFilter } from "src/components/List/FilterProvider"; import { useFilter } from "src/components/List/FilterProvider";
import { IFilteredListToolbar } from "src/components/List/FilteredListToolbar"; import {
IFilteredListToolbar,
IItemListOperation,
} from "src/components/List/FilteredListToolbar";
import { import {
showWhenNoneSelected, showWhenNoneSelected,
showWhenSelected, showWhenSelected,
@@ -28,6 +31,7 @@ import { useIntl } from "react-intl";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { useModal } from "src/hooks/modal"; import { useModal } from "src/hooks/modal";
import { AddSubGroupsDialog } from "./AddGroupsDialog"; import { AddSubGroupsDialog } from "./AddGroupsDialog";
import { PatchComponent } from "src/patch";
const useContainingGroupFilterHook = ( const useContainingGroupFilterHook = (
group: Pick<GQL.StudioDataFragment, "id" | "name">, group: Pick<GQL.StudioDataFragment, "id" | "name">,
@@ -99,6 +103,7 @@ const Toolbar: React.FC<IFilteredListToolbar> = ({
interface IGroupSubGroupsPanel { interface IGroupSubGroupsPanel {
active: boolean; active: boolean;
group: GQL.GroupDataFragment; group: GQL.GroupDataFragment;
extraOperations?: IItemListOperation<GQL.FindGroupsQueryResult>[];
} }
const defaultFilter = (() => { const defaultFilter = (() => {
@@ -113,92 +118,99 @@ const defaultFilter = (() => {
return ret; return ret;
})(); })();
export const GroupSubGroupsPanel: React.FC<IGroupSubGroupsPanel> = ({ export const GroupSubGroupsPanel: React.FC<IGroupSubGroupsPanel> =
active, PatchComponent(
group, "GroupSubGroupsPanel",
}) => { ({ active, group, extraOperations = [] }) => {
const intl = useIntl(); const intl = useIntl();
const Toast = useToast(); const Toast = useToast();
const { modal, showModal, closeModal } = useModal(); const { modal, showModal, closeModal } = useModal();
const [reorderSubGroups] = useReorderSubGroupsMutation(); const [reorderSubGroups] = useReorderSubGroupsMutation();
const mutateRemoveSubGroups = useRemoveSubGroups(); const mutateRemoveSubGroups = useRemoveSubGroups();
const filterHook = useContainingGroupFilterHook(group); const filterHook = useContainingGroupFilterHook(group);
async function removeSubGroups( async function removeSubGroups(
result: GQL.FindGroupsQueryResult, result: GQL.FindGroupsQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string> selectedIds: Set<string>
) { ) {
try { try {
await mutateRemoveSubGroups(group.id, Array.from(selectedIds.values())); await mutateRemoveSubGroups(
group.id,
Array.from(selectedIds.values())
);
Toast.success( Toast.success(
intl.formatMessage( intl.formatMessage(
{ id: "toast.removed_entity" }, { id: "toast.removed_entity" },
{ {
count: selectedIds.size, count: selectedIds.size,
singularEntity: intl.formatMessage({ id: "group" }), singularEntity: intl.formatMessage({ id: "group" }),
pluralEntity: intl.formatMessage({ id: "groups" }), pluralEntity: intl.formatMessage({ id: "groups" }),
} }
) )
); );
} catch (e) { } catch (e) {
Toast.error(e); Toast.error(e);
} }
} }
async function onAddSubGroups() { async function onAddSubGroups() {
showModal( showModal(
<AddSubGroupsDialog containingGroup={group} onClose={closeModal} /> <AddSubGroupsDialog containingGroup={group} onClose={closeModal} />
); );
} }
const otherOperations = [ const otherOperations = [
{ ...extraOperations,
text: intl.formatMessage({ id: "actions.add_sub_groups" }), {
onClick: onAddSubGroups, text: intl.formatMessage({ id: "actions.add_sub_groups" }),
isDisplayed: showWhenNoneSelected, onClick: onAddSubGroups,
postRefetch: true, isDisplayed: showWhenNoneSelected,
icon: faPlus, postRefetch: true,
buttonVariant: "secondary", icon: faPlus,
}, buttonVariant: "secondary",
{
text: intl.formatMessage({ id: "actions.remove_from_containing_group" }),
onClick: removeSubGroups,
isDisplayed: showWhenSelected,
postRefetch: true,
icon: faMinus,
buttonVariant: "danger",
},
];
function onMove(srcIds: string[], targetId: string, after: boolean) {
reorderSubGroups({
variables: {
input: {
group_id: group.id,
sub_group_ids: srcIds,
insert_at_id: targetId,
insert_after: after,
}, },
}, {
}); text: intl.formatMessage({
} id: "actions.remove_from_containing_group",
}),
onClick: removeSubGroups,
isDisplayed: showWhenSelected,
postRefetch: true,
icon: faMinus,
buttonVariant: "danger",
},
];
return ( function onMove(srcIds: string[], targetId: string, after: boolean) {
<> reorderSubGroups({
{modal} variables: {
<GroupList input: {
defaultFilter={defaultFilter} group_id: group.id,
filterHook={filterHook} sub_group_ids: srcIds,
alterQuery={active} insert_at_id: targetId,
fromGroupId={group.id} insert_after: after,
otherOperations={otherOperations} },
onMove={onMove} },
renderToolbar={(props) => <Toolbar {...props} />} });
/> }
</>
return (
<>
{modal}
<GroupList
defaultFilter={defaultFilter}
filterHook={filterHook}
alterQuery={active}
fromGroupId={group.id}
otherOperations={otherOperations}
onMove={onMove}
renderToolbar={(props) => <Toolbar {...props} />}
/>
</>
);
}
); );
};

View File

@@ -21,6 +21,7 @@ import {
IFilteredListToolbar, IFilteredListToolbar,
IItemListOperation, IItemListOperation,
} from "../List/FilteredListToolbar"; } from "../List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
const GroupExportDialog: React.FC<{ const GroupExportDialog: React.FC<{
open?: boolean; open?: boolean;
@@ -90,150 +91,153 @@ interface IGroupList extends IGroupListContext {
otherOperations?: IItemListOperation<GQL.FindGroupsQueryResult>[]; otherOperations?: IItemListOperation<GQL.FindGroupsQueryResult>[];
} }
export const GroupList: React.FC<IGroupList> = ({ export const GroupList: React.FC<IGroupList> = PatchComponent(
filterHook, "GroupList",
alterQuery, ({
defaultFilter, filterHook,
view, alterQuery,
fromGroupId, defaultFilter,
onMove, view,
selectable, fromGroupId,
renderToolbar, onMove,
otherOperations: providedOperations = [], selectable,
}) => { renderToolbar,
const intl = useIntl(); otherOperations: providedOperations = [],
const history = useHistory(); }) => {
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); const intl = useIntl();
const [isExportAll, setIsExportAll] = useState(false); const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const otherOperations = [ const otherOperations = [
{ {
text: intl.formatMessage({ id: "actions.view_random" }), text: intl.formatMessage({ id: "actions.view_random" }),
onClick: viewRandom, onClick: viewRandom,
}, },
{ {
text: intl.formatMessage({ id: "actions.export" }), text: intl.formatMessage({ id: "actions.export" }),
onClick: onExport, onClick: onExport,
isDisplayed: showWhenSelected, isDisplayed: showWhenSelected,
}, },
{ {
text: intl.formatMessage({ id: "actions.export_all" }), text: intl.formatMessage({ id: "actions.export_all" }),
onClick: onExportAll, onClick: onExportAll,
}, },
...providedOperations, ...providedOperations,
]; ];
function addKeybinds( function addKeybinds(
result: GQL.FindGroupsQueryResult, result: GQL.FindGroupsQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
Mousetrap.bind("p r", () => { Mousetrap.bind("p r", () => {
viewRandom(result, filter); viewRandom(result, filter);
}); });
return () => { return () => {
Mousetrap.unbind("p r"); Mousetrap.unbind("p r");
}; };
} }
async function viewRandom( async function viewRandom(
result: GQL.FindGroupsQueryResult, result: GQL.FindGroupsQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
// query for a random image // query for a random image
if (result.data?.findGroups) { if (result.data?.findGroups) {
const { count } = result.data.findGroups; const { count } = result.data.findGroups;
const index = Math.floor(Math.random() * count); const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter); const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await queryFindGroups(filterCopy); const singleResult = await queryFindGroups(filterCopy);
if (singleResult.data.findGroups.groups.length === 1) { if (singleResult.data.findGroups.groups.length === 1) {
const { id } = singleResult.data.findGroups.groups[0]; const { id } = singleResult.data.findGroups.groups[0];
// navigate to the group page // navigate to the group page
history.push(`/groups/${id}`); history.push(`/groups/${id}`);
}
} }
} }
}
async function onExport() { async function onExport() {
setIsExportAll(false); setIsExportAll(false);
setIsExportDialogOpen(true); setIsExportDialogOpen(true);
} }
async function onExportAll() { async function onExportAll() {
setIsExportAll(true); setIsExportAll(true);
setIsExportDialogOpen(true); setIsExportDialogOpen(true);
} }
function renderContent( function renderContent(
result: GQL.FindGroupsQueryResult, result: GQL.FindGroupsQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) { ) {
return ( return (
<> <>
<GroupExportDialog <GroupExportDialog
open={isExportDialogOpen} open={isExportDialogOpen}
selectedIds={selectedIds}
isExportAll={isExportAll}
onClose={() => setIsExportDialogOpen(false)}
/>
{filter.displayMode === DisplayMode.Grid && (
<GroupCardGrid
groups={result.data?.findGroups.groups ?? []}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds} selectedIds={selectedIds}
onSelectChange={onSelectChange} isExportAll={isExportAll}
fromGroupId={fromGroupId} onClose={() => setIsExportDialogOpen(false)}
onMove={onMove}
/> />
)} {filter.displayMode === DisplayMode.Grid && (
</> <GroupCardGrid
); groups={result.data?.findGroups.groups ?? []}
} zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
fromGroupId={fromGroupId}
onMove={onMove}
/>
)}
</>
);
}
function renderEditDialog( function renderEditDialog(
selectedGroups: GQL.GroupDataFragment[], selectedGroups: GQL.GroupDataFragment[],
onClose: (applied: boolean) => void onClose: (applied: boolean) => void
) { ) {
return <EditGroupsDialog selected={selectedGroups} onClose={onClose} />; return <EditGroupsDialog selected={selectedGroups} onClose={onClose} />;
} }
function renderDeleteDialog(
selectedGroups: GQL.SlimGroupDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteEntityDialog
selected={selectedGroups}
onClose={onClose}
singularEntity={intl.formatMessage({ id: "group" })}
pluralEntity={intl.formatMessage({ id: "groups" })}
destroyMutation={useGroupsDestroy}
/>
);
}
function renderDeleteDialog(
selectedGroups: GQL.SlimGroupDataFragment[],
onClose: (confirmed: boolean) => void
) {
return ( return (
<DeleteEntityDialog <GroupListContext
selected={selectedGroups} alterQuery={alterQuery}
onClose={onClose} filterHook={filterHook}
singularEntity={intl.formatMessage({ id: "group" })} view={view}
pluralEntity={intl.formatMessage({ id: "groups" })} defaultFilter={defaultFilter}
destroyMutation={useGroupsDestroy} selectable={selectable}
/> >
<ItemList
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
renderToolbar={renderToolbar}
/>
</GroupListContext>
); );
} }
);
return (
<GroupListContext
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
defaultFilter={defaultFilter}
selectable={selectable}
>
<ItemList
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
renderToolbar={renderToolbar}
/>
</GroupListContext>
);
};

View File

@@ -26,6 +26,7 @@ import { ImageGridCard } from "./ImageGridCard";
import { View } from "../List/views"; import { View } from "../List/views";
import { IItemListOperation } from "../List/FilteredListToolbar"; import { IItemListOperation } from "../List/FilteredListToolbar";
import { FileSize } from "../Shared/FileSize"; import { FileSize } from "../Shared/FileSize";
import { PatchComponent } from "src/patch";
interface IImageWallProps { interface IImageWallProps {
images: GQL.SlimImageDataFragment[]; images: GQL.SlimImageDataFragment[];
@@ -318,167 +319,168 @@ interface IImageList {
chapters?: GQL.GalleryChapterDataFragment[]; chapters?: GQL.GalleryChapterDataFragment[];
} }
export const ImageList: React.FC<IImageList> = ({ export const ImageList: React.FC<IImageList> = PatchComponent(
filterHook, "ImageList",
view, ({ filterHook, view, alterQuery, extraOperations = [], chapters = [] }) => {
alterQuery, const intl = useIntl();
extraOperations, const history = useHistory();
chapters = [], const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
}) => { const [isExportAll, setIsExportAll] = useState(false);
const intl = useIntl(); const [slideshowRunning, setSlideshowRunning] = useState<boolean>(false);
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 filterMode = GQL.FilterMode.Images;
const otherOperations = [ const otherOperations = [
...(extraOperations ?? []), ...extraOperations,
{ {
text: intl.formatMessage({ id: "actions.view_random" }), text: intl.formatMessage({ id: "actions.view_random" }),
onClick: viewRandom, onClick: viewRandom,
}, },
{ {
text: intl.formatMessage({ id: "actions.export" }), text: intl.formatMessage({ id: "actions.export" }),
onClick: onExport, onClick: onExport,
isDisplayed: showWhenSelected, isDisplayed: showWhenSelected,
}, },
{ {
text: intl.formatMessage({ id: "actions.export_all" }), text: intl.formatMessage({ id: "actions.export_all" }),
onClick: onExportAll, onClick: onExportAll,
}, },
]; ];
function addKeybinds( function addKeybinds(
result: GQL.FindImagesQueryResult, result: GQL.FindImagesQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
Mousetrap.bind("p r", () => { Mousetrap.bind("p r", () => {
viewRandom(result, filter); viewRandom(result, filter);
}); });
return () => { return () => {
Mousetrap.unbind("p r"); Mousetrap.unbind("p r");
}; };
} }
async function viewRandom( async function viewRandom(
result: GQL.FindImagesQueryResult, result: GQL.FindImagesQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
// query for a random image // query for a random image
if (result.data?.findImages) { if (result.data?.findImages) {
const { count } = result.data.findImages; const { count } = result.data.findImages;
const index = Math.floor(Math.random() * count); const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter); const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await queryFindImages(filterCopy); const singleResult = await queryFindImages(filterCopy);
if (singleResult.data.findImages.images.length === 1) { if (singleResult.data.findImages.images.length === 1) {
const { id } = singleResult.data.findImages.images[0]; const { id } = singleResult.data.findImages.images[0];
// navigate to the image player page // navigate to the image player page
history.push(`/images/${id}`); history.push(`/images/${id}`);
}
} }
} }
}
async function onExport() { async function onExport() {
setIsExportAll(false); setIsExportAll(false);
setIsExportDialogOpen(true); setIsExportDialogOpen(true);
} }
async function onExportAll() { async function onExportAll() {
setIsExportAll(true); setIsExportAll(true);
setIsExportDialogOpen(true); setIsExportDialogOpen(true);
} }
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;
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 ( return (
<ExportDialog <ImageListImages
exportInput={{ filter={filter}
images: { images={result.data.findImages.images}
ids: Array.from(selectedIds.values()), onChangePage={onChangePage}
all: isExportAll, onSelectChange={onSelectChange}
}, pageCount={pageCount}
}} selectedIds={selectedIds}
onClose={() => setIsExportDialogOpen(false)} slideshowRunning={slideshowRunning}
setSlideshowRunning={setSlideshowRunning}
chapters={chapters}
/> />
); );
} }
}
function renderImages() {
if (!result.data?.findImages) return;
return ( return (
<ImageListImages <>
filter={filter} {maybeRenderImageExportDialog()}
images={result.data.findImages.images} {renderImages()}
onChangePage={onChangePage} </>
onSelectChange={onSelectChange}
pageCount={pageCount}
selectedIds={selectedIds}
slideshowRunning={slideshowRunning}
setSlideshowRunning={setSlideshowRunning}
chapters={chapters}
/>
); );
} }
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} />;
}
return ( return (
<> <ItemListContext
{maybeRenderImageExportDialog()} filterMode={filterMode}
{renderImages()} useResult={useFindImages}
</> useMetadataInfo={useFindImagesMetadata}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
renderMetadataByline={renderMetadataByline}
/>
</ItemListContext>
); );
} }
);
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} />;
}
return (
<ItemListContext
filterMode={filterMode}
useResult={useFindImages}
useMetadataInfo={useFindImagesMetadata}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
renderMetadataByline={renderMetadataByline}
/>
</ItemListContext>
);
};

View File

@@ -22,6 +22,8 @@ import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { PerformerCardGrid } from "./PerformerCardGrid"; import { PerformerCardGrid } from "./PerformerCardGrid";
import { View } from "../List/views"; import { View } from "../List/views";
import { IItemListOperation } from "../List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
function getItems(result: GQL.FindPerformersQueryResult) { function getItems(result: GQL.FindPerformersQueryResult) {
return result?.data?.findPerformers?.performers ?? []; return result?.data?.findPerformers?.performers ?? [];
@@ -159,183 +161,185 @@ interface IPerformerList {
view?: View; view?: View;
alterQuery?: boolean; alterQuery?: boolean;
extraCriteria?: IPerformerCardExtraCriteria; extraCriteria?: IPerformerCardExtraCriteria;
extraOperations?: IItemListOperation<GQL.FindPerformersQueryResult>[];
} }
export const PerformerList: React.FC<IPerformerList> = ({ export const PerformerList: React.FC<IPerformerList> = PatchComponent(
filterHook, "PerformerList",
view, ({ filterHook, view, alterQuery, extraCriteria, extraOperations = [] }) => {
alterQuery, const intl = useIntl();
extraCriteria, const history = useHistory();
}) => { const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const intl = useIntl(); const [isExportAll, setIsExportAll] = useState(false);
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const filterMode = GQL.FilterMode.Performers; const filterMode = GQL.FilterMode.Performers;
const otherOperations = [ const otherOperations = [
{ ...extraOperations,
text: intl.formatMessage({ id: "actions.open_random" }), {
onClick: openRandom, text: intl.formatMessage({ id: "actions.open_random" }),
}, onClick: openRandom,
{ },
text: intl.formatMessage({ id: "actions.export" }), {
onClick: onExport, text: intl.formatMessage({ id: "actions.export" }),
isDisplayed: showWhenSelected, onClick: onExport,
}, isDisplayed: showWhenSelected,
{ },
text: intl.formatMessage({ id: "actions.export_all" }), {
onClick: onExportAll, text: intl.formatMessage({ id: "actions.export_all" }),
}, onClick: onExportAll,
]; },
];
function addKeybinds( function addKeybinds(
result: GQL.FindPerformersQueryResult, result: GQL.FindPerformersQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
Mousetrap.bind("p r", () => { Mousetrap.bind("p r", () => {
openRandom(result, filter); openRandom(result, filter);
}); });
return () => { return () => {
Mousetrap.unbind("p r"); Mousetrap.unbind("p r");
}; };
} }
async function openRandom( async function openRandom(
result: GQL.FindPerformersQueryResult, result: GQL.FindPerformersQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
if (result.data?.findPerformers) { if (result.data?.findPerformers) {
const { count } = result.data.findPerformers; const { count } = result.data.findPerformers;
const index = Math.floor(Math.random() * count); const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter); const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await queryFindPerformers(filterCopy); const singleResult = await queryFindPerformers(filterCopy);
if (singleResult.data.findPerformers.performers.length === 1) { if (singleResult.data.findPerformers.performers.length === 1) {
const { id } = singleResult.data.findPerformers.performers[0]!; const { id } = singleResult.data.findPerformers.performers[0]!;
history.push(`/performers/${id}`); history.push(`/performers/${id}`);
}
} }
} }
}
async function onExport() { async function onExport() {
setIsExportAll(false); setIsExportAll(false);
setIsExportDialogOpen(true); setIsExportDialogOpen(true);
} }
async function onExportAll() { async function onExportAll() {
setIsExportAll(true); setIsExportAll(true);
setIsExportDialogOpen(true); setIsExportDialogOpen(true);
} }
function renderContent( function renderContent(
result: GQL.FindPerformersQueryResult, result: GQL.FindPerformersQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) { ) {
function maybeRenderPerformerExportDialog() { function maybeRenderPerformerExportDialog() {
if (isExportDialogOpen) { if (isExportDialogOpen) {
return ( return (
<> <>
<ExportDialog <ExportDialog
exportInput={{ exportInput={{
performers: { performers: {
ids: Array.from(selectedIds.values()), ids: Array.from(selectedIds.values()),
all: isExportAll, all: isExportAll,
}, },
}} }}
onClose={() => setIsExportDialogOpen(false)} onClose={() => setIsExportDialogOpen(false)}
/>
</>
);
}
}
function renderPerformers() {
if (!result.data?.findPerformers) return;
if (filter.displayMode === DisplayMode.Grid) {
return (
<PerformerCardGrid
performers={result.data.findPerformers.performers}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
extraCriteria={extraCriteria}
/> />
</> );
); }
if (filter.displayMode === DisplayMode.List) {
return (
<PerformerListTable
performers={result.data.findPerformers.performers}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
if (filter.displayMode === DisplayMode.Tagger) {
return (
<PerformerTagger
performers={result.data.findPerformers.performers}
/>
);
}
} }
return (
<>
{maybeRenderPerformerExportDialog()}
{renderPerformers()}
</>
);
} }
function renderPerformers() { function renderEditDialog(
if (!result.data?.findPerformers) return; selectedPerformers: GQL.SlimPerformerDataFragment[],
onClose: (applied: boolean) => void
) {
return (
<EditPerformersDialog selected={selectedPerformers} onClose={onClose} />
);
}
if (filter.displayMode === DisplayMode.Grid) { function renderDeleteDialog(
return ( selectedPerformers: GQL.SlimPerformerDataFragment[],
<PerformerCardGrid onClose: (confirmed: boolean) => void
performers={result.data.findPerformers.performers} ) {
zoomIndex={filter.zoomIndex} return (
selectedIds={selectedIds} <DeleteEntityDialog
onSelectChange={onSelectChange} selected={selectedPerformers}
extraCriteria={extraCriteria} onClose={onClose}
/> singularEntity={intl.formatMessage({ id: "performer" })}
); pluralEntity={intl.formatMessage({ id: "performers" })}
} destroyMutation={usePerformersDestroy}
if (filter.displayMode === DisplayMode.List) { />
return ( );
<PerformerListTable
performers={result.data.findPerformers.performers}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
if (filter.displayMode === DisplayMode.Tagger) {
return (
<PerformerTagger performers={result.data.findPerformers.performers} />
);
}
} }
return ( return (
<> <ItemListContext
{maybeRenderPerformerExportDialog()} filterMode={filterMode}
{renderPerformers()} useResult={useFindPerformers}
</> getItems={getItems}
); getCount={getCount}
} alterQuery={alterQuery}
filterHook={filterHook}
function renderEditDialog(
selectedPerformers: GQL.SlimPerformerDataFragment[],
onClose: (applied: boolean) => void
) {
return (
<EditPerformersDialog selected={selectedPerformers} onClose={onClose} />
);
}
function renderDeleteDialog(
selectedPerformers: GQL.SlimPerformerDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteEntityDialog
selected={selectedPerformers}
onClose={onClose}
singularEntity={intl.formatMessage({ id: "performer" })}
pluralEntity={intl.formatMessage({ id: "performers" })}
destroyMutation={usePerformersDestroy}
/>
);
}
return (
<ItemListContext
filterMode={filterMode}
useResult={useFindPerformers}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
view={view} view={view}
otherOperations={otherOperations} selectable
addKeybinds={addKeybinds} >
renderContent={renderContent} <ItemList
renderEditDialog={renderEditDialog} view={view}
renderDeleteDialog={renderDeleteDialog} otherOperations={otherOperations}
/> addKeybinds={addKeybinds}
</ItemListContext> renderContent={renderContent}
); renderEditDialog={renderEditDialog}
}; renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
}
);

View File

@@ -66,7 +66,7 @@ import {
FilteredSidebarHeader, FilteredSidebarHeader,
useFilteredSidebarKeybinds, useFilteredSidebarKeybinds,
} from "../List/Filters/FilterSidebar"; } from "../List/Filters/FilterSidebar";
import { PatchContainerComponent } from "src/patch"; import { PatchComponent, PatchContainerComponent } from "src/patch";
import { Pagination, PaginationIndex } from "../List/Pagination"; import { Pagination, PaginationIndex } from "../List/Pagination";
import { Button, ButtonGroup } from "react-bootstrap"; import { Button, ButtonGroup } from "react-bootstrap";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
@@ -380,83 +380,86 @@ const SceneListOperations: React.FC<{
onDelete: () => void; onDelete: () => void;
onPlay: () => void; onPlay: () => void;
onCreateNew: () => void; onCreateNew: () => void;
}> = ({ }> = PatchComponent(
items, "SceneListOperations",
hasSelection, ({
operations, items,
onEdit, hasSelection,
onDelete, operations,
onPlay, onEdit,
onCreateNew, onDelete,
}) => { onPlay,
const intl = useIntl(); onCreateNew,
}) => {
const intl = useIntl();
return ( return (
<div className="scene-list-operations"> <div className="scene-list-operations">
<ButtonGroup> <ButtonGroup>
{!!items && ( {!!items && (
<Button
className="play-button"
variant="secondary"
onClick={() => onPlay()}
title={intl.formatMessage({ id: "actions.play" })}
>
<Icon icon={faPlay} />
</Button>
)}
{!hasSelection && (
<Button
className="create-new-button"
variant="secondary"
onClick={() => onCreateNew()}
title={intl.formatMessage(
{ id: "actions.create_entity" },
{ entityType: intl.formatMessage({ id: "scene" }) }
)}
>
<Icon icon={faPlus} />
</Button>
)}
{hasSelection && (
<>
<Button variant="secondary" onClick={() => onEdit()}>
<Icon icon={faPencil} />
</Button>
<Button <Button
variant="danger" className="play-button"
className="btn-danger-minimal" variant="secondary"
onClick={() => onDelete()} onClick={() => onPlay()}
title={intl.formatMessage({ id: "actions.play" })}
> >
<Icon icon={faTrash} /> <Icon icon={faPlay} />
</Button> </Button>
</> )}
)} {!hasSelection && (
<Button
className="create-new-button"
variant="secondary"
onClick={() => onCreateNew()}
title={intl.formatMessage(
{ id: "actions.create_entity" },
{ entityType: intl.formatMessage({ id: "scene" }) }
)}
>
<Icon icon={faPlus} />
</Button>
)}
<OperationDropdown {hasSelection && (
className="scene-list-operations" <>
menuClassName="scene-list-operations-dropdown" <Button variant="secondary" onClick={() => onEdit()}>
menuPortalTarget={document.body} <Icon icon={faPencil} />
> </Button>
{operations.map((o) => { <Button
if (o.isDisplayed && !o.isDisplayed()) { variant="danger"
return null; className="btn-danger-minimal"
} onClick={() => onDelete()}
>
<Icon icon={faTrash} />
</Button>
</>
)}
return ( <OperationDropdown
<OperationDropdownItem className="scene-list-operations"
key={o.text} menuClassName="scene-list-operations-dropdown"
onClick={o.onClick} menuPortalTarget={document.body}
text={o.text} >
className={o.className} {operations.map((o) => {
/> if (o.isDisplayed && !o.isDisplayed()) {
); return null;
})} }
</OperationDropdown>
</ButtonGroup> return (
</div> <OperationDropdownItem
); key={o.text}
}; onClick={o.onClick}
text={o.text}
className={o.className}
/>
);
})}
</OperationDropdown>
</ButtonGroup>
</div>
);
}
);
interface IFilteredScenes { interface IFilteredScenes {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;

View File

@@ -17,6 +17,8 @@ import { View } from "../List/views";
import { SceneMarkerCardsGrid } from "./SceneMarkerCardsGrid"; import { SceneMarkerCardsGrid } from "./SceneMarkerCardsGrid";
import { DeleteSceneMarkersDialog } from "./DeleteSceneMarkersDialog"; import { DeleteSceneMarkersDialog } from "./DeleteSceneMarkersDialog";
import { EditSceneMarkersDialog } from "./EditSceneMarkersDialog"; import { EditSceneMarkersDialog } from "./EditSceneMarkersDialog";
import { PatchComponent } from "src/patch";
import { IItemListOperation } from "../List/FilteredListToolbar";
function getItems(result: GQL.FindSceneMarkersQueryResult) { function getItems(result: GQL.FindSceneMarkersQueryResult) {
return result?.data?.findSceneMarkers?.scene_markers ?? []; return result?.data?.findSceneMarkers?.scene_markers ?? [];
@@ -31,132 +33,133 @@ interface ISceneMarkerList {
view?: View; view?: View;
alterQuery?: boolean; alterQuery?: boolean;
defaultSort?: string; defaultSort?: string;
extraOperations?: IItemListOperation<GQL.FindSceneMarkersQueryResult>[];
} }
export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ export const SceneMarkerList: React.FC<ISceneMarkerList> = PatchComponent(
filterHook, "SceneMarkerList",
view, ({ filterHook, view, alterQuery, extraOperations = [] }) => {
alterQuery, const intl = useIntl();
}) => { const history = useHistory();
const intl = useIntl();
const history = useHistory();
const filterMode = GQL.FilterMode.SceneMarkers; const filterMode = GQL.FilterMode.SceneMarkers;
const otherOperations = [ const otherOperations = [
{ ...extraOperations,
text: intl.formatMessage({ id: "actions.play_random" }), {
onClick: playRandom, text: intl.formatMessage({ id: "actions.play_random" }),
}, onClick: playRandom,
]; },
];
function addKeybinds( function addKeybinds(
result: GQL.FindSceneMarkersQueryResult, result: GQL.FindSceneMarkersQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
Mousetrap.bind("p r", () => { Mousetrap.bind("p r", () => {
playRandom(result, filter); playRandom(result, filter);
}); });
return () => { return () => {
Mousetrap.unbind("p r"); Mousetrap.unbind("p r");
}; };
} }
async function playRandom( async function playRandom(
result: GQL.FindSceneMarkersQueryResult, result: GQL.FindSceneMarkersQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
// query for a random scene // query for a random scene
if (result.data?.findSceneMarkers) { if (result.data?.findSceneMarkers) {
const { count } = result.data.findSceneMarkers; const { count } = result.data.findSceneMarkers;
const index = Math.floor(Math.random() * count); const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter); const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await queryFindSceneMarkers(filterCopy); const singleResult = await queryFindSceneMarkers(filterCopy);
if (singleResult.data.findSceneMarkers.scene_markers.length === 1) { if (singleResult.data.findSceneMarkers.scene_markers.length === 1) {
// navigate to the scene player page // navigate to the scene player page
const url = NavUtils.makeSceneMarkerUrl( const url = NavUtils.makeSceneMarkerUrl(
singleResult.data.findSceneMarkers.scene_markers[0] singleResult.data.findSceneMarkers.scene_markers[0]
); );
history.push(url); history.push(url);
}
} }
} }
}
function renderContent( function renderContent(
result: GQL.FindSceneMarkersQueryResult, result: GQL.FindSceneMarkersQueryResult,
filter: ListFilterModel, filter: ListFilterModel,
selectedIds: Set<string>, selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) { ) {
if (!result.data?.findSceneMarkers) return; if (!result.data?.findSceneMarkers) return;
if (filter.displayMode === DisplayMode.Wall) { if (filter.displayMode === DisplayMode.Wall) {
return (
<MarkerWallPanel
markers={result.data.findSceneMarkers.scene_markers}
zoomIndex={filter.zoomIndex}
/>
);
}
if (filter.displayMode === DisplayMode.Grid) {
return (
<SceneMarkerCardsGrid
markers={result.data.findSceneMarkers.scene_markers}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
}
function renderEditDialog(
selectedMarkers: GQL.SceneMarkerDataFragment[],
onClose: (applied: boolean) => void
) {
return ( return (
<MarkerWallPanel <EditSceneMarkersDialog selected={selectedMarkers} onClose={onClose} />
markers={result.data.findSceneMarkers.scene_markers} );
zoomIndex={filter.zoomIndex} }
function renderDeleteDialog(
selectedSceneMarkers: GQL.SceneMarkerDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteSceneMarkersDialog
selected={selectedSceneMarkers}
onClose={onClose}
/> />
); );
} }
if (filter.displayMode === DisplayMode.Grid) {
return (
<SceneMarkerCardsGrid
markers={result.data.findSceneMarkers.scene_markers}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
}
function renderEditDialog(
selectedMarkers: GQL.SceneMarkerDataFragment[],
onClose: (applied: boolean) => void
) {
return ( return (
<EditSceneMarkersDialog selected={selectedMarkers} onClose={onClose} /> <ItemListContext
); filterMode={filterMode}
} useResult={useFindSceneMarkers}
getItems={getItems}
function renderDeleteDialog( getCount={getCount}
selectedSceneMarkers: GQL.SceneMarkerDataFragment[], alterQuery={alterQuery}
onClose: (confirmed: boolean) => void filterHook={filterHook}
) {
return (
<DeleteSceneMarkersDialog
selected={selectedSceneMarkers}
onClose={onClose}
/>
);
}
return (
<ItemListContext
filterMode={filterMode}
useResult={useFindSceneMarkers}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
view={view} view={view}
otherOperations={otherOperations} selectable
addKeybinds={addKeybinds} >
renderContent={renderContent} <ItemList
renderEditDialog={renderEditDialog} view={view}
renderDeleteDialog={renderDeleteDialog} otherOperations={otherOperations}
/> addKeybinds={addKeybinds}
</ItemListContext> renderContent={renderContent}
); renderEditDialog={renderEditDialog}
}; renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
}
);
export default SceneMarkerList; export default SceneMarkerList;

View File

@@ -18,6 +18,8 @@ import { StudioTagger } from "../Tagger/studios/StudioTagger";
import { StudioCardGrid } from "./StudioCardGrid"; import { StudioCardGrid } from "./StudioCardGrid";
import { View } from "../List/views"; import { View } from "../List/views";
import { EditStudiosDialog } from "./EditStudiosDialog"; import { EditStudiosDialog } from "./EditStudiosDialog";
import { IItemListOperation } from "../List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
function getItems(result: GQL.FindStudiosQueryResult) { function getItems(result: GQL.FindStudiosQueryResult) {
return result?.data?.findStudios?.studios ?? []; return result?.data?.findStudios?.studios ?? [];
@@ -32,177 +34,177 @@ interface IStudioList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View; view?: View;
alterQuery?: boolean; alterQuery?: boolean;
extraOperations?: IItemListOperation<GQL.FindStudiosQueryResult>[];
} }
export const StudioList: React.FC<IStudioList> = ({ export const StudioList: React.FC<IStudioList> = PatchComponent(
fromParent, "StudioList",
filterHook, ({ fromParent, filterHook, view, alterQuery, extraOperations = [] }) => {
view, const intl = useIntl();
alterQuery, const history = useHistory();
}) => { const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const intl = useIntl(); const [isExportAll, setIsExportAll] = useState(false);
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const filterMode = GQL.FilterMode.Studios; const filterMode = GQL.FilterMode.Studios;
const otherOperations = [ const otherOperations = [
{ ...extraOperations,
text: intl.formatMessage({ id: "actions.view_random" }), {
onClick: viewRandom, text: intl.formatMessage({ id: "actions.view_random" }),
}, onClick: viewRandom,
{ },
text: intl.formatMessage({ id: "actions.export" }), {
onClick: onExport, text: intl.formatMessage({ id: "actions.export" }),
isDisplayed: showWhenSelected, onClick: onExport,
}, isDisplayed: showWhenSelected,
{ },
text: intl.formatMessage({ id: "actions.export_all" }), {
onClick: onExportAll, text: intl.formatMessage({ id: "actions.export_all" }),
}, onClick: onExportAll,
]; },
];
function addKeybinds( function addKeybinds(
result: GQL.FindStudiosQueryResult, result: GQL.FindStudiosQueryResult,
filter: ListFilterModel filter: ListFilterModel
) { ) {
Mousetrap.bind("p r", () => { Mousetrap.bind("p r", () => {
viewRandom(result, filter); viewRandom(result, filter);
}); });
return () => { return () => {
Mousetrap.unbind("p r"); Mousetrap.unbind("p r");
}; };
}
async function viewRandom(
result: GQL.FindStudiosQueryResult,
filter: ListFilterModel
) {
// query for a random studio
if (result.data?.findStudios) {
const { count } = result.data.findStudios;
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindStudios(filterCopy);
if (singleResult.data.findStudios.studios.length === 1) {
const { id } = singleResult.data.findStudios.studios[0];
// navigate to the studio page
history.push(`/studios/${id}`);
}
} }
}
async function onExport() { async function viewRandom(
setIsExportAll(false); result: GQL.FindStudiosQueryResult,
setIsExportDialogOpen(true); filter: ListFilterModel
} ) {
// query for a random studio
if (result.data?.findStudios) {
const { count } = result.data.findStudios;
async function onExportAll() { const index = Math.floor(Math.random() * count);
setIsExportAll(true); const filterCopy = cloneDeep(filter);
setIsExportDialogOpen(true); filterCopy.itemsPerPage = 1;
} filterCopy.currentPage = index + 1;
const singleResult = await queryFindStudios(filterCopy);
function renderContent( if (singleResult.data.findStudios.studios.length === 1) {
result: GQL.FindStudiosQueryResult, const { id } = singleResult.data.findStudios.studios[0];
filter: ListFilterModel, // navigate to the studio page
selectedIds: Set<string>, history.push(`/studios/${id}`);
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void }
) {
function maybeRenderExportDialog() {
if (isExportDialogOpen) {
return (
<ExportDialog
exportInput={{
studios: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
}}
onClose={() => setIsExportDialogOpen(false)}
/>
);
} }
} }
function renderStudios() { async function onExport() {
if (!result.data?.findStudios) return; setIsExportAll(false);
setIsExportDialogOpen(true);
}
if (filter.displayMode === DisplayMode.Grid) { async function onExportAll() {
return ( setIsExportAll(true);
<StudioCardGrid setIsExportDialogOpen(true);
studios={result.data.findStudios.studios} }
zoomIndex={filter.zoomIndex}
fromParent={fromParent} function renderContent(
selectedIds={selectedIds} result: GQL.FindStudiosQueryResult,
onSelectChange={onSelectChange} filter: ListFilterModel,
/> selectedIds: Set<string>,
); onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) {
function maybeRenderExportDialog() {
if (isExportDialogOpen) {
return (
<ExportDialog
exportInput={{
studios: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
}}
onClose={() => setIsExportDialogOpen(false)}
/>
);
}
} }
if (filter.displayMode === DisplayMode.List) {
return <h1>TODO</h1>; function renderStudios() {
} if (!result.data?.findStudios) return;
if (filter.displayMode === DisplayMode.Wall) {
return <h1>TODO</h1>; if (filter.displayMode === DisplayMode.Grid) {
} return (
if (filter.displayMode === DisplayMode.Tagger) { <StudioCardGrid
return <StudioTagger studios={result.data.findStudios.studios} />; studios={result.data.findStudios.studios}
zoomIndex={filter.zoomIndex}
fromParent={fromParent}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
if (filter.displayMode === DisplayMode.List) {
return <h1>TODO</h1>;
}
if (filter.displayMode === DisplayMode.Wall) {
return <h1>TODO</h1>;
}
if (filter.displayMode === DisplayMode.Tagger) {
return <StudioTagger studios={result.data.findStudios.studios} />;
}
} }
return (
<>
{maybeRenderExportDialog()}
{renderStudios()}
</>
);
}
function renderEditDialog(
selectedStudios: GQL.SlimStudioDataFragment[],
onClose: (applied: boolean) => void
) {
return <EditStudiosDialog selected={selectedStudios} onClose={onClose} />;
}
function renderDeleteDialog(
selectedStudios: GQL.SlimStudioDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteEntityDialog
selected={selectedStudios}
onClose={onClose}
singularEntity={intl.formatMessage({ id: "studio" })}
pluralEntity={intl.formatMessage({ id: "studios" })}
destroyMutation={useStudiosDestroy}
/>
);
} }
return ( return (
<> <ItemListContext
{maybeRenderExportDialog()} filterMode={filterMode}
{renderStudios()} useResult={useFindStudios}
</> getItems={getItems}
); getCount={getCount}
} alterQuery={alterQuery}
filterHook={filterHook}
function renderEditDialog(
selectedStudios: GQL.SlimStudioDataFragment[],
onClose: (applied: boolean) => void
) {
return <EditStudiosDialog selected={selectedStudios} onClose={onClose} />;
}
function renderDeleteDialog(
selectedStudios: GQL.SlimStudioDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteEntityDialog
selected={selectedStudios}
onClose={onClose}
singularEntity={intl.formatMessage({ id: "studio" })}
pluralEntity={intl.formatMessage({ id: "studios" })}
destroyMutation={useStudiosDestroy}
/>
);
}
return (
<ItemListContext
filterMode={filterMode}
useResult={useFindStudios}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
view={view} view={view}
otherOperations={otherOperations} selectable
addKeybinds={addKeybinds} >
renderContent={renderContent} <ItemList
renderEditDialog={renderEditDialog} view={view}
renderDeleteDialog={renderDeleteDialog} otherOperations={otherOperations}
/> addKeybinds={addKeybinds}
</ItemListContext> renderContent={renderContent}
); renderEditDialog={renderEditDialog}
}; renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
}
);

View File

@@ -26,6 +26,8 @@ import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { TagCardGrid } from "./TagCardGrid"; import { TagCardGrid } from "./TagCardGrid";
import { EditTagsDialog } from "./EditTagsDialog"; import { EditTagsDialog } from "./EditTagsDialog";
import { View } from "../List/views"; import { View } from "../List/views";
import { IItemListOperation } from "../List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
function getItems(result: GQL.FindTagsForListQueryResult) { function getItems(result: GQL.FindTagsForListQueryResult) {
return result?.data?.findTags?.tags ?? []; return result?.data?.findTags?.tags ?? [];
@@ -38,341 +40,346 @@ function getCount(result: GQL.FindTagsForListQueryResult) {
interface ITagList { interface ITagList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
alterQuery?: boolean; alterQuery?: boolean;
extraOperations?: IItemListOperation<GQL.FindTagsForListQueryResult>[];
} }
export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => { export const TagList: React.FC<ITagList> = PatchComponent(
const Toast = useToast(); "TagList",
const [deletingTag, setDeletingTag] = ({ filterHook, alterQuery, extraOperations = [] }) => {
useState<Partial<GQL.TagListDataFragment> | null>(null); const Toast = useToast();
const [deletingTag, setDeletingTag] =
useState<Partial<GQL.TagListDataFragment> | null>(null);
const filterMode = GQL.FilterMode.Tags; const filterMode = GQL.FilterMode.Tags;
const view = View.Tags; const view = View.Tags;
function getDeleteTagInput() { function getDeleteTagInput() {
const tagInput: Partial<GQL.TagDestroyInput> = {}; const tagInput: Partial<GQL.TagDestroyInput> = {};
if (deletingTag) { if (deletingTag) {
tagInput.id = deletingTag.id; tagInput.id = deletingTag.id;
}
return tagInput as GQL.TagDestroyInput;
}
const [deleteTag] = useTagDestroy(getDeleteTagInput());
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const otherOperations = [
{
text: intl.formatMessage({ id: "actions.view_random" }),
onClick: viewRandom,
},
{
text: intl.formatMessage({ id: "actions.export" }),
onClick: onExport,
isDisplayed: showWhenSelected,
},
{
text: intl.formatMessage({ id: "actions.export_all" }),
onClick: onExportAll,
},
];
function addKeybinds(
result: GQL.FindTagsForListQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
return () => {
Mousetrap.unbind("p r");
};
}
async function viewRandom(
result: GQL.FindTagsForListQueryResult,
filter: ListFilterModel
) {
// query for a random tag
if (result.data?.findTags) {
const { count } = result.data.findTags;
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindTagsForList(filterCopy);
if (singleResult.data.findTags.tags.length === 1) {
const { id } = singleResult.data.findTags.tags[0];
// navigate to the tag page
history.push(`/tags/${id}`);
} }
return tagInput as GQL.TagDestroyInput;
} }
} const [deleteTag] = useTagDestroy(getDeleteTagInput());
async function onExport() { const intl = useIntl();
setIsExportAll(false); const history = useHistory();
setIsExportDialogOpen(true); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
} const [isExportAll, setIsExportAll] = useState(false);
async function onExportAll() { const otherOperations = [
setIsExportAll(true); ...extraOperations,
setIsExportDialogOpen(true); {
} text: intl.formatMessage({ id: "actions.view_random" }),
onClick: viewRandom,
},
{
text: intl.formatMessage({ id: "actions.export" }),
onClick: onExport,
isDisplayed: showWhenSelected,
},
{
text: intl.formatMessage({ id: "actions.export_all" }),
onClick: onExportAll,
},
];
async function onAutoTag(tag: GQL.TagListDataFragment) { function addKeybinds(
if (!tag) return; result: GQL.FindTagsForListQueryResult,
try { filter: ListFilterModel
await mutateMetadataAutoTag({ tags: [tag.id] }); ) {
Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" })); Mousetrap.bind("p r", () => {
} catch (e) { viewRandom(result, filter);
Toast.error(e);
}
}
async function onDelete() {
try {
const oldRelations = {
parents: deletingTag?.parents ?? [],
children: deletingTag?.children ?? [],
};
await deleteTag();
tagRelationHook(deletingTag as GQL.TagListDataFragment, oldRelations, {
parents: [],
children: [],
}); });
Toast.success(
intl.formatMessage(
{ id: "toast.delete_past_tense" },
{
count: 1,
singularEntity: intl.formatMessage({ id: "tag" }),
pluralEntity: intl.formatMessage({ id: "tags" }),
}
)
);
setDeletingTag(null);
} catch (e) {
Toast.error(e);
}
}
function renderContent( return () => {
result: GQL.FindTagsForListQueryResult, Mousetrap.unbind("p r");
filter: ListFilterModel, };
selectedIds: Set<string>, }
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) { async function viewRandom(
function maybeRenderExportDialog() { result: GQL.FindTagsForListQueryResult,
if (isExportDialogOpen) { filter: ListFilterModel
return ( ) {
<ExportDialog // query for a random tag
exportInput={{ if (result.data?.findTags) {
tags: { const { count } = result.data.findTags;
ids: Array.from(selectedIds.values()),
all: isExportAll, const index = Math.floor(Math.random() * count);
}, const filterCopy = cloneDeep(filter);
}} filterCopy.itemsPerPage = 1;
onClose={() => setIsExportDialogOpen(false)} filterCopy.currentPage = index + 1;
/> const singleResult = await queryFindTagsForList(filterCopy);
); if (singleResult.data.findTags.tags.length === 1) {
const { id } = singleResult.data.findTags.tags[0];
// navigate to the tag page
history.push(`/tags/${id}`);
}
} }
} }
function renderTags() { async function onExport() {
if (!result.data?.findTags) return; setIsExportAll(false);
setIsExportDialogOpen(true);
}
if (filter.displayMode === DisplayMode.Grid) { async function onExportAll() {
return ( setIsExportAll(true);
<TagCardGrid setIsExportDialogOpen(true);
tags={result.data.findTags.tags} }
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds} async function onAutoTag(tag: GQL.TagListDataFragment) {
onSelectChange={onSelectChange} if (!tag) return;
/> try {
); await mutateMetadataAutoTag({ tags: [tag.id] });
Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" }));
} catch (e) {
Toast.error(e);
} }
if (filter.displayMode === DisplayMode.List) { }
const deleteAlert = (
<ModalComponent
onHide={() => {}}
show={!!deletingTag}
icon={faTrashAlt}
accept={{
onClick: onDelete,
variant: "danger",
text: intl.formatMessage({ id: "actions.delete" }),
}}
cancel={{ onClick: () => setDeletingTag(null) }}
>
<span>
<FormattedMessage
id="dialogs.delete_confirm"
values={{ entityName: deletingTag && deletingTag.name }}
/>
</span>
</ModalComponent>
);
const tagElements = result.data.findTags.tags.map((tag) => { async function onDelete() {
try {
const oldRelations = {
parents: deletingTag?.parents ?? [],
children: deletingTag?.children ?? [],
};
await deleteTag();
tagRelationHook(deletingTag as GQL.TagListDataFragment, oldRelations, {
parents: [],
children: [],
});
Toast.success(
intl.formatMessage(
{ id: "toast.delete_past_tense" },
{
count: 1,
singularEntity: intl.formatMessage({ id: "tag" }),
pluralEntity: intl.formatMessage({ id: "tags" }),
}
)
);
setDeletingTag(null);
} catch (e) {
Toast.error(e);
}
}
function renderContent(
result: GQL.FindTagsForListQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) {
function maybeRenderExportDialog() {
if (isExportDialogOpen) {
return ( return (
<div key={tag.id} className="tag-list-row row"> <ExportDialog
<Link to={`/tags/${tag.id}`}>{tag.name}</Link> exportInput={{
tags: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
}}
onClose={() => setIsExportDialogOpen(false)}
/>
);
}
}
<div className="ml-auto"> function renderTags() {
<Button if (!result.data?.findTags) return;
variant="secondary"
className="tag-list-button" if (filter.displayMode === DisplayMode.Grid) {
onClick={() => onAutoTag(tag)} return (
> <TagCardGrid
<FormattedMessage id="actions.auto_tag" /> tags={result.data.findTags.tags}
</Button> zoomIndex={filter.zoomIndex}
<Button variant="secondary" className="tag-list-button"> selectedIds={selectedIds}
<Link onSelectChange={onSelectChange}
to={NavUtils.makeTagScenesUrl(tag)} />
className="tag-list-anchor" );
}
if (filter.displayMode === DisplayMode.List) {
const deleteAlert = (
<ModalComponent
onHide={() => {}}
show={!!deletingTag}
icon={faTrashAlt}
accept={{
onClick: onDelete,
variant: "danger",
text: intl.formatMessage({ id: "actions.delete" }),
}}
cancel={{ onClick: () => setDeletingTag(null) }}
>
<span>
<FormattedMessage
id="dialogs.delete_confirm"
values={{ entityName: deletingTag && deletingTag.name }}
/>
</span>
</ModalComponent>
);
const tagElements = result.data.findTags.tags.map((tag) => {
return (
<div key={tag.id} className="tag-list-row row">
<Link to={`/tags/${tag.id}`}>{tag.name}</Link>
<div className="ml-auto">
<Button
variant="secondary"
className="tag-list-button"
onClick={() => onAutoTag(tag)}
> >
<FormattedMessage <FormattedMessage id="actions.auto_tag" />
id="countables.scenes" </Button>
values={{ <Button variant="secondary" className="tag-list-button">
count: tag.scene_count ?? 0, <Link
}} to={NavUtils.makeTagScenesUrl(tag)}
className="tag-list-anchor"
>
<FormattedMessage
id="countables.scenes"
values={{
count: tag.scene_count ?? 0,
}}
/>
: <FormattedNumber value={tag.scene_count ?? 0} />
</Link>
</Button>
<Button variant="secondary" className="tag-list-button">
<Link
to={NavUtils.makeTagImagesUrl(tag)}
className="tag-list-anchor"
>
<FormattedMessage
id="countables.images"
values={{
count: tag.image_count ?? 0,
}}
/>
: <FormattedNumber value={tag.image_count ?? 0} />
</Link>
</Button>
<Button variant="secondary" className="tag-list-button">
<Link
to={NavUtils.makeTagGalleriesUrl(tag)}
className="tag-list-anchor"
>
<FormattedMessage
id="countables.galleries"
values={{
count: tag.gallery_count ?? 0,
}}
/>
: <FormattedNumber value={tag.gallery_count ?? 0} />
</Link>
</Button>
<Button variant="secondary" className="tag-list-button">
<Link
to={NavUtils.makeTagSceneMarkersUrl(tag)}
className="tag-list-anchor"
>
<FormattedMessage
id="countables.markers"
values={{
count: tag.scene_marker_count ?? 0,
}}
/>
: <FormattedNumber value={tag.scene_marker_count ?? 0} />
</Link>
</Button>
<span className="tag-list-count">
<FormattedMessage id="total" />:{" "}
<FormattedNumber
value={
(tag.scene_count || 0) +
(tag.scene_marker_count || 0) +
(tag.image_count || 0) +
(tag.gallery_count || 0)
}
/> />
: <FormattedNumber value={tag.scene_count ?? 0} /> </span>
</Link> <Button variant="danger" onClick={() => setDeletingTag(tag)}>
</Button> <Icon icon={faTrashAlt} color="danger" />
<Button variant="secondary" className="tag-list-button"> </Button>
<Link </div>
to={NavUtils.makeTagImagesUrl(tag)}
className="tag-list-anchor"
>
<FormattedMessage
id="countables.images"
values={{
count: tag.image_count ?? 0,
}}
/>
: <FormattedNumber value={tag.image_count ?? 0} />
</Link>
</Button>
<Button variant="secondary" className="tag-list-button">
<Link
to={NavUtils.makeTagGalleriesUrl(tag)}
className="tag-list-anchor"
>
<FormattedMessage
id="countables.galleries"
values={{
count: tag.gallery_count ?? 0,
}}
/>
: <FormattedNumber value={tag.gallery_count ?? 0} />
</Link>
</Button>
<Button variant="secondary" className="tag-list-button">
<Link
to={NavUtils.makeTagSceneMarkersUrl(tag)}
className="tag-list-anchor"
>
<FormattedMessage
id="countables.markers"
values={{
count: tag.scene_marker_count ?? 0,
}}
/>
: <FormattedNumber value={tag.scene_marker_count ?? 0} />
</Link>
</Button>
<span className="tag-list-count">
<FormattedMessage id="total" />:{" "}
<FormattedNumber
value={
(tag.scene_count || 0) +
(tag.scene_marker_count || 0) +
(tag.image_count || 0) +
(tag.gallery_count || 0)
}
/>
</span>
<Button variant="danger" onClick={() => setDeletingTag(tag)}>
<Icon icon={faTrashAlt} color="danger" />
</Button>
</div> </div>
);
});
return (
<div className="col col-sm-8 m-auto">
{tagElements}
{deleteAlert}
</div> </div>
); );
}); }
if (filter.displayMode === DisplayMode.Wall) {
return ( return <h1>TODO</h1>;
<div className="col col-sm-8 m-auto"> }
{tagElements}
{deleteAlert}
</div>
);
}
if (filter.displayMode === DisplayMode.Wall) {
return <h1>TODO</h1>;
} }
return (
<>
{maybeRenderExportDialog()}
{renderTags()}
</>
);
} }
function renderEditDialog(
selectedTags: GQL.TagListDataFragment[],
onClose: (confirmed: boolean) => void
) {
return <EditTagsDialog selected={selectedTags} onClose={onClose} />;
}
function renderDeleteDialog(
selectedTags: GQL.TagListDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteEntityDialog
selected={selectedTags}
onClose={onClose}
singularEntity={intl.formatMessage({ id: "tag" })}
pluralEntity={intl.formatMessage({ id: "tags" })}
destroyMutation={useTagsDestroy}
onDeleted={() => {
selectedTags.forEach((t) =>
tagRelationHook(
t,
{ parents: t.parents ?? [], children: t.children ?? [] },
{ parents: [], children: [] }
)
);
}}
/>
);
}
return ( return (
<> <ItemListContext
{maybeRenderExportDialog()} filterMode={filterMode}
{renderTags()} useResult={useFindTagsForList}
</> getItems={getItems}
); getCount={getCount}
} alterQuery={alterQuery}
filterHook={filterHook}
function renderEditDialog(
selectedTags: GQL.TagListDataFragment[],
onClose: (confirmed: boolean) => void
) {
return <EditTagsDialog selected={selectedTags} onClose={onClose} />;
}
function renderDeleteDialog(
selectedTags: GQL.TagListDataFragment[],
onClose: (confirmed: boolean) => void
) {
return (
<DeleteEntityDialog
selected={selectedTags}
onClose={onClose}
singularEntity={intl.formatMessage({ id: "tag" })}
pluralEntity={intl.formatMessage({ id: "tags" })}
destroyMutation={useTagsDestroy}
onDeleted={() => {
selectedTags.forEach((t) =>
tagRelationHook(
t,
{ parents: t.parents ?? [], children: t.children ?? [] },
{ parents: [], children: [] }
)
);
}}
/>
);
}
return (
<ItemListContext
filterMode={filterMode}
useResult={useFindTagsForList}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
view={view} view={view}
otherOperations={otherOperations} selectable
addKeybinds={addKeybinds} >
renderContent={renderContent} <ItemList
renderEditDialog={renderEditDialog} view={view}
renderDeleteDialog={renderDeleteDialog} otherOperations={otherOperations}
/> addKeybinds={addKeybinds}
</ItemListContext> renderContent={renderContent}
); renderEditDialog={renderEditDialog}
}; renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
}
);

View File

@@ -671,14 +671,20 @@ declare namespace PluginApi {
"GalleryCard.Image": React.FC<any>; "GalleryCard.Image": React.FC<any>;
"GalleryCard.Overlays": React.FC<any>; "GalleryCard.Overlays": React.FC<any>;
"GalleryCard.Popovers": React.FC<any>; "GalleryCard.Popovers": React.FC<any>;
GalleryAddPanel: React.FC<any>;
GalleryIDSelect: React.FC<any>; GalleryIDSelect: React.FC<any>;
GalleryImagesPanel: React.FC<any>;
GalleryList: React.FC<any>;
GallerySelect: React.FC<any>; GallerySelect: React.FC<any>;
GroupIDSelect: React.FC<any>; GroupIDSelect: React.FC<any>;
GroupList: React.FC<any>;
GroupSelect: React.FC<any>; GroupSelect: React.FC<any>;
GroupSubGroupsPanel: React.FC<any>;
HeaderImage: React.FC<any>; HeaderImage: React.FC<any>;
HoverPopover: React.FC<any>; HoverPopover: React.FC<any>;
Icon: React.FC<any>; Icon: React.FC<any>;
ImageInput: React.FC<any>; ImageInput: React.FC<any>;
ImageList: React.FC<any>;
LightboxLink: React.FC<any>; LightboxLink: React.FC<any>;
LoadingIndicator: React.FC<any>; LoadingIndicator: React.FC<any>;
"MainNavBar.MenuItems": React.FC<any>; "MainNavBar.MenuItems": React.FC<any>;
@@ -699,6 +705,7 @@ declare namespace PluginApi {
PerformerHeaderImage: React.FC<any>; PerformerHeaderImage: React.FC<any>;
PerformerIDSelect: React.FC<any>; PerformerIDSelect: React.FC<any>;
PerformerImagesPanel: React.FC<any>; PerformerImagesPanel: React.FC<any>;
PerformerList: React.FC<any>;
PerformerPage: React.FC<any>; PerformerPage: React.FC<any>;
PerformerScenesPanel: React.FC<any>; PerformerScenesPanel: React.FC<any>;
PerformerSelect: React.FC<any>; PerformerSelect: React.FC<any>;
@@ -717,6 +724,9 @@ declare namespace PluginApi {
"SceneCard.Image": React.FC<any>; "SceneCard.Image": React.FC<any>;
"SceneCard.Overlays": React.FC<any>; "SceneCard.Overlays": React.FC<any>;
"SceneCard.Popovers": React.FC<any>; "SceneCard.Popovers": React.FC<any>;
SceneList: React.FC<any>;
SceneListOperations: React.FC<any>;
SceneMarkerList: React.FC<any>;
SelectSetting: React.FC<any>; SelectSetting: React.FC<any>;
Setting: React.FC<any>; Setting: React.FC<any>;
SettingGroup: React.FC<any>; SettingGroup: React.FC<any>;
@@ -724,6 +734,7 @@ declare namespace PluginApi {
StringListSetting: React.FC<any>; StringListSetting: React.FC<any>;
StringSetting: React.FC<any>; StringSetting: React.FC<any>;
StudioIDSelect: React.FC<any>; StudioIDSelect: React.FC<any>;
StudioList: React.FC<any>;
StudioSelect: React.FC<any>; StudioSelect: React.FC<any>;
SweatDrops: React.FC<any>; SweatDrops: React.FC<any>;
TabTitleCounter: React.FC<any>; TabTitleCounter: React.FC<any>;
@@ -734,6 +745,7 @@ declare namespace PluginApi {
"TagCard.Popovers": React.FC<any>; "TagCard.Popovers": React.FC<any>;
"TagCard.Title": React.FC<any>; "TagCard.Title": React.FC<any>;
TagLink: React.FC<any>; TagLink: React.FC<any>;
TagList: React.FC<any>;
TagSelect: React.FC<any>; TagSelect: React.FC<any>;
TruncatedText: React.FC<any>; TruncatedText: React.FC<any>;
}; };