From 725bf8fe714a8eadb669d9a9de67728f88e47e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=B2=A9=E5=B2=A9?= Date: Wed, 11 Feb 2026 14:07:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(M2.9):=20=E5=9B=BE=E6=A0=87=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=BC=80=E5=85=B3=20(v0.1.9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Popup 中添加'启用划词' Toggle 开关 - 开关状态保存到 chrome.storage.local - 关闭开关后网页划词不再显示图标 - 配置管理统一封装到 src/shared/config.js - ConfigManager 提供 get/set/onChange API --- docs/QUICK_REF.md | 2 +- docs/README.md | 6 +- docs/VERSION.md | 2 +- manifest.json | 2 +- package.json | 2 +- src/content/selection.js | 44 +++++++++++- src/popup/index.html | 96 ++++++++++++++++++++++++-- src/popup/index.js | 39 +++++++++-- src/shared/config.js | 143 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 src/shared/config.js diff --git a/docs/QUICK_REF.md b/docs/QUICK_REF.md index c6c613e..157295b 100644 --- a/docs/QUICK_REF.md +++ b/docs/QUICK_REF.md @@ -7,7 +7,7 @@ ## 版本速查 ### 当前版本 -`0.1.8` → 下一目标 `0.1.9` ([M2.9](./M2.md)) +`0.1.9` → 下一目标 `0.2.1` ([M3.1](./M3.md)) ### 模块版本范围 diff --git a/docs/README.md b/docs/README.md index 7e77102..a1aa6fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,9 +65,9 @@ M11.10完成 → 1.0.0 (正式发布) ## 当前状态 -**当前版本**: `0.1.8` -**当前进度**: 13/97 (13%) -**下一任务**: [M2.9 图标显示开关](./M2.md#m29-图标显示开关--目标版本-0119) +**当前版本**: `0.1.9` +**当前进度**: 14/97 (14%) +**下一任务**: [M3.1 词典接口基类设计](./M3.md#m31-词典接口基类设计--目标版本-021) --- diff --git a/docs/VERSION.md b/docs/VERSION.md index 0e0e78e..44704d5 100644 --- a/docs/VERSION.md +++ b/docs/VERSION.md @@ -28,7 +28,7 @@ | M2.6 | 0.1.6 | 基础面板组件 | ✅ | 2026-02-11 | | M2.7 | 0.1.7 | 面板位置计算 | ✅ | 2026-02-09 | | M2.8 | 0.1.8 | 图标-面板联动 | ✅ | 2026-02-09 | -| M2.9 | 0.1.9 | 图标显示开关 | ⬜ | - | +| M2.9 | 0.1.9 | 图标显示开关 | ✅ | 2026-02-09 | --- diff --git a/manifest.json b/manifest.json index 46c29e4..d37bc7a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "沙拉查词", - "version": "0.1.8", + "version": "0.1.9", "description": "聚合词典划词翻译", "permissions": [ "storage", diff --git a/package.json b/package.json index 3eb687d..1ab9f7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salad-dict", - "version": "0.1.8", + "version": "0.1.9", "description": "聚合词典划词翻译", "private": true, "type": "module", diff --git a/src/content/selection.js b/src/content/selection.js index 50d14a1..a4012c6 100644 --- a/src/content/selection.js +++ b/src/content/selection.js @@ -6,6 +6,7 @@ import { logger } from './logger.js'; import { createSaladIcon } from './components/SaladIcon.js'; import { DictPanel } from './components/DictPanel.js'; +import { ConfigManager, isSelectionEnabled } from '../shared/config.js'; let currentIcon = null; let currentPanel = null; @@ -96,7 +97,14 @@ function handleMouseUp(event) { } // 延迟执行,等待选区完成 - setTimeout(() => { + setTimeout(async () => { + // 检查划词功能是否启用 + const enabled = await isSelectionEnabled(); + if (!enabled) { + logger.info('Selection is disabled, skipping icon display'); + return; + } + const selectedText = getSelectedText(); if (selectedText.length > 0) { @@ -192,6 +200,30 @@ function handleKeyDown(event) { } } +let unbindConfigListener = null; + +/** + * 处理配置变更 + * @param {Object} newConfig + */ +function handleConfigChange(newConfig) { + const enabled = newConfig?.general?.enableSelection ?? true; + + logger.info('Config changed, selection enabled:', enabled); + + // 如果禁用划词,隐藏当前图标和面板 + if (!enabled) { + if (currentIcon) { + currentIcon.destroy(); + currentIcon = null; + } + if (currentPanel) { + currentPanel.destroy(); + currentPanel = null; + } + } +} + /** * 初始化文本选择监听 */ @@ -206,6 +238,9 @@ export function initSelectionListener() { document.addEventListener('click', handleDocumentClick); }, 100); + // 监听配置变更 + unbindConfigListener = ConfigManager.onChange(handleConfigChange); + logger.info('Selection listener initialized'); } @@ -216,5 +251,12 @@ export function destroySelectionListener() { document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('click', handleDocumentClick); document.removeEventListener('keydown', handleKeyDown); + + // 取消配置监听 + if (unbindConfigListener) { + unbindConfigListener(); + unbindConfigListener = null; + } + logger.info('Selection listener destroyed'); } diff --git a/src/popup/index.html b/src/popup/index.html index 05365ad..48c7b3c 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -5,23 +5,107 @@ 沙拉查词 -

沙拉查词

-

Popup 页面占位

+
+

沙拉查词

+
+ +
+
+ 启用划词 +
+
+ +
+

在网页中划选文本即可查词

+
+
+ diff --git a/src/popup/index.js b/src/popup/index.js index f73de75..e95e950 100644 --- a/src/popup/index.js +++ b/src/popup/index.js @@ -1,6 +1,37 @@ // Popup entry -console.log('[SaladDict] Popup opened') +import { ConfigManager } from '../shared/config.js'; -document.addEventListener('DOMContentLoaded', () => { - console.log('[SaladDict] Popup DOM ready') -}) +console.log('[SaladDict] Popup opened'); + +/** + * 初始化 Popup + */ +async function initPopup() { + const toggle = document.getElementById('enableSelectionToggle'); + + // 加载当前状态 + const isEnabled = await ConfigManager.get('general.enableSelection', true); + + // 设置开关状态 + if (isEnabled) { + toggle.classList.add('active'); + } + + // 监听开关点击 + toggle.addEventListener('click', async () => { + const newState = !toggle.classList.contains('active'); + + // 更新 UI + toggle.classList.toggle('active'); + + // 保存配置 + await ConfigManager.set('general.enableSelection', newState); + + console.log('[SaladDict] Selection enabled:', newState); + }); + + console.log('[SaladDict] Popup initialized, selection enabled:', isEnabled); +} + +// DOM 就绪后初始化 +document.addEventListener('DOMContentLoaded', initPopup); diff --git a/src/shared/config.js b/src/shared/config.js new file mode 100644 index 0000000..9962ff9 --- /dev/null +++ b/src/shared/config.js @@ -0,0 +1,143 @@ +/** + * @file 配置管理模块 + * @description 统一的配置读写和监听接口 + */ + +const STORAGE_KEY = 'salad_config'; + +const DEFAULT_CONFIG = { + general: { + enableSelection: true + } +}; + +/** + * 深度合并对象 + */ +function deepMerge(target, source) { + const result = { ...target }; + for (const key in source) { + if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { + result[key] = deepMerge(result[key] || {}, source[key]); + } else { + result[key] = source[key]; + } + } + return result; +} + +/** + * 根据路径获取对象值 + * @param {Object} obj - 对象 + * @param {string} path - 路径,如 'general.enableSelection' + * @returns {any} + */ +function getByPath(obj, path) { + const keys = path.split('.'); + let value = obj; + for (const key of keys) { + if (value === null || value === undefined) return undefined; + value = value[key]; + } + return value; +} + +/** + * 根据路径设置对象值 + * @param {Object} obj - 对象 + * @param {string} path - 路径 + * @param {any} value - 值 + */ +function setByPath(obj, path, value) { + const keys = path.split('.'); + let target = obj; + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + if (!(key in target) || typeof target[key] !== 'object') { + target[key] = {}; + } + target = target[key]; + } + target[keys[keys.length - 1]] = value; +} + +/** + * 配置管理器 + */ +export const ConfigManager = { + /** + * 获取完整配置 + * @returns {Promise} + */ + async getConfig() { + try { + const result = await chrome.storage.local.get(STORAGE_KEY); + return deepMerge(DEFAULT_CONFIG, result[STORAGE_KEY] || {}); + } catch (error) { + console.error('[ConfigManager] Failed to get config:', error); + return DEFAULT_CONFIG; + } + }, + + /** + * 保存完整配置 + * @param {Object} config + */ + async saveConfig(config) { + try { + await chrome.storage.local.set({ [STORAGE_KEY]: config }); + } catch (error) { + console.error('[ConfigManager] Failed to save config:', error); + } + }, + + /** + * 获取指定配置项 + * @param {string} path - 路径,如 'general.enableSelection' + * @param {any} defaultValue - 默认值 + * @returns {Promise} + */ + async get(path, defaultValue) { + const config = await this.getConfig(); + const value = getByPath(config, path); + return value !== undefined ? value : defaultValue; + }, + + /** + * 设置指定配置项 + * @param {string} path - 路径 + * @param {any} value - 值 + */ + async set(path, value) { + const config = await this.getConfig(); + setByPath(config, path, value); + await this.saveConfig(config); + }, + + /** + * 监听配置变更 + * @param {Function} callback - 回调函数(changes, newConfig) + * @returns {Function} 取消监听的函数 + */ + onChange(callback) { + const handler = (changes) => { + if (changes[STORAGE_KEY]) { + const newValue = changes[STORAGE_KEY].newValue; + const oldValue = changes[STORAGE_KEY].oldValue; + callback(newValue, oldValue); + } + }; + chrome.storage.onChanged.addListener(handler); + + // 返回取消监听的函数 + return () => chrome.storage.onChanged.removeListener(handler); + } +}; + +/** + * 快速检查划词功能是否启用 + * @returns {Promise} + */ +export async function isSelectionEnabled() { + return ConfigManager.get('general.enableSelection', true); +}