diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index c09f988fde..32903df6e5 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -186,6 +186,8 @@ export enum IpcChannel { Selection_WriteToClipboard = 'selection:write-to-clipboard', Selection_SetEnabled = 'selection:set-enabled', Selection_SetTriggerMode = 'selection:set-trigger-mode', + Selection_SetFilterMode = 'selection:set-filter-mode', + Selection_SetFilterList = 'selection:set-filter-list', Selection_SetFollowToolbar = 'selection:set-follow-toolbar', Selection_ActionWindowClose = 'selection:action-window-close', Selection_ActionWindowMinimize = 'selection:action-window-minimize', diff --git a/src/main/services/ConfigManager.ts b/src/main/services/ConfigManager.ts index 6249322019..a51f1a076b 100644 --- a/src/main/services/ConfigManager.ts +++ b/src/main/services/ConfigManager.ts @@ -19,7 +19,9 @@ export enum ConfigKeys { EnableDataCollection = 'enableDataCollection', SelectionAssistantEnabled = 'selectionAssistantEnabled', SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode', - SelectionAssistantFollowToolbar = 'selectionAssistantFollowToolbar' + SelectionAssistantFollowToolbar = 'selectionAssistantFollowToolbar', + SelectionAssistantFilterMode = 'selectionAssistantFilterMode', + SelectionAssistantFilterList = 'selectionAssistantFilterList' } export class ConfigManager { @@ -173,6 +175,22 @@ export class ConfigManager { this.setAndNotify(ConfigKeys.SelectionAssistantFollowToolbar, value) } + getSelectionAssistantFilterMode(): string { + return this.get(ConfigKeys.SelectionAssistantFilterMode, 'default') + } + + setSelectionAssistantFilterMode(value: string) { + this.setAndNotify(ConfigKeys.SelectionAssistantFilterMode, value) + } + + getSelectionAssistantFilterList(): string[] { + return this.get(ConfigKeys.SelectionAssistantFilterList, []) + } + + setSelectionAssistantFilterList(value: string[]) { + this.setAndNotify(ConfigKeys.SelectionAssistantFilterList, value) + } + setAndNotify(key: string, value: unknown) { this.set(key, value, true) } diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index 512240e564..2a33a05376 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -60,6 +60,8 @@ export class SelectionService { private triggerMode = 'selected' private isFollowToolbar = true + private filterMode = 'default' + private filterList: string[] = [] private toolbarWindow: BrowserWindow | null = null private actionWindows = new Set() @@ -138,6 +140,10 @@ export class SelectionService { private initConfig() { this.triggerMode = configManager.getSelectionAssistantTriggerMode() this.isFollowToolbar = configManager.getSelectionAssistantFollowToolbar() + this.filterMode = configManager.getSelectionAssistantFilterMode() + this.filterList = configManager.getSelectionAssistantFilterList() + + this.setHookClipboardMode(this.filterMode, this.filterList) configManager.subscribe(ConfigKeys.SelectionAssistantTriggerMode, (triggerMode: string) => { this.triggerMode = triggerMode @@ -147,6 +153,34 @@ export class SelectionService { configManager.subscribe(ConfigKeys.SelectionAssistantFollowToolbar, (isFollowToolbar: boolean) => { this.isFollowToolbar = isFollowToolbar }) + + configManager.subscribe(ConfigKeys.SelectionAssistantFilterMode, (filterMode: string) => { + this.filterMode = filterMode + this.setHookClipboardMode(this.filterMode, this.filterList) + }) + + configManager.subscribe(ConfigKeys.SelectionAssistantFilterList, (filterList: string[]) => { + this.filterList = filterList + this.setHookClipboardMode(this.filterMode, this.filterList) + }) + } + + /** + * Set the clipboard mode for the selection-hook + * @param mode - The mode to set, either 'default', 'whitelist', or 'blacklist' + * @param list - An array of strings representing the list of items to include or exclude + */ + private setHookClipboardMode(mode: string, list: string[]) { + if (!this.selectionHook) return + + const modeMap = { + default: 0, + whitelist: 1, + blacklist: 2 + } + if (!this.selectionHook.setClipboardMode(modeMap[mode], list)) { + this.logError(new Error('Failed to set selection-hook clipboard mode')) + } } /** @@ -454,6 +488,30 @@ export class SelectionService { return startTop.y === endTop.y && startBottom.y === endBottom.y } + /** + * Determine if the text selection should be processed by filter mode&list + * @param selectionData Text selection information and coordinates + * @returns {boolean} True if the selection should be processed, false otherwise + */ + private shouldProcessTextSelection(selectionData: TextSelectionData): boolean { + if (selectionData.programName === '' || this.filterMode === 'default') { + return true + } + + const programName = selectionData.programName.toLowerCase() + //items in filterList are already in lower case + const isFound = this.filterList.some((item) => programName.includes(item)) + + switch (this.filterMode) { + case 'whitelist': + return isFound + case 'blacklist': + return !isFound + } + + return false + } + /** * Process text selection data and show toolbar * Handles different selection scenarios: @@ -468,6 +526,10 @@ export class SelectionService { return } + if (!this.shouldProcessTextSelection(selectionData)) { + return + } + // Determine reference point and position for toolbar let refPoint: { x: number; y: number } = { x: 0, y: 0 } let isLogical = false @@ -946,6 +1008,14 @@ export class SelectionService { configManager.setSelectionAssistantFollowToolbar(isFollowToolbar) }) + ipcMain.handle(IpcChannel.Selection_SetFilterMode, (_, filterMode: string) => { + configManager.setSelectionAssistantFilterMode(filterMode) + }) + + ipcMain.handle(IpcChannel.Selection_SetFilterList, (_, filterList: string[]) => { + configManager.setSelectionAssistantFilterList(filterList) + }) + ipcMain.handle(IpcChannel.Selection_ProcessAction, (_, actionItem: ActionItem) => { selectionService?.processAction(actionItem) }) diff --git a/src/preload/index.ts b/src/preload/index.ts index f209191bbe..625fbfbfde 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -218,6 +218,8 @@ const api = { setTriggerMode: (triggerMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetTriggerMode, triggerMode), setFollowToolbar: (isFollowToolbar: boolean) => ipcRenderer.invoke(IpcChannel.Selection_SetFollowToolbar, isFollowToolbar), + setFilterMode: (filterMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterMode, filterMode), + setFilterList: (filterList: string[]) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterList, filterList), processAction: (actionItem: ActionItem) => ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem), closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose), minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize), diff --git a/src/renderer/src/hooks/useSelectionAssistant.ts b/src/renderer/src/hooks/useSelectionAssistant.ts index ef168a799c..17bd14e977 100644 --- a/src/renderer/src/hooks/useSelectionAssistant.ts +++ b/src/renderer/src/hooks/useSelectionAssistant.ts @@ -2,6 +2,8 @@ import { useAppDispatch, useAppSelector } from '@renderer/store' import { setActionItems, setActionWindowOpacity, + setFilterList, + setFilterMode, setIsAutoClose, setIsAutoPin, setIsCompact, @@ -9,7 +11,7 @@ import { setSelectionEnabled, setTriggerMode } from '@renderer/store/selectionStore' -import { ActionItem, TriggerMode } from '@renderer/types/selectionTypes' +import { ActionItem, FilterMode, TriggerMode } from '@renderer/types/selectionTypes' export function useSelectionAssistant() { const dispatch = useAppDispatch() @@ -38,6 +40,14 @@ export function useSelectionAssistant() { dispatch(setIsFollowToolbar(isFollowToolbar)) window.api.selection.setFollowToolbar(isFollowToolbar) }, + setFilterMode: (mode: FilterMode) => { + dispatch(setFilterMode(mode)) + window.api.selection.setFilterMode(mode) + }, + setFilterList: (list: string[]) => { + dispatch(setFilterList(list)) + window.api.selection.setFilterList(list) + }, setActionWindowOpacity: (opacity: number) => { dispatch(setActionWindowOpacity(opacity)) }, diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index d1e774cfe7..90f8f1bc09 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1908,6 +1908,20 @@ "delete_confirm": "Are you sure you want to delete this custom action?", "drag_hint": "Drag to reorder. Move above to enable action ({{enabled}}/{{max}})" }, + "advanced": { + "title": "Advanced", + "filter_mode": { + "title": "Application Filter", + "description": "Can limit the selection assistant to only work in specific applications (whitelist) or not work (blacklist)", + "default": "Off", + "whitelist": "Whitelist", + "blacklist": "Blacklist" + }, + "filter_list": { + "title": "Filter List", + "description": "Advanced feature, recommended for users with experience" + } + }, "user_modal": { "title": { "add": "Add Custom Action", @@ -1964,6 +1978,10 @@ }, "test": "Test" } + }, + "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." } } } diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 81b16577d6..874e1efafb 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -1908,6 +1908,20 @@ "delete_confirm": "このカスタム機能を削除しますか?", "drag_hint": "ドラッグで並べ替え (有効{{enabled}}/最大{{max}})" }, + "advanced": { + "title": "進階", + "filter_mode": { + "title": "アプリケーションフィルター", + "description": "特定のアプリケーションでのみ選択ツールを有効にするか、無効にするかを選択できます。", + "default": "オフ", + "whitelist": "ホワイトリスト", + "blacklist": "ブラックリスト" + }, + "filter_list": { + "title": "フィルターリスト", + "description": "進階機能です。経験豊富なユーザー向けです。" + } + }, "user_modal": { "title": { "add": "カスタム機能追加", @@ -1964,6 +1978,10 @@ }, "test": "テスト" } + }, + "filter_modal": { + "title": "アプリケーションフィルターリスト", + "user_tips": "アプリケーションの実行ファイル名を1行ずつ入力してください。大文字小文字は区別しません。例: chrome.exe, weixin.exe, Cherry Studio.exe, など。" } } } diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 54194628b3..6e05dae645 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -1909,6 +1909,20 @@ "delete_confirm": "Удалить это действие?", "drag_hint": "Перетащите для сортировки. Включено: {{enabled}}/{{max}}" }, + "advanced": { + "title": "Расширенные", + "filter_mode": { + "title": "Режим фильтрации", + "description": "Можно ограничить выборку по определенным приложениям (белый список) или исключить их (черный список)", + "default": "Выключено", + "whitelist": "Белый список", + "blacklist": "Черный список" + }, + "filter_list": { + "title": "Список фильтрации", + "description": "Расширенная функция, рекомендуется для пользователей с опытом" + } + }, "user_modal": { "title": { "add": "Добавить действие", @@ -1965,6 +1979,10 @@ }, "test": "Тест" } + }, + "filter_modal": { + "title": "Список фильтрации", + "user_tips": "Введите имя исполняемого файла приложения, один на строку, не учитывая регистр, можно использовать подстановку *" } } } diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 6c22d830e6..5f9027a5e5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1908,6 +1908,20 @@ "delete_confirm": "确定要删除这个自定义功能吗?", "drag_hint": "拖拽排序,移动到上方以启用功能 ({{enabled}}/{{max}})" }, + "advanced": { + "title": "高级", + "filter_mode": { + "title": "应用筛选", + "description": "可以限制划词助手只在特定应用中生效(白名单)或不生效(黑名单)", + "default": "关闭", + "whitelist": "白名单", + "blacklist": "黑名单" + }, + "filter_list": { + "title": "筛选名单", + "description": "高级功能,建议有经验的用户在了解的情况下再进行设置" + } + }, "user_modal": { "title": { "add": "添加自定义功能", @@ -1964,6 +1978,10 @@ }, "test": "测试" } + }, + "filter_modal": { + "title": "应用筛选名单", + "user_tips": "请输入应用的执行文件名,每行一个,不区分大小写,可以模糊匹配。例如:chrome.exe、weixin.exe、Cherry Studio.exe等" } } } diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index cd234a2934..3a01de8625 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1909,6 +1909,20 @@ "delete_confirm": "確定要刪除這個自訂功能嗎?", "drag_hint": "拖曳排序,移動到上方以啟用功能 ({{enabled}}/{{max}})" }, + "advanced": { + "title": "進階", + "filter_mode": { + "title": "應用篩選", + "description": "可以限制劃詞助手只在特定應用中生效(白名單)或不生效(黑名單)", + "default": "關閉", + "whitelist": "白名單", + "blacklist": "黑名單" + }, + "filter_list": { + "title": "篩選名單", + "description": "進階功能,建議有經驗的用戶在了解情況下再進行設置" + } + }, "user_modal": { "title": { "add": "新增自訂功能", @@ -1965,6 +1979,10 @@ }, "test": "測試" } + }, + "filter_modal": { + "title": "應用篩選名單", + "user_tips": "請輸入應用的執行檔名稱,每行一個,不區分大小寫,可以模糊匹配。例如:chrome.exe、weixin.exe、Cherry Studio.exe等" } } } diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx index 7429080d07..47bd71b8ea 100644 --- a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionAssistantSettings.tsx @@ -1,11 +1,11 @@ import { isWindows } from '@renderer/config/constant' import { useTheme } from '@renderer/context/ThemeProvider' import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant' -import { TriggerMode } from '@renderer/types/selectionTypes' +import { FilterMode, TriggerMode } from '@renderer/types/selectionTypes' import SelectionToolbar from '@renderer/windows/selection/toolbar/SelectionToolbar' import { Button, Radio, Row, Slider, Switch, Tooltip } from 'antd' -import { CircleHelp } from 'lucide-react' -import { FC, useEffect } from 'react' +import { CircleHelp, Edit2 } from 'lucide-react' +import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -19,6 +19,7 @@ import { SettingTitle } from '..' import SelectionActionsList from './SelectionActionsList' +import SelectionFilterListModal from './SelectionFilterListModal' const SelectionAssistantSettings: FC = () => { const { theme } = useTheme() @@ -32,6 +33,8 @@ const SelectionAssistantSettings: FC = () => { isFollowToolbar, actionItems, actionWindowOpacity, + filterMode, + filterList, setSelectionEnabled, setTriggerMode, setIsCompact, @@ -39,8 +42,11 @@ const SelectionAssistantSettings: FC = () => { setIsAutoPin, setIsFollowToolbar, setActionWindowOpacity, - setActionItems + setActionItems, + setFilterMode, + setFilterList } = useSelectionAssistant() + const [isFilterListModalOpen, setIsFilterListModalOpen] = useState(false) // force disable selection assistant on non-windows systems useEffect(() => { @@ -86,6 +92,7 @@ const SelectionAssistantSettings: FC = () => { <> {t('selection.settings.toolbar.title')} + @@ -106,7 +113,9 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.toolbar.trigger_mode.ctrlkey')} + + {t('selection.settings.toolbar.compact_mode.title')} @@ -118,6 +127,7 @@ const SelectionAssistantSettings: FC = () => { {t('selection.settings.window.title')} + @@ -127,7 +137,9 @@ const SelectionAssistantSettings: FC = () => { setIsFollowToolbar(checked)} /> + + {t('selection.settings.window.auto_close.title')} @@ -135,7 +147,9 @@ const SelectionAssistantSettings: FC = () => { setIsAutoClose(checked)} /> + + {t('selection.settings.window.auto_pin.title')} @@ -143,7 +157,9 @@ const SelectionAssistantSettings: FC = () => { setIsAutoPin(checked)} /> + + {t('selection.settings.window.opacity.title')} @@ -163,6 +179,49 @@ const SelectionAssistantSettings: FC = () => { + + + 高级 + + + + + + {t('selection.settings.advanced.filter_mode.title')} + {t('selection.settings.advanced.filter_mode.description')} + + setFilterMode(e.target.value as FilterMode)} + buttonStyle="solid"> + {t('selection.settings.advanced.filter_mode.default')} + {t('selection.settings.advanced.filter_mode.whitelist')} + {t('selection.settings.advanced.filter_mode.blacklist')} + + + + {filterMode !== 'default' && ( + <> + + + + {t('selection.settings.advanced.filter_list.title')} + {t('selection.settings.advanced.filter_list.description')} + + + + + setIsFilterListModalOpen(false)} + filterList={filterList} + onSave={setFilterList} + /> + + )} + )} diff --git a/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx new file mode 100644 index 0000000000..8b9ca31fc2 --- /dev/null +++ b/src/renderer/src/pages/settings/SelectionAssistantSettings/SelectionFilterListModal.tsx @@ -0,0 +1,76 @@ +import { Button, Form, Input, Modal } from 'antd' +import { FC, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +interface SelectionFilterListModalProps { + open: boolean + onClose: () => void + filterList?: string[] + onSave: (list: string[]) => void +} + +const SelectionFilterListModal: FC = ({ open, onClose, filterList = [], onSave }) => { + const { t } = useTranslation() + const [form] = Form.useForm() + + useEffect(() => { + if (open) { + form.setFieldsValue({ + filterList: (filterList || []).join('\n') + }) + } + }, [open, filterList, form]) + + const handleSave = async () => { + try { + const values = await form.validateFields() + const newList = values.filterList + .trim() + .toLowerCase() + .split('\n') + .map((line: string) => line.trim()) + .filter((line: string) => line.length > 0) + onSave(newList) + onClose() + } catch (error) { + // validation failed + } + } + + return ( + + {t('common.cancel')} + , + + ]}> + {t('selection.settings.filter_modal.user_tips')} +
+ + + +
+
+ ) +} + +const StyledTextArea = styled(Input.TextArea)` + margin-top: 16px; + width: 100%; +` + +const UserTip = styled.div` + font-size: 14px; +` + +export default SelectionFilterListModal diff --git a/src/renderer/src/store/selectionStore.ts b/src/renderer/src/store/selectionStore.ts index 78c80b498d..a4269db2bf 100644 --- a/src/renderer/src/store/selectionStore.ts +++ b/src/renderer/src/store/selectionStore.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { ActionItem, SelectionState, TriggerMode } from '@renderer/types/selectionTypes' +import { ActionItem, FilterMode, SelectionState, TriggerMode } from '@renderer/types/selectionTypes' export const defaultActionItems: ActionItem[] = [ { id: 'translate', name: 'selection.action.builtin.translate', enabled: true, isBuiltIn: true, icon: 'languages' }, @@ -24,6 +24,8 @@ export const initialState: SelectionState = { isAutoClose: false, isAutoPin: false, isFollowToolbar: true, + filterMode: 'default', + filterList: [], actionWindowOpacity: 100, actionItems: defaultActionItems } @@ -50,6 +52,12 @@ const selectionSlice = createSlice({ setIsFollowToolbar: (state, action: PayloadAction) => { state.isFollowToolbar = action.payload }, + setFilterMode: (state, action: PayloadAction) => { + state.filterMode = action.payload + }, + setFilterList: (state, action: PayloadAction) => { + state.filterList = action.payload + }, setActionWindowOpacity: (state, action: PayloadAction) => { state.actionWindowOpacity = action.payload }, @@ -66,6 +74,8 @@ export const { setIsAutoClose, setIsAutoPin, setIsFollowToolbar, + setFilterMode, + setFilterList, setActionWindowOpacity, setActionItems } = selectionSlice.actions diff --git a/src/renderer/src/types/selectionTypes.d.ts b/src/renderer/src/types/selectionTypes.d.ts index e5ae9490d8..3efe539b64 100644 --- a/src/renderer/src/types/selectionTypes.d.ts +++ b/src/renderer/src/types/selectionTypes.d.ts @@ -1,5 +1,5 @@ export type TriggerMode = 'selected' | 'ctrlkey' - +export type FilterMode = 'default' | 'whitelist' | 'blacklist' export interface ActionItem { id: string name: string @@ -19,6 +19,8 @@ export interface SelectionState { isAutoClose: boolean isAutoPin: boolean isFollowToolbar: boolean + filterMode: FilterMode + filterList: string[] actionWindowOpacity: number actionItems: ActionItem[] }