feat: added translations and ui improvements

This commit is contained in:
kangfenmao 2024-10-31 13:18:35 +08:00
parent ca2a9ed84a
commit f6aa0dc55a
7 changed files with 247 additions and 144 deletions

View File

@ -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",

View File

@ -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版本#结尾<EFBFBD><EFBFBD>制使用输入地址",
"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": "英文",

View File

@ -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": "英文",

View File

@ -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<ArtboardProps> = ({
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: <CopyOutlined />,
onClick: () => {
navigator.clipboard.writeText(painting.urls[currentImageIndex])
}
},
{
key: 'download',
label: t('common.download'),
icon: <DownloadOutlined />,
onClick: () => download(getCurrentImageUrl())
}
]
}
return (
<Container>
<LoadingContainer spinning={isLoading}>
{painting.files.length > 0 ? (
<ImageContainer>
{painting.files.length > 1 && (
<NavigationButton onClick={onPrevImage} style={{ left: 10 }}>
</NavigationButton>
)}
<Dropdown menu={{ items: getContextMenuItems() }} trigger={['contextMenu']}>
<ImagePreview
src={getCurrentImageUrl()}
preview={{ mask: false }}
onContextMenu={handleContextMenu}
style={{
width: '70vh',
height: '70vh',
objectFit: 'contain',
backgroundColor: 'var(--color-background-soft)',
cursor: 'pointer'
}}
/>
</Dropdown>
{painting.files.length > 1 && (
<NavigationButton onClick={onNextImage} style={{ right: 10 }}>
</NavigationButton>
)}
<ImageCounter>
{currentImageIndex + 1} / {painting.files.length}
</ImageCounter>
</ImageContainer>
) : (
<ImagePlaceholder />
)}
{isLoading && (
<LoadingOverlay>
<Spin size="large" />
<CancelButton onClick={onCancel}>{t('common.cancel')}</CancelButton>
</LoadingOverlay>
)}
</LoadingContainer>
</Container>
)
}
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

View File

@ -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 (
<Container>
<Navbar>
@ -336,46 +348,14 @@ const PaintingsPage: FC = () => {
/>
</LeftContainer>
<MainContainer>
<Artboard>
<LoadingContainer spinning={isLoading}>
{painting.files.length > 0 ? (
<ImageContainer>
{painting.files.length > 1 && (
<NavigationButton onClick={prevImage} style={{ left: 10 }}>
</NavigationButton>
)}
<ImagePreview
src={getCurrentImageUrl()}
preview={{ mask: false }}
style={{
width: '70vh',
height: '70vh',
objectFit: 'contain',
backgroundColor: 'var(--color-background-soft)',
cursor: 'pointer'
}}
/>
{painting.files.length > 1 && (
<NavigationButton onClick={nextImage} style={{ right: 10 }}>
</NavigationButton>
)}
<ImageCounter>
{currentImageIndex + 1} / {painting.files.length}
</ImageCounter>
</ImageContainer>
) : (
<ImagePlaceholder />
)}
{isLoading && (
<LoadingOverlay>
<Spin size="large" />
<CancelButton onClick={onCancel}>{t('common.cancel')}</CancelButton>
</LoadingOverlay>
)}
</LoadingContainer>
</Artboard>
<Artboard
painting={painting}
isLoading={isLoading}
currentImageIndex={currentImageIndex}
onPrevImage={prevImage}
onNextImage={nextImage}
onCancel={onCancel}
/>
<InputContainer>
<Textarea
ref={textareaRef}
@ -389,7 +369,7 @@ const PaintingsPage: FC = () => {
<ToolbarMenu>
<TranslateButton
text={textareaRef.current?.resizableTextArea?.textArea?.value}
onTranslated={(translatedText) => updatePaintingState({ prompt: translatedText })}
onTranslated={handleTranslation}
disabled={isLoading}
style={{ marginRight: 6 }}
/>
@ -444,14 +424,6 @@ const MainContainer = styled.div`
background-color: var(--color-background);
`
const Artboard = styled.div`
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
`
const InputContainer = styled.div`
display: flex;
flex-direction: column;
@ -492,16 +464,6 @@ const ToolbarMenu = styled.div`
gap: 6px;
`
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 ImageSizeImage = styled.img<{ theme: string }>`
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
margin-top: 8px;
@ -532,73 +494,4 @@ const RefreshIcon = styled.span`
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 PaintingsPage

View File

@ -36,7 +36,7 @@ const ShortcutSettings: FC = () => {
return (
<span>
{keys.map((key) => (
<Tag key={key} style={{ padding: '0 8px' }}>
<Tag key={key} style={{ padding: '2px 8px', fontSize: '13px' }}>
<span style={{ fontFamily: 'monospace' }}>{key}</span>
</Tag>
))}

View File

@ -1,4 +1,15 @@
export const download = (url: string) => {
// 处理 file:// 协议
if (url.startsWith('file://')) {
const link = document.createElement('a')
link.href = url
link.download = url.split('/').pop() || 'download'
document.body.appendChild(link)
link.click()
link.remove()
return
}
fetch(url)
.then((response) => {
// 尝试从Content-Disposition头获取文件名