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..18cbeff96 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,7 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { } const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false); function onDeleteDialogClosed(deleted: boolean) { setIsDeleteAlertOpen(false); @@ -184,6 +190,18 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { } } + function maybeRenderGenerateDialog() { + if (isGenerateDialogOpen) { + return ( + setIsGenerateDialogOpen(false)} + type="gallery" + /> + ); + } + } + function renderOperations() { return ( @@ -210,6 +228,12 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { > + setIsGenerateDialogOpen(true)} + > + {`${intl.formatMessage({ id: "actions.generate" })}…`} + setIsDeleteAlertOpen(true)} @@ -387,6 +411,7 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { {title} {maybeRenderDeleteDialog()} + {maybeRenderGenerateDialog()}
diff --git a/ui/v2.5/src/components/Galleries/GalleryList.tsx b/ui/v2.5/src/components/Galleries/GalleryList.tsx index 9a4fc5236..4afbab620 100644 --- a/ui/v2.5/src/components/Galleries/GalleryList.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryList.tsx @@ -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 = 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 = PatchComponent( text: intl.formatMessage({ id: "actions.view_random" }), onClick: viewRandom, }, + { + text: `${intl.formatMessage({ id: "actions.generate" })}…`, + onClick: ( + _result: GQL.FindGalleriesQueryResult, + _filter: ListFilterModel, + selectedIds: Set + ) => { + showModal( + closeModal()} + /> + ); + return Promise.resolve(); + }, + isDisplayed: showWhenSelected, + }, { text: intl.formatMessage({ id: "actions.export" }), onClick: onExport, @@ -172,6 +193,7 @@ export const GalleryList: React.FC = PatchComponent( return ( <> {maybeRenderGalleryExportDialog()} + {modal} {renderGalleries()} ); 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 ( <>