This commit is contained in:
Gykes 2025-12-20 22:57:41 -08:00
parent 0e54a5ceb0
commit cce025d940
5 changed files with 96 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,10 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
}
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
const [generateImageIds, setGenerateImageIds] = useState<string[]>([]);
const [fetchGalleryImages] = GQL.useFindImagesLazyQuery();
function onDeleteDialogClosed(deleted: boolean) {
setIsDeleteAlertOpen(false);
@ -173,6 +182,28 @@ export const GalleryPage: React.FC<IProps> = ({ 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<IProps> = ({ gallery, add }) => {
}
}
function maybeRenderGenerateDialog() {
if (isGenerateDialogOpen) {
return (
<GenerateDialog
selectedIds={generateImageIds}
onClose={() => setIsGenerateDialogOpen(false)}
type="image"
/>
);
}
}
function renderOperations() {
return (
<Dropdown>
@ -210,6 +253,12 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
>
<FormattedMessage id="actions.reset_cover" />
</Dropdown.Item>
<Dropdown.Item
className="bg-secondary text-white"
onClick={() => onGenerate()}
>
<FormattedMessage id="actions.generate" />
</Dropdown.Item>
<Dropdown.Item
className="bg-secondary text-white"
onClick={() => setIsDeleteAlertOpen(true)}
@ -387,6 +436,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

@ -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 (
<>