mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-26 11:44:28 +08:00
commit
370cfd6e9f
@ -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))
|
||||
|
||||
@ -112,10 +112,10 @@ function convertToFileStat(serverBase: string, item: WebDAVResponse['multistatus
|
||||
const props = item.propstat.prop
|
||||
const isDir = !isNil(props.resourcetype?.collection)
|
||||
const href = decodeURIComponent(item.href)
|
||||
const filename = serverBase === '/' ? href : path.join('/', href.replace(serverBase, ''))
|
||||
const filename = serverBase === '/' ? href : path.posix.join('/', href.replace(serverBase, ''))
|
||||
|
||||
return {
|
||||
filename,
|
||||
filename: filename.endsWith('/') ? filename.slice(0, -1) : filename,
|
||||
basename: path.basename(filename),
|
||||
lastmod: props.getlastmodified || '',
|
||||
size: props.getcontentlength ? parseInt(props.getcontentlength, 10) : 0,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -1231,7 +1231,140 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
group: 'Step 1'
|
||||
}
|
||||
],
|
||||
doubao: [],
|
||||
doubao: [
|
||||
{
|
||||
id: 'doubao-1-5-vision-pro-32k-250115',
|
||||
provider: 'doubao',
|
||||
name: 'doubao-1.5-vision-pro',
|
||||
group: 'Doubao-1.5-vision-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-1-5-pro-32k-250115',
|
||||
provider: 'doubao',
|
||||
name: 'doubao-1.5-pro-32k',
|
||||
group: 'Doubao-1.5-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-1-5-pro-32k-character-250228',
|
||||
provider: 'doubao',
|
||||
name: 'doubao-1.5-pro-32k-character',
|
||||
group: 'Doubao-1.5-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-1-5-pro-256k-250115',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-1.5-pro-256k',
|
||||
group: 'Doubao-1.5-pro'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-r1-250120',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-R1',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-r1-distill-qwen-32b-250120',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-R1-Distill-Qwen-32B',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-r1-distill-qwen-7b-250120',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-R1-Distill-Qwen-7B',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-v3-250324',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-V3',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-v3-250324',
|
||||
provider: 'doubao',
|
||||
name: 'DeepSeek-V3',
|
||||
group: 'DeepSeek'
|
||||
},
|
||||
{
|
||||
id: 'doubao-pro-32k-241215',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-pro-32k',
|
||||
group: 'Doubao-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-pro-32k-functioncall-241028',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-pro-32k-functioncall-241028',
|
||||
group: 'Doubao-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-pro-32k-character-241215',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-pro-32k-character-241215',
|
||||
group: 'Doubao-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-pro-256k-241115',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-pro-256k',
|
||||
group: 'Doubao-pro'
|
||||
},
|
||||
{
|
||||
id: 'doubao-lite-4k-character-240828',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-lite-4k-character-240828',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-lite-32k-240828',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-lite-32k',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-lite-32k-character-241015',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-lite-32k-character-241015',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-lite-128k-240828',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-lite-128k',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-1-5-lite-32k-250115',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-1.5-lite-32k',
|
||||
group: 'Doubao-lite'
|
||||
},
|
||||
{
|
||||
id: 'doubao-embedding-large-text-240915',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-embedding-large',
|
||||
group: 'Doubao-embedding'
|
||||
},
|
||||
{
|
||||
id: 'doubao-embedding-text-240715',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-embedding',
|
||||
group: 'Doubao-embedding'
|
||||
},
|
||||
{
|
||||
id: 'doubao-embedding-vision-241215',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-embedding-vision',
|
||||
group: 'Doubao-embedding'
|
||||
},
|
||||
{
|
||||
id: 'doubao-vision-lite-32k-241015',
|
||||
provider: 'doubao',
|
||||
name: 'Doubao-vision-lite-32k',
|
||||
group: 'Doubao-vision-lite-32k'
|
||||
}
|
||||
],
|
||||
minimax: [
|
||||
{
|
||||
id: 'abab6.5s-chat',
|
||||
|
||||
@ -586,15 +586,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": {
|
||||
|
||||
@ -588,13 +588,17 @@
|
||||
"copy_last_message": "C キーを押してコピー",
|
||||
"esc": "ESC キーを押して{{action}}",
|
||||
"esc_back": "戻る",
|
||||
"esc_close": "ウィンドウを閉じる"
|
||||
"esc_close": "ウィンドウを閉じる",
|
||||
"backspace_clear": "バックスペースを押してクリアします"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"empty": "{{model}} に質問してください...",
|
||||
"title": "下のテキストに対して何をしますか?"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"pin": "上部ウィンドウ"
|
||||
}
|
||||
},
|
||||
"models": {
|
||||
|
||||
@ -588,13 +588,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": {
|
||||
|
||||
@ -586,15 +586,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": {
|
||||
|
||||
@ -588,13 +588,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": {
|
||||
|
||||
@ -262,7 +262,6 @@ const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageSty
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
|
||||
overflow-y: auto;
|
||||
grid-template-columns: repeat(
|
||||
${({ $layout, $count }) => (['fold', 'vertical'].includes($layout) ? 1 : $count)},
|
||||
minmax(550px, 1fr)
|
||||
@ -286,7 +285,13 @@ const GridContainer = styled.div<{ $count: number; $layout: MultiModelMessageSty
|
||||
grid-template-rows: auto;
|
||||
gap: 16px;
|
||||
`}
|
||||
overflow-y: visible;
|
||||
${({ $layout }) => {
|
||||
return $layout === 'horizontal'
|
||||
? css`
|
||||
overflow-y: auto;
|
||||
`
|
||||
: 'overflow-y: visible;'
|
||||
}}
|
||||
`
|
||||
|
||||
interface MessageWrapperProps {
|
||||
@ -325,17 +330,20 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
}}
|
||||
|
||||
${({ $layout, $isInPopover, $isGrouped }) => {
|
||||
// 如果布局是grid,并且是组消息,则设置最大高度和溢出行为(卡片不可滚动,点击展开后可滚动)
|
||||
// 如果布局是horizontal,则设置溢出行为(卡片可滚动)
|
||||
// 如果布局是fold、vertical,高度不限制,与正常消息流布局一致,则设置卡片不可滚动(visible)
|
||||
return $layout === 'grid' && $isGrouped
|
||||
? css`
|
||||
max-height: ${$isInPopover ? '50vh' : '300px'};
|
||||
overflow-y: ${$isInPopover ? 'visible' : 'hidden'};
|
||||
overflow-y: ${$isInPopover ? 'auto' : 'hidden'};
|
||||
border: 0.5px solid ${$isInPopover ? 'transparent' : 'var(--color-border)'};
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
: css`
|
||||
overflow-y: visible;
|
||||
overflow-y: ${$layout === 'horizontal' ? 'auto' : 'visible'};
|
||||
border-radius: 6px;
|
||||
`
|
||||
}}
|
||||
|
||||
@ -252,8 +252,10 @@ const NutstoreSettings: FC = () => {
|
||||
placeholder={t('settings.data.nutstore.path.placeholder')}
|
||||
style={{ width: 250 }}
|
||||
value={nutstorePath}
|
||||
onChange={(e) => setStoragePath(e.target.value)}
|
||||
onBlur={() => dispatch(setNutstorePath(storagePath || ''))}
|
||||
onChange={(e) => {
|
||||
setStoragePath(e.target.value)
|
||||
dispatch(setNutstorePath(e.target.value))
|
||||
}}
|
||||
/>
|
||||
<Button type="default" onClick={handleClickPathChange}>
|
||||
<FolderOutlined />
|
||||
|
||||
@ -1,16 +1,19 @@
|
||||
import { Tool, ToolUnion, ToolUseBlock } from '@anthropic-ai/sdk/resources'
|
||||
import { FunctionCall, FunctionDeclaration, SchemaType, Tool as geminiTool } from '@google/generative-ai'
|
||||
import {
|
||||
ArraySchema,
|
||||
BaseSchema,
|
||||
BooleanSchema,
|
||||
EnumStringSchema,
|
||||
FunctionCall,
|
||||
FunctionDeclaration,
|
||||
FunctionDeclarationSchema,
|
||||
FunctionDeclarationSchemaProperty,
|
||||
IntegerSchema,
|
||||
NumberSchema,
|
||||
ObjectSchema,
|
||||
SimpleStringSchema
|
||||
SchemaType,
|
||||
SimpleStringSchema,
|
||||
Tool as geminiTool
|
||||
} from '@google/generative-ai'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import store from '@renderer/store'
|
||||
@ -182,7 +185,9 @@ export function openAIToolsToMcpTool(
|
||||
return undefined
|
||||
}
|
||||
|
||||
const tool = mcpTools.find((mcptool) => mcptool.id === llmTool.function.name)
|
||||
const tool = mcpTools.find(
|
||||
(mcptool) => mcptool.id === llmTool.function.name || mcptool.name === llmTool.function.name
|
||||
)
|
||||
|
||||
if (!tool) {
|
||||
console.warn('No MCP Tool found for tool call:', llmTool)
|
||||
|
||||
@ -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…
Reference in New Issue
Block a user