From cce025d9404f627f7733e5a2f87393d9a1a7192f Mon Sep 17 00:00:00 2001 From: Gykes <24581046+Gykes@users.noreply.github.com> Date: Sat, 20 Dec 2025 22:57:41 -0800 Subject: [PATCH] initial --- graphql/schema/types/metadata.graphql | 6 ++- internal/manager/task_generate.go | 28 +++++++++-- .../src/components/Dialogs/GenerateDialog.tsx | 21 +++++--- .../Galleries/GalleryDetails/Gallery.tsx | 50 +++++++++++++++++++ .../Settings/Tasks/GenerateOptions.tsx | 4 +- 5 files changed, 96 insertions(+), 13 deletions(-) diff --git a/graphql/schema/types/metadata.graphql b/graphql/schema/types/metadata.graphql index 66b74bb86..3d004ccb3 100644 --- a/graphql/schema/types/metadata.graphql +++ b/graphql/schema/types/metadata.graphql @@ -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 diff --git a/internal/manager/task_generate.go b/internal/manager/task_generate.go index 5631db87b..2b330bcf3 100644 --- a/internal/manager/task_generate.go +++ b/internal/manager/task_generate.go @@ -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 diff --git a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx index c30073e48..a5688aed0 100644 --- a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -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 = ({ +export const GenerateDialog: React.FC = ({ 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 = ({ }, [configuration, configRead]); const selectionStatus = useMemo(() => { + const countableIds: Record = { + scene: "countables.scenes", + image: "countables.images", + gallery: "countables.galleries", + }; + const countableId = countableIds[type]; + if (selectedIds) { return ( @@ -101,7 +109,7 @@ export const GenerateDialog: React.FC = ({ num: selectedIds.length, scene: intl.formatMessage( { - id: "countables.scenes", + id: countableId, }, { count: selectedIds.length, @@ -121,7 +129,7 @@ export const GenerateDialog: React.FC = ({ num: intl.formatMessage({ id: "all" }), scene: intl.formatMessage( { - id: "countables.scenes", + id: countableId, }, { count: 0, @@ -138,7 +146,7 @@ export const GenerateDialog: React.FC = ({
{message}
); - }, [selectedIds, intl]); + }, [selectedIds, intl, type]); async function onGenerate() { try { @@ -146,6 +154,7 @@ export const GenerateDialog: React.FC = ({ ...options, sceneIDs, imageIDs, + galleryIDs, }); Toast.success( intl.formatMessage( diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 195766e03..390f32e92 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -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,10 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { } const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false); + const [generateImageIds, setGenerateImageIds] = useState([]); + + const [fetchGalleryImages] = GQL.useFindImagesLazyQuery(); function onDeleteDialogClosed(deleted: boolean) { setIsDeleteAlertOpen(false); @@ -173,6 +182,28 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { } } + async function onGenerate() { + const result = await fetchGalleryImages({ + variables: { + image_filter: { + galleries: { + modifier: GQL.CriterionModifier.Includes, + value: [gallery.id], + }, + }, + filter: { + per_page: -1, + }, + }, + }); + + if (result.data?.findImages?.images) { + const imageIds = result.data.findImages.images.map((img) => img.id); + setGenerateImageIds(imageIds); + setIsGenerateDialogOpen(true); + } + } + function maybeRenderDeleteDialog() { if (isDeleteAlertOpen && gallery) { return ( @@ -184,6 +215,18 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { } } + function maybeRenderGenerateDialog() { + if (isGenerateDialogOpen) { + return ( + setIsGenerateDialogOpen(false)} + type="image" + /> + ); + } + } + function renderOperations() { return ( @@ -210,6 +253,12 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { > + onGenerate()} + > + + setIsDeleteAlertOpen(true)} @@ -387,6 +436,7 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { {title} {maybeRenderDeleteDialog()} + {maybeRenderGenerateDialog()}
diff --git a/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx b/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx index 24be2c7fe..c68b6d5eb 100644 --- a/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx @@ -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 = ({ } const showSceneOptions = !type || type === "scene"; - const showImageOptions = !type || type === "image"; + const showImageOptions = !type || type === "image" || type === "gallery"; return ( <>