FR: Add Generate Task to Galleries (#6442)

This commit is contained in:
Gykes 2026-02-03 14:34:56 -08:00 committed by GitHub
parent badf9ec35e
commit b76edffc5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 93 additions and 13 deletions

View file

@ -20,10 +20,12 @@ input GenerateMetadataInput {
"scene ids to generate for"
sceneIDs: [ID!]
"image ids to generate for"
imageIDs: [ID!]
"marker ids to generate for"
markerIDs: [ID!]
"image ids to generate for"
imageIDs: [ID!]
"gallery ids to generate for"
galleryIDs: [ID!]
"overwrite existing media"
overwrite: Boolean

View file

@ -35,10 +35,12 @@ type GenerateMetadataInput struct {
ImageThumbnails bool `json:"imageThumbnails"`
// scene ids to generate for
SceneIDs []string `json:"sceneIDs"`
// image ids to generate for
ImageIDs []string `json:"imageIDs"`
// marker ids to generate for
MarkerIDs []string `json:"markerIDs"`
// image ids to generate for
ImageIDs []string `json:"imageIDs"`
// gallery ids to generate for
GalleryIDs []string `json:"galleryIDs"`
// overwrite existing media
Overwrite bool `json:"overwrite"`
}
@ -114,6 +116,10 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error
if err != nil {
logger.Error(err.Error())
}
galleryIDs, err := stringslice.StringSliceToIntSlice(j.input.GalleryIDs)
if err != nil {
logger.Error(err.Error())
}
g := &generate.Generator{
Encoder: instance.FFMpeg,
@ -127,7 +133,7 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error
r := j.repository
if err := r.WithReadTxn(ctx, func(ctx context.Context) error {
qb := r.Scene
if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 && len(j.input.ImageIDs) == 0 {
if len(j.input.SceneIDs) == 0 && len(j.input.MarkerIDs) == 0 && len(j.input.ImageIDs) == 0 && len(j.input.GalleryIDs) == 0 {
j.queueTasks(ctx, g, queue)
} else {
if len(j.input.SceneIDs) > 0 {
@ -161,6 +167,22 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) error
j.queueImageJob(g, i, queue)
}
}
if len(j.input.GalleryIDs) > 0 {
for _, galleryID := range galleryIDs {
imgs, err := r.Image.FindByGalleryID(ctx, galleryID)
if err != nil {
return err
}
for _, img := range imgs {
if err := img.LoadFiles(ctx, r.Image); err != nil {
return err
}
j.queueImageJob(g, img, queue)
}
}
}
}
return nil

View file

@ -14,19 +14,20 @@ import { SettingSection } from "../Settings/SettingSection";
import { faCogs, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { SettingsContext } from "../Settings/context";
interface ISceneGenerateDialog {
interface IGenerateDialog {
selectedIds?: string[];
onClose: () => void;
type: "scene" | "image";
type: "scene" | "image" | "gallery";
}
export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
export const GenerateDialog: React.FC<IGenerateDialog> = ({
selectedIds,
onClose,
type,
}) => {
const sceneIDs = type === "scene" ? selectedIds : undefined;
const imageIDs = type === "image" ? selectedIds : undefined;
const galleryIDs = type === "gallery" ? selectedIds : undefined;
const { configuration } = useConfigurationContext();
@ -92,6 +93,13 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
}, [configuration, configRead]);
const selectionStatus = useMemo(() => {
const countableIds: Record<typeof type, string> = {
scene: "countables.scenes",
image: "countables.images",
gallery: "countables.galleries",
};
const countableId = countableIds[type];
if (selectedIds) {
return (
<Form.Group id="selected-generate-ids">
@ -101,7 +109,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
num: selectedIds.length,
scene: intl.formatMessage(
{
id: "countables.scenes",
id: countableId,
},
{
count: selectedIds.length,
@ -121,7 +129,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
num: intl.formatMessage({ id: "all" }),
scene: intl.formatMessage(
{
id: "countables.scenes",
id: countableId,
},
{
count: 0,
@ -138,7 +146,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
<div>{message}</div>
</Form.Group>
);
}, [selectedIds, intl]);
}, [selectedIds, intl, type]);
async function onGenerate() {
try {
@ -146,6 +154,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
...options,
sceneIDs,
imageIDs,
galleryIDs,
});
Toast.success(
intl.formatMessage(

View file

@ -15,6 +15,11 @@ import {
useFindGallery,
useGalleryUpdate,
} from "src/core/StashService";
import { lazyComponent } from "src/utils/lazyComponent";
const GenerateDialog = lazyComponent(
() => import("../../Dialogs/GenerateDialog")
);
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { Icon } from "src/components/Shared/Icon";
@ -165,6 +170,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
}
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
function onDeleteDialogClosed(deleted: boolean) {
setIsDeleteAlertOpen(false);
@ -184,6 +190,18 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
}
}
function maybeRenderGenerateDialog() {
if (isGenerateDialogOpen) {
return (
<GenerateDialog
selectedIds={[gallery.id]}
onClose={() => setIsGenerateDialogOpen(false)}
type="gallery"
/>
);
}
}
function renderOperations() {
return (
<Dropdown>
@ -210,6 +228,12 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
>
<FormattedMessage id="actions.reset_cover" />
</Dropdown.Item>
<Dropdown.Item
className="bg-secondary text-white"
onClick={() => setIsGenerateDialogOpen(true)}
>
{`${intl.formatMessage({ id: "actions.generate" })}…`}
</Dropdown.Item>
<Dropdown.Item
className="bg-secondary text-white"
onClick={() => setIsDeleteAlertOpen(true)}
@ -387,6 +411,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
<title>{title}</title>
</Helmet>
{maybeRenderDeleteDialog()}
{maybeRenderGenerateDialog()}
<div className={`gallery-tabs ${collapsed ? "collapsed" : ""}`}>
<div>
<div className="gallery-header-container">

View file

@ -12,11 +12,13 @@ import GalleryWallCard from "./GalleryWallCard";
import { EditGalleriesDialog } from "./EditGalleriesDialog";
import { DeleteGalleriesDialog } from "./DeleteGalleriesDialog";
import { ExportDialog } from "../Shared/ExportDialog";
import { GenerateDialog } from "../Dialogs/GenerateDialog";
import { GalleryListTable } from "./GalleryListTable";
import { GalleryCardGrid } from "./GalleryCardGrid";
import { View } from "../List/views";
import { PatchComponent } from "src/patch";
import { IItemListOperation } from "../List/FilteredListToolbar";
import { useModal } from "src/hooks/modal";
function getItems(result: GQL.FindGalleriesQueryResult) {
return result?.data?.findGalleries?.galleries ?? [];
@ -40,6 +42,7 @@ export const GalleryList: React.FC<IGalleryList> = PatchComponent(
const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);
const { modal, showModal, closeModal } = useModal();
const filterMode = GQL.FilterMode.Galleries;
@ -49,6 +52,24 @@ export const GalleryList: React.FC<IGalleryList> = PatchComponent(
text: intl.formatMessage({ id: "actions.view_random" }),
onClick: viewRandom,
},
{
text: `${intl.formatMessage({ id: "actions.generate" })}…`,
onClick: (
_result: GQL.FindGalleriesQueryResult,
_filter: ListFilterModel,
selectedIds: Set<string>
) => {
showModal(
<GenerateDialog
type="gallery"
selectedIds={Array.from(selectedIds.values())}
onClose={() => closeModal()}
/>
);
return Promise.resolve();
},
isDisplayed: showWhenSelected,
},
{
text: intl.formatMessage({ id: "actions.export" }),
onClick: onExport,
@ -172,6 +193,7 @@ export const GalleryList: React.FC<IGalleryList> = PatchComponent(
return (
<>
{maybeRenderGalleryExportDialog()}
{modal}
{renderGalleries()}
</>
);

View file

@ -7,7 +7,7 @@ import {
} from "../GeneratePreviewOptions";
interface IGenerateOptions {
type?: "scene" | "image";
type?: "scene" | "image" | "gallery";
selection?: boolean;
options: GQL.GenerateMetadataInput;
setOptions: (s: GQL.GenerateMetadataInput) => void;
@ -27,7 +27,7 @@ export const GenerateOptions: React.FC<IGenerateOptions> = ({
}
const showSceneOptions = !type || type === "scene";
const showImageOptions = !type || type === "image";
const showImageOptions = !type || type === "image" || type === "gallery";
return (
<>