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", "back": "Back",
"chat": "Chat", "chat": "Chat",
"close": "Close", "close": "Close",
"cancel": "Cancel" "cancel": "Cancel",
"download": "Download"
}, },
"button": { "button": {
"add": "Add", "add": "Add",
@ -351,7 +352,8 @@
"button.translate": "Translate", "button.translate": "Translate",
"error.not_configured": "Translation model is not configured", "error.not_configured": "Translation model is not configured",
"input.placeholder": "Enter text to translate", "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": { "languages": {
"english": "English", "english": "English",

View File

@ -29,7 +29,8 @@
"back": "返回", "back": "返回",
"chat": "聊天", "chat": "聊天",
"close": "关闭", "close": "关闭",
"cancel": "取消" "cancel": "取消",
"download": "下载"
}, },
"button": { "button": {
"add": "添加", "add": "添加",
@ -285,7 +286,7 @@
"provider.search_placeholder": "搜索模型 ID 或名称", "provider.search_placeholder": "搜索模型 ID 或名称",
"provider.api.url.reset": "重置", "provider.api.url.reset": "重置",
"provider.api.url.preview": "预览: {{url}}", "provider.api.url.preview": "预览: {{url}}",
"provider.api.url.tip": "/结尾忽略v1版本#结尾<EFBFBD><EFBFBD>制使用输入地址", "provider.api.url.tip": "/结尾忽略v1版本#结尾制使用输入地址",
"models.default_assistant_model": "默认助手模型", "models.default_assistant_model": "默认助手模型",
"models.topic_naming_model": "话题命名模型", "models.topic_naming_model": "话题命名模型",
"models.translate_model": "翻译模型", "models.translate_model": "翻译模型",
@ -351,7 +352,8 @@
"button.translate": "翻译", "button.translate": "翻译",
"error.not_configured": "翻译模型未配置", "error.not_configured": "翻译模型未配置",
"input.placeholder": "输入文本进行翻译", "input.placeholder": "输入文本进行翻译",
"output.placeholder": "翻译" "output.placeholder": "翻译",
"confirm": "原文已复制到剪贴板,是否用翻译后的文本替换?"
}, },
"languages": { "languages": {
"english": "英文", "english": "英文",

View File

@ -29,7 +29,8 @@
"back": "返回", "back": "返回",
"chat": "聊天", "chat": "聊天",
"close": "關閉", "close": "關閉",
"cancel": "取消" "cancel": "取消",
"download": "下載"
}, },
"button": { "button": {
"add": "添加", "add": "添加",
@ -351,7 +352,8 @@
"button.translate": "翻譯", "button.translate": "翻譯",
"error.not_configured": "翻譯模型未配置", "error.not_configured": "翻譯模型未配置",
"input.placeholder": "輸入文字進行翻譯", "input.placeholder": "輸入文字進行翻譯",
"output.placeholder": "翻譯" "output.placeholder": "翻譯",
"confirm": "原文已複製到剪貼簿,是否用翻譯後的文字替換?"
}, },
"languages": { "languages": {
"english": "英文", "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 { DEFAULT_PAINTING } from '@renderer/store/paintings'
import { FileType, Painting } from '@renderer/types' import { FileType, Painting } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils' 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 TextArea from 'antd/es/input/TextArea'
import { FC, useRef, useState } from 'react' import { FC, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import SendMessageButton from '../home/Inputbar/SendMessageButton' import SendMessageButton from '../home/Inputbar/SendMessageButton'
import ImagePreview from '../home/Markdown/ImagePreview'
import { SettingTitle } from '../settings' import { SettingTitle } from '../settings'
import Artboard from './Artboard'
import PaintingsList from './PaintingsList' import PaintingsList from './PaintingsList'
const IMAGE_SIZES = [ const IMAGE_SIZES = [
@ -168,7 +168,7 @@ const PaintingsPage: FC = () => {
await FileManager.addFiles(validFiles) await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles }) updatePaintingState({ files: validFiles, urls })
} }
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error && error.name !== 'AbortError') { if (error instanceof Error && error.name !== 'AbortError') {
@ -192,11 +192,6 @@ const PaintingsPage: FC = () => {
size && updatePaintingState({ imageSize: size.value }) size && updatePaintingState({ imageSize: size.value })
} }
const getCurrentImageUrl = () => {
const currentFile = painting.files[currentImageIndex]
return currentFile ? FileManager.getFileUrl(currentFile) : ''
}
const nextImage = () => { const nextImage = () => {
setCurrentImageIndex((prev) => (prev + 1) % painting.files.length) setCurrentImageIndex((prev) => (prev + 1) % painting.files.length)
} }
@ -228,6 +223,23 @@ const PaintingsPage: FC = () => {
setCurrentImageIndex(0) 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 ( return (
<Container> <Container>
<Navbar> <Navbar>
@ -336,46 +348,14 @@ const PaintingsPage: FC = () => {
/> />
</LeftContainer> </LeftContainer>
<MainContainer> <MainContainer>
<Artboard> <Artboard
<LoadingContainer spinning={isLoading}> painting={painting}
{painting.files.length > 0 ? ( isLoading={isLoading}
<ImageContainer> currentImageIndex={currentImageIndex}
{painting.files.length > 1 && ( onPrevImage={prevImage}
<NavigationButton onClick={prevImage} style={{ left: 10 }}> onNextImage={nextImage}
onCancel={onCancel}
</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>
<InputContainer> <InputContainer>
<Textarea <Textarea
ref={textareaRef} ref={textareaRef}
@ -389,7 +369,7 @@ const PaintingsPage: FC = () => {
<ToolbarMenu> <ToolbarMenu>
<TranslateButton <TranslateButton
text={textareaRef.current?.resizableTextArea?.textArea?.value} text={textareaRef.current?.resizableTextArea?.textArea?.value}
onTranslated={(translatedText) => updatePaintingState({ prompt: translatedText })} onTranslated={handleTranslation}
disabled={isLoading} disabled={isLoading}
style={{ marginRight: 6 }} style={{ marginRight: 6 }}
/> />
@ -444,14 +424,6 @@ const MainContainer = styled.div`
background-color: var(--color-background); 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` const InputContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -492,16 +464,6 @@ const ToolbarMenu = styled.div`
gap: 6px; 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 }>` const ImageSizeImage = styled.img<{ theme: string }>`
filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')}; filter: ${({ theme }) => (theme === 'dark' ? 'invert(100%)' : 'none')};
margin-top: 8px; margin-top: 8px;
@ -532,73 +494,4 @@ const RefreshIcon = styled.span`
cursor: pointer; 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 export default PaintingsPage

View File

@ -36,7 +36,7 @@ const ShortcutSettings: FC = () => {
return ( return (
<span> <span>
{keys.map((key) => ( {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> <span style={{ fontFamily: 'monospace' }}>{key}</span>
</Tag> </Tag>
))} ))}

View File

@ -1,4 +1,15 @@
export const download = (url: string) => { 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) fetch(url)
.then((response) => { .then((response) => {
// 尝试从Content-Disposition头获取文件名 // 尝试从Content-Disposition头获取文件名