diff --git a/electron-builder.yml b/electron-builder.yml index c65f20ed3..1303a4a3c 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -53,7 +53,9 @@ files: - '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}' - '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}' - '!node_modules/selection-hook/prebuilds/**/*' # we rebuild .node, don't use prebuilds - - '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters}' # filter .node build files + - '!node_modules/selection-hook/node_modules' # we don't need what in the node_modules dir + - '!node_modules/selection-hook/src' # we don't need source files + - '!**/*.{h,iobj,ipdb,tlog,recipe,vcxproj,vcxproj.filters,Makefile,*.Makefile}' # filter .node build files asarUnpack: - resources/** - '**/*.{metal,exp,lib}' diff --git a/package.json b/package.json index 69bf4268c..35a85bf16 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "node-stream-zip": "^1.15.0", "notion-helper": "^1.3.22", "os-proxy-config": "^1.1.2", - "selection-hook": "^0.9.23", + "selection-hook": "^1.0.3", "turndown": "7.2.0" }, "devDependencies": { diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index daea5dad6..811806527 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -32,6 +32,9 @@ export enum IpcChannel { App_InstallUvBinary = 'app:install-uv-binary', App_InstallBunBinary = 'app:install-bun-binary', + App_MacIsProcessTrusted = 'app:mac-is-process-trusted', + App_MacRequestProcessTrust = 'app:mac-request-process-trust', + App_QuoteToMain = 'app:quote-to-main', Notification_Send = 'notification:send', diff --git a/src/main/configs/SelectionConfig.ts b/src/main/configs/SelectionConfig.ts index 59988ded7..31868a470 100644 --- a/src/main/configs/SelectionConfig.ts +++ b/src/main/configs/SelectionConfig.ts @@ -1,6 +1,6 @@ interface IFilterList { WINDOWS: string[] - MAC?: string[] + MAC: string[] } interface IFinetunedList { @@ -45,14 +45,17 @@ export const SELECTION_PREDEFINED_BLACKLIST: IFilterList = { 'sldworks.exe', // Remote Desktop 'mstsc.exe' - ] + ], + MAC: [] } export const SELECTION_FINETUNED_LIST: IFinetunedList = { EXCLUDE_CLIPBOARD_CURSOR_DETECT: { - WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe'] + WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe'], + MAC: [] }, INCLUDE_CLIPBOARD_DELAY_READ: { - WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe'] + WINDOWS: ['acrobat.exe', 'wps.exe', 'cajviewer.exe', 'foxitphantom.exe'], + MAC: [] } } diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 87aad8d93..0176a2752 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -8,7 +8,7 @@ import { handleZoomFactor } from '@main/utils/zoom' import { UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { Shortcut, ThemeMode } from '@types' -import { BrowserWindow, dialog, ipcMain, session, shell, webContents } from 'electron' +import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences, webContents } from 'electron' import log from 'electron-log' import { Notification } from 'src/renderer/src/types/notification' @@ -158,6 +158,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { } }) + //only for mac + if (isMac) { + ipcMain.handle(IpcChannel.App_MacIsProcessTrusted, (): boolean => { + return systemPreferences.isTrustedAccessibilityClient(false) + }) + + //return is only the current state, not the new state + ipcMain.handle(IpcChannel.App_MacRequestProcessTrust, (): boolean => { + return systemPreferences.isTrustedAccessibilityClient(true) + }) + } + ipcMain.handle(IpcChannel.Config_Set, (_, key: string, value: any, isNotify: boolean = false) => { configManager.set(key, value, isNotify) }) diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index eba97179b..23578b75e 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -1,7 +1,7 @@ import { SELECTION_FINETUNED_LIST, SELECTION_PREDEFINED_BLACKLIST } from '@main/configs/SelectionConfig' -import { isDev, isWin } from '@main/constant' +import { isDev, isMac, isWin } from '@main/constant' import { IpcChannel } from '@shared/IpcChannel' -import { BrowserWindow, ipcMain, screen } from 'electron' +import { BrowserWindow, ipcMain, screen, systemPreferences } from 'electron' import Logger from 'electron-log' import { join } from 'path' import type { @@ -16,9 +16,12 @@ import type { ActionItem } from '../../renderer/src/types/selectionTypes' import { ConfigKeys, configManager } from './ConfigManager' import storeSyncService from './StoreSyncService' +const isSupportedOS = isWin || isMac + let SelectionHook: SelectionHookConstructor | null = null try { - if (isWin) { + //since selection-hook v1.0.0, it supports macOS + if (isSupportedOS) { SelectionHook = require('selection-hook') } } catch (error) { @@ -118,7 +121,7 @@ export class SelectionService { } public static getInstance(): SelectionService | null { - if (!isWin) return null + if (!isSupportedOS) return null if (!SelectionService.instance) { SelectionService.instance = new SelectionService() @@ -213,6 +216,8 @@ export class SelectionService { blacklist: SelectionHook!.FilterMode.EXCLUDE_LIST } + const predefinedBlacklist = isWin ? SELECTION_PREDEFINED_BLACKLIST.WINDOWS : SELECTION_PREDEFINED_BLACKLIST.MAC + let combinedList: string[] = list let combinedMode = mode @@ -221,7 +226,7 @@ export class SelectionService { switch (mode) { case 'blacklist': //combine the predefined blacklist with the user-defined blacklist - combinedList = [...new Set([...list, ...SELECTION_PREDEFINED_BLACKLIST.WINDOWS])] + combinedList = [...new Set([...list, ...predefinedBlacklist])] break case 'whitelist': combinedList = [...list] @@ -229,7 +234,7 @@ export class SelectionService { case 'default': default: //use the predefined blacklist as the default filter list - combinedList = [...SELECTION_PREDEFINED_BLACKLIST.WINDOWS] + combinedList = [...predefinedBlacklist] combinedMode = 'blacklist' break } @@ -243,14 +248,21 @@ export class SelectionService { private setHookFineTunedList() { if (!this.selectionHook) return + const excludeClipboardCursorDetectList = isWin + ? SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.WINDOWS + : SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.MAC + const includeClipboardDelayReadList = isWin + ? SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.WINDOWS + : SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.MAC + this.selectionHook.setFineTunedList( SelectionHook!.FineTunedListType.EXCLUDE_CLIPBOARD_CURSOR_DETECT, - SELECTION_FINETUNED_LIST.EXCLUDE_CLIPBOARD_CURSOR_DETECT.WINDOWS + excludeClipboardCursorDetectList ) this.selectionHook.setFineTunedList( SelectionHook!.FineTunedListType.INCLUDE_CLIPBOARD_DELAY_READ, - SELECTION_FINETUNED_LIST.INCLUDE_CLIPBOARD_DELAY_READ.WINDOWS + includeClipboardDelayReadList ) } @@ -259,11 +271,28 @@ export class SelectionService { * @returns {boolean} Success status of service start */ public start(): boolean { - if (!this.selectionHook || this.started) { - this.logError(new Error('SelectionService start(): instance is null or already started')) + if (!this.selectionHook) { + this.logError(new Error('SelectionService start(): instance is null')) return false } + if (this.started) { + this.logError(new Error('SelectionService start(): already started')) + return false + } + + //On macOS, we need to check if the process is trusted + if (isMac) { + if (!systemPreferences.isTrustedAccessibilityClient(false)) { + this.logError( + new Error( + 'SelectionSerice not started: process is not trusted on macOS, please turn on the Accessibility permission' + ) + ) + return false + } + } + try { //make sure the toolbar window is ready this.createToolbarWindow() @@ -306,6 +335,7 @@ export class SelectionService { if (!this.selectionHook) return false this.selectionHook.stop() + this.selectionHook.cleanup() //already remove all listeners //reset the listener states @@ -316,6 +346,7 @@ export class SelectionService { this.toolbarWindow.close() this.toolbarWindow = null } + this.closePreloadedActionWindows() this.started = false @@ -366,21 +397,29 @@ export class SelectionService { this.toolbarWindow = new BrowserWindow({ width: toolbarWidth, height: toolbarHeight, + show: false, frame: false, transparent: true, alwaysOnTop: true, skipTaskbar: true, + autoHideMenuBar: true, resizable: false, minimizable: false, maximizable: false, + fullscreenable: false, // [macOS] must be false movable: true, - focusable: false, hasShadow: false, thickFrame: false, roundedCorners: true, backgroundMaterial: 'none', - type: 'toolbar', - show: false, + + // Platform specific settings + // [macOS] DO NOT set type to 'panel', it will not work because it conflicts with other settings + // [macOS] DO NOT set focusable to false, it will make other windows bring to front together + ...(isWin ? { type: 'toolbar', focusable: false } : {}), + hiddenInMissionControl: true, // [macOS only] + acceptFirstMouse: true, // [macOS only] + webPreferences: { preload: join(__dirname, '../preload/index.js'), contextIsolation: true, @@ -392,7 +431,9 @@ export class SelectionService { // Hide when losing focus this.toolbarWindow.on('blur', () => { - this.hideToolbar() + if (this.toolbarWindow!.isVisible()) { + this.hideToolbar() + } }) // Clean up when closed @@ -406,6 +447,13 @@ export class SelectionService { // Add show/hide event listeners this.toolbarWindow.on('show', () => { this.toolbarWindow?.webContents.send(IpcChannel.Selection_ToolbarVisibilityChange, true) + + // [macOS] force the toolbar window to be visible on current desktop + // but it will make docker icon flash. And we found that it's not necessary now. + // will remove after testing + // if (isMac) { + // this.toolbarWindow!.setVisibleOnAllWorkspaces(false) + // } }) this.toolbarWindow.on('hide', () => { @@ -460,11 +508,22 @@ export class SelectionService { //set the window to always on top (highest level) //should set every time the window is shown this.toolbarWindow!.setAlwaysOnTop(true, 'screen-saver') - this.toolbarWindow!.show() + + // [macOS] force the toolbar window to be visible on current desktop + // but it will make docker icon flash. And we found that it's not necessary now. + // will remove after testing + // if (isMac) { + // this.toolbarWindow!.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) + // } + + // [macOS] MUST use `showInactive()` to prevent other windows bring to front together + // [Windows] is OK for both `show()` and `showInactive()` because of `focusable: false` + this.toolbarWindow!.showInactive() /** - * In Windows 10, setOpacity(1) will make the window completely transparent - * It's a strange behavior, so we don't use it for compatibility + * [Windows] + * In Windows 10, setOpacity(1) will make the window completely transparent + * It's a strange behavior, so we don't use it for compatibility */ // this.toolbarWindow!.setOpacity(1) @@ -477,10 +536,52 @@ export class SelectionService { public hideToolbar(): void { if (!this.isToolbarAlive()) return - // this.toolbarWindow!.setOpacity(0) + this.stopHideByMouseKeyListener() + + // [Windows] just hide the toolbar window is enough + if (!isMac) { + this.toolbarWindow!.hide() + return + } + + /************************************************ + * [macOS] the following code is only for macOS + *************************************************/ + + // [macOS] a HACKY way + // make sure other windows do not bring to front when toolbar is hidden + // get all focusable windows and set them to not focusable + const focusableWindows: BrowserWindow[] = [] + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed() && window.isVisible()) { + if (window.isFocusable()) { + focusableWindows.push(window) + window.setFocusable(false) + } + } + } + this.toolbarWindow!.hide() - this.stopHideByMouseKeyListener() + // set them back to focusable after 50ms + setTimeout(() => { + for (const window of focusableWindows) { + if (!window.isDestroyed()) { + window.setFocusable(true) + } + } + }, 50) + + // [macOS] hacky way + // Because toolbar is not a FOCUSED window, so the hover status will remain when next time show + // so we just send mouseMove event to the toolbar window to make the hover status disappear + this.toolbarWindow!.webContents.sendInputEvent({ + type: 'mouseMove', + x: -1, + y: -1 + }) + + return } /** @@ -520,71 +621,71 @@ export class SelectionService { /** * Calculate optimal toolbar position based on selection context * Ensures toolbar stays within screen boundaries and follows selection direction - * @param point Reference point for positioning, must be INTEGER + * @param refPoint Reference point for positioning, must be INTEGER * @param orientation Preferred position relative to reference point * @returns Calculated screen coordinates for toolbar, INTEGER */ - private calculateToolbarPosition(point: Point, orientation: RelativeOrientation): Point { + private calculateToolbarPosition(refPoint: Point, orientation: RelativeOrientation): Point { // Calculate initial position based on the specified anchor - let posX: number, posY: number + const posPoint: Point = { x: 0, y: 0 } const { toolbarWidth, toolbarHeight } = this.getToolbarRealSize() switch (orientation) { case 'topLeft': - posX = point.x - toolbarWidth - posY = point.y - toolbarHeight + posPoint.x = refPoint.x - toolbarWidth + posPoint.y = refPoint.y - toolbarHeight break case 'topRight': - posX = point.x - posY = point.y - toolbarHeight + posPoint.x = refPoint.x + posPoint.y = refPoint.y - toolbarHeight break case 'topMiddle': - posX = point.x - toolbarWidth / 2 - posY = point.y - toolbarHeight + posPoint.x = refPoint.x - toolbarWidth / 2 + posPoint.y = refPoint.y - toolbarHeight break case 'bottomLeft': - posX = point.x - toolbarWidth - posY = point.y + posPoint.x = refPoint.x - toolbarWidth + posPoint.y = refPoint.y break case 'bottomRight': - posX = point.x - posY = point.y + posPoint.x = refPoint.x + posPoint.y = refPoint.y break case 'bottomMiddle': - posX = point.x - toolbarWidth / 2 - posY = point.y + posPoint.x = refPoint.x - toolbarWidth / 2 + posPoint.y = refPoint.y break case 'middleLeft': - posX = point.x - toolbarWidth - posY = point.y - toolbarHeight / 2 + posPoint.x = refPoint.x - toolbarWidth + posPoint.y = refPoint.y - toolbarHeight / 2 break case 'middleRight': - posX = point.x - posY = point.y - toolbarHeight / 2 + posPoint.x = refPoint.x + posPoint.y = refPoint.y - toolbarHeight / 2 break case 'center': - posX = point.x - toolbarWidth / 2 - posY = point.y - toolbarHeight / 2 + posPoint.x = refPoint.x - toolbarWidth / 2 + posPoint.y = refPoint.y - toolbarHeight / 2 break default: // Default to 'topMiddle' if invalid position - posX = point.x - toolbarWidth / 2 - posY = point.y - toolbarHeight / 2 + posPoint.x = refPoint.x - toolbarWidth / 2 + posPoint.y = refPoint.y - toolbarHeight / 2 } //use original point to get the display - const display = screen.getDisplayNearestPoint({ x: point.x, y: point.y }) + const display = screen.getDisplayNearestPoint(refPoint) // Ensure toolbar stays within screen boundaries - posX = Math.round( - Math.max(display.workArea.x, Math.min(posX, display.workArea.x + display.workArea.width - toolbarWidth)) + posPoint.x = Math.round( + Math.max(display.workArea.x, Math.min(posPoint.x, display.workArea.x + display.workArea.width - toolbarWidth)) ) - posY = Math.round( - Math.max(display.workArea.y, Math.min(posY, display.workArea.y + display.workArea.height - toolbarHeight)) + posPoint.y = Math.round( + Math.max(display.workArea.y, Math.min(posPoint.y, display.workArea.y + display.workArea.height - toolbarHeight)) ) - return { x: posX, y: posY } + return posPoint } private isSamePoint(point1: Point, point2: Point): boolean { @@ -773,8 +874,11 @@ export class SelectionService { } if (!isLogical) { + // [macOS] don't need to convert by screenToDipPoint + if (!isMac) { + refPoint = screen.screenToDipPoint(refPoint) + } //screenToDipPoint can be float, so we need to round it - refPoint = screen.screenToDipPoint(refPoint) refPoint = { x: Math.round(refPoint.x), y: Math.round(refPoint.y) } } @@ -832,8 +936,8 @@ export class SelectionService { return } - //data point is physical coordinates, convert to logical coordinates - const mousePoint = screen.screenToDipPoint({ x: data.x, y: data.y }) + //data point is physical coordinates, convert to logical coordinates(only for windows/linux) + const mousePoint = isMac ? { x: data.x, y: data.y } : screen.screenToDipPoint({ x: data.x, y: data.y }) const bounds = this.toolbarWindow!.getBounds() @@ -966,7 +1070,8 @@ export class SelectionService { frame: false, transparent: true, autoHideMenuBar: true, - titleBarStyle: 'hidden', + titleBarStyle: 'hidden', // [macOS] + trafficLightPosition: { x: 12, y: 9 }, // [macOS] hasShadow: false, thickFrame: false, show: false, @@ -1043,6 +1148,27 @@ export class SelectionService { if (!actionWindow.isDestroyed()) { actionWindow.destroy() } + + // [macOS] a HACKY way + // make sure other windows do not bring to front when action window is closed + if (isMac) { + const focusableWindows: BrowserWindow[] = [] + for (const window of BrowserWindow.getAllWindows()) { + if (!window.isDestroyed() && window.isVisible()) { + if (window.isFocusable()) { + focusableWindows.push(window) + window.setFocusable(false) + } + } + } + setTimeout(() => { + for (const window of focusableWindows) { + if (!window.isDestroyed()) { + window.setFocusable(true) + } + } + }, 50) + } }) //remember the action window size @@ -1088,22 +1214,26 @@ export class SelectionService { //center way if (!this.isFollowToolbar || !this.toolbarWindow) { - if (this.isRemeberWinSize) { - actionWindow.setBounds({ - width: actionWindowWidth, - height: actionWindowHeight - }) - } + const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) + const workArea = display.workArea + + const centerX = workArea.x + (workArea.width - actionWindowWidth) / 2 + const centerY = workArea.y + (workArea.height - actionWindowHeight) / 2 + + actionWindow.setBounds({ + width: actionWindowWidth, + height: actionWindowHeight, + x: Math.round(centerX), + y: Math.round(centerY) + }) actionWindow.show() - this.hideToolbar() return } //follow toolbar - const toolbarBounds = this.toolbarWindow!.getBounds() - const display = screen.getDisplayNearestPoint({ x: toolbarBounds.x, y: toolbarBounds.y }) + const display = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) const workArea = display.workArea const GAP = 6 // 6px gap from screen edges @@ -1214,7 +1344,7 @@ export class SelectionService { selectionService?.hideToolbar() }) - ipcMain.handle(IpcChannel.Selection_WriteToClipboard, (_, text: string) => { + ipcMain.handle(IpcChannel.Selection_WriteToClipboard, (_, text: string): boolean => { return selectionService?.writeToClipboard(text) ?? false }) @@ -1291,7 +1421,7 @@ export class SelectionService { * @returns {boolean} Success status of initialization */ export function initSelectionService(): boolean { - if (!isWin) return false + if (!isSupportedOS) return false configManager.subscribe(ConfigKeys.SelectionAssistantEnabled, (enabled: boolean) => { //avoid closure diff --git a/src/main/services/TrayService.ts b/src/main/services/TrayService.ts index 89c88bc0a..205d7fdee 100644 --- a/src/main/services/TrayService.ts +++ b/src/main/services/TrayService.ts @@ -84,10 +84,8 @@ export class TrayService { label: trayLocale.show_mini_window, click: () => windowService.showMiniWindow() }, - isWin && { + (isWin || isMac) && { label: selectionLocale.name + (selectionAssistantEnabled ? ' - On' : ' - Off'), - // type: 'checkbox', - // checked: selectionAssistantEnabled, click: () => { if (selectionService) { selectionService.toggleEnabled() diff --git a/src/preload/index.ts b/src/preload/index.ts index 8412e00bc..beabfa1a2 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -42,6 +42,10 @@ const api = { openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url), getCacheSize: () => ipcRenderer.invoke(IpcChannel.App_GetCacheSize), clearCache: () => ipcRenderer.invoke(IpcChannel.App_ClearCache), + mac: { + isProcessTrusted: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted), + requestProcessTrust: (): Promise => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust) + }, notification: { send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification) }, diff --git a/src/renderer/src/assets/styles/selection-toolbar.scss b/src/renderer/src/assets/styles/selection-toolbar.scss index bfe329c69..23f0edfb3 100644 --- a/src/renderer/src/assets/styles/selection-toolbar.scss +++ b/src/renderer/src/assets/styles/selection-toolbar.scss @@ -18,25 +18,37 @@ html { --selection-toolbar-logo-display: flex; // values: flex | none --selection-toolbar-logo-size: 22px; // default: 22px - --selection-toolbar-logo-margin: 0 0 0 5px; // default: 0 0 05px + --selection-toolbar-logo-border-width: 0.5px 0 0.5px 0.5px; // default: none + --selection-toolbar-logo-border-style: solid; // default: none + --selection-toolbar-logo-border-color: rgba(255, 255, 255, 0.2); + --selection-toolbar-logo-margin: 0; // default: 0 + --selection-toolbar-logo-padding: 0 6px 0 8px; // default: 0 4px 0 8px + --selection-toolbar-logo-background: transparent; // default: transparent // DO NOT MODIFY THESE VALUES, IF YOU DON'T KNOW WHAT YOU ARE DOING - --selection-toolbar-padding: 2px 4px 2px 2px; // default: 2px 4px 2px 2px + --selection-toolbar-padding: 0; // default: 0 --selection-toolbar-margin: 2px 3px 5px 3px; // default: 2px 3px 5px 3px // ------------------------------------------------------------ - --selection-toolbar-border-radius: 6px; - --selection-toolbar-border: 1px solid rgba(55, 55, 55, 0.5); + --selection-toolbar-border-radius: 10px; + --selection-toolbar-border: none; --selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.3); --selection-toolbar-background: rgba(20, 20, 20, 0.95); // Buttons + --selection-toolbar-buttons-border-width: 0.5px 0.5px 0.5px 0; + --selection-toolbar-buttons-border-style: solid; + --selection-toolbar-buttons-border-color: rgba(255, 255, 255, 0.2); + --selection-toolbar-buttons-border-radius: 0 var(--selection-toolbar-border-radius) + var(--selection-toolbar-border-radius) 0; --selection-toolbar-button-icon-size: 16px; // default: 16px - --selection-toolbar-button-text-margin: 0 0 0 3px; // default: 0 0 0 3px - --selection-toolbar-button-margin: 0 2px; // default: 0 2px - --selection-toolbar-button-padding: 4px 6px; // default: 4px 6px - --selection-toolbar-button-border-radius: 4px; // default: 4px + --selection-toolbar-button-direction: row; // default: row | column + --selection-toolbar-button-text-margin: 0 0 0 0; // default: 0 0 0 0 + --selection-toolbar-button-margin: 0; // default: 0 + --selection-toolbar-button-padding: 0 8px; // default: 0 8px + --selection-toolbar-button-last-padding: 0 12px 0 8px; + --selection-toolbar-button-border-radius: 0; // default: 0 --selection-toolbar-button-border: none; // default: none --selection-toolbar-button-box-shadow: none; // default: none @@ -45,14 +57,19 @@ html { --selection-toolbar-button-text-color-hover: var(--selection-toolbar-color-primary); --selection-toolbar-button-icon-color-hover: var(--selection-toolbar-color-primary); --selection-toolbar-button-bgcolor: transparent; // default: transparent - --selection-toolbar-button-bgcolor-hover: #222222; + --selection-toolbar-button-bgcolor-hover: #333333; } [theme-mode='light'] { - --selection-toolbar-border: 1px solid rgba(200, 200, 200, 0.5); - --selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.3); + --selection-toolbar-border: none; + --selection-toolbar-box-shadow: 0px 2px 3px rgba(50, 50, 50, 0.1); --selection-toolbar-background: rgba(245, 245, 245, 0.95); + // Buttons + --selection-toolbar-buttons-border-color: rgba(0, 0, 0, 0.08); + + --selection-toolbar-logo-border-color: rgba(0, 0, 0, 0.08); + --selection-toolbar-button-text-color: rgba(0, 0, 0, 1); --selection-toolbar-button-icon-color: var(--selection-toolbar-button-text-color); --selection-toolbar-button-text-color-hover: var(--selection-toolbar-color-primary); diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 0a529756b..4511878b8 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -2034,14 +2034,29 @@ "experimental": "Experimental Features", "enable": { "title": "Enable", - "description": "Currently only supported on Windows systems" + "description": "Currently only supported on Windows & macOS", + "mac_process_trust_hint": { + "title": "Accessibility Permission", + "description": [ + "Selection Assistant requires Accessibility Permission to work properly.", + "Please click \"Go to Settings\" and click the \"Open System Settings\" button in the permission request popup that appears later. Then find \"Cherry Studio\" in the application list that appears later and turn on the permission switch.", + "After completing the settings, please reopen the selection assistant." + ], + "button": { + "open_accessibility_settings": "Open Accessibility Settings", + "go_to_settings": "Go to Settings" + } + } }, "toolbar": { "title": "Toolbar", "trigger_mode": { "title": "Trigger Mode", "description": "The way to trigger the selection assistant and show the toolbar", - "description_note": "Some applications do not support selecting text with the Ctrl key. If you have remapped the Ctrl key using tools like AHK, it may cause some applications to fail to select text.", + "description_note": { + "windows": "Some applications do not support selecting text with the Ctrl key. If you have remapped the Ctrl key using tools like AHK, it may cause some applications to fail to select text.", + "mac": "If you have remapped the ⌘ key using shortcuts or keyboard mapping tools, it may cause some applications to fail to select text." + }, "selected": "Selection", "selected_note": "Show toolbar immediately when text is selected", "ctrlkey": "Ctrl Key", @@ -2166,7 +2181,10 @@ }, "filter_modal": { "title": "Application Filter List", - "user_tips": "Please enter the executable file name of the application, one per line, case insensitive, can be fuzzy matched. For example: chrome.exe, weixin.exe, Cherry Studio.exe, etc." + "user_tips": { + "windows": "Please enter the executable file name of the application, one per line, case insensitive, can be fuzzy matched. For example: chrome.exe, weixin.exe, Cherry Studio.exe, etc.", + "mac": "Please enter the Bundle ID of the application, one per line, case insensitive, can be fuzzy matched. For example: com.google.Chrome, com.apple.mail, etc." + } } } } diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 30f5f2fb0..2082cf0c2 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -2037,14 +2037,29 @@ "experimental": "実験的機能", "enable": { "title": "有効化", - "description": "現在Windowsのみ対応" + "description": "現在Windows & macOSのみ対応", + "mac_process_trust_hint": { + "title": "アクセシビリティー権限", + "description": [ + "テキスト選択ツールは、アクセシビリティー権限が必要です。", + "「設定に移動」をクリックし、後で表示される権限要求ポップアップで「システム設定を開く」ボタンをクリックします。その後、表示されるアプリケーションリストで「Cherry Studio」を見つけ、権限スイッチをオンにしてください。", + "設定が完了したら、テキスト選択ツールを再起動してください。" + ], + "button": { + "open_accessibility_settings": "アクセシビリティー設定を開く", + "go_to_settings": "設定に移動" + } + } }, "toolbar": { "title": "ツールバー", "trigger_mode": { "title": "単語の取り出し方", "description": "テキスト選択後、取詞ツールバーを表示する方法", - "description_note": "一部のアプリケーションでは、Ctrl キーでテキストを選択できません。AHK などのツールを使用して Ctrl キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。", + "description_note": { + "windows": "一部のアプリケーションでは、Ctrl キーでテキストを選択できません。AHK などのツールを使用して Ctrl キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。", + "mac": "一部のアプリケーションでは、⌘ キーでテキストを選択できません。ショートカットキーまたはキーボードマッピングツールを使用して ⌘ キーを再マップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。" + }, "selected": "選択時", "selected_note": "テキスト選択時に即時表示", "ctrlkey": "Ctrlキー", @@ -2169,7 +2184,10 @@ }, "filter_modal": { "title": "アプリケーションフィルターリスト", - "user_tips": "アプリケーションの実行ファイル名を1行ずつ入力してください。大文字小文字は区別しません。例: chrome.exe, weixin.exe, Cherry Studio.exe, など。" + "user_tips": { + "windows": "アプリケーションの実行ファイル名を1行ずつ入力してください。大文字小文字は区別しません。例: chrome.exe, weixin.exe, Cherry Studio.exe, など。", + "mac": "アプリケーションのBundle IDを1行ずつ入力してください。大文字小文字は区別しません。例: com.google.Chrome, com.apple.mail, など。" + } } } } diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b850bf3f2..43b53f4a4 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -2037,14 +2037,29 @@ "experimental": "Экспериментальные функции", "enable": { "title": "Включить", - "description": "Поддерживается только в Windows" + "description": "Поддерживается только в Windows & macOS", + "mac_process_trust_hint": { + "title": "Права доступа", + "description": [ + "Помощник выбора требует Права доступа для правильной работы.", + "Пожалуйста, перейдите в \"Настройки\" и нажмите \"Открыть системные настройки\" в запросе разрешения, который появится позже. Затем найдите \"Cherry Studio\" в списке приложений, который появится позже, и включите переключатель разрешения.", + "После завершения настроек, пожалуйста, перезапустите помощник выбора." + ], + "button": { + "open_accessibility_settings": "Открыть системные настройки", + "go_to_settings": "Настройки" + } + } }, "toolbar": { "title": "Панель инструментов", "trigger_mode": { "title": "Режим активации", "description": "Показывать панель сразу при выделении, или только при удержании Ctrl, или только при нажатии на сочетание клавиш", - "description_note": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.", + "description_note": { + "windows": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.", + "mac": "В некоторых приложениях ⌘ может не работать. Если вы используете сочетания клавиш или инструменты для переназначения ⌘, это может привести к тому, что некоторые приложения не смогут выделить текст." + }, "selected": "При выделении", "selected_note": "После выделения", "ctrlkey": "По Ctrl", @@ -2169,7 +2184,10 @@ }, "filter_modal": { "title": "Список фильтрации", - "user_tips": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *" + "user_tips": { + "windows": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *", + "mac": "Введите Bundle ID приложения, один на строку, не учитывая регистр, можно использовать подстановку *" + } } } } diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 2b2176a45..2dd042200 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -2037,14 +2037,29 @@ "experimental": "实验性功能", "enable": { "title": "启用", - "description": "当前仅支持 Windows 系统" + "description": "当前仅支持 Windows & macOS", + "mac_process_trust_hint": { + "title": "辅助功能权限", + "description": [ + "划词助手需「辅助功能权限」才能正常工作。", + "请点击「去设置」,并在稍后弹出的权限请求弹窗中点击 「打开系统设置」 按钮,然后在之后的应用列表中找到 「Cherry Studio」,并打开权限开关。", + "完成设置后,请再次开启划词助手。" + ], + "button": { + "open_accessibility_settings": "打开辅助功能设置", + "go_to_settings": "去设置" + } + } }, "toolbar": { "title": "工具栏", "trigger_mode": { "title": "取词方式", "description": "划词后,触发取词并显示工具栏的方式", - "description_note": "少数应用不支持通过 Ctrl 键划词。若使用了 AHK 等工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。", + "description_note": { + "windows": "少数应用不支持通过 Ctrl 键划词。若使用了AHK等按键映射工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。", + "mac": "若使用了快捷键或键盘映射工具对 ⌘ 键进行了重映射,可能导致部分应用无法划词。" + }, "selected": "划词", "selected_note": "划词后立即显示工具栏", "ctrlkey": "Ctrl 键", @@ -2169,7 +2184,10 @@ }, "filter_modal": { "title": "应用筛选名单", - "user_tips": "请输入应用的执行文件名,每行一个,不区分大小写,可以模糊匹配。例如:chrome.exe、weixin.exe、Cherry Studio.exe 等" + "user_tips": { + "windows": "请输入应用的执行文件名,每行一个,不区分大小写,可以模糊匹配。例如:chrome.exe、weixin.exe、Cherry Studio.exe等", + "mac": "请输入应用的Bundle ID,每行一个,不区分大小写,可以模糊匹配。例如:com.google.Chrome、com.apple.mail等" + } } } } diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 8640d4642..12addbba0 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -2037,14 +2037,29 @@ "experimental": "實驗性功能", "enable": { "title": "啟用", - "description": "目前僅支援 Windows 系統" + "description": "目前僅支援 Windows & macOS", + "mac_process_trust_hint": { + "title": "輔助使用權限", + "description": [ + "劃詞助手需「輔助使用權限」才能正常工作。", + "請點擊「去設定」,並在稍後彈出的權限請求彈窗中點擊 「打開系統設定」 按鈕,然後在之後的應用程式列表中找到 「Cherry Studio」,並開啟權限開關。", + "完成設定後,請再次開啟劃詞助手。" + ], + "button": { + "open_accessibility_settings": "打開輔助使用設定", + "go_to_settings": "去設定" + } + } }, "toolbar": { "title": "工具列", "trigger_mode": { "title": "取詞方式", "description": "劃詞後,觸發取詞並顯示工具列的方式", - "description_note": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了 AHK 等工具對 Ctrl 鍵進行了重新對應,可能導致部分應用程式無法劃詞。", + "description_note": { + "windows": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了 AHK 等工具對 Ctrl 鍵進行了重新對應,可能導致部分應用程式無法劃詞。", + "mac": "若使用了快捷鍵或鍵盤映射工具對 ⌘ 鍵進行了重新對應,可能導致部分應用程式無法劃詞。" + }, "selected": "劃詞", "selected_note": "劃詞後,立即顯示工具列", "ctrlkey": "Ctrl 鍵", @@ -2169,7 +2184,10 @@ }, "filter_modal": { "title": "應用篩選名單", - "user_tips": "請輸入應用的執行檔名稱,每行一個,不區分大小寫,可以模糊匹配。例如:chrome.exe、weixin.exe、Cherry Studio.exe 等" + "user_tips": { + "windows": "請輸入應用的執行檔名稱,每行一個,不區分大小寫,可以模糊匹配。例如:chrome.exe、weixin.exe、Cherry Studio.exe等", + "mac": "請輸入應用的 Bundle ID,每行一個,不區分大小寫,可以模糊匹配。例如:com.google.Chrome、com.apple.mail等" + } } } } diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx index 0bbebf57e..1707b10dd 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx @@ -1,4 +1,4 @@ -import { isWin } from '@renderer/config/constant' +import { isMac, isWin } from '@renderer/config/constant' import { useTheme } from '@renderer/context/ThemeProvider' import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant' import { FilterMode, TriggerMode } from '@renderer/types/selectionTypes' @@ -19,8 +19,9 @@ import { SettingRowTitle, SettingTitle } from '..' -import SelectionActionsList from './SelectionActionsList' -import SelectionFilterListModal from './SelectionFilterListModal' +import MacProcessTrustHintModal from './components/MacProcessTrustHintModal' +import SelectionActionsList from './components/SelectionActionsList' +import SelectionFilterListModal from './components/SelectionFilterListModal' const SelectionAssistantSettings: FC = () => { const { theme } = useTheme() @@ -49,15 +50,43 @@ const SelectionAssistantSettings: FC = () => { setFilterMode, setFilterList } = useSelectionAssistant() + + const isSupportedOS = isWin || isMac + const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false) + const [isMacTrustModalOpen, setIsMacTrustModalOpen] = useState(false) const [opacityValue, setOpacityValue] = useState(actionWindowOpacity) // force disable selection assistant on non-windows systems useEffect(() => { - if (!isWin && selectionEnabled) { - setSelectionEnabled(false) + const checkMacProcessTrust = async () => { + const isTrusted = await window.api.mac.isProcessTrusted() + if (!isTrusted) { + setSelectionEnabled(false) + } } - }, [selectionEnabled, setSelectionEnabled]) + + if (!isSupportedOS && selectionEnabled) { + setSelectionEnabled(false) + return + } else if (isMac && selectionEnabled) { + checkMacProcessTrust() + } + }, [isSupportedOS, selectionEnabled, setSelectionEnabled]) + + const handleEnableCheckboxChange = async (checked: boolean) => { + if (!isSupportedOS) return + + if (isMac && checked) { + const isTrusted = await window.api.mac.isProcessTrusted() + if (!isTrusted) { + setIsMacTrustModalOpen(true) + return + } + } + + setSelectionEnabled(checked) + } return ( @@ -71,18 +100,18 @@ const SelectionAssistantSettings: FC = () => { style={{ fontSize: 12 }}> {'FAQ & ' + t('settings.about.feedback.button')} - {t('selection.settings.experimental')} + {isMac && {t('selection.settings.experimental')}} {t('selection.settings.enable.title')} - {!isWin && {t('selection.settings.enable.description')}} + {!isSupportedOS && {t('selection.settings.enable.description')}} setSelectionEnabled(checked)} - disabled={!isWin} + checked={isSupportedOS && selectionEnabled} + onChange={(checked) => handleEnableCheckboxChange(checked)} + disabled={!isSupportedOS} /> @@ -103,7 +132,10 @@ const SelectionAssistantSettings: FC = () => {
{t('selection.settings.toolbar.trigger_mode.title')}
- +
@@ -116,9 +148,11 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.toolbar.trigger_mode.selected')} - - {t('selection.settings.toolbar.trigger_mode.ctrlkey')} - + {isWin && ( + + {t('selection.settings.toolbar.trigger_mode.ctrlkey')} + + )} { )} + + {isMac && setIsMacTrustModalOpen(false)} />}
) } diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/components/MacProcessTrustHintModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/MacProcessTrustHintModal.tsx new file mode 100644 index 000000000..d3e192655 --- /dev/null +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/MacProcessTrustHintModal.tsx @@ -0,0 +1,68 @@ +import { Button, Modal, Typography } from 'antd' +import { FC } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import styled from 'styled-components' + +const { Text, Paragraph } = Typography + +interface MacProcessTrustHintModalProps { + open: boolean + onClose: () => void +} + +const MacProcessTrustHintModal: FC = ({ open, onClose }) => { + const { t } = useTranslation() + + const handleOpenAccessibility = () => { + window.api.shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility') + onClose() + } + + const handleConfirm = async () => { + window.api.mac.requestProcessTrust() + onClose() + } + + return ( + + + + + } + centered + destroyOnClose> + + + + + + + + + + + + + + + + + + + ) +} + +const ContentContainer = styled.div` + padding: 16px 0; +` + +export default MacProcessTrustHintModal diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionSearchModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionSearchModal.tsx similarity index 100% rename from src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionSearchModal.tsx rename to src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionSearchModal.tsx diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionUserModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionUserModal.tsx similarity index 100% rename from src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionUserModal.tsx rename to src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionUserModal.tsx diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionsList.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionsList.tsx similarity index 91% rename from src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionsList.tsx rename to src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionsList.tsx index 91106f762..7077de2f4 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionActionsList.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionActionsList.tsx @@ -6,13 +6,13 @@ import { Row } from 'antd' import { FC } from 'react' import styled from 'styled-components' -import { SettingDivider, SettingGroup } from '..' -import ActionsList from './components/ActionsList' -import ActionsListDivider from './components/ActionsListDivider' -import SettingsActionsListHeader from './components/SettingsActionsListHeader' -import { useActionItems } from './hooks/useSettingsActionsList' +import { SettingDivider, SettingGroup } from '../..' +import { useActionItems } from '../hooks/useSettingsActionsList' +import ActionsList from './ActionsList' +import ActionsListDivider from './ActionsListDivider' import SelectionActionSearchModal from './SelectionActionSearchModal' import SelectionActionUserModal from './SelectionActionUserModal' +import SettingsActionsListHeader from './SettingsActionsListHeader' // Component for managing selection actions in settings // Handles drag-and-drop reordering, enabling/disabling actions, and custom action management diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionFilterListModal.tsx similarity index 89% rename from src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx rename to src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionFilterListModal.tsx index e3076b408..2b98377a8 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/components/SelectionFilterListModal.tsx @@ -1,3 +1,4 @@ +import { isWin } from '@renderer/config/constant' import { Button, Form, Input, Modal } from 'antd' import { FC, useEffect } from 'react' import { useTranslation } from 'react-i18next' @@ -54,7 +55,11 @@ const SelectionFilterListModal: FC = ({ open, onC {t('common.save')} ]}> - {t('selection.settings.filter_modal.user_tips')} + + {isWin + ? t('selection.settings.filter_modal.user_tips.windows') + : t('selection.settings.filter_modal.user_tips.mac')} +
diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/hooks/useSettingsActionsList.ts b/src/renderer/src/pages/settings/SelectionAssistantSettings/hooks/useSettingsActionsList.ts index 12d6ba0d3..ee416ad40 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/hooks/useSettingsActionsList.ts +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/hooks/useSettingsActionsList.ts @@ -4,7 +4,7 @@ import type { ActionItem } from '@renderer/types/selectionTypes' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { DEFAULT_SEARCH_ENGINES } from '../SelectionActionSearchModal' +import { DEFAULT_SEARCH_ENGINES } from '../components/SelectionActionSearchModal' const MAX_CUSTOM_ITEMS = 8 const MAX_ENABLED_ITEMS = 6 diff --git a/src/renderer/src/pages/settings/ShortcutSettings.tsx b/src/renderer/src/pages/settings/ShortcutSettings.tsx index 9c8eec065..909790f95 100644 --- a/src/renderer/src/pages/settings/ShortcutSettings.tsx +++ b/src/renderer/src/pages/settings/ShortcutSettings.tsx @@ -24,7 +24,7 @@ const ShortcutSettings: FC = () => { //if shortcut is not available on all the platforms, block the shortcut here let shortcuts = originalShortcuts - if (!isWin) { + if (!isWin && !isMac) { //Selection Assistant only available on Windows now const excludedShortcuts = ['selection_assistant_toggle', 'selection_assistant_select_text'] shortcuts = shortcuts.filter((s) => !excludedShortcuts.includes(s.key)) diff --git a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx index 57c2b5190..94c1c575e 100644 --- a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx +++ b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx @@ -1,3 +1,4 @@ +import { isMac } from '@renderer/config/constant' import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant' import { useSettings } from '@renderer/hooks/useSettings' import i18n from '@renderer/i18n' @@ -182,7 +183,7 @@ const SelectionActionApp: FC = () => { return ( - + {action.icon && ( { /> )} - - } onClick={handleMinimize} /> - } onClick={handleClose} className="close" /> + {!isMac && ( + <> + } onClick={handleMinimize} /> + } onClick={handleClose} className="close" /> + + )} diff --git a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx index 00f38e01b..49b3c2fcf 100644 --- a/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx +++ b/src/renderer/src/windows/selection/toolbar/SelectionToolbar.tsx @@ -259,7 +259,7 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => { const Container = styled.div` display: inline-flex; flex-direction: row; - align-items: center; + align-items: stretch; height: var(--selection-toolbar-height); border-radius: var(--selection-toolbar-border-radius); border: var(--selection-toolbar-border); @@ -269,6 +269,7 @@ const Container = styled.div` margin: var(--selection-toolbar-margin) !important; user-select: none; box-sizing: border-box; + overflow: hidden; ` const LogoWrapper = styled.div<{ $draggable: boolean }>` @@ -276,8 +277,13 @@ const LogoWrapper = styled.div<{ $draggable: boolean }>` align-items: center; justify-content: center; margin: var(--selection-toolbar-logo-margin); - background-color: transparent; - ${({ $draggable }) => $draggable && ' -webkit-app-region: drag;'} + padding: var(--selection-toolbar-logo-padding); + background-color: var(--selection-toolbar-logo-background); + border-width: var(--selection-toolbar-logo-border-width); + border-style: var(--selection-toolbar-logo-border-style); + border-color: var(--selection-toolbar-logo-border-color); + border-radius: var(--selection-toolbar-border-radius) 0 0 var(--selection-toolbar-border-radius); + ${({ $draggable }) => $draggable && ' -webkit-app-region: drag;'}; ` const Logo = styled(Avatar)` @@ -307,14 +313,19 @@ const ActionWrapper = styled.div` flex-direction: row; align-items: center; justify-content: center; - margin-left: 3px; background-color: transparent; + border-width: var(--selection-toolbar-buttons-border-width); + border-style: var(--selection-toolbar-buttons-border-style); + border-color: var(--selection-toolbar-buttons-border-color); + border-radius: var(--selection-toolbar-buttons-border-radius); ` const ActionButton = styled.div` + height: 100%; display: flex; flex-direction: row; align-items: center; justify-content: center; + gap: 2px; cursor: pointer !important; margin: var(--selection-toolbar-button-margin); padding: var(--selection-toolbar-button-padding); @@ -324,6 +335,10 @@ const ActionButton = styled.div` box-shadow: var(--selection-toolbar-button-box-shadow); transition: all 0.1s ease-in-out; will-change: color, background-color; + &:last-child { + border-radius: 0 var(--selection-toolbar-border-radius) var(--selection-toolbar-border-radius) 0; + padding: var(--selection-toolbar-button-last-padding); + } .btn-icon { width: var(--selection-toolbar-button-icon-size); @@ -337,6 +352,7 @@ const ActionButton = styled.div` color: var(--selection-toolbar-button-text-color); transition: color 0.1s ease-in-out; will-change: color; + line-height: 1.1; } &:hover { .btn-icon { diff --git a/yarn.lock b/yarn.lock index cc0fd6b62..dce63d4d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5793,7 +5793,7 @@ __metadata: remove-markdown: "npm:^0.6.2" rollup-plugin-visualizer: "npm:^5.12.0" sass: "npm:^1.88.0" - selection-hook: "npm:^0.9.23" + selection-hook: "npm:^1.0.3" shiki: "npm:^3.7.0" string-width: "npm:^7.2.0" styled-components: "npm:^6.1.11" @@ -13955,6 +13955,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^8.4.0": + version: 8.4.0 + resolution: "node-addon-api@npm:8.4.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/d51be099e1b9a6ac4a72f1a60787004d44c8ffe4be1efa38755d54b2a9f4f66647cc6913070e0ed20256d0e6eacceabfff90175fba2ef71153c2d06f8db8e7a9 + languageName: node + linkType: hard + "node-api-version@npm:^0.2.0": version: 0.2.1 resolution: "node-api-version@npm:0.2.1" @@ -16714,13 +16723,14 @@ __metadata: languageName: node linkType: hard -"selection-hook@npm:^0.9.23": - version: 0.9.23 - resolution: "selection-hook@npm:0.9.23" +"selection-hook@npm:^1.0.3": + version: 1.0.3 + resolution: "selection-hook@npm:1.0.3" dependencies: + node-addon-api: "npm:^8.4.0" node-gyp: "npm:latest" node-gyp-build: "npm:^4.8.4" - checksum: 10c0/3b91193814c063e14dd788cff3b27020821bbeae24eab106d2ce5bf600c034c1b3db96ce573c456b74d0553346dfcf4c7cc8d49386a22797b42667f7ed3eee01 + checksum: 10c0/812df47050d470d398974ca9833caba3bc55fcacf76aec5207cffcef4b81cf22d5a0992263e2074afd05c21781f903ffac25177cd9553417525648136405b474 languageName: node linkType: hard