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