From 2ced1b2d714e73c579c72a9e5d77faf0c3323dea Mon Sep 17 00:00:00 2001 From: Caelan <79105826+jin-wang-c@users.noreply.github.com> Date: Fri, 1 Aug 2025 20:55:57 +0800 Subject: [PATCH] feature/dmxapi_painting_custom_size (#8689) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修改生成图片尺寸 * fix:known problem * fix:Switching but no recovery occurred * fix:The problem of loading images * fix:text i18n --- src/renderer/src/i18n/locales/en-us.json | 2 + src/renderer/src/i18n/locales/ja-jp.json | 2 + src/renderer/src/i18n/locales/ru-ru.json | 2 + src/renderer/src/i18n/locales/zh-cn.json | 2 + src/renderer/src/i18n/locales/zh-tw.json | 2 + .../src/pages/paintings/DmxapiPage.tsx | 250 ++++++++++++------ .../pages/paintings/config/DmxapiConfig.ts | 48 +--- 7 files changed, 189 insertions(+), 119 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 45bd943398..01c44a3fda 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1507,6 +1507,7 @@ "image": "New Image" } }, + "custom_size": "Custom Size", "edit": { "image_file": "Edited Image", "magic_prompt_option_tip": "Intelligently enhances editing prompts", @@ -1629,6 +1630,7 @@ }, "text_desc_required": "Please enter image description first", "title": "Images", + "top_up": "Top up ", "translating": "Translating...", "uploaded_input": "Uploaded input", "upscale": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index e753d8010d..e9e19ae633 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1507,6 +1507,7 @@ "image": "新しい画像" } }, + "custom_size": "カスタムサイズ", "edit": { "image_file": "編集画像", "magic_prompt_option_tip": "編集効果を向上させるための提示詞を最適化します", @@ -1629,6 +1630,7 @@ }, "text_desc_required": "画像の説明を先に入力してください", "title": "画像", + "top_up": "チャージする", "translating": "翻訳中...", "uploaded_input": "アップロード済みの入力", "upscale": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 08d1bf6dba..59e965dae5 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1507,6 +1507,7 @@ "image": "Новое изображение" } }, + "custom_size": "Пользовательский размер", "edit": { "image_file": "Изображение для редактирования", "magic_prompt_option_tip": "Интеллектуально оптимизирует подсказки для улучшения эффекта редактирования", @@ -1629,6 +1630,7 @@ }, "text_desc_required": "Пожалуйста, сначала введите описание изображения", "title": "Изображения", + "top_up": "пополнить счёт", "translating": "Перевод...", "uploaded_input": "Загруженный ввод", "upscale": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index fa455db0ee..1d60875212 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1507,6 +1507,7 @@ "image": "新建图片" } }, + "custom_size": "自定义尺寸", "edit": { "image_file": "编辑的图像", "magic_prompt_option_tip": "智能优化编辑提示词", @@ -1629,6 +1630,7 @@ }, "text_desc_required": "请先输入图片描述", "title": "图片", + "top_up": "充值", "translating": "翻译中...", "uploaded_input": "已上传输入", "upscale": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 27657edc2a..0f44b308ac 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1507,6 +1507,7 @@ "image": "新繪圖" } }, + "custom_size": "自訂尺寸", "edit": { "image_file": "編輯圖像", "magic_prompt_option_tip": "智能優化編輯提示詞", @@ -1629,6 +1630,7 @@ }, "text_desc_required": "請先輸入圖片描述", "title": "繪圖", + "top_up": "儲值", "translating": "翻譯中...", "uploaded_input": "已上傳輸入", "upscale": { diff --git a/src/renderer/src/pages/paintings/DmxapiPage.tsx b/src/renderer/src/pages/paintings/DmxapiPage.tsx index e4e9647981..a829320ddd 100644 --- a/src/renderer/src/pages/paintings/DmxapiPage.tsx +++ b/src/renderer/src/pages/paintings/DmxapiPage.tsx @@ -1,11 +1,10 @@ import { PlusOutlined, RedoOutlined } from '@ant-design/icons' import DMXAPIToImg from '@renderer/assets/images/providers/DMXAPI-to-img.webp' import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar' -import { HStack, VStack } from '@renderer/components/Layout' +import { HStack } from '@renderer/components/Layout' import Scrollbar from '@renderer/components/Scrollbar' import { isMac } from '@renderer/config/constant' import { getProviderLogo } from '@renderer/config/providers' -import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' import { useRuntime } from '@renderer/hooks/useRuntime' @@ -16,7 +15,7 @@ import { setGenerating } from '@renderer/store/runtime' import type { FileMetadata, PaintingsState } from '@renderer/types' import { uuid } from '@renderer/utils' import { DmxapiPainting } from '@types' -import { Avatar, Button, Input, Radio, Segmented, Select, Switch, Tooltip } from 'antd' +import { Avatar, Button, Input, InputNumber, Segmented, Select, Switch, Tooltip } from 'antd' import TextArea from 'antd/es/input/TextArea' import { Info } from 'lucide-react' import React, { FC, useEffect, useRef, useState } from 'react' @@ -34,9 +33,9 @@ import { COURSE_URL, DEFAULT_PAINTING, GetModelGroup, - IMAGE_SIZES, MODEOPTIONS, - STYLE_TYPE_OPTIONS + STYLE_TYPE_OPTIONS, + TOP_UP_URL } from './config/DmxapiConfig' const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString() @@ -45,7 +44,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const [mode] = useState('DMXAPIPaintings') const { DMXAPIPaintings, addPainting, removePainting, updatePainting } = usePaintings() const [painting, setPainting] = useState(DMXAPIPaintings?.[0] || DEFAULT_PAINTING) - const { theme } = useTheme() const { t } = useTranslation() const providers = useAllProviders() const providerOptions = Options.map((option) => { @@ -88,6 +86,11 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { paths: [] }) + // 自定义尺寸相关状态 + const [isCustomSize, setIsCustomSize] = useState(false) + const [customWidth, setCustomWidth] = useState() + const [customHeight, setCustomHeight] = useState() + const modeOptions = MODEOPTIONS.map((ele) => { return { label: t(ele.label), @@ -144,25 +147,45 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { updatePainting('DMXAPIPaintings', updatedPainting) } - const getNewPainting = (params?: Partial) => { - clearImages() - const generationMode = params?.generationMode || painting?.generationMode || MODEOPTIONS[0].value - const modelGroups = getModelOptions(generationMode as generationModeType) - // 获取第一个非空分组的第一个模型 - let firstModel = '' + const getFirstModelInfo = (v: generationModeType) => { + const modelGroups = getModelOptions(v) + + let model = '' + let priceModel = '' + let image_size = '' for (const provider of Object.keys(modelGroups)) { - if (modelGroups[provider].length > 0) { - firstModel = modelGroups[provider][0].id + if (modelGroups[provider] && modelGroups[provider].length > 0) { + model = modelGroups[provider][0].id + priceModel = modelGroups[provider][0].price + image_size = modelGroups[provider][0].image_sizes[0].value break } } + return { + model, + priceModel, + image_size, + modelGroups + } + } + + const getNewPainting = (params?: Partial) => { + clearImages() + + const generationMode = params?.generationMode || painting?.generationMode || MODEOPTIONS[0].value + + const { model, priceModel, image_size, modelGroups } = getFirstModelInfo(generationMode) + return { ...DEFAULT_PAINTING, id: uuid(), seed: generateRandomSeed(), generationMode, - model: firstModel, + model, + modelGroups, + priceModel, + image_size, ...params } } @@ -180,7 +203,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const onSelectModel = (modelId: string) => { const model = allModels.find((m) => m.id === modelId) if (model) { - updatePaintingState({ model: modelId, priceModel: model.price }) + updatePaintingState({ model: modelId, priceModel: model.price, image_size: model.image_sizes[0].value }) } } @@ -189,8 +212,34 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { } const onSelectImageSize = (v: string) => { - const size = IMAGE_SIZES.find((i) => i.value === v) - size && updatePaintingState({ image_size: size.value, aspect_ratio: size.label }) + if (v === 'custom') { + setIsCustomSize(true) + // 如果有自定义尺寸值,使用它们 + if (customWidth && customHeight) { + updatePaintingState({ image_size: `${customWidth}x${customHeight}`, aspect_ratio: 'custom' }) + } + } else { + setIsCustomSize(false) + const currentModel = allModels.find((m) => m.id === painting.model) + const size = currentModel?.image_sizes?.find((i) => i.value === v) + size && updatePaintingState({ image_size: size.value, aspect_ratio: size.label }) + } + } + + const onCustomSizeChange = (value: number | null, type: string) => { + if (value === null) return + + if (type === 'width') { + setCustomWidth(value) + if (customHeight) { + updatePaintingState({ image_size: `${value}x${customHeight}`, aspect_ratio: 'custom' }) + } + } else if (type === 'height') { + setCustomHeight(value) + if (customWidth) { + updatePaintingState({ image_size: `${customWidth}x${value}`, aspect_ratio: 'custom' }) + } + } } const onSelectStyleType = (v: string) => { @@ -251,27 +300,21 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { } const onGenerationModeChange = (v: generationModeType) => { - clearImages() - const newModelGroups = getModelOptions(v) - setModelOptions(newModelGroups) - - // 获取第一个非空分组的第一个模型 - let firstModel = '' - let priceModel = '' - for (const provider of Object.keys(newModelGroups)) { - if (newModelGroups[provider] && newModelGroups[provider].length > 0) { - firstModel = newModelGroups[provider][0].id - priceModel = newModelGroups[provider][0].price - break - } + if (isLoading) { + return } + clearImages() + + const { model, priceModel, image_size, modelGroups } = getFirstModelInfo(v) + + setModelOptions(modelGroups) + // 如果有urls,创建新的painting if (Array.isArray(painting.urls) && painting.urls.length > 0) { const newPainting = getNewPainting({ generationMode: v, - model: firstModel, // 使用新模式下的第一个模型 - priceModel: priceModel + model }) const addedPainting = addPainting('DMXAPIPaintings', newPainting) setPainting(addedPainting) @@ -279,12 +322,20 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { // 否则更新当前painting updatePaintingState({ generationMode: v, - model: firstModel, // 使用新模式下的第一个模型 + model: model, + image_size: image_size, priceModel: priceModel }) } } + const createNewPainting = () => { + if (isLoading) { + return + } + setPainting(addPainting('DMXAPIPaintings', getNewPainting())) + } + // 检查提供者状态函数 const checkProviderStatus = () => { if (!dmxapiProvider.enabled) { @@ -324,10 +375,6 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { 'Content-Type': 'application/json' } - if (painting.aspect_ratio) { - params['aspect_ratio'] = painting.aspect_ratio - } - if (painting.image_size) { params['size'] = painting.image_size } @@ -360,7 +407,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { } if (painting.image_size) { - params['size'] = '1024x1024' + params['size'] = painting.image_size } if (painting.style_type) { @@ -562,6 +609,10 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { const onDeletePainting = async (paintingToDelete: DmxapiPainting) => { if (paintingToDelete.id === painting.id) { + if (isLoading) { + return + } + const currentIndex = DMXAPIPaintings.findIndex((p) => p.id === paintingToDelete.id) if (currentIndex > 0) { @@ -715,17 +766,21 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoadingModels, dynamicModelGroups]) // 依赖模型加载状态 + // 当模型切换时,检查是否支持自定义尺寸 + useEffect(() => { + const currentModel = allModels.find((m) => m.id === painting.model) + if (currentModel && !currentModel.is_custom_size && isCustomSize) { + setIsCustomSize(false) + } + }, [painting.model, allModels, isCustomSize]) + return ( {t('paintings.title')} {isMac && ( - @@ -735,15 +790,20 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { {t('common.provider')} - - {t('paintings.paint_course')} +
+ + {t('paintings.paint_course')} + + + {t('paintings.top_up')} + - +
+ {t('paintings.image.size')} + + + {/* 自定义尺寸输入框 */} + {isCustomSize && allModels.find((m) => m.id === painting.model)?.is_custom_size && ( +
+ + onCustomSizeChange(value, 'width')} + min={parseInt(allModels.find((m) => m.id === painting.model)?.min_image_size || '512')} + max={parseInt(allModels.find((m) => m.id === painting.model)?.max_image_size || '2048')} + style={{ width: 80, flex: 1 }} + /> + x + onCustomSizeChange(value, 'height')} + min={parseInt(allModels.find((m) => m.id === painting.model)?.min_image_size || 512)} + max={parseInt(allModels.find((m) => m.id === painting.model)?.max_image_size || 2048)} + style={{ width: 80, flex: 1 }} + /> + px + +
+ )} + {painting.generationMode === generationModeType.GENERATION && ( <> - {t('paintings.image.size')} - onSelectImageSize(e.target.value)} - style={{ display: 'flex' }}> - {IMAGE_SIZES.map((size) => ( - - - - {size.label} - - - ))} - - {t('paintings.seed')} @@ -896,7 +999,7 @@ const DmxapiPage: FC<{ Options: string[] }> = ({ Options }) => { selectedPainting={painting} onSelectPainting={onSelectPainting} onDeletePainting={onDeletePainting} - onNewPainting={() => setPainting(addPainting('DMXAPIPaintings', getNewPainting()))} + onNewPainting={createNewPainting} />
@@ -991,22 +1094,6 @@ const ToolbarMenu = styled.div` align-items: center; gap: 6px; ` - -const ImageSizeImage = styled.img<{ theme: string }>` - filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')}; - margin-top: 8px; -` - -const RadioButton = styled(Radio.Button)` - width: 30px; - height: 55px; - display: flex; - flex-direction: column; - flex: 1; - justify-content: center; - align-items: center; -` - const InfoIcon = styled(Info)` margin-left: 5px; cursor: help; @@ -1078,8 +1165,11 @@ const EmptyImgBox = styled.div` const EmptyImg = styled.div<{ bgUrl?: string }>` width: 70vh; height: 70vh; - background-size: cover; + background-size: contain; + background-repeat: no-repeat; + background-position: center; background-image: ${(props) => (props.bgUrl ? `url(${props.bgUrl})` : `url(${DMXAPIToImg})`)}; + background-color: #ffffff; ` const LoadTextWrap = styled.div` diff --git a/src/renderer/src/pages/paintings/config/DmxapiConfig.ts b/src/renderer/src/pages/paintings/config/DmxapiConfig.ts index 7336b66451..261dcb6a48 100644 --- a/src/renderer/src/pages/paintings/config/DmxapiConfig.ts +++ b/src/renderer/src/pages/paintings/config/DmxapiConfig.ts @@ -1,9 +1,3 @@ -import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg' -import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg' -import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg' -import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg' -import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg' -import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg' import { uuid } from '@renderer/utils' import { t } from 'i18next' @@ -15,6 +9,13 @@ export type DMXApiModelData = { provider: string name: string price: string + image_sizes: Array<{ + label: string + value: string + }> + is_custom_size: boolean + max_image_size?: number + min_image_size?: number } // 模型分组类型 @@ -54,41 +55,10 @@ export const STYLE_TYPE_OPTIONS = [ { label: '巴洛克', value: '巴洛克' } ] -export const IMAGE_SIZES = [ - { - label: '1:1', - value: '1328x1328', - icon: ImageSize1_1 - }, - { - label: '1:2', - value: '800x1600', - icon: ImageSize1_2 - }, - { - label: '3:2', - value: '1584x1056', - icon: ImageSize3_2 - }, - { - label: '3:4', - value: '1104x1472', - icon: ImageSize3_4 - }, - { - label: '16:9', - value: '1664x936', - icon: ImageSize16_9 - }, - { - label: '9:16', - value: '936x1664', - icon: ImageSize9_16 - } -] - export const COURSE_URL = 'http://seedream.dmxapi.cn/' +export const TOP_UP_URL = 'https://www.dmxapi.cn/topup' + export const DEFAULT_PAINTING: DmxapiPainting = { id: uuid(), urls: [],