diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 750376b34..09e3e99c6 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -29,7 +29,8 @@ "back": "Back", "chat": "Chat", "close": "Close", - "cancel": "Cancel" + "cancel": "Cancel", + "download": "Download" }, "button": { "add": "Add", @@ -351,7 +352,8 @@ "button.translate": "Translate", "error.not_configured": "Translation model is not configured", "input.placeholder": "Enter text to translate", - "output.placeholder": "Translation" + "output.placeholder": "Translation", + "confirm": "Original text has been copied to clipboard. Do you want to replace it with the translated text?" }, "languages": { "english": "English", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 69fcc0503..a17fd5cbd 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -29,7 +29,8 @@ "back": "返回", "chat": "聊天", "close": "关闭", - "cancel": "取消" + "cancel": "取消", + "download": "下载" }, "button": { "add": "添加", @@ -285,7 +286,7 @@ "provider.search_placeholder": "搜索模型 ID 或名称", "provider.api.url.reset": "重置", "provider.api.url.preview": "预览: {{url}}", - "provider.api.url.tip": "/结尾忽略v1版本,#结尾��制使用输入地址", + "provider.api.url.tip": "/结尾忽略v1版本,#结尾制使用输入地址", "models.default_assistant_model": "默认助手模型", "models.topic_naming_model": "话题命名模型", "models.translate_model": "翻译模型", @@ -351,7 +352,8 @@ "button.translate": "翻译", "error.not_configured": "翻译模型未配置", "input.placeholder": "输入文本进行翻译", - "output.placeholder": "翻译" + "output.placeholder": "翻译", + "confirm": "原文已复制到剪贴板,是否用翻译后的文本替换?" }, "languages": { "english": "英文", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 83a461d74..b2d8eab52 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -29,7 +29,8 @@ "back": "返回", "chat": "聊天", "close": "關閉", - "cancel": "取消" + "cancel": "取消", + "download": "下載" }, "button": { "add": "添加", @@ -351,7 +352,8 @@ "button.translate": "翻譯", "error.not_configured": "翻譯模型未配置", "input.placeholder": "輸入文字進行翻譯", - "output.placeholder": "翻譯" + "output.placeholder": "翻譯", + "confirm": "原文已複製到剪貼簿,是否用翻譯後的文字替換?" }, "languages": { "english": "英文", diff --git a/src/renderer/src/pages/paintings/Artboard.tsx b/src/renderer/src/pages/paintings/Artboard.tsx new file mode 100644 index 000000000..3669e2274 --- /dev/null +++ b/src/renderer/src/pages/paintings/Artboard.tsx @@ -0,0 +1,193 @@ +import { CopyOutlined, DownloadOutlined } from '@ant-design/icons' +import FileManager from '@renderer/services/FileManager' +import { Painting } from '@renderer/types' +import { download } from '@renderer/utils/download' +import { Button, Dropdown, Spin } from 'antd' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import ImagePreview from '../home/Markdown/ImagePreview' + +interface ArtboardProps { + painting: Painting + isLoading: boolean + currentImageIndex: number + onPrevImage: () => void + onNextImage: () => void + onCancel: () => void +} + +const Artboard: FC = ({ + painting, + isLoading, + currentImageIndex, + onPrevImage, + onNextImage, + onCancel +}) => { + const { t } = useTranslation() + + const getCurrentImageUrl = () => { + const currentFile = painting.files[currentImageIndex] + return currentFile ? FileManager.getFileUrl(currentFile) : '' + } + + const handleContextMenu = (e: React.MouseEvent) => { + e.preventDefault() + } + + const getContextMenuItems = () => { + return [ + { + key: 'copy', + label: t('common.copy'), + icon: , + onClick: () => { + navigator.clipboard.writeText(painting.urls[currentImageIndex]) + } + }, + { + key: 'download', + label: t('common.download'), + icon: , + onClick: () => download(getCurrentImageUrl()) + } + ] + } + + return ( + + + {painting.files.length > 0 ? ( + + {painting.files.length > 1 && ( + + ← + + )} + + + + {painting.files.length > 1 && ( + + → + + )} + + {currentImageIndex + 1} / {painting.files.length} + + + ) : ( + + )} + {isLoading && ( + + + {t('common.cancel')} + + )} + + + ) +} + +const Container = styled.div` + display: flex; + flex: 1; + flex-direction: row; + justify-content: center; + align-items: center; +` + +const ImagePlaceholder = styled.div` + display: flex; + width: 70vh; + height: 70vh; + background-color: var(--color-background-soft); + align-items: center; + justify-content: center; + cursor: pointer; +` + +const ImageContainer = styled.div` + position: relative; + display: flex; + align-items: center; + justify-content: center; + + .ant-spin { + max-height: none; + } + + .ant-spin-spinning { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 3; + } +` + +const NavigationButton = styled(Button)` + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 2; + opacity: 0.7; + &:hover { + opacity: 1; + } +` + +const ImageCounter = styled.div` + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + background-color: rgba(0, 0, 0, 0.5); + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; +` + +const LoadingContainer = styled.div<{ spinning: boolean }>` + position: relative; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + opacity: ${(props) => (props.spinning ? 0.5 : 1)}; + transition: opacity 0.3s; +` + +const LoadingOverlay = styled.div` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; +` + +const CancelButton = styled(Button)` + margin-top: 10px; + z-index: 1001; +` + +export default Artboard diff --git a/src/renderer/src/pages/paintings/PaintingsPage.tsx b/src/renderer/src/pages/paintings/PaintingsPage.tsx index 7f56778ac..9b696e646 100644 --- a/src/renderer/src/pages/paintings/PaintingsPage.tsx +++ b/src/renderer/src/pages/paintings/PaintingsPage.tsx @@ -19,15 +19,15 @@ import FileManager from '@renderer/services/FileManager' import { DEFAULT_PAINTING } from '@renderer/store/paintings' import { FileType, Painting } from '@renderer/types' import { getErrorMessage } from '@renderer/utils' -import { Button, Input, InputNumber, Radio, Select, Slider, Spin, Tooltip } from 'antd' +import { Button, Input, InputNumber, Radio, Select, Slider, Tooltip } from 'antd' import TextArea from 'antd/es/input/TextArea' import { FC, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import SendMessageButton from '../home/Inputbar/SendMessageButton' -import ImagePreview from '../home/Markdown/ImagePreview' import { SettingTitle } from '../settings' +import Artboard from './Artboard' import PaintingsList from './PaintingsList' const IMAGE_SIZES = [ @@ -168,7 +168,7 @@ const PaintingsPage: FC = () => { await FileManager.addFiles(validFiles) - updatePaintingState({ files: validFiles }) + updatePaintingState({ files: validFiles, urls }) } } catch (error: unknown) { if (error instanceof Error && error.name !== 'AbortError') { @@ -192,11 +192,6 @@ const PaintingsPage: FC = () => { size && updatePaintingState({ imageSize: size.value }) } - const getCurrentImageUrl = () => { - const currentFile = painting.files[currentImageIndex] - return currentFile ? FileManager.getFileUrl(currentFile) : '' - } - const nextImage = () => { setCurrentImageIndex((prev) => (prev + 1) % painting.files.length) } @@ -228,6 +223,23 @@ const PaintingsPage: FC = () => { setCurrentImageIndex(0) } + const handleTranslation = async (translatedText: string) => { + const currentText = textareaRef.current?.resizableTextArea?.textArea?.value + + if (currentText) { + await navigator.clipboard.writeText(currentText) + + const confirmed = await window.modal.confirm({ + content: t('translate.confirm'), + centered: true + }) + + if (confirmed) { + updatePaintingState({ prompt: translatedText }) + } + } + } + return ( @@ -336,46 +348,14 @@ const PaintingsPage: FC = () => { /> - - - {painting.files.length > 0 ? ( - - {painting.files.length > 1 && ( - - ← - - )} - - {painting.files.length > 1 && ( - - → - - )} - - {currentImageIndex + 1} / {painting.files.length} - - - ) : ( - - )} - {isLoading && ( - - - {t('common.cancel')} - - )} - - +