diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 5540916b..bea67835 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -33,6 +33,14 @@ export default defineConfig({ alias: { '@shared': resolve('src/shared') } + }, + build: { + rollupOptions: { + input: { + index: resolve('src/preload/index.ts'), + floating: resolve('src/preload/floating-preload.ts') + } + } } }, renderer: { @@ -77,7 +85,8 @@ export default defineConfig({ rollupOptions: { input: { shell: resolve('src/renderer/shell/index.html'), - index: resolve('src/renderer/index.html') + index: resolve('src/renderer/index.html'), + floating: resolve('src/renderer/floating/index.html') } } } diff --git a/src/main/events.ts b/src/main/events.ts index 67710a04..3f1dcb6f 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -164,3 +164,11 @@ export const TRAY_EVENTS = { export const MEETING_EVENTS = { INSTRUCTION: 'mcp:meeting-instruction', // 主进程向渲染进程发送指令 } + +// 悬浮按钮相关事件 +export const FLOATING_BUTTON_EVENTS = { + CLICKED: 'floating-button:clicked', // 悬浮按钮被点击 + VISIBILITY_CHANGED: 'floating-button:visibility-changed', // 悬浮按钮显示状态改变 + POSITION_CHANGED: 'floating-button:position-changed', // 悬浮按钮位置改变 + ENABLED_CHANGED: 'floating-button:enabled-changed' // 悬浮按钮启用状态改变 +} diff --git a/src/main/index.ts b/src/main/index.ts index d6e5f82a..9a40a2c9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,9 +5,10 @@ import { ProxyMode, proxyConfig } from './presenter/proxyConfig' import path from 'path' import fs from 'fs' import { eventBus } from './eventbus' -import { WINDOW_EVENTS, TRAY_EVENTS } from './events' +import { WINDOW_EVENTS, TRAY_EVENTS, FLOATING_BUTTON_EVENTS } from './events' import { setLoggingEnabled } from '@shared/logger' import { is } from '@electron-toolkit/utils' // 确保导入 is +import { handleShowHiddenWindow } from './utils' // 设置应用命令行参数 app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required') // 允许视频自动播放 @@ -30,7 +31,7 @@ if (process.platform === 'darwin') { presenter.deeplinkPresenter.init() // 等待 Electron 初始化完成 -app.whenReady().then(() => { +app.whenReady().then(async () => { // Set app user model id for windows electronApp.setAppUserModelId('com.wefonk.deepchat') @@ -41,6 +42,9 @@ app.whenReady().then(() => { // 初始化托盘图标和菜单,并存储 presenter 实例 presenter.setupTray() + // 立即进行基本初始化,不等待窗口ready-to-show事件 + presenter.init() + // 从配置中读取代理设置并初始化 const proxyMode = presenter.configPresenter.getProxyMode() as ProxyMode const customProxyUrl = presenter.configPresenter.getCustomProxyUrl() @@ -80,16 +84,37 @@ app.whenReady().then(() => { // 如果没有窗口,创建主窗口 (应用首次启动时) if (presenter.windowPresenter.getAllWindows().length === 0) { - presenter.windowPresenter.createShellWindow({ - initialTab: { - url: 'local://chat' + console.log('Main: Creating initial shell window on app startup') + try { + const windowId = await presenter.windowPresenter.createShellWindow({ + initialTab: { + url: 'local://chat' + } + }) + if (windowId) { + console.log(`Main: Initial shell window created successfully with ID: ${windowId}`) + } else { + console.error('Main: Failed to create initial shell window - returned null') } - }) + } catch (error) { + console.error('Main: Error creating initial shell window:', error) + } + } else { + console.log('Main: Shell windows already exist, skipping initial window creation') } // 注册全局快捷键 presenter.shortcutPresenter.registerShortcuts() + // 监听悬浮按钮配置变化事件 + eventBus.on(FLOATING_BUTTON_EVENTS.ENABLED_CHANGED, async (enabled: boolean) => { + try { + await presenter.floatingButtonPresenter.setEnabled(enabled) + } catch (error) { + console.error('Failed to set floating button enabled state:', error) + } + }) + // 托盘 检测更新 eventBus.on(TRAY_EVENTS.CHECK_FOR_UPDATES, () => { const allWindows = presenter.windowPresenter.getAllWindows() @@ -103,38 +128,8 @@ app.whenReady().then(() => { presenter.upgradePresenter.checkUpdate() }) - // 监听显示/隐藏窗口事件 (从托盘或快捷键触发) - eventBus.on(TRAY_EVENTS.SHOW_HIDDEN_WINDOW, (trayClick: boolean) => { - const allWindows = presenter.windowPresenter.getAllWindows() - if (allWindows.length === 0) { - presenter.windowPresenter.createShellWindow({ - initialTab: { - url: 'local://chat' - } - }) - } else { - // 查找目标窗口 (焦点窗口或第一个窗口) - const targetWindow = presenter.windowPresenter.getFocusedWindow() || allWindows[0] - - if (!targetWindow.isDestroyed()) { - // 逻辑: 如果窗口可见且不是从托盘点击触发,则隐藏;否则显示并置顶 - if (targetWindow.isVisible() && !trayClick) { - presenter.windowPresenter.hide(targetWindow.id) - } else { - presenter.windowPresenter.show(targetWindow.id) - targetWindow.focus() // 确保窗口置顶 - } - } else { - console.warn('Target window for SHOW_HIDDEN_WINDOW event is destroyed.') // 保持 warn - // 如果目标窗口已销毁,创建新窗口 - presenter.windowPresenter.createShellWindow({ - initialTab: { - url: 'local://chat' - } - }) - } - } - }) + // 监听显示/隐藏窗口事件 (从托盘或快捷键或悬浮窗口触发) + eventBus.on(TRAY_EVENTS.SHOW_HIDDEN_WINDOW, handleShowHiddenWindow) // 监听浏览器窗口获得焦点事件 app.on('browser-window-focus', () => { @@ -267,15 +262,23 @@ app.whenReady().then(() => { }) }) // app.whenReady().then 结束 -// 当所有窗口都关闭时,不退出应用。macOS 平台会保留在 Dock 中,Windows 会保留在托盘。 -// 用户需要通过托盘菜单或 Cmd+Q 来真正退出应用。 -// 因此移除 'window-all-closed' 事件监听 -/* +// 当所有主窗口都关闭时的处理逻辑 +// macOS 平台会保留在 Dock 中,Windows 会保留在托盘。 +// 悬浮按钮窗口不计入主窗口数量 app.on('window-all-closed', () => { - presenter.destroy() - // trayPresenter.destroy() // <-- 已移动到 will-quit + // 检查是否还有非悬浮按钮的窗口 + const mainWindows = presenter.windowPresenter.getAllWindows() + + if (mainWindows.length === 0) { + // 只有悬浮按钮窗口时,在非 macOS 平台退出应用 + if (process.platform !== 'darwin') { + console.log('main: All main windows closed on non-macOS platform, quitting app') + app.quit() + } else { + console.log('main: All main windows closed on macOS, keeping app running in dock') + } + } }) -*/ // 在应用即将退出时触发,适合进行最终的资源清理 (如销毁托盘) // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -298,9 +301,11 @@ app.on('will-quit', (_event) => { }) // 在应用退出之前触发,早于 will-quit。通常不如 will-quit 适合资源清理。 -// 移除在此处销毁托盘的逻辑。 +// 在这里销毁悬浮按钮,确保应用能正常退出 app.on('before-quit', () => { - console.log('main: app before-quit event triggered.') // 保留关键日志 - // presenter.destroy() // 如果需要在 will-quit 之前清理 presenter,可以保留 - // trayPresenter.destroy() // <-- 从此处移除托盘销毁 + try { + presenter.floatingButtonPresenter.destroy() + } catch (error) { + console.error('main: Error destroying floating button during before-quit:', error) + } }) diff --git a/src/main/presenter/configPresenter/index.ts b/src/main/presenter/configPresenter/index.ts index c5149680..5f472b93 100644 --- a/src/main/presenter/configPresenter/index.ts +++ b/src/main/presenter/configPresenter/index.ts @@ -16,7 +16,7 @@ import { DEFAULT_PROVIDERS } from './providers' import path from 'path' import { app, nativeTheme, shell } from 'electron' import fs from 'fs' -import { CONFIG_EVENTS, SYSTEM_EVENTS } from '@/events' +import { CONFIG_EVENTS, SYSTEM_EVENTS, FLOATING_BUTTON_EVENTS } from '@/events' import { McpConfHelper, SYSTEM_INMEM_MCP_SERVERS } from './mcpConfHelper' import { presenter } from '@/presenter' import { compare } from 'compare-versions' @@ -43,6 +43,7 @@ interface IAppSettings { soundEnabled?: boolean // 音效是否启用 copyWithCotEnabled?: boolean loggingEnabled?: boolean // 日志记录是否启用 + floatingButtonEnabled?: boolean // 悬浮按钮是否启用 default_system_prompt?: string // 默认系统提示词 [key: string]: unknown // 允许任意键,使用unknown类型替代any } @@ -101,6 +102,7 @@ export class ConfigPresenter implements IConfigPresenter { soundEnabled: false, copyWithCotEnabled: true, loggingEnabled: false, + floatingButtonEnabled: true, default_system_prompt: '', appVersion: this.currentAppVersion } @@ -741,6 +743,24 @@ export class ConfigPresenter implements IConfigPresenter { eventBus.sendToRenderer(CONFIG_EVENTS.COPY_WITH_COT_CHANGED, SendTarget.ALL_WINDOWS, enabled) } + // 获取悬浮按钮开关状态 + getFloatingButtonEnabled(): boolean { + const value = this.getSetting('floatingButtonEnabled') ?? false + return value === undefined || value === null ? false : value + } + + // 设置悬浮按钮开关状态 + setFloatingButtonEnabled(enabled: boolean): void { + this.setSetting('floatingButtonEnabled', enabled) + eventBus.sendToMain(FLOATING_BUTTON_EVENTS.ENABLED_CHANGED, enabled) + + try { + presenter.floatingButtonPresenter.setEnabled(enabled) + } catch (error) { + console.error('Failed to directly call floatingButtonPresenter:', error) + } + } + // ===================== MCP配置相关方法 ===================== // 获取MCP服务器配置 diff --git a/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts new file mode 100644 index 00000000..46a4d2e7 --- /dev/null +++ b/src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts @@ -0,0 +1,228 @@ +import { BrowserWindow, screen } from 'electron'; +import path from 'path'; +import { FloatingButtonConfig, FloatingButtonState } from './types'; +import logger from '../../../shared/logger'; + +export class FloatingButtonWindow { + private window: BrowserWindow | null = null; + private config: FloatingButtonConfig; + private state: FloatingButtonState; + + constructor(config: FloatingButtonConfig) { + this.config = config; + this.state = { + isVisible: false, + bounds: { + x: 0, + y: 0, + width: config.size.width, + height: config.size.height + } + }; + } + + /** + * 创建悬浮窗口 + */ + public async create(): Promise { + if (this.window) { + return; + } + + try { + const position = this.calculatePosition(); + + // 根据环境选择正确的预加载脚本路径 + const isDev = process.env.NODE_ENV === 'development'; + const preloadPath = isDev + ? path.join(process.cwd(), 'out/preload/floating.mjs') + : path.join(__dirname, '../../preload/floating.mjs'); + + this.window = new BrowserWindow({ + width: this.config.size.width, + height: this.config.size.height, + x: position.x, + y: position.y, + frame: false, + transparent: true, + alwaysOnTop: this.config.alwaysOnTop, + skipTaskbar: true, + resizable: false, + minimizable: false, + maximizable: false, + closable: false, + show: false, + movable: true, // 允许拖拽 + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: preloadPath, + webSecurity: false, // 开发模式下允许跨域 + devTools: true, // 开发模式下启用开发者工具 + sandbox: false // 禁用沙盒模式,确保预加载脚本能正常工作 + } + }); + + // 设置窗口透明度 + this.window.setOpacity(this.config.opacity); + + // 加载悬浮按钮页面 + if (isDev) { + await this.window.loadURL('http://localhost:5173/floating/'); + // 开发模式下可选择性打开开发者工具(暂时禁用,避免影响拖拽) + this.window.webContents.openDevTools({ mode: 'detach' }); + } else { + await this.window.loadFile(path.join(__dirname, '../../../renderer/floating/index.html')); + } + + // 监听窗口事件 + this.setupWindowEvents(); + + logger.info('FloatingButtonWindow created successfully'); + } catch (error) { + logger.error('Failed to create FloatingButtonWindow:', error); + throw error; + } + } + + /** + * 显示悬浮窗口 + */ + public show(): void { + if (!this.window) { + return; + } + + this.window.show(); + this.state.isVisible = true; + logger.debug('FloatingButtonWindow shown'); + } + + /** + * 隐藏悬浮窗口 + */ + public hide(): void { + if (!this.window) { + return; + } + + this.window.hide(); + this.state.isVisible = false; + logger.debug('FloatingButtonWindow hidden'); + } + + /** + * 销毁悬浮窗口 + */ + public destroy(): void { + if (this.window) { + this.window.destroy(); + this.window = null; + this.state.isVisible = false; + logger.debug('FloatingButtonWindow destroyed'); + } + } + + /** + * 更新配置 + */ + public updateConfig(config: Partial): void { + this.config = { ...this.config, ...config }; + + if (this.window) { + // 更新窗口属性 + if (config.size) { + this.window.setSize(this.config.size.width, this.config.size.height); + this.state.bounds.width = this.config.size.width; + this.state.bounds.height = this.config.size.height; + } + + if (config.position || config.offset) { + const position = this.calculatePosition(); + this.window.setPosition(position.x, position.y); + this.state.bounds.x = position.x; + this.state.bounds.y = position.y; + } + + if (config.opacity !== undefined) { + this.window.setOpacity(this.config.opacity); + } + + if (config.alwaysOnTop !== undefined) { + this.window.setAlwaysOnTop(this.config.alwaysOnTop); + } + } + } + + /** + * 获取当前状态 + */ + public getState(): FloatingButtonState { + return { ...this.state }; + } + + /** + * 检查窗口是否存在 + */ + public exists(): boolean { + return this.window !== null && !this.window.isDestroyed(); + } + + /** + * 计算悬浮按钮位置 + */ + private calculatePosition(): { x: number; y: number } { + const primaryDisplay = screen.getPrimaryDisplay(); + const { workAreaSize } = primaryDisplay; + + let x: number, y: number; + + switch (this.config.position) { + case 'top-left': + x = this.config.offset.x; + y = this.config.offset.y; + break; + case 'top-right': + x = workAreaSize.width - this.config.size.width - this.config.offset.x; + y = this.config.offset.y; + break; + case 'bottom-left': + x = this.config.offset.x; + y = workAreaSize.height - this.config.size.height - this.config.offset.y; + break; + case 'bottom-right': + default: + x = workAreaSize.width - this.config.size.width - this.config.offset.x; + y = workAreaSize.height - this.config.size.height - this.config.offset.y; + break; + } + + return { x, y }; + } + + /** + * 设置窗口事件监听 + */ + private setupWindowEvents(): void { + if (!this.window) { + return; + } + + // 窗口关闭事件 + this.window.on('closed', () => { + this.window = null; + this.state.isVisible = false; + }); + + // 窗口移动事件 + this.window.on('moved', () => { + if (this.window) { + const bounds = this.window.getBounds(); + this.state.bounds.x = bounds.x; + this.state.bounds.y = bounds.y; + } + }); + + // 注意:悬浮按钮点击事件的 IPC 处理器在主进程的 index.ts 中设置 + } +} diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts new file mode 100644 index 00000000..23c28c58 --- /dev/null +++ b/src/main/presenter/floatingButtonPresenter/index.ts @@ -0,0 +1,122 @@ +import { FloatingButtonWindow } from './FloatingButtonWindow'; +import { FloatingButtonConfig, FloatingButtonState, DEFAULT_FLOATING_BUTTON_CONFIG } from './types'; +import { ConfigPresenter } from '../configPresenter'; +import { ipcMain } from 'electron'; +import { FLOATING_BUTTON_EVENTS } from '@/events'; +import { handleShowHiddenWindow } from '@/utils'; + +export class FloatingButtonPresenter { + private floatingWindow: FloatingButtonWindow | null = null; + private config: FloatingButtonConfig; + private configPresenter: ConfigPresenter + + constructor(configPresenter: ConfigPresenter) { + this.configPresenter = configPresenter + this.config = { + ...DEFAULT_FLOATING_BUTTON_CONFIG, + }; + } + + /** + * 初始化悬浮按钮功能 + */ + public async initialize(config?: Partial): Promise { + const floatingButtonEnabled = this.configPresenter.getFloatingButtonEnabled() + try { + this.config = { + ...this.config, + ...config || {}, + enabled: floatingButtonEnabled + }; + + if (!this.config.enabled) { + console.log('FloatingButton is disabled, skipping window creation'); + return; + } + + await this.createFloatingWindow(); + } catch (error) { + console.error('Failed to initialize FloatingButtonPresenter:', error); + throw error; + } + } + + /** + * 销毁悬浮按钮功能 + */ + public destroy(): void { + this.config.enabled = false; + + ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED); + if (this.floatingWindow) { + this.floatingWindow.destroy(); + this.floatingWindow = null; + } + } + + /** + * 启用悬浮按钮 + */ + public async enable(): Promise { + console.log('FloatingButtonPresenter.enable called, current enabled:', this.config.enabled, 'has window:', !!this.floatingWindow) + + this.config.enabled = true; + + if (this.floatingWindow) { + console.log('FloatingButton window already exists, showing it') + this.floatingWindow.show(); + return; // 已经存在窗口,只需显示 + } + + console.log('Creating new floating button window') + await this.createFloatingWindow(); + } + + /** + * 设置悬浮按钮启用状态 + */ + public async setEnabled(enabled: boolean): Promise { + if (enabled) { + await this.enable(); + } else { + this.destroy(); + } + } + + /** + * 获取当前配置 + */ + public getConfig(): FloatingButtonConfig { + return { ...this.config }; + } + + /** + * 获取当前状态 + */ + public getState(): FloatingButtonState | null { + return this.floatingWindow?.getState() || null; + } + + /** + * 创建悬浮窗口 + */ + private async createFloatingWindow(): Promise { + ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED); + + ipcMain.on(FLOATING_BUTTON_EVENTS.CLICKED, () => { + try { + // 触发内置事件处理器 + handleShowHiddenWindow(true) + } catch (error) { + } + }) + + if (!this.floatingWindow) { + this.floatingWindow = new FloatingButtonWindow(this.config); + await this.floatingWindow.create(); + } + + // 悬浮按钮创建后立即显示 + this.floatingWindow.show(); + } +} diff --git a/src/main/presenter/floatingButtonPresenter/types.ts b/src/main/presenter/floatingButtonPresenter/types.ts new file mode 100644 index 00000000..2662716e --- /dev/null +++ b/src/main/presenter/floatingButtonPresenter/types.ts @@ -0,0 +1,56 @@ +export interface FloatingButtonConfig { + /** 是否启用悬浮按钮 */ + enabled: boolean; + /** 悬浮按钮位置 */ + position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + /** 距离边缘的偏移量 */ + offset: { + x: number; + y: number; + }; + /** 悬浮按钮大小 */ + size: { + width: number; + height: number; + }; + /** 是否置顶显示 */ + alwaysOnTop: boolean; + /** 透明度 (0-1) */ + opacity: number; +} + +export interface FloatingButtonState { + /** 是否正在显示 */ + isVisible: boolean; + /** 当前位置 */ + bounds: { + x: number; + y: number; + width: number; + height: number; + }; +} + +export interface FloatingButtonEvents { + /** 悬浮按钮被点击 */ + 'floating-button-clicked': void; + /** 悬浮按钮显示状态改变 */ + 'floating-button-visibility-changed': { visible: boolean }; + /** 悬浮按钮位置改变 */ + 'floating-button-position-changed': { x: number; y: number }; +} + +export const DEFAULT_FLOATING_BUTTON_CONFIG: FloatingButtonConfig = { + enabled: true, + position: 'bottom-right', + offset: { + x: 20, + y: 20 + }, + size: { + width: 60, + height: 60 + }, + alwaysOnTop: true, + opacity: 0.8 +}; diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts index 6e726af7..92002695 100644 --- a/src/main/presenter/index.ts +++ b/src/main/presenter/index.ts @@ -19,6 +19,7 @@ import { NotificationPresenter } from './notifactionPresenter' import { TabPresenter } from './tabPresenter' import { TrayPresenter } from './trayPresenter' import { OAuthPresenter } from './oauthPresenter' +import { FloatingButtonPresenter } from './floatingButtonPresenter' import { CONFIG_EVENTS, WINDOW_EVENTS } from '@/events' // IPC调用上下文接口 @@ -52,6 +53,7 @@ export class Presenter implements IPresenter { tabPresenter: TabPresenter trayPresenter: TrayPresenter oauthPresenter: OAuthPresenter + floatingButtonPresenter: FloatingButtonPresenter // llamaCppPresenter: LlamaCppPresenter // 保留原始注释 constructor() { @@ -79,6 +81,7 @@ export class Presenter implements IPresenter { this.notificationPresenter = new NotificationPresenter() this.oauthPresenter = new OAuthPresenter() this.trayPresenter = new TrayPresenter() + this.floatingButtonPresenter = new FloatingButtonPresenter(this.configPresenter) // this.llamaCppPresenter = new LlamaCppPresenter() // 保留原始注释 this.setupEventBus() // 设置事件总线监听 @@ -123,6 +126,19 @@ export class Presenter implements IPresenter { // 同步所有 provider 的自定义模型 this.syncCustomModels() + + // 初始化悬浮按钮 + this.initializeFloatingButton() + } + + // 初始化悬浮按钮 + private async initializeFloatingButton() { + try { + await this.floatingButtonPresenter.initialize() + console.log('FloatingButtonPresenter initialized successfully') + } catch (error) { + console.error('Failed to initialize FloatingButtonPresenter:', error) + } } // 从配置中同步自定义模型到 LLMProviderPresenter @@ -147,6 +163,7 @@ export class Presenter implements IPresenter { // 在应用退出时进行清理,关闭数据库连接 destroy() { + this.floatingButtonPresenter.destroy() // 销毁悬浮按钮 this.tabPresenter.destroy() this.sqlitePresenter.close() // 关闭数据库连接 this.shortcutPresenter.destroy() // 销毁快捷键监听 diff --git a/src/main/utils/index.ts b/src/main/utils/index.ts new file mode 100644 index 00000000..a0f45190 --- /dev/null +++ b/src/main/utils/index.ts @@ -0,0 +1,33 @@ +import { presenter } from '@/presenter' + +export function handleShowHiddenWindow(mustShow: boolean) { + const allWindows = presenter.windowPresenter.getAllWindows() + if (allWindows.length === 0) { + presenter.windowPresenter.createShellWindow({ + initialTab: { + url: 'local://chat' + } + }) + } else { + // 查找目标窗口 (焦点窗口或第一个窗口) + const targetWindow = presenter.windowPresenter.getFocusedWindow() || allWindows[0] + + if (!targetWindow.isDestroyed()) { + // 逻辑: 如果窗口可见且不是从托盘点击触发,则隐藏;否则显示并置顶 + if (targetWindow.isVisible() && !mustShow) { + presenter.windowPresenter.hide(targetWindow.id) + } else { + presenter.windowPresenter.show(targetWindow.id) + targetWindow.focus() // 确保窗口置顶 + } + } else { + console.warn('Target window for SHOW_HIDDEN_WINDOW event is destroyed.') // 保持 warn + // 如果目标窗口已销毁,创建新窗口 + presenter.windowPresenter.createShellWindow({ + initialTab: { + url: 'local://chat' + } + }) + } + } +} diff --git a/src/preload/floating-preload.ts b/src/preload/floating-preload.ts new file mode 100644 index 00000000..2866dfd2 --- /dev/null +++ b/src/preload/floating-preload.ts @@ -0,0 +1,46 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +// 直接定义事件常量,避免路径解析问题 +const FLOATING_BUTTON_EVENTS = { + CLICKED: 'floating-button:clicked' +} as const; + +// 定义悬浮按钮的 API +const floatingButtonAPI = { + // 通知主进程悬浮按钮被点击 + onClick: () => { + try { + ipcRenderer.send(FLOATING_BUTTON_EVENTS.CLICKED); + } catch (error) { + console.error('FloatingPreload: Error sending IPC message:', error); + } + }, + + // 监听来自主进程的事件 + onConfigUpdate: (callback: (config: any) => void) => { + ipcRenderer.on('floating-button-config-update', (_event, config) => { + callback(config); + }); + }, + + // 移除事件监听器 + removeAllListeners: () => { + console.log('FloatingPreload: Removing all listeners'); + ipcRenderer.removeAllListeners('floating-button-config-update'); + } +}; + +// 尝试不同的方式暴露API +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('floatingButtonAPI', floatingButtonAPI); + } catch (error) { + console.error('=== FloatingPreload: Error exposing API via contextBridge ===:', error); + } +} else { + try { + (window as any).floatingButtonAPI = floatingButtonAPI; + } catch (error) { + console.error('=== FloatingPreload: Error attaching API to window ===:', error); + } +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index b1ed4114..fbba44e4 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -9,6 +9,7 @@ declare global { getPathForFile(file: File): string getWindowId(): number | null getWebContentsId(): number - } + }, + floatingButtonAPI: typeof floatingButtonAPI; } } diff --git a/src/renderer/floating/FloatingButton.vue b/src/renderer/floating/FloatingButton.vue new file mode 100644 index 00000000..1be12da7 --- /dev/null +++ b/src/renderer/floating/FloatingButton.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/src/renderer/floating/env.d.ts b/src/renderer/floating/env.d.ts new file mode 100644 index 00000000..6f624dda --- /dev/null +++ b/src/renderer/floating/env.d.ts @@ -0,0 +1,20 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type + const component: DefineComponent<{}, {}, any> + export default component +} + +declare global { + interface Window { + floatingButtonAPI: { + onClick: () => void + onConfigUpdate: (callback: (config: any) => void) => void + removeAllListeners: () => void + } + } +} + +export {} diff --git a/src/renderer/floating/index.html b/src/renderer/floating/index.html new file mode 100644 index 00000000..77e31249 --- /dev/null +++ b/src/renderer/floating/index.html @@ -0,0 +1,27 @@ + + + + + + + Floating Button + + + +
+ + + diff --git a/src/renderer/floating/main.ts b/src/renderer/floating/main.ts new file mode 100644 index 00000000..c0d45909 --- /dev/null +++ b/src/renderer/floating/main.ts @@ -0,0 +1,6 @@ +import '../src/assets/main.css' +import { createApp } from 'vue' +import FloatingButton from './FloatingButton.vue' + +const app = createApp(FloatingButton) +app.mount('#app') diff --git a/src/renderer/src/components/settings/DisplaySettings.vue b/src/renderer/src/components/settings/DisplaySettings.vue index 5147f5f4..c7b2849b 100644 --- a/src/renderer/src/components/settings/DisplaySettings.vue +++ b/src/renderer/src/components/settings/DisplaySettings.vue @@ -89,6 +89,28 @@ /> + + +
+
+ + + {{ + t('settings.display.floatingButton') + }} + +
+ +
+
+
+ {{ t('settings.display.floatingButtonDesc') }} +
+
@@ -141,6 +163,7 @@ import { ref, onMounted, watch, computed } from 'vue' import { useSettingsStore } from '@/stores/settings' import { useLanguageStore } from '@/stores/language' +import { useFloatingButtonStore } from '@/stores/floatingButton' import { Dialog, DialogContent, @@ -155,6 +178,7 @@ import { Slider } from '@/components/ui/slider' const languageStore = useLanguageStore() const settingsStore = useSettingsStore() +const floatingButtonStore = useFloatingButtonStore() const { t } = useI18n() // --- Language Settings --- @@ -221,6 +245,10 @@ const handleNotificationsChange = (value: boolean) => { settingsStore.setNotificationsEnabled(value) } +const handleFloatingButtonChange = (value: boolean) => { + floatingButtonStore.setFloatingButtonEnabled(value) +} + // --- Lifecycle --- onMounted(async () => { selectedLanguage.value = languageStore.language diff --git a/src/renderer/src/events.ts b/src/renderer/src/events.ts index 0153cc03..05b7a7eb 100644 --- a/src/renderer/src/events.ts +++ b/src/renderer/src/events.ts @@ -115,3 +115,11 @@ export const TAB_EVENTS = { RENDERER_TAB_READY: 'tab:renderer-ready', // 渲染进程标签页就绪 RENDERER_TAB_ACTIVATED: 'tab:renderer-activated' // 渲染进程标签页激活 } + +// 悬浮按钮相关事件 +export const FLOATING_BUTTON_EVENTS = { + CLICKED: 'floating-button:clicked', // 悬浮按钮被点击 + VISIBILITY_CHANGED: 'floating-button:visibility-changed', // 悬浮按钮显示状态改变 + POSITION_CHANGED: 'floating-button:position-changed', // 悬浮按钮位置改变 + ENABLED_CHANGED: 'floating-button:enabled-changed' // 悬浮按钮启用状态改变 +} diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index ab387914..8769fb38 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -438,7 +438,9 @@ "text-base": "Default", "text-lg": "Big", "text-sm": "Small", - "text-xl": "Large" + "text-xl": "Large", + "floatingButton": "Floating Button", + "floatingButtonDesc": "Display a floating button on the desktop to quickly activate the application window" }, "shortcuts": { "title": "Shortcut Key Settings", diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 07516e3d..a48e1202 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -438,7 +438,9 @@ "text-base": "پیش‌فرض", "text-lg": "بزرگ", "text-sm": "کوچک", - "text-xl": "بسیار بزرگ" + "text-xl": "بسیار بزرگ", + "floatingButton": "دکمه شناور", + "floatingButtonDesc": "نمایش یک دکمه شناور بر روی دسکتاپ برای فعال‌سازی سریع پنجره برنامه" }, "shortcuts": { "title": "تنظیمات کلید میانبر", diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index 3fb74ab7..d1992356 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -438,7 +438,9 @@ "text-base": "défaut", "text-lg": "grand", "text-sm": "Petit", - "text-xl": "Extrêmement grand" + "text-xl": "Extrêmement grand", + "floatingButton": "Bouton flottant", + "floatingButtonDesc": "Afficher un bouton flottant sur le bureau pour activer rapidement la fenêtre de l'application" }, "shortcuts": { "title": "Paramètres des raccourcis clavier", diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index 9ac0a8f0..d3a0c7ba 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -438,7 +438,9 @@ "text-base": "デフォルト", "text-lg": "大きい", "text-sm": "小さい", - "text-xl": "非常に大きい" + "text-xl": "非常に大きい", + "floatingButton": "フローティングボタン", + "floatingButtonDesc": "デスクトップにフローティングボタンを表示し、アプリケーションウィンドウを素早く起動できます" }, "shortcuts": { "title": "ショートカットキー設定", diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index fd978a79..7354d1e1 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -438,7 +438,9 @@ "text-base": "기본", "text-lg": "큰", "text-sm": "작은", - "text-xl": "매우 큽니다" + "text-xl": "매우 큽니다", + "floatingButton": "플로팅 버튼", + "floatingButtonDesc": "데스크톱에 플로팅 버튼을 표시하여 애플리케이션 창을 빠르게 활성화할 수 있습니다" }, "shortcuts": { "title": "단축키 설정", diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index 1af776bc..55fad2f7 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -438,7 +438,9 @@ "text-base": "Маленький", "text-lg": "по умолчанию", "text-sm": "Чрезвычайно маленький", - "text-xl": "большой" + "text-xl": "большой", + "floatingButton": "Плавающая кнопка", + "floatingButtonDesc": "Отображать плавающую кнопку на рабочем столе для быстрого активации окна приложения" }, "shortcuts": { "title": "Настройки сочетаний клавиш", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 90848d78..857e71d5 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -438,7 +438,9 @@ "text-base": "默认", "text-lg": "大", "text-xl": "特大", - "text-2xl": "超大" + "text-2xl": "超大", + "floatingButton": "悬浮按钮", + "floatingButtonDesc": "在桌面显示一个悬浮按钮,可以快速唤起应用窗口" }, "shortcuts": { "title": "快捷键设置", diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index d14f426b..332bebf1 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -438,7 +438,9 @@ "text-base": "預設", "text-lg": "大", "text-sm": "小", - "text-xl": "特大" + "text-xl": "特大", + "floatingButton": "懸浮按鈕", + "floatingButtonDesc": "在桌面顯示一個懸浮按鈕,可以快速喚起應用程式視窗" }, "shortcuts": { "title": "快捷鍵設置", diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 396147a3..280ec0da 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -438,7 +438,9 @@ "text-base": "預設", "text-lg": "大", "text-sm": "小", - "text-xl": "特大" + "text-xl": "特大", + "floatingButton": "懸浮按鈕", + "floatingButtonDesc": "在桌面顯示一個懸浮按鈕,可以快速喚起應用程式視窗" }, "shortcuts": { "title": "快捷鍵設置", diff --git a/src/renderer/src/stores/floatingButton.ts b/src/renderer/src/stores/floatingButton.ts new file mode 100644 index 00000000..46a5aee0 --- /dev/null +++ b/src/renderer/src/stores/floatingButton.ts @@ -0,0 +1,58 @@ +import { defineStore } from 'pinia' +import { ref, onMounted } from 'vue' +import { usePresenter } from '@/composables/usePresenter' + +export const useFloatingButtonStore = defineStore('floatingButton', () => { + const configP = usePresenter('configPresenter') + + // 悬浮按钮是否启用的状态 + const enabled = ref(false) + + // 获取悬浮按钮启用状态 + const getFloatingButtonEnabled = async (): Promise => { + try { + return await configP.getFloatingButtonEnabled() + } catch (error) { + console.error('Failed to get floating button enabled status:', error) + return false + } + } + + // 设置悬浮按钮启用状态 + const setFloatingButtonEnabled = async (value: boolean) => { + try { + enabled.value = Boolean(value) + await configP.setFloatingButtonEnabled(value) + } catch (error) { + console.error('Failed to set floating button enabled status:', error) + // 如果设置失败,回滚本地状态 + enabled.value = !value + } + } + + // 初始化状态 + const initializeState = async () => { + try { + const currentEnabled = await getFloatingButtonEnabled() + enabled.value = currentEnabled + } catch (error) { + console.error('Failed to initialize floating button state:', error) + enabled.value = false + } + } + + // 在组件挂载时初始化 + onMounted(async () => { + await initializeState() + }) + + return { + // 状态 + enabled, + + // 方法 + getFloatingButtonEnabled, + setFloatingButtonEnabled, + initializeState, + } +}) diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index c4fa6a0c..51f6b8aa 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -338,6 +338,9 @@ export interface IConfigPresenter { // COT拷贝设置 getCopyWithCotEnabled(): boolean setCopyWithCotEnabled(enabled: boolean): void + // 悬浮按钮设置 + getFloatingButtonEnabled(): boolean + setFloatingButtonEnabled(enabled: boolean): void // 日志设置 getLoggingEnabled(): boolean setLoggingEnabled(enabled: boolean): void diff --git a/tsconfig.web.json b/tsconfig.web.json index 2dc238ab..01c13567 100644 --- a/tsconfig.web.json +++ b/tsconfig.web.json @@ -1,6 +1,7 @@ { "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", "include": [ + "src/renderer/src/floating/env.d.ts", "src/renderer/shell/env.d.ts", "src/renderer/shell/**/*", "src/renderer/shell/**/*.vue", @@ -9,6 +10,7 @@ "src/renderer/src/**/*", "src/renderer/src/**/*.vue", "src/renderer/src/assets/**/*", + "src/renderer/src/floating/**/*", "src/preload/*.d.ts", "src/shared/**/*" ],