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 { faPlus } from "@fortawesome/free-solid-svg-icons";
import { galleryTitle } from "src/core/galleries";
import { IItemListOperation } from "src/components/List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
interface IGalleryAddProps {
active: boolean;
gallery: GQL.GalleryDataFragment;
extraOperations?: IItemListOperation<GQL.FindImagesQueryResult>[];
}
export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({
active,
gallery,
}) => {
const Toast = useToast();
const intl = useIntl();
export const GalleryAddPanel: React.FC<IGalleryAddProps> = PatchComponent(
"GalleryAddPanel",
({ active, gallery, extraOperations = [] }) => {
const Toast = useToast();
const intl = useIntl();
function filterHook(filter: ListFilterModel) {
const galleryValue = {
id: gallery.id,
label: galleryTitle(gallery),
};
// if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "galleries";
}) as GalleriesCriterion | undefined;
function filterHook(filter: ListFilterModel) {
const galleryValue = {
id: gallery.id,
label: galleryTitle(gallery),
};
// if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "galleries";
}) as GalleriesCriterion | undefined;
if (
galleryCriterion &&
galleryCriterion.modifier === GQL.CriterionModifier.Excludes
) {
// add the gallery if not present
if (
!galleryCriterion.value.find((p) => {
return p.id === gallery.id;
})
galleryCriterion &&
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;
} else {
// overwrite
galleryCriterion = new GalleriesCriterion();
galleryCriterion.modifier = GQL.CriterionModifier.Excludes;
galleryCriterion.value = [galleryValue];
filter.criteria.push(galleryCriterion);
return filter;
}
return filter;
}
async function addImages(
result: GQL.FindImagesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>
) {
try {
await mutateAddGalleryImages({
gallery_id: gallery.id!,
image_ids: Array.from(selectedIds.values()),
});
const imageCount = selectedIds.size;
Toast.success(
intl.formatMessage(
{ id: "toast.added_entity" },
{
count: imageCount,
singularEntity: intl.formatMessage({ id: "image" }),
pluralEntity: intl.formatMessage({ id: "images" }),
}
)
);
} catch (e) {
Toast.error(e);
async function addImages(
result: GQL.FindImagesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>
) {
try {
await mutateAddGalleryImages({
gallery_id: gallery.id!,
image_ids: Array.from(selectedIds.values()),
});
const imageCount = selectedIds.size;
Toast.success(
intl.formatMessage(
{ id: "toast.added_entity" },
{
count: imageCount,
singularEntity: intl.formatMessage({ id: "image" }),
pluralEntity: intl.formatMessage({ id: "images" }),
}
)
);
} 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 { galleryTitle } from "src/core/galleries";
import { View } from "src/components/List/views";
import { PatchComponent } from "src/patch";
import { IItemListOperation } from "src/components/List/FilteredListToolbar";
interface IGalleryDetailsProps {
active: boolean;
gallery: GQL.GalleryDataFragment;
extraOperations?: IItemListOperation<GQL.FindImagesQueryResult>[];
}
export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
active,
gallery,
}) => {
const intl = useIntl();
const Toast = useToast();
export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> =
PatchComponent(
"GalleryImagesPanel",
({ active, gallery, extraOperations = [] }) => {
const intl = useIntl();
const Toast = useToast();
function filterHook(filter: ListFilterModel) {
const galleryValue = {
id: gallery.id!,
label: galleryTitle(gallery),
};
// if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "galleries";
}) as GalleriesCriterion | undefined;
function filterHook(filter: ListFilterModel) {
const galleryValue = {
id: gallery.id!,
label: galleryTitle(gallery),
};
// if galleries is already present, then we modify it, otherwise add
let galleryCriterion = filter.criteria.find((c) => {
return c.criterionOption.type === "galleries";
}) as GalleriesCriterion | undefined;
if (
galleryCriterion &&
(galleryCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
galleryCriterion.modifier === GQL.CriterionModifier.Includes)
) {
// add the gallery if not present
if (
!galleryCriterion.value.find((p) => {
return p.id === gallery.id;
})
) {
galleryCriterion.value.push(galleryValue);
if (
galleryCriterion &&
(galleryCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
galleryCriterion.modifier === GQL.CriterionModifier.Includes)
) {
// add the gallery if not present
if (
!galleryCriterion.value.find((p) => {
return p.id === gallery.id;
})
) {
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;
} else {
// overwrite
galleryCriterion = new GalleriesCriterion();
galleryCriterion.value = [galleryValue];
filter.criteria.push(galleryCriterion);
}
async function setCover(
result: GQL.FindImagesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>
) {
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(
result: GQL.FindImagesQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>
) {
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,
});
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.updated_entity" },
{
entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase(),
}
)
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 = [
...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 { GalleryCardGrid } from "./GalleryGridCard";
import { View } from "../List/views";
import { PatchComponent } from "src/patch";
import { IItemListOperation } from "../List/FilteredListToolbar";
function getItems(result: GQL.FindGalleriesQueryResult) {
return result?.data?.findGalleries?.galleries ?? [];
@@ -28,180 +30,183 @@ interface IGalleryList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View;
alterQuery?: boolean;
extraOperations?: IItemListOperation<GQL.FindGalleriesQueryResult>[];
}
export const GalleryList: React.FC<IGalleryList> = ({
filterHook,
view,
alterQuery,
}) => {
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
export const GalleryList: React.FC<IGalleryList> = PatchComponent(
"GalleryList",
({ filterHook, view, alterQuery, extraOperations = [] }) => {
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const filterMode = GQL.FilterMode.Galleries;
const filterMode = GQL.FilterMode.Galleries;
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,
},
];
const otherOperations = [
...extraOperations,
{
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.FindGalleriesQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
function addKeybinds(
result: GQL.FindGalleriesQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
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}`);
}
return () => {
Mousetrap.unbind("p r");
};
}
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function viewRandom(
result: GQL.FindGalleriesQueryResult,
filter: ListFilterModel
) {
// query for a random image
if (result.data?.findGalleries) {
const { count } = result.data.findGalleries;
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)}
/>
);
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}`);
}
}
}
function renderGalleries() {
if (!result.data?.findGalleries) return;
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
if (filter.displayMode === DisplayMode.Grid) {
return (
<GalleryCardGrid
galleries={result.data.findGalleries.galleries}
selectedIds={selectedIds}
zoomIndex={filter.zoomIndex}
onSelectChange={onSelectChange}
/>
);
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)}
/>
);
}
}
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} />
))}
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} />
))}
</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 (
<>
{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 (
<ItemListContext
filterMode={filterMode}
useResult={useFindGalleries}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
<ItemListContext
filterMode={filterMode}
useResult={useFindGalleries}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
};
selectable
>
<ItemList
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
}
);

