From 8583808dd3cb2c7e44d8ebcf3861926921e48f03 Mon Sep 17 00:00:00 2001 From: Zain Date: Tue, 30 Dec 2025 14:55:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(popup):=20=E5=B0=86=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8=E6=9B=BF=E6=8D=A2=E4=B8=BA=E6=9B=B4?= =?UTF-8?q?=E7=BE=8E=E8=A7=82=E7=9A=84=E4=B8=8B=E6=8B=89=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构语言选择器为现代风格的下拉菜单,改进用户体验 添加菜单动画效果和键盘导航支持 优化无障碍访问特性 --- src/popup/index.html | 151 +++++++++++++++++++++++++++++++++--------- src/popup/popup.ts | 132 ++++++++++++++++++++++++++++-------- src/shared/version.ts | 2 +- 3 files changed, 224 insertions(+), 61 deletions(-) diff --git a/src/popup/index.html b/src/popup/index.html index 9b96c6c..a21d04f 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -21,12 +21,29 @@ .header { margin-bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; } h1 { font-size: 18px; color: #333; - margin-bottom: 8px; + margin: 0; + flex: 1; + } + + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } .platform-status { @@ -106,43 +123,105 @@ color: #999; } - /* 语言选择器样式 */ - .language-selector { - padding: 12px; - background: white; - border-radius: 8px; - margin-bottom: 16px; + /* Header actions */ + .header-actions { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 8px; } - .language-label { - font-size: 12px; - color: #666; - margin-bottom: 6px; - display: block; + .menu-container { + position: relative; } - .language-select { - width: 100%; - padding: 8px 12px; - font-size: 14px; - border: 1px solid #ddd; + .icon-button { + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid transparent; border-radius: 6px; - background: white; - color: #333; + background: transparent; + color: #666; cursor: pointer; transition: all 0.2s; } - .language-select:hover { - border-color: #1890ff; + .icon-button:hover { + background: rgba(24, 144, 255, 0.08); + border-color: rgba(24, 144, 255, 0.35); + color: #1890ff; } - .language-select:focus { + .icon-button:focus { outline: none; border-color: #1890ff; box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); } + .icon { + width: 18px; + height: 18px; + display: block; + } + + .menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 160px; + padding: 6px; + background: white; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12); + z-index: 1000; + } + + .menu-item { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 10px; + border: none; + border-radius: 6px; + background: transparent; + color: #333; + font-size: 13px; + cursor: pointer; + transition: background 0.2s, color 0.2s; + } + + .menu-item::after { + content: ''; + width: 16px; + text-align: right; + color: #1890ff; + font-size: 12px; + line-height: 1; + } + + .menu-item:hover { + background: #f5f5f5; + } + + .menu-item:focus { + outline: none; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1); + } + + .menu-item[data-selected='true'] { + background: rgba(24, 144, 255, 0.08); + color: #1890ff; + } + + .menu-item[data-selected='true']::after { + content: '✓'; + } + /* Toast提示样式 */ .toast { position: fixed; @@ -283,16 +362,24 @@

CloudDrive Renamer

-
- - -
- - +
+ +
diff --git a/src/popup/popup.ts b/src/popup/popup.ts index dcf1f4d..ab3c9f9 100644 --- a/src/popup/popup.ts +++ b/src/popup/popup.ts @@ -4,7 +4,8 @@ import { PlatformName } from '../types/platform'; import { PlatformUsageStats, STORAGE_KEYS } from '../types/stats'; import { APP_VERSION_WITH_PREFIX } from '../shared/version'; import { I18nService } from '../utils/i18n'; -import { LanguageChangeMessage } from '../types/i18n'; +import { LANGUAGE_DISPLAY_NAMES } from '../types/i18n'; +import type { LanguageChangeMessage, SupportedLanguage } from '../types/i18n'; import { detectPlatformFromUrl } from '../utils/platform-detector'; /** @@ -46,6 +47,38 @@ function localizeHTML() { }); } +function updateLanguageMenuSelection(language: SupportedLanguage) { + const languageMenu = document.getElementById('language-menu'); + if (!languageMenu) return; + + languageMenu.querySelectorAll('[data-lang]').forEach((item) => { + const isSelected = item.dataset.lang === language; + item.setAttribute('aria-checked', isSelected ? 'true' : 'false'); + item.dataset.selected = isSelected ? 'true' : 'false'; + }); +} + +function setLanguageMenuOpen(open: boolean, options?: { focusButton?: boolean }) { + const languageButton = document.getElementById('language-button') as HTMLButtonElement | null; + const languageMenu = document.getElementById('language-menu') as HTMLElement | null; + if (!languageButton || !languageMenu) return; + + languageMenu.hidden = !open; + languageButton.setAttribute('aria-expanded', open ? 'true' : 'false'); + + if (open) { + const selectedItem = + languageMenu.querySelector('[data-selected="true"]') || + languageMenu.querySelector('[data-lang]'); + selectedItem?.focus(); + return; + } + + if (options?.focusButton) { + languageButton.focus(); + } +} + /** * Update platform display name based on current tab * Extracts duplicated logic and removes global state mutation @@ -78,6 +111,16 @@ function handleLanguageChange(message: any) { // Update all internationalized text when language changes localizeHTML(); + // Update language menu selection + const newLanguage = (message as LanguageChangeMessage).language; + updateLanguageMenuSelection(newLanguage); + + // Keep tooltip up-to-date for icon-only button + const languageButton = document.getElementById('language-button') as HTMLButtonElement | null; + if (languageButton) { + languageButton.title = I18nService.t('popup_language_label'); + } + // Update platform name with new translations updatePlatformDisplayName().catch(error => { logger.error('Failed to update platform name after language change:', error); @@ -99,8 +142,8 @@ function setupLanguageChangeListener() { // 初始化Popup async function initPopup() { - // 1. First initialize language selector (loads and sets currentLanguage asynchronously) - await initLanguageSelector(); + // 1. First initialize language menu (loads and sets currentLanguage asynchronously) + await initLanguageMenu(); // 2. After language initialization, localize HTML with correct language localizeHTML(); @@ -315,42 +358,75 @@ function showToast(message: string, duration = 2000) { } /** - * 初始化语言选择器 + * 初始化语言菜单 */ -async function initLanguageSelector() { - const languageSelect = document.getElementById('language-select') as HTMLSelectElement; - if (!languageSelect) return; +async function initLanguageMenu() { + const languageButton = document.getElementById('language-button') as HTMLButtonElement | null; + const languageMenu = document.getElementById('language-menu') as HTMLElement | null; + if (!languageButton || !languageMenu) return; + + const menuContainer = languageButton.closest('.menu-container'); + if (!menuContainer) return; // 获取当前语言并设置选中状态 const currentLang = await I18nService.getCurrentLanguage(); - languageSelect.value = currentLang; + updateLanguageMenuSelection(currentLang); + + // Icon-only button: add localized tooltip + languageButton.title = I18nService.t('popup_language_label'); + + languageButton.addEventListener('click', (e) => { + e.preventDefault(); + setLanguageMenuOpen(languageMenu.hidden); + }); // 监听语言切换 - languageSelect.addEventListener('change', async (e) => { - const newLang = (e.target as HTMLSelectElement).value as 'zh_CN' | 'zh_TW' | 'en'; + languageMenu.querySelectorAll('[data-lang]').forEach((item) => { + item.addEventListener('click', async () => { + const newLang = item.dataset.lang as SupportedLanguage; - try { - // 设置新语言 - await I18nService.setLanguage(newLang); + // Close menu immediately for better UX + setLanguageMenuOpen(false, { focusButton: true }); - // 更新页面所有文本 - localizeHTML(); + try { + // 设置新语言 + await I18nService.setLanguage(newLang); - // 显示Toast提示 - const langName = { - 'zh_CN': '简体中文', - 'zh_TW': '繁體中文', - 'en': 'English' - }[newLang]; + // 更新页面所有文本 + localizeHTML(); - showToast(I18nService.t('toast_language_changed', [langName])); + // 更新 tooltip + languageButton.title = I18nService.t('popup_language_label'); - // 更新平台名称翻译(使用提取的函数,消除重复代码) - await updatePlatformDisplayName(); - } catch (error) { - logger.error('Failed to change language:', error as Error); - showToast(I18nService.t('toast_language_change_failed')); - } + // 更新菜单选中状态 + updateLanguageMenuSelection(newLang); + + // 显示Toast提示 + showToast(I18nService.t('toast_language_changed', [LANGUAGE_DISPLAY_NAMES[newLang]])); + + // 更新平台名称翻译(使用提取的函数,消除重复代码) + await updatePlatformDisplayName(); + } catch (error) { + logger.error('Failed to change language:', error as Error); + showToast(I18nService.t('toast_language_change_failed')); + } + }); + }); + + // 点击其他区域关闭菜单 + document.addEventListener('click', (e) => { + if (languageMenu.hidden) return; + const target = e.target as Node | null; + if (target && menuContainer.contains(target)) return; + setLanguageMenuOpen(false); + }); + + // ESC 关闭菜单 + document.addEventListener('keydown', (e) => { + if (e.key !== 'Escape') return; + if (languageMenu.hidden) return; + e.preventDefault(); + setLanguageMenuOpen(false, { focusButton: true }); }); } diff --git a/src/shared/version.ts b/src/shared/version.ts index fa34a61..3cd2f2e 100644 --- a/src/shared/version.ts +++ b/src/shared/version.ts @@ -10,7 +10,7 @@ * 1. Edit version in package.json * 2. Run: npm run sync:version (or automatically via prebuild/predev) * - * Generated at: 2025-12-30T05:36:20.170Z + * Generated at: 2025-12-30T06:54:19.507Z */ /**