feat: miniWindow Pin/Resize (#3201)
feat: [#2030] miniWindow pin/resizable/copy toast/move optimized
This commit is contained in:
parent
91b9a48c48
commit
0fd9b6e56c
@ -255,6 +255,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
|
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
|
||||||
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
|
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
|
||||||
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
|
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
|
||||||
|
ipcMain.handle('miniwindow:set-pin', (_, isPinned) => windowService.setPinMiniWindow(isPinned))
|
||||||
|
|
||||||
// aes
|
// aes
|
||||||
ipcMain.handle('aes:encrypt', (_, text: string, secretKey: string, iv: string) => encrypt(text, secretKey, iv))
|
ipcMain.handle('aes:encrypt', (_, text: string, secretKey: string, iv: string) => encrypt(text, secretKey, iv))
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export class WindowService {
|
|||||||
private static instance: WindowService | null = null
|
private static instance: WindowService | null = null
|
||||||
private mainWindow: BrowserWindow | null = null
|
private mainWindow: BrowserWindow | null = null
|
||||||
private miniWindow: BrowserWindow | null = null
|
private miniWindow: BrowserWindow | null = null
|
||||||
|
private isPinnedMiniWindow: boolean = false
|
||||||
private wasFullScreen: boolean = false
|
private wasFullScreen: boolean = false
|
||||||
//hacky-fix: store the focused status of mainWindow before miniWindow shows
|
//hacky-fix: store the focused status of mainWindow before miniWindow shows
|
||||||
//to restore the focus status when miniWindow hides
|
//to restore the focus status when miniWindow hides
|
||||||
@ -378,8 +379,12 @@ export class WindowService {
|
|||||||
|
|
||||||
public createMiniWindow(isPreload: boolean = false): BrowserWindow {
|
public createMiniWindow(isPreload: boolean = false): BrowserWindow {
|
||||||
this.miniWindow = new BrowserWindow({
|
this.miniWindow = new BrowserWindow({
|
||||||
width: 500,
|
width: 550,
|
||||||
height: 520,
|
height: 400,
|
||||||
|
minWidth: 350,
|
||||||
|
minHeight: 380,
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 768,
|
||||||
show: false,
|
show: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
transparent: isMac,
|
transparent: isMac,
|
||||||
@ -388,7 +393,7 @@ export class WindowService {
|
|||||||
center: true,
|
center: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
resizable: false,
|
resizable: true,
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
...(isMac ? { type: 'panel' } : {}),
|
...(isMac ? { type: 'panel' } : {}),
|
||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
@ -419,7 +424,9 @@ export class WindowService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.miniWindow.on('blur', () => {
|
this.miniWindow.on('blur', () => {
|
||||||
this.hideMiniWindow()
|
if (!this.isPinnedMiniWindow) {
|
||||||
|
this.hideMiniWindow()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.miniWindow.on('closed', () => {
|
this.miniWindow.on('closed', () => {
|
||||||
@ -503,6 +510,10 @@ export class WindowService {
|
|||||||
this.showMiniWindow()
|
this.showMiniWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setPinMiniWindow(isPinned) {
|
||||||
|
this.isPinnedMiniWindow = isPinned
|
||||||
|
}
|
||||||
|
|
||||||
public showSelectionMenu(bounds: { x: number; y: number }) {
|
public showSelectionMenu(bounds: { x: number; y: number }) {
|
||||||
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||||
this.selectionMenuWindow.setPosition(bounds.x, bounds.y)
|
this.selectionMenuWindow.setPosition(bounds.x, bounds.y)
|
||||||
|
|||||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@ -137,6 +137,7 @@ declare global {
|
|||||||
hide: () => Promise<void>
|
hide: () => Promise<void>
|
||||||
close: () => Promise<void>
|
close: () => Promise<void>
|
||||||
toggle: () => Promise<void>
|
toggle: () => Promise<void>
|
||||||
|
setPin: (isPinned: boolean) => Promise<void>
|
||||||
}
|
}
|
||||||
aes: {
|
aes: {
|
||||||
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
||||||
|
|||||||
@ -112,7 +112,8 @@ const api = {
|
|||||||
show: () => ipcRenderer.invoke('miniwindow:show'),
|
show: () => ipcRenderer.invoke('miniwindow:show'),
|
||||||
hide: () => ipcRenderer.invoke('miniwindow:hide'),
|
hide: () => ipcRenderer.invoke('miniwindow:hide'),
|
||||||
close: () => ipcRenderer.invoke('miniwindow:close'),
|
close: () => ipcRenderer.invoke('miniwindow:close'),
|
||||||
toggle: () => ipcRenderer.invoke('miniwindow:toggle')
|
toggle: () => ipcRenderer.invoke('miniwindow:toggle'),
|
||||||
|
setPin: (isPinned: boolean) => ipcRenderer.invoke('miniwindow:set-pin', isPinned)
|
||||||
},
|
},
|
||||||
aes: {
|
aes: {
|
||||||
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
|
encrypt: (text: string, secretKey: string, iv: string) => ipcRenderer.invoke('aes:encrypt', text, secretKey, iv),
|
||||||
|
|||||||
@ -580,15 +580,19 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copy_last_message": "Press C to copy",
|
"copy_last_message": "Press C to copy",
|
||||||
"esc": "Press ESC {{action}}",
|
"backspace_clear": "Backspace to clear",
|
||||||
"esc_back": "back",
|
"esc": "ESC to {{action}}",
|
||||||
"esc_close": "close the window"
|
"esc_back": "return",
|
||||||
|
"esc_close": "close"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"empty": "Ask {{model}} for help...",
|
"empty": "Ask {{model}} for help...",
|
||||||
"title": "What do you want to do with this text?"
|
"title": "What do you want to do with this text?"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"pin": "Keep Window on Top"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@ -582,13 +582,17 @@
|
|||||||
"copy_last_message": "C キーを押してコピー",
|
"copy_last_message": "C キーを押してコピー",
|
||||||
"esc": "ESC キーを押して{{action}}",
|
"esc": "ESC キーを押して{{action}}",
|
||||||
"esc_back": "戻る",
|
"esc_back": "戻る",
|
||||||
"esc_close": "ウィンドウを閉じる"
|
"esc_close": "ウィンドウを閉じる",
|
||||||
|
"backspace_clear": "バックスペースを押してクリアします"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"empty": "{{model}} に質問してください...",
|
"empty": "{{model}} に質問してください...",
|
||||||
"title": "下のテキストに対して何をしますか?"
|
"title": "下のテキストに対して何をしますか?"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"pin": "上部ウィンドウ"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@ -582,13 +582,17 @@
|
|||||||
"copy_last_message": "Нажмите C для копирования",
|
"copy_last_message": "Нажмите C для копирования",
|
||||||
"esc": "Нажмите ESC {{action}}",
|
"esc": "Нажмите ESC {{action}}",
|
||||||
"esc_back": "возвращения",
|
"esc_back": "возвращения",
|
||||||
"esc_close": "закрытия окна"
|
"esc_close": "закрытия окна",
|
||||||
|
"backspace_clear": "Нажмите Backspace, чтобы очистить"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"empty": "Задайте вопрос {{model}}...",
|
"empty": "Задайте вопрос {{model}}...",
|
||||||
"title": "Что вы хотите сделать с этим текстом?"
|
"title": "Что вы хотите сделать с этим текстом?"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"pin": "Верхнее окно"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@ -580,15 +580,19 @@
|
|||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"copy_last_message": "按 C 键复制",
|
"copy_last_message": "按 C 键复制",
|
||||||
|
"backspace_clear": "按 Backspace 清空",
|
||||||
"esc": "按 ESC {{action}}",
|
"esc": "按 ESC {{action}}",
|
||||||
"esc_back": "返回",
|
"esc_back": "返回",
|
||||||
"esc_close": "关闭窗口"
|
"esc_close": "关闭"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"empty": "询问 {{model}} 获取帮助...",
|
"empty": "询问 {{model}} 获取帮助...",
|
||||||
"title": "你想对下方文字做什么"
|
"title": "你想对下方文字做什么"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"pin": "窗口置顶"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@ -582,13 +582,17 @@
|
|||||||
"copy_last_message": "按 C 鍵複製",
|
"copy_last_message": "按 C 鍵複製",
|
||||||
"esc": "按 ESC {{action}}",
|
"esc": "按 ESC {{action}}",
|
||||||
"esc_back": "返回",
|
"esc_back": "返回",
|
||||||
"esc_close": "關閉視窗"
|
"esc_close": "關閉視窗",
|
||||||
|
"backspace_clear": "按 Backspace 清空"
|
||||||
},
|
},
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"empty": "詢問 {{model}} 取得幫助...",
|
"empty": "詢問 {{model}} 取得幫助...",
|
||||||
"title": "你想對下方文字做什麼"
|
"title": "你想對下方文字做什麼"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"pin": "窗口置頂"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import '@renderer/databases'
|
import '@renderer/databases'
|
||||||
|
|
||||||
import store, { persistor } from '@renderer/store'
|
import store, { persistor } from '@renderer/store'
|
||||||
|
import { message } from 'antd'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
|
|
||||||
@ -10,12 +11,17 @@ import { ThemeProvider } from '../../context/ThemeProvider'
|
|||||||
import HomeWindow from './home/HomeWindow'
|
import HomeWindow from './home/HomeWindow'
|
||||||
|
|
||||||
function MiniWindow(): React.ReactElement {
|
function MiniWindow(): React.ReactElement {
|
||||||
|
//miniWindow should register its own message component
|
||||||
|
const [messageApi, messageContextHolder] = message.useMessage()
|
||||||
|
window.message = messageApi
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AntdProvider>
|
<AntdProvider>
|
||||||
<SyntaxHighlighterProvider>
|
<SyntaxHighlighterProvider>
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
|
{messageContextHolder}
|
||||||
<HomeWindow />
|
<HomeWindow />
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</SyntaxHighlighterProvider>
|
</SyntaxHighlighterProvider>
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const MessageItem: FC<Props> = ({ message: _message, index, total, route, onSetM
|
|||||||
|
|
||||||
const messageBackground = getMessageBackground(true, isAssistantMessage)
|
const messageBackground = getMessageBackground(true, isAssistantMessage)
|
||||||
|
|
||||||
const maxWidth = isMiniWindow() ? '480px' : '100%'
|
const maxWidth = isMiniWindow() ? '800px' : '100%'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onGetMessages && onSetMessages) {
|
if (onGetMessages && onSetMessages) {
|
||||||
@ -93,6 +93,7 @@ const MessageItem: FC<Props> = ({ message: _message, index, total, route, onSetM
|
|||||||
|
|
||||||
const MessageContainer = styled.div`
|
const MessageContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
|
|||||||
@ -77,6 +77,7 @@ const Messages: FC<Props> = ({ assistant, route }) => {
|
|||||||
const Container = styled(Scrollbar)<ContainerProps>`
|
const Container = styled(Scrollbar)<ContainerProps>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
align-items: center;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
|||||||
@ -278,6 +278,8 @@ const HomeWindow: FC = () => {
|
|||||||
<Divider style={{ margin: '10px 0' }} />
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
<Footer
|
<Footer
|
||||||
route={route}
|
route={route}
|
||||||
|
canUseBackspace={text.length > 0 || clipboardText.length == 0}
|
||||||
|
clearClipboard={clearClipboard}
|
||||||
onExit={() => {
|
onExit={() => {
|
||||||
setRoute('home')
|
setRoute('home')
|
||||||
setText('')
|
setText('')
|
||||||
@ -292,6 +294,7 @@ const Container = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
@ -299,6 +302,8 @@ const Container = styled.div`
|
|||||||
|
|
||||||
const Main = styled.main`
|
const Main = styled.main`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`
|
`
|
||||||
|
|||||||
@ -19,7 +19,10 @@ const ClipboardPreview: FC<ClipboardPreviewProps> = ({ referenceText, clearClipb
|
|||||||
<Container>
|
<Container>
|
||||||
<ClipboardContent>
|
<ClipboardContent>
|
||||||
<CopyIcon style={{ fontSize: '14px', flexShrink: 0, cursor: 'pointer' }} className="nodrag" />
|
<CopyIcon style={{ fontSize: '14px', flexShrink: 0, cursor: 'pointer' }} className="nodrag" />
|
||||||
<Paragraph ellipsis={{ rows: 2 }} style={{ margin: '0 12px', fontSize: 12, flex: 1, minWidth: 0 }}>
|
<Paragraph
|
||||||
|
ellipsis={{ rows: 2 }}
|
||||||
|
style={{ margin: '0 12px', fontSize: 12, flex: 1, minWidth: 0 }}
|
||||||
|
className="nodrag">
|
||||||
{referenceText || t('miniwindow.clipboard.empty')}
|
{referenceText || t('miniwindow.clipboard.empty')}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<CloseButton onClick={clearClipboard} className="nodrag">
|
<CloseButton onClick={clearClipboard} className="nodrag">
|
||||||
|
|||||||
@ -103,7 +103,8 @@ const FeatureMenus = ({
|
|||||||
FeatureMenus.displayName = 'FeatureMenus'
|
FeatureMenus.displayName = 'FeatureMenus'
|
||||||
|
|
||||||
const FeatureList = styled(Scrollbar)`
|
const FeatureList = styled(Scrollbar)`
|
||||||
flex: 1;
|
flex-shrink: 0;
|
||||||
|
height: auto;
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,60 @@
|
|||||||
import { CopyOutlined, LoginOutlined } from '@ant-design/icons'
|
import { ArrowLeftOutlined, CopyOutlined, LogoutOutlined, PushpinFilled, PushpinOutlined } from '@ant-design/icons'
|
||||||
import { Tag } from 'antd'
|
import { Tag, Tooltip } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface FooterProps {
|
interface FooterProps {
|
||||||
route: string
|
route: string
|
||||||
|
canUseBackspace?: boolean
|
||||||
|
clearClipboard?: () => void
|
||||||
onExit: () => void
|
onExit: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Footer: FC<FooterProps> = ({ route, onExit }) => {
|
const Footer: FC<FooterProps> = ({ route, canUseBackspace, clearClipboard, onExit }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const [isPinned, setIsPinned] = useState(false)
|
||||||
|
|
||||||
|
const onClickPin = () => {
|
||||||
|
window.api.miniWindow.setPin(!isPinned).then(() => {
|
||||||
|
setIsPinned(!isPinned)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WindowFooter>
|
<WindowFooter className="drag">
|
||||||
<FooterText className="nodrag">
|
<PinButtonArea onClick={() => onClickPin()} className="nodrag">
|
||||||
<Tag bordered={false} icon={<LoginOutlined />} onClick={() => onExit()}>
|
<Tooltip title={t('miniwindow.tooltip.pin')} mouseEnterDelay={0.8} placement="left">
|
||||||
|
{isPinned ? (
|
||||||
|
<PushpinFilled style={{ fontSize: '18px', color: 'var(--color-primary)' }} />
|
||||||
|
) : (
|
||||||
|
<PushpinOutlined style={{ fontSize: '18px' }} />
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</PinButtonArea>
|
||||||
|
<FooterText>
|
||||||
|
<Tag
|
||||||
|
bordered={false}
|
||||||
|
icon={<LogoutOutlined />}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
className="nodrag"
|
||||||
|
onClick={() => onExit()}>
|
||||||
{t('miniwindow.footer.esc', {
|
{t('miniwindow.footer.esc', {
|
||||||
action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back')
|
action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back')
|
||||||
})}
|
})}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{route === 'home' && !canUseBackspace && (
|
||||||
|
<Tag
|
||||||
|
bordered={false}
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
className="nodrag"
|
||||||
|
onClick={() => clearClipboard!()}>
|
||||||
|
{t('miniwindow.footer.backspace_clear')}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
{route !== 'home' && (
|
{route !== 'home' && (
|
||||||
<Tag bordered={false} icon={<CopyOutlined />}>
|
<Tag bordered={false} icon={<CopyOutlined />} style={{ cursor: 'pointer' }} className="nodrag">
|
||||||
{t('miniwindow.footer.copy_last_message')}
|
{t('miniwindow.footer.copy_last_message')}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
@ -31,14 +64,17 @@ const Footer: FC<FooterProps> = ({ route, onExit }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const WindowFooter = styled.div`
|
const WindowFooter = styled.div`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const FooterText = styled.div`
|
const FooterText = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -47,4 +83,12 @@ const FooterText = styled.div`
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const PinButtonArea = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
export default Footer
|
export default Footer
|
||||||
|
|||||||
@ -74,6 +74,7 @@ const Translate: FC<Props> = ({ text }) => {
|
|||||||
|
|
||||||
useHotkeys('c', () => {
|
useHotkeys('c', () => {
|
||||||
navigator.clipboard.writeText(result)
|
navigator.clipboard.writeText(result)
|
||||||
|
window.message.success(t('message.copy.success'))
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -82,7 +83,7 @@ const Translate: FC<Props> = ({ text }) => {
|
|||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
value="any"
|
value="any"
|
||||||
style={{ width: 200 }}
|
style={{ maxWidth: 200, minWidth: 100, flex: 1 }}
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
disabled
|
disabled
|
||||||
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
options={[{ label: t('translate.any.language'), value: 'any' }]}
|
||||||
@ -91,7 +92,7 @@ const Translate: FC<Props> = ({ text }) => {
|
|||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
value={targetLanguage}
|
value={targetLanguage}
|
||||||
style={{ width: 200 }}
|
style={{ maxWidth: 200, minWidth: 130, flex: 1 }}
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
options={TranslateLanguageOptions}
|
options={TranslateLanguageOptions}
|
||||||
onChange={async (value) => {
|
onChange={async (value) => {
|
||||||
@ -126,7 +127,7 @@ const Container = styled.div`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
padding-right: 0;
|
/* padding-right: 0; */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
`
|
`
|
||||||
@ -151,8 +152,10 @@ const LoadingText = styled.div`
|
|||||||
|
|
||||||
const MenuContainer = styled.div`
|
const MenuContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
`
|
`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user