View File

@@ -18,7 +18,10 @@ import {
SearchTermInput,
} from "src/components/List/ListFilter";
import { useFilter } from "src/components/List/FilterProvider";
import { IFilteredListToolbar } from "src/components/List/FilteredListToolbar";
import {
IFilteredListToolbar,
IItemListOperation,
} from "src/components/List/FilteredListToolbar";
import {
showWhenNoneSelected,
showWhenSelected,
@@ -28,6 +31,7 @@ import { useIntl } from "react-intl";
import { useToast } from "src/hooks/Toast";
import { useModal } from "src/hooks/modal";
import { AddSubGroupsDialog } from "./AddGroupsDialog";
import { PatchComponent } from "src/patch";
const useContainingGroupFilterHook = (
group: Pick<GQL.StudioDataFragment, "id" | "name">,
@@ -99,6 +103,7 @@ const Toolbar: React.FC<IFilteredListToolbar> = ({
interface IGroupSubGroupsPanel {
active: boolean;
group: GQL.GroupDataFragment;
extraOperations?: IItemListOperation<GQL.FindGroupsQueryResult>[];
}
const defaultFilter = (() => {
@@ -113,92 +118,99 @@ const defaultFilter = (() => {
return ret;
})();
export const GroupSubGroupsPanel: React.FC<IGroupSubGroupsPanel> = ({
active,
group,
}) => {
const intl = useIntl();
const Toast = useToast();
const { modal, showModal, closeModal } = useModal();
export const GroupSubGroupsPanel: React.FC<IGroupSubGroupsPanel> =
PatchComponent(
"GroupSubGroupsPanel",
({ active, group, extraOperations = [] }) => {
const intl = useIntl();
const Toast = useToast();
const { modal, showModal, closeModal } = useModal();
const [reorderSubGroups] = useReorderSubGroupsMutation();
const mutateRemoveSubGroups = useRemoveSubGroups();
const [reorderSubGroups] = useReorderSubGroupsMutation();
const mutateRemoveSubGroups = useRemoveSubGroups();
const filterHook = useContainingGroupFilterHook(group);
const filterHook = useContainingGroupFilterHook(group);
async function removeSubGroups(
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>
) {
try {
await mutateRemoveSubGroups(group.id, Array.from(selectedIds.values()));
async function removeSubGroups(
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>
) {
try {
await mutateRemoveSubGroups(
group.id,
Array.from(selectedIds.values())
);
Toast.success(
intl.formatMessage(
{ id: "toast.removed_entity" },
{
count: selectedIds.size,
singularEntity: intl.formatMessage({ id: "group" }),
pluralEntity: intl.formatMessage({ id: "groups" }),
}
)
);
} catch (e) {
Toast.error(e);
}
}
Toast.success(
intl.formatMessage(
{ id: "toast.removed_entity" },
{
count: selectedIds.size,
singularEntity: intl.formatMessage({ id: "group" }),
pluralEntity: intl.formatMessage({ id: "groups" }),
}
)
);
} catch (e) {
Toast.error(e);
}
}
async function onAddSubGroups() {
showModal(
<AddSubGroupsDialog containingGroup={group} onClose={closeModal} />
);
}
async function onAddSubGroups() {
showModal(
<AddSubGroupsDialog containingGroup={group} onClose={closeModal} />
);
}
const otherOperations = [
{
text: intl.formatMessage({ id: "actions.add_sub_groups" }),
onClick: onAddSubGroups,
isDisplayed: showWhenNoneSelected,
postRefetch: true,
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,
const otherOperations = [
...extraOperations,
{
text: intl.formatMessage({ id: "actions.add_sub_groups" }),
onClick: onAddSubGroups,
isDisplayed: showWhenNoneSelected,
postRefetch: true,
icon: faPlus,
buttonVariant: "secondary",
},
},
});
}
{
text: intl.formatMessage({
id: "actions.remove_from_containing_group",
}),
onClick: removeSubGroups,
isDisplayed: showWhenSelected,
postRefetch: true,
icon: faMinus,
buttonVariant: "danger",
},
];
return (
<>
{modal}
<GroupList
defaultFilter={defaultFilter}
filterHook={filterHook}
alterQuery={active}
fromGroupId={group.id}
otherOperations={otherOperations}
onMove={onMove}
renderToolbar={(props) => <Toolbar {...props} />}
/>
</>
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,
},
},
});
}
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,
IItemListOperation,
} from "../List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
const GroupExportDialog: React.FC<{
open?: boolean;
@@ -90,150 +91,153 @@ interface IGroupList extends IGroupListContext {
otherOperations?: IItemListOperation<GQL.FindGroupsQueryResult>[];
}
export const GroupList: React.FC<IGroupList> = ({
filterHook,
alterQuery,
defaultFilter,
view,
fromGroupId,
onMove,
selectable,
renderToolbar,
otherOperations: providedOperations = [],
}) => {
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
export const GroupList: React.FC<IGroupList> = PatchComponent(
"GroupList",
({
filterHook,
alterQuery,
defaultFilter,
view,
fromGroupId,
onMove,
selectable,
renderToolbar,
otherOperations: providedOperations = [],
}) => {
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,
},
...providedOperations,
];
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,
},
...providedOperations,
];
function addKeybinds(
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
function addKeybinds(
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
return () => {
Mousetrap.unbind("p r");
};
}
return () => {
Mousetrap.unbind("p r");
};
}
async function viewRandom(
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel
) {
// query for a random image
if (result.data?.findGroups) {
const { count } = result.data.findGroups;
async function viewRandom(
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel
) {
// query for a random image
if (result.data?.findGroups) {
const { count } = result.data.findGroups;
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindGroups(filterCopy);
if (singleResult.data.findGroups.groups.length === 1) {
const { id } = singleResult.data.findGroups.groups[0];
// navigate to the group page
history.push(`/groups/${id}`);
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindGroups(filterCopy);
if (singleResult.data.findGroups.groups.length === 1) {
const { id } = singleResult.data.findGroups.groups[0];
// navigate to the group page
history.push(`/groups/${id}`);
}
}
}
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
function renderContent(
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) {
return (
<>
<GroupExportDialog
open={isExportDialogOpen}
selectedIds={selectedIds}
isExportAll={isExportAll}
onClose={() => setIsExportDialogOpen(false)}
/>
{filter.displayMode === DisplayMode.Grid && (
<GroupCardGrid
groups={result.data?.findGroups.groups ?? []}
zoomIndex={filter.zoomIndex}
function renderContent(
result: GQL.FindGroupsQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) {
return (
<>
<GroupExportDialog
open={isExportDialogOpen}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
fromGroupId={fromGroupId}
onMove={onMove}
isExportAll={isExportAll}
onClose={() => setIsExportDialogOpen(false)}
/>
)}
</>
);
}
{filter.displayMode === DisplayMode.Grid && (
<GroupCardGrid
groups={result.data?.findGroups.groups ?? []}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
fromGroupId={fromGroupId}
onMove={onMove}
/>
)}
</>
);
}
function renderEditDialog(
selectedGroups: GQL.GroupDataFragment[],
onClose: (applied: boolean) => void
) {
return <EditGroupsDialog selected={selectedGroups} onClose={onClose} />;
}
function renderEditDialog(
selectedGroups: GQL.GroupDataFragment[],
onClose: (applied: boolean) => void
) {
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 (
<DeleteEntityDialog
selected={selectedGroups}
onClose={onClose}
singularEntity={intl.formatMessage({ id: "group" })}
pluralEntity={intl.formatMessage({ id: "groups" })}
destroyMutation={useGroupsDestroy}
/>
<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>
);
}
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 { IItemListOperation } from "../List/FilteredListToolbar";
import { FileSize } from "../Shared/FileSize";
import { PatchComponent } from "src/patch";
interface IImageWallProps {
images: GQL.SlimImageDataFragment[];
@@ -318,167 +319,168 @@ interface IImageList {
chapters?: GQL.GalleryChapterDataFragment[];
}
export const ImageList: React.FC<IImageList> = ({
filterHook,
view,
alterQuery,
extraOperations,
chapters = [],
}) => {
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const [slideshowRunning, setSlideshowRunning] = useState<boolean>(false);
export const ImageList: React.FC<IImageList> = PatchComponent(
"ImageList",
({ filterHook, view, alterQuery, extraOperations = [], chapters = [] }) => {
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const [slideshowRunning, setSlideshowRunning] = useState<boolean>(false);
const filterMode = GQL.FilterMode.Images;
const filterMode = GQL.FilterMode.Images;
const otherOperations = [
...(extraOperations ?? []),
{
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,
},
];
const otherOperations = [
...extraOperations,
{
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.FindImagesQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
function addKeybinds(
result: GQL.FindImagesQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
return () => {
Mousetrap.unbind("p r");
};
}
return () => {
Mousetrap.unbind("p r");
};
}
async function viewRandom(
result: GQL.FindImagesQueryResult,
filter: ListFilterModel
) {
// query for a random image
if (result.data?.findImages) {
const { count } = result.data.findImages;
async function viewRandom(
result: GQL.FindImagesQueryResult,
filter: ListFilterModel
) {
// query for a random image
if (result.data?.findImages) {
const { count } = result.data.findImages;
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindImages(filterCopy);
if (singleResult.data.findImages.images.length === 1) {
const { id } = singleResult.data.findImages.images[0];
// navigate to the image player page
history.push(`/images/${id}`);
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindImages(filterCopy);
if (singleResult.data.findImages.images.length === 1) {
const { id } = singleResult.data.findImages.images[0];
// navigate to the image player page
history.push(`/images/${id}`);
}
}
}
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
async function onExportAll() {
setIsExportAll(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 (
<ExportDialog
exportInput={{
images: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
}}
onClose={() => setIsExportDialogOpen(false)}
<ImageListImages
filter={filter}
images={result.data.findImages.images}
onChangePage={onChangePage}
onSelectChange={onSelectChange}
pageCount={pageCount}
selectedIds={selectedIds}
slideshowRunning={slideshowRunning}
setSlideshowRunning={setSlideshowRunning}
chapters={chapters}
/>
);
}
}
function renderImages() {
if (!result.data?.findImages) return;
return (
<ImageListImages
filter={filter}
images={result.data.findImages.images}
onChangePage={onChangePage}
onSelectChange={onSelectChange}
pageCount={pageCount}
selectedIds={selectedIds}
slideshowRunning={slideshowRunning}
setSlideshowRunning={setSlideshowRunning}
chapters={chapters}
/>
<>
{maybeRenderImageExportDialog()}
{renderImages()}
</>
);
}
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 (
<>
{maybeRenderImageExportDialog()}
{renderImages()}
</>
<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>
);
}
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 { PerformerCardGrid } from "./PerformerCardGrid";
import { View } from "../List/views";
import { IItemListOperation } from "../List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
function getItems(result: GQL.FindPerformersQueryResult) {
return result?.data?.findPerformers?.performers ?? [];
@@ -159,183 +161,185 @@ interface IPerformerList {
view?: View;
alterQuery?: boolean;
extraCriteria?: IPerformerCardExtraCriteria;
extraOperations?: IItemListOperation<GQL.FindPerformersQueryResult>[];
}
export const PerformerList: React.FC<IPerformerList> = ({
filterHook,
view,
alterQuery,
extraCriteria,
}) => {
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
export const PerformerList: React.FC<IPerformerList> = PatchComponent(
"PerformerList",
({ filterHook, view, alterQuery, extraCriteria, extraOperations = [] }) => {
const intl = useIntl();
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 = [
{
text: intl.formatMessage({ id: "actions.open_random" }),
onClick: openRandom,
},
{
text: intl.formatMessage({ id: "actions.export" }),
onClick: onExport,
isDisplayed: showWhenSelected,
},
{
text: intl.formatMessage({ id: "actions.export_all" }),
onClick: onExportAll,
},
];
const otherOperations = [
...extraOperations,
{
text: intl.formatMessage({ id: "actions.open_random" }),
onClick: openRandom,
},
{
text: intl.formatMessage({ id: "actions.export" }),
onClick: onExport,
isDisplayed: showWhenSelected,
},
{
text: intl.formatMessage({ id: "actions.export_all" }),
onClick: onExportAll,
},
];
function addKeybinds(
result: GQL.FindPerformersQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
openRandom(result, filter);
});
function addKeybinds(
result: GQL.FindPerformersQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
openRandom(result, filter);
});
return () => {
Mousetrap.unbind("p r");
};
}
return () => {
Mousetrap.unbind("p r");
};
}
async function openRandom(
result: GQL.FindPerformersQueryResult,
filter: ListFilterModel
) {
if (result.data?.findPerformers) {
const { count } = result.data.findPerformers;
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindPerformers(filterCopy);
if (singleResult.data.findPerformers.performers.length === 1) {
const { id } = singleResult.data.findPerformers.performers[0]!;
history.push(`/performers/${id}`);
async function openRandom(
result: GQL.FindPerformersQueryResult,
filter: ListFilterModel
) {
if (result.data?.findPerformers) {
const { count } = result.data.findPerformers;
const index = Math.floor(Math.random() * count);
const filterCopy = cloneDeep(filter);
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindPerformers(filterCopy);
if (singleResult.data.findPerformers.performers.length === 1) {
const { id } = singleResult.data.findPerformers.performers[0]!;
history.push(`/performers/${id}`);
}
}
}
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
function renderContent(
result: GQL.FindPerformersQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) {
function maybeRenderPerformerExportDialog() {
if (isExportDialogOpen) {
return (
<>
<ExportDialog
exportInput={{
performers: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
}}
onClose={() => setIsExportDialogOpen(false)}
function renderContent(
result: GQL.FindPerformersQueryResult,
filter: ListFilterModel,
selectedIds: Set<string>,
onSelectChange: (id: string, selected: boolean, shiftKey: boolean) => void
) {
function maybeRenderPerformerExportDialog() {
if (isExportDialogOpen) {
return (
<>
<ExportDialog
exportInput={{
performers: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
}}
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() {
if (!result.data?.findPerformers) return;
function renderEditDialog(
selectedPerformers: GQL.SlimPerformerDataFragment[],
onClose: (applied: boolean) => void
) {
return (
<EditPerformersDialog selected={selectedPerformers} onClose={onClose} />
);
}
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} />
);
}
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 (
<>
{maybeRenderPerformerExportDialog()}
{renderPerformers()}
</>
);
}
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
<ItemListContext
filterMode={filterMode}
useResult={useFindPerformers}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
};
selectable
>
<ItemList
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
}
);

View File

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

View File

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

View File

@@ -18,6 +18,8 @@ import { StudioTagger } from "../Tagger/studios/StudioTagger";
import { StudioCardGrid } from "./StudioCardGrid";
import { View } from "../List/views";
import { EditStudiosDialog } from "./EditStudiosDialog";
import { IItemListOperation } from "../List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
function getItems(result: GQL.FindStudiosQueryResult) {
return result?.data?.findStudios?.studios ?? [];
@@ -32,177 +34,177 @@ interface IStudioList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
view?: View;
alterQuery?: boolean;
extraOperations?: IItemListOperation<GQL.FindStudiosQueryResult>[];
}
export const StudioList: React.FC<IStudioList> = ({
fromParent,
filterHook,
view,
alterQuery,
}) => {
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
export const StudioList: React.FC<IStudioList> = PatchComponent(
"StudioList",
({ fromParent, filterHook, view, alterQuery, extraOperations = [] }) => {
const intl = useIntl();
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 = [
{
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,
},
];
const otherOperations = [
...extraOperations,
{
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.FindStudiosQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
function addKeybinds(
result: GQL.FindStudiosQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
return () => {
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}`);
}
return () => {
Mousetrap.unbind("p r");
};
}
}
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
async function viewRandom(
result: GQL.FindStudiosQueryResult,
filter: ListFilterModel
) {
// query for a random studio
if (result.data?.findStudios) {
const { count } = result.data.findStudios;
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
function renderContent(
result: GQL.FindStudiosQueryResult,
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)}
/>
);
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}`);
}
}
}
function renderStudios() {
if (!result.data?.findStudios) return;
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
if (filter.displayMode === DisplayMode.Grid) {
return (
<StudioCardGrid
studios={result.data.findStudios.studios}
zoomIndex={filter.zoomIndex}
fromParent={fromParent}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
function renderContent(
result: GQL.FindStudiosQueryResult,
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>;
}
if (filter.displayMode === DisplayMode.Wall) {
return <h1>TODO</h1>;
}
if (filter.displayMode === DisplayMode.Tagger) {
return <StudioTagger studios={result.data.findStudios.studios} />;
function renderStudios() {
if (!result.data?.findStudios) return;
if (filter.displayMode === DisplayMode.Grid) {
return (
<StudioCardGrid
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 (
<>
{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 (
<ItemListContext
filterMode={filterMode}
useResult={useFindStudios}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
<ItemListContext
filterMode={filterMode}
useResult={useFindStudios}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
};
selectable
>
<ItemList
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
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 { EditTagsDialog } from "./EditTagsDialog";
import { View } from "../List/views";
import { IItemListOperation } from "../List/FilteredListToolbar";
import { PatchComponent } from "src/patch";
function getItems(result: GQL.FindTagsForListQueryResult) {
return result?.data?.findTags?.tags ?? [];
@@ -38,341 +40,346 @@ function getCount(result: GQL.FindTagsForListQueryResult) {
interface ITagList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
alterQuery?: boolean;
extraOperations?: IItemListOperation<GQL.FindTagsForListQueryResult>[];
}
export const TagList: React.FC<ITagList> = ({ filterHook, alterQuery }) => {
const Toast = useToast();
const [deletingTag, setDeletingTag] =
useState<Partial<GQL.TagListDataFragment> | null>(null);
export const TagList: React.FC<ITagList> = PatchComponent(
"TagList",
({ filterHook, alterQuery, extraOperations = [] }) => {
const Toast = useToast();
const [deletingTag, setDeletingTag] =
useState<Partial<GQL.TagListDataFragment> | null>(null);
const filterMode = GQL.FilterMode.Tags;
const view = View.Tags;
const filterMode = GQL.FilterMode.Tags;
const view = View.Tags;
function getDeleteTagInput() {
const tagInput: Partial<GQL.TagDestroyInput> = {};
if (deletingTag) {
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}`);
function getDeleteTagInput() {
const tagInput: Partial<GQL.TagDestroyInput> = {};
if (deletingTag) {
tagInput.id = deletingTag.id;
}
return tagInput as GQL.TagDestroyInput;
}
}
const [deleteTag] = useTagDestroy(getDeleteTagInput());
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
const intl = useIntl();
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
const otherOperations = [
...extraOperations,
{
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) {
if (!tag) return;
try {
await mutateMetadataAutoTag({ tags: [tag.id] });
Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" }));
} catch (e) {
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: [],
function addKeybinds(
result: GQL.FindTagsForListQueryResult,
filter: ListFilterModel
) {
Mousetrap.bind("p r", () => {
viewRandom(result, filter);
});
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 (
<ExportDialog
exportInput={{
tags: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
}}
onClose={() => setIsExportDialogOpen(false)}
/>
);
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}`);
}
}
}
function renderTags() {
if (!result.data?.findTags) return;
async function onExport() {
setIsExportAll(false);
setIsExportDialogOpen(true);
}
if (filter.displayMode === DisplayMode.Grid) {
return (
<TagCardGrid
tags={result.data.findTags.tags}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
async function onExportAll() {
setIsExportAll(true);
setIsExportDialogOpen(true);
}
async function onAutoTag(tag: GQL.TagListDataFragment) {
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 (
<div key={tag.id} className="tag-list-row row">
<Link to={`/tags/${tag.id}`}>{tag.name}</Link>
<ExportDialog
exportInput={{
tags: {
ids: Array.from(selectedIds.values()),
all: isExportAll,
},
}}
onClose={() => setIsExportDialogOpen(false)}
/>
);
}
}
<div className="ml-auto">
<Button
variant="secondary"
className="tag-list-button"
onClick={() => onAutoTag(tag)}
>
<FormattedMessage id="actions.auto_tag" />
</Button>
<Button variant="secondary" className="tag-list-button">
<Link
to={NavUtils.makeTagScenesUrl(tag)}
className="tag-list-anchor"
function renderTags() {
if (!result.data?.findTags) return;
if (filter.displayMode === DisplayMode.Grid) {
return (
<TagCardGrid
tags={result.data.findTags.tags}
zoomIndex={filter.zoomIndex}
selectedIds={selectedIds}
onSelectChange={onSelectChange}
/>
);
}
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
id="countables.scenes"
values={{
count: tag.scene_count ?? 0,
}}
<FormattedMessage id="actions.auto_tag" />
</Button>
<Button variant="secondary" className="tag-list-button">
<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} />
</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)
}
/>
</span>
<Button variant="danger" onClick={() => setDeletingTag(tag)}>
<Icon icon={faTrashAlt} color="danger" />
</Button>
</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>
);
});
return (
<div className="col col-sm-8 m-auto">
{tagElements}
{deleteAlert}
</div>
);
}
if (filter.displayMode === DisplayMode.Wall) {
return <h1>TODO</h1>;
}
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 (
<>
{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 (
<ItemListContext
filterMode={filterMode}
useResult={useFindTagsForList}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
selectable
>
<ItemList
<ItemListContext
filterMode={filterMode}
useResult={useFindTagsForList}
getItems={getItems}
getCount={getCount}
alterQuery={alterQuery}
filterHook={filterHook}
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
};
selectable
>
<ItemList
view={view}
otherOperations={otherOperations}
addKeybinds={addKeybinds}
renderContent={renderContent}
renderEditDialog={renderEditDialog}
renderDeleteDialog={renderDeleteDialog}
/>
</ItemListContext>
);
}
);

View File

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