feat(SelectionAssistant): add macOS support and process trust handling
- Updated the selection assistant to support macOS, including new IPC channels for process trust verification. - Enhanced the SelectionService to check for accessibility permissions on macOS before starting the service. - Added user interface elements to guide macOS users in granting necessary permissions. - Updated localization files to reflect macOS support and provide relevant user instructions. - Refactored selection-related configurations to accommodate both Windows and macOS environments.
This commit is contained in:
+1
-1
@@ -65,7 +65,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.1",
|
||||
"turndown": "7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -31,6 +31,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',
|
||||
|
||||
@@ -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: []
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -8,7 +8,7 @@ import { handleZoomFactor } from '@main/utils/zoom'
|
||||
import { FeedUrl } from '@shared/config/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, dialog, ipcMain, session, shell } from 'electron'
|
||||
import { BrowserWindow, dialog, ipcMain, session, shell, systemPreferences } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { Notification } from 'src/renderer/src/types/notification'
|
||||
|
||||
@@ -145,6 +145,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
appUpdater.setFeedUrl(feedUrl)
|
||||
})
|
||||
|
||||
//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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
@@ -374,12 +405,16 @@ export class SelectionService {
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
movable: true,
|
||||
focusable: false,
|
||||
hasShadow: false,
|
||||
thickFrame: false,
|
||||
roundedCorners: true,
|
||||
backgroundMaterial: 'none',
|
||||
type: 'toolbar',
|
||||
|
||||
// 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 } : {}),
|
||||
|
||||
show: false,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
@@ -406,6 +441,11 @@ 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
|
||||
if (isMac) {
|
||||
this.toolbarWindow!.setVisibleOnAllWorkspaces(false)
|
||||
}
|
||||
})
|
||||
|
||||
this.toolbarWindow.on('hide', () => {
|
||||
@@ -460,11 +500,19 @@ 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
|
||||
if (isMac) {
|
||||
this.toolbarWindow!.setVisibleOnAllWorkspaces(true)
|
||||
}
|
||||
|
||||
// [macOS] must use showInactive() to prevent other windows bring to front together
|
||||
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)
|
||||
|
||||
@@ -773,8 +821,11 @@ export class SelectionService {
|
||||
}
|
||||
|
||||
if (!isLogical) {
|
||||
//mac 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 +883,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()
|
||||
|
||||
@@ -967,6 +1018,7 @@ export class SelectionService {
|
||||
transparent: true,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hidden',
|
||||
trafficLightPosition: { x: 12, y: 9 },
|
||||
hasShadow: false,
|
||||
thickFrame: false,
|
||||
show: false,
|
||||
@@ -1096,6 +1148,7 @@ export class SelectionService {
|
||||
}
|
||||
|
||||
actionWindow.show()
|
||||
// actionWindow.focus()
|
||||
this.hideToolbar()
|
||||
return
|
||||
}
|
||||
@@ -1214,7 +1267,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 +1344,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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -41,6 +41,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<boolean> => ipcRenderer.invoke(IpcChannel.App_MacIsProcessTrusted),
|
||||
requestProcessTrust: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.App_MacRequestProcessTrust)
|
||||
},
|
||||
notification: {
|
||||
send: (notification: Notification) => ipcRenderer.invoke(IpcChannel.Notification_Send, notification)
|
||||
},
|
||||
|
||||
@@ -1981,14 +1981,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 <strong>Accessibility Permission</strong> to work properly.",
|
||||
"Please click \"<strong>Go to Settings</strong>\" and click the \"<strong>Open System Settings</strong>\" button in the permission request popup that appears later. Then find \"<strong>Cherry Studio</strong>\" 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",
|
||||
@@ -2113,7 +2128,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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1981,14 +1981,29 @@
|
||||
"experimental": "実験的機能",
|
||||
"enable": {
|
||||
"title": "有効化",
|
||||
"description": "現在Windowsのみ対応"
|
||||
"description": "現在Windows & macOSのみ対応",
|
||||
"mac_process_trust_hint": {
|
||||
"title": "アクセシビリティー権限",
|
||||
"description": [
|
||||
"テキスト選択ツールは、<strong>アクセシビリティー権限</strong>が必要です。",
|
||||
"「<strong>設定に移動</strong>」をクリックし、後で表示される権限要求ポップアップで「<strong>システム設定を開く</strong>」ボタンをクリックします。その後、表示されるアプリケーションリストで「<strong>Cherry Studio</strong>」を見つけ、権限スイッチをオンにしてください。",
|
||||
"設定が完了したら、テキスト選択ツールを再起動してください。"
|
||||
],
|
||||
"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キー",
|
||||
@@ -2113,7 +2128,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, など。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1981,14 +1981,29 @@
|
||||
"experimental": "Экспериментальные функции",
|
||||
"enable": {
|
||||
"title": "Включить",
|
||||
"description": "Поддерживается только в Windows"
|
||||
"description": "Поддерживается только в Windows & macOS",
|
||||
"mac_process_trust_hint": {
|
||||
"title": "Права доступа",
|
||||
"description": [
|
||||
"Помощник выбора требует <strong>Права доступа</strong> для правильной работы.",
|
||||
"Пожалуйста, перейдите в \"<strong>Настройки</strong>\" и нажмите \"<strong>Открыть системные настройки</strong>\" в запросе разрешения, который появится позже. Затем найдите \"<strong>Cherry Studio</strong>\" в списке приложений, который появится позже, и включите переключатель разрешения.",
|
||||
"После завершения настроек, пожалуйста, перезапустите помощник выбора."
|
||||
],
|
||||
"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",
|
||||
@@ -2113,7 +2128,10 @@
|
||||
},
|
||||
"filter_modal": {
|
||||
"title": "Список фильтрации",
|
||||
"user_tips": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *"
|
||||
"user_tips": {
|
||||
"windows": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *",
|
||||
"mac": "Введите Bundle ID приложения, один на строку, не учитывая регистр, можно использовать подстановку *"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1981,14 +1981,29 @@
|
||||
"experimental": "实验性功能",
|
||||
"enable": {
|
||||
"title": "启用",
|
||||
"description": "当前仅支持 Windows 系统"
|
||||
"description": "当前仅支持 Windows & macOS",
|
||||
"mac_process_trust_hint": {
|
||||
"title": "辅助功能权限",
|
||||
"description": [
|
||||
"划词助手需「<strong>辅助功能权限</strong>」才能正常工作。",
|
||||
"请点击「<strong>去设置</strong>」,并在稍后弹出的权限请求弹窗中点击 「<strong>打开系统设置</strong>」 按钮,然后在之后的应用列表中找到 「<strong>Cherry Studio</strong>」,并打开权限开关。",
|
||||
"完成设置后,请再次开启划词助手。"
|
||||
],
|
||||
"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 键",
|
||||
@@ -2113,7 +2128,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等"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1981,14 +1981,29 @@
|
||||
"experimental": "實驗性功能",
|
||||
"enable": {
|
||||
"title": "啟用",
|
||||
"description": "目前僅支援 Windows 系統"
|
||||
"description": "目前僅支援 Windows & macOS",
|
||||
"mac_process_trust_hint": {
|
||||
"title": "輔助使用權限",
|
||||
"description": [
|
||||
"劃詞助手需「<strong>輔助使用權限</strong>」才能正常工作。",
|
||||
"請點擊「<strong>去設定</strong>」,並在稍後彈出的權限請求彈窗中點擊 「<strong>打開系統設定</strong>」 按鈕,然後在之後的應用程式列表中找到 「<strong>Cherry Studio</strong>」,並開啟權限開關。",
|
||||
"完成設定後,請再次開啟劃詞助手。"
|
||||
],
|
||||
"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 鍵",
|
||||
@@ -2113,7 +2128,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等"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-15
@@ -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 (
|
||||
<SettingContainer theme={theme}>
|
||||
@@ -71,18 +100,18 @@ const SelectionAssistantSettings: FC = () => {
|
||||
style={{ fontSize: 12 }}>
|
||||
{'FAQ & ' + t('settings.about.feedback.button')}
|
||||
</Button>
|
||||
<ExperimentalText>{t('selection.settings.experimental')}</ExperimentalText>
|
||||
{isMac && <ExperimentalText>{t('selection.settings.experimental')}</ExperimentalText>}
|
||||
</Row>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingLabel>
|
||||
<SettingRowTitle>{t('selection.settings.enable.title')}</SettingRowTitle>
|
||||
{!isWin && <SettingDescription>{t('selection.settings.enable.description')}</SettingDescription>}
|
||||
{!isSupportedOS && <SettingDescription>{t('selection.settings.enable.description')}</SettingDescription>}
|
||||
</SettingLabel>
|
||||
<Switch
|
||||
checked={isWin && selectionEnabled}
|
||||
onChange={(checked) => setSelectionEnabled(checked)}
|
||||
disabled={!isWin}
|
||||
checked={isSupportedOS && selectionEnabled}
|
||||
onChange={(checked) => handleEnableCheckboxChange(checked)}
|
||||
disabled={!isSupportedOS}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
@@ -103,7 +132,10 @@ const SelectionAssistantSettings: FC = () => {
|
||||
<SettingLabel>
|
||||
<SettingRowTitle>
|
||||
<div style={{ marginRight: '4px' }}>{t('selection.settings.toolbar.trigger_mode.title')}</div>
|
||||
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.description_note')} arrow>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={t(`selection.settings.toolbar.trigger_mode.description_note.${isWin ? 'windows' : 'mac'}`)}
|
||||
arrow>
|
||||
<QuestionIcon size={14} />
|
||||
</Tooltip>
|
||||
</SettingRowTitle>
|
||||
@@ -116,9 +148,11 @@ const SelectionAssistantSettings: FC = () => {
|
||||
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.selected_note')} arrow>
|
||||
<Radio.Button value="selected">{t('selection.settings.toolbar.trigger_mode.selected')}</Radio.Button>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.ctrlkey_note')} arrow>
|
||||
<Radio.Button value="ctrlkey">{t('selection.settings.toolbar.trigger_mode.ctrlkey')}</Radio.Button>
|
||||
</Tooltip>
|
||||
{isWin && (
|
||||
<Tooltip placement="top" title={t('selection.settings.toolbar.trigger_mode.ctrlkey_note')} arrow>
|
||||
<Radio.Button value="ctrlkey">{t('selection.settings.toolbar.trigger_mode.ctrlkey')}</Radio.Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
placement="topRight"
|
||||
title={
|
||||
@@ -256,6 +290,8 @@ const SelectionAssistantSettings: FC = () => {
|
||||
</SettingGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isMac && <MacProcessTrustHintModal open={isMacTrustModalOpen} onClose={() => setIsMacTrustModalOpen(false)} />}
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { Button, Modal, Space, 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<MacProcessTrustHintModalProps> = ({ 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 (
|
||||
<Modal
|
||||
title={t('selection.settings.enable.mac_process_trust_hint.title')}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={handleOpenAccessibility}>
|
||||
{t('selection.settings.enable.mac_process_trust_hint.button.open_accessibility_settings')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleConfirm}>
|
||||
{t('selection.settings.enable.mac_process_trust_hint.button.go_to_settings')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
centered
|
||||
destroyOnClose>
|
||||
<ContentContainer>
|
||||
<Paragraph>
|
||||
<Text>
|
||||
<Trans i18nKey="selection.settings.enable.mac_process_trust_hint.description.0" />
|
||||
</Text>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Text>
|
||||
<Trans i18nKey="selection.settings.enable.mac_process_trust_hint.description.1" />
|
||||
</Text>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Text>
|
||||
<Trans i18nKey="selection.settings.enable.mac_process_trust_hint.description.2" />
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</ContentContainer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
padding: 16px 0;
|
||||
`
|
||||
|
||||
export default MacProcessTrustHintModal
|
||||
+5
-5
@@ -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
|
||||
+6
-1
@@ -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<SelectionFilterListModalProps> = ({ open, onC
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
]}>
|
||||
<UserTip>{t('selection.settings.filter_modal.user_tips')}</UserTip>
|
||||
<UserTip>
|
||||
{isWin
|
||||
? t('selection.settings.filter_modal.user_tips.windows')
|
||||
: t('selection.settings.filter_modal.user_tips.mac')}
|
||||
</UserTip>
|
||||
<Form form={form} layout="vertical" initialValues={{ filterList: '' }}>
|
||||
<Form.Item name="filterList" noStyle>
|
||||
<StyledTextArea autoSize={{ minRows: 6, maxRows: 16 }} spellCheck={false} autoFocus />
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 (
|
||||
<WindowFrame $opacity={opacity / 100}>
|
||||
<TitleBar $isWindowFocus={isWindowFocus}>
|
||||
<TitleBar $isWindowFocus={isWindowFocus} style={isMac ? { paddingLeft: '70px' } : {}}>
|
||||
{action.icon && (
|
||||
<TitleBarIcon>
|
||||
<DynamicIcon
|
||||
@@ -230,9 +231,12 @@ const SelectionActionApp: FC = () => {
|
||||
/>
|
||||
</OpacitySlider>
|
||||
)}
|
||||
|
||||
<WinButton type="text" icon={<Minus size={16} />} onClick={handleMinimize} />
|
||||
<WinButton type="text" icon={<X size={16} />} onClick={handleClose} className="close" />
|
||||
{!isMac && (
|
||||
<>
|
||||
<WinButton type="text" icon={<Minus size={16} />} onClick={handleMinimize} />
|
||||
<WinButton type="text" icon={<X size={16} />} onClick={handleClose} className="close" />
|
||||
</>
|
||||
)}
|
||||
</TitleBarButtons>
|
||||
</TitleBar>
|
||||
<MainContainer>
|
||||
|
||||
@@ -277,7 +277,7 @@ const LogoWrapper = styled.div<{ $draggable: boolean }>`
|
||||
justify-content: center;
|
||||
margin: var(--selection-toolbar-logo-margin);
|
||||
background-color: transparent;
|
||||
${({ $draggable }) => $draggable && ' -webkit-app-region: drag;'}
|
||||
${({ $draggable }) => $draggable && ' -webkit-app-region: drag;'};
|
||||
`
|
||||
|
||||
const Logo = styled(Avatar)`
|
||||
|
||||
@@ -5748,7 +5748,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.1"
|
||||
shiki: "npm:^3.4.2"
|
||||
string-width: "npm:^7.2.0"
|
||||
styled-components: "npm:^6.1.11"
|
||||
@@ -16510,13 +16510,13 @@ __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.1":
|
||||
version: 1.0.1
|
||||
resolution: "selection-hook@npm:1.0.1"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
node-gyp-build: "npm:^4.8.4"
|
||||
checksum: 10c0/3b91193814c063e14dd788cff3b27020821bbeae24eab106d2ce5bf600c034c1b3db96ce573c456b74d0553346dfcf4c7cc8d49386a22797b42667f7ed3eee01
|
||||
checksum: 10c0/2253b5073c592f3f6137d720e2d1034b0e6abcfd50f383c9a7da4c58d26bc9c89a8f4bbd0df3cb693798aec4711fc68d3c699360a3eee54ec6c89f79398775f4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user