From e104beef22bbacac510b154d3dd304acb8ccdd69 Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Thu, 13 Aug 2026 04:07:06 +0800 Subject: [PATCH] feat(ui): add a WeChat-inspired skin with light/dark variants and synchronized candidate and toolbar previews --- .../horizontal_candidate_window_wechat.html | 599 +++++++++++++++ ...izontal_candidate_window_wechat_light.html | 594 ++++++++++++++ .../vertical_candidate_window_wechat.html | 727 ++++++++++++++++++ ...ertical_candidate_window_wechat_light.html | 722 +++++++++++++++++ ui-html/webview2/ftb/wechat.html | 429 +++++++++++ ui-html/webview2/ftb/wechat_light.html | 428 +++++++++++ .../ime-settings/src/modules/config-sync.ts | 2 + .../settings/ime-settings/src/modules/skin.ts | 134 +++- .../ime-settings/src/modules/theme.ts | 4 +- .../ime-settings/src/partials/skin.html | 43 +- .../ime-settings/src/styles/modules/skin.css | 51 +- 11 files changed, 3677 insertions(+), 56 deletions(-) create mode 100644 ui-html/webview2/candwnd/horizontal_candidate_window_wechat.html create mode 100644 ui-html/webview2/candwnd/horizontal_candidate_window_wechat_light.html create mode 100644 ui-html/webview2/candwnd/vertical_candidate_window_wechat.html create mode 100644 ui-html/webview2/candwnd/vertical_candidate_window_wechat_light.html create mode 100644 ui-html/webview2/ftb/wechat.html create mode 100644 ui-html/webview2/ftb/wechat_light.html diff --git a/ui-html/webview2/candwnd/horizontal_candidate_window_wechat.html b/ui-html/webview2/candwnd/horizontal_candidate_window_wechat.html new file mode 100644 index 00000000..f012d6d0 --- /dev/null +++ b/ui-html/webview2/candwnd/horizontal_candidate_window_wechat.html @@ -0,0 +1,599 @@ + + + + + + + (IME)WechatHCandWnd + + + + + + +
+
+
+ +
+
ni'mf
+
n
+
+ +
+
+
1 你们(rR)
+
+
+ +
+
+
2 你(rX)
+
+
+ +
+
+
3 尼(uV)
+
+
+ +
+
+
4 妮(nV)
+
+
+ +
+
+
5 泥(dV)
+
+
+ +
+
+
6 逆(zQ)
+
+
+ +
+
+
7 拟(fR)
+
+
+ +
+
+
8 腻(oD)
+
+
+ +
+
+
9 拧(nG)
+
+
+
+
+
+ + + +
+
置顶
+
固定排位
+
第 1 位
+
第 2 位
+
第 3 位
+
第 4 位
+
第 5 位
+
取消固定
+
+ +
+ + + + diff --git a/ui-html/webview2/candwnd/horizontal_candidate_window_wechat_light.html b/ui-html/webview2/candwnd/horizontal_candidate_window_wechat_light.html new file mode 100644 index 00000000..7c46ac1c --- /dev/null +++ b/ui-html/webview2/candwnd/horizontal_candidate_window_wechat_light.html @@ -0,0 +1,594 @@ + + + + + + + (IME)WechatLightHCandWnd + + + + + + +
+
+
+ +
+
ni'mf
+
n
+
+ +
+
+
1 你们(rR)
+
+
+ +
+
+
2 你(rX)
+
+
+ +
+
+
3 尼(uV)
+
+
+ +
+
+
4 妮(nV)
+
+
+ +
+
+
5 泥(dV)
+
+
+ +
+
+
6 逆(zQ)
+
+
+ +
+
+
7 拟(fR)
+
+
+ +
+
+
8 腻(oD)
+
+
+ +
+
+
9 拧(nG)
+
+
+
+
+
+ + + +
+
置顶
+
固定排位
+
第 1 位
+
第 2 位
+
第 3 位
+
第 4 位
+
第 5 位
+
取消固定
+
+ +
+ + + + diff --git a/ui-html/webview2/candwnd/vertical_candidate_window_wechat.html b/ui-html/webview2/candwnd/vertical_candidate_window_wechat.html new file mode 100644 index 00000000..57de0bd5 --- /dev/null +++ b/ui-html/webview2/candwnd/vertical_candidate_window_wechat.html @@ -0,0 +1,727 @@ + + + + + + + (IME)WechatVCandWnd + + + + + + +
+
+
+ +
+
ni'mf
+
n
+
+ +
+
+
1你们(rR)
+
+
+ +
+
+
2你(rX)
+
+
+ +
+
+
3尼(uV)
+
+
+ +
+
+
4妮(nV)
+
+
+ +
+
+
5泥(dV)
+
+
+ +
+
+
6逆(zQ)
+
+
+ +
+
+
7拟(fR)
+
+
+ +
+
+
8腻(oD)
+
+
+ +
+
+
9拧(nG)
+
+
+
+
+
+ + + + + + +
+ +
固定排位 +
+
第 1 位
+
第 2 位
+
第 3 位
+
第 4 位
+
第 5 位
+
+
取消固定
+
+
+ +
+ + + + diff --git a/ui-html/webview2/candwnd/vertical_candidate_window_wechat_light.html b/ui-html/webview2/candwnd/vertical_candidate_window_wechat_light.html new file mode 100644 index 00000000..899a33b6 --- /dev/null +++ b/ui-html/webview2/candwnd/vertical_candidate_window_wechat_light.html @@ -0,0 +1,722 @@ + + + + + + + (IME)WechatLightVCandWnd + + + + + + +
+
+
+ +
+
ni'mf
+
n
+
+ +
+
+
1你们(rR)
+
+
+ +
+
+
2你(rX)
+
+
+ +
+
+
3尼(uV)
+
+
+ +
+
+
4妮(nV)
+
+
+ +
+
+
5泥(dV)
+
+
+ +
+
+
6逆(zQ)
+
+
+ +
+
+
7拟(fR)
+
+
+ +
+
+
8腻(oD)
+
+
+ +
+
+
9拧(nG)
+
+
+
+
+
+ + + + + + +
+ +
固定排位 +
+
第 1 位
+
第 2 位
+
第 3 位
+
第 4 位
+
第 5 位
+
+
取消固定
+
+
+ +
+ + + + diff --git a/ui-html/webview2/ftb/wechat.html b/ui-html/webview2/ftb/wechat.html new file mode 100644 index 00000000..b41d7e10 --- /dev/null +++ b/ui-html/webview2/ftb/wechat.html @@ -0,0 +1,429 @@ + + + + + + IME(WeChat FTB) + + + + + +
+
+
+
+ +
+ +
+ + +
+
+ + + +
+ +
+ + + +
+ + +
+ +
+ + +
+ + + +
+ +
+ + + +
+
+ +
+ +
+ + + + + +
+ +
+ + + + +
+
+ +
+ + + + +
+
+ + + + + + +
+ +
+ + + + +
+
+
+ + + + + diff --git a/ui-html/webview2/ftb/wechat_light.html b/ui-html/webview2/ftb/wechat_light.html new file mode 100644 index 00000000..f770f0dd --- /dev/null +++ b/ui-html/webview2/ftb/wechat_light.html @@ -0,0 +1,428 @@ + + + + + + IME(WeChat Light FTB) + + + + + +
+
+
+
+ +
+ +
+ + +
+
+ + + +
+ +
+ + + +
+ + +
+ +
+ + +
+ + + +
+ +
+ + + +
+
+ +
+ +
+ + + + + +
+ +
+ + + + +
+
+ +
+ + + + +
+
+ + + + + + +
+ +
+ + + + +
+
+
+ + + + + diff --git a/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts b/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts index d700985c..afd8f51b 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/config-sync.ts @@ -5,6 +5,7 @@ import { applyVoiceConfig } from './voice'; import { applyAiConfig } from './ai-settings'; import { applyFloatingToolbarAppearanceConfig, applyFloatingToolbarItemsConfig } from './floating-toolbar'; import { applyShortcutConfig } from './shortcut'; +import { applyCandidateSkin } from './skin'; function safeParseJson(value: string): unknown { try { @@ -26,6 +27,7 @@ export function setupConfigSync(): void { } applyCandidateArrange(payload.data?.appearance?.candidate_window_layout); + applyCandidateSkin(payload.data?.appearance?.candidate_skin); applyAppearanceConfig( payload.data?.appearance?.candidate_window_preedit_style, payload.data?.appearance?.tsf_preedit_style, diff --git a/ui-html/webview2/settings/ime-settings/src/modules/skin.ts b/ui-html/webview2/settings/ime-settings/src/modules/skin.ts index 30055dad..bf31f27c 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/skin.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/skin.ts @@ -2,11 +2,21 @@ import { loadHTML } from '../utils/common-utils'; import ftbHTML from '../../../../ftb/default.html?raw'; export type SkinPreviewTheme = 'dark' | 'light'; +export type CandidateSkin = 'fluent' | 'wechat'; -let previewOverride: SkinPreviewTheme | null = null; +const SKINS: CandidateSkin[] = ['fluent', 'wechat']; +const previewOverrides: Record = { + fluent: null, + wechat: null +}; let activeTheme: SkinPreviewTheme = 'dark'; +let activeSkin: CandidateSkin = 'fluent'; const SKIN_PREVIEW_PAGE_SIZE = 6; +function normalizeCandidateSkin(value: unknown): CandidateSkin { + return value === 'wechat' ? 'wechat' : 'fluent'; +} + function limitCandidatePreview(host: HTMLElement): void { host.querySelectorAll('.row-wrapper').forEach((wrapper, index) => { wrapper.style.display = index < SKIN_PREVIEW_PAGE_SIZE ? '' : 'none'; @@ -22,58 +32,108 @@ function fillToolbar(host: HTMLElement): void { host.replaceChildren(statusBar); } -function applySkinPreviewTheme(theme: SkinPreviewTheme): void { - document.querySelectorAll('#skinCandidateHorizontal, #skinCandidateVertical').forEach((element) => { - element.classList.toggle('theme-light', theme === 'light'); - element.classList.toggle('theme-dark', theme === 'dark'); - }); - document.querySelectorAll('#skinToolbarPreview').forEach((element) => { - element.classList.toggle('theme-light', theme === 'light'); - element.classList.toggle('theme-dark', theme === 'dark'); - }); +function applySkinPreviewTheme(skin: CandidateSkin, theme: SkinPreviewTheme): void { + const card = document.querySelector(`[data-candidate-skin="${skin}"]`); + if (!card) return; - const title = document.getElementById('skinThemeTitle'); - const button = document.getElementById('skinPreviewSwitch'); + card.querySelectorAll('[data-skin-horizontal], [data-skin-vertical], [data-skin-toolbar]') + .forEach((element) => { + element.classList.toggle('theme-light', theme === 'light'); + element.classList.toggle('theme-dark', theme === 'dark'); + }); + + const title = card.querySelector('.section-title'); if (title) { - title.textContent = theme === 'light' ? 'Fluent 主题(Light)' : 'Fluent 主题(Dark)'; + const name = skin === 'wechat' ? '微信绿主题' : 'Fluent 主题'; + title.textContent = `${name}(${theme === 'light' ? 'Light' : 'Dark'})`; } - if (button) { + card.querySelectorAll('[data-skin-preview-switch]').forEach((button) => { button.textContent = theme === 'light' ? '预览深色' : '预览浅色'; + }); +} + +function resolvedPreviewTheme(skin: CandidateSkin): SkinPreviewTheme { + return previewOverrides[skin] ?? activeTheme; +} + +function syncSkinSwitches(): void { + document.querySelectorAll('[data-skin-switch]').forEach((element) => { + const selected = element.dataset.skinSwitch === activeSkin; + element.classList.toggle('active', selected); + element.setAttribute('aria-checked', String(selected)); + }); +} + +function syncAppearancePreviews(): void { + const useWechat = activeSkin === 'wechat'; + document.querySelectorAll('.cand-preview .candidate').forEach((element) => { + element.classList.toggle('skin-wechat', useWechat); + }); + document.querySelectorAll('.ftb-preview-host:not([data-skin-toolbar])').forEach((element) => { + element.classList.toggle('skin-wechat', useWechat); + }); +} + +function selectSkin(value: unknown, persist: boolean): void { + activeSkin = normalizeCandidateSkin(value); + syncSkinSwitches(); + syncAppearancePreviews(); + if (persist) { + window.chrome?.webview?.postMessage(JSON.stringify({ + type: 'configUpdate', + data: { path: 'appearance.candidate_skin', value: activeSkin } + })); } } -function resolvedPreviewTheme(): SkinPreviewTheme { - return previewOverride ?? activeTheme; +export function applyCandidateSkin(value: unknown): void { + selectSkin(value, false); } export function syncSkinPreviewTheme(theme: SkinPreviewTheme): void { activeTheme = theme; - // When the real theme changes, drop manual override so preview follows config again. - previewOverride = null; - applySkinPreviewTheme(resolvedPreviewTheme()); + SKINS.forEach((skin) => { + previewOverrides[skin] = null; + applySkinPreviewTheme(skin, resolvedPreviewTheme(skin)); + }); } export async function setupSkin(): Promise { - const vertical = document.getElementById('skinCandidateVertical'); - const horizontal = document.getElementById('skinCandidateHorizontal'); - if (vertical) { - vertical.innerHTML = await loadHTML('/src/partials/candidate/candidate-wnd-v.html'); - limitCandidatePreview(vertical); - } - if (horizontal) { - horizontal.innerHTML = await loadHTML('/src/partials/candidate/candidate-wnd-h.html'); - limitCandidatePreview(horizontal); - } + const horizontalHtml = await loadHTML('/src/partials/candidate/candidate-wnd-h.html'); + const verticalHtml = await loadHTML('/src/partials/candidate/candidate-wnd-v.html'); - const toolbar = document.getElementById('skinToolbarPreview'); - if (toolbar) fillToolbar(toolbar); + document.querySelectorAll('[data-skin-horizontal]').forEach((host) => { + host.innerHTML = horizontalHtml; + limitCandidatePreview(host); + }); + document.querySelectorAll('[data-skin-vertical]').forEach((host) => { + host.innerHTML = verticalHtml; + limitCandidatePreview(host); + }); + document.querySelectorAll('[data-skin-toolbar]').forEach(fillToolbar); - applySkinPreviewTheme(resolvedPreviewTheme()); + SKINS.forEach((skin) => applySkinPreviewTheme(skin, resolvedPreviewTheme(skin))); + syncSkinSwitches(); + syncAppearancePreviews(); - const switchBtn = document.getElementById('skinPreviewSwitch'); - switchBtn?.addEventListener('click', () => { - const current = resolvedPreviewTheme(); - previewOverride = current === 'light' ? 'dark' : 'light'; - applySkinPreviewTheme(previewOverride); + document.querySelectorAll('[data-skin-switch]').forEach((element) => { + const activate = () => selectSkin(element.dataset.skinSwitch, true); + element.addEventListener('click', activate); + element.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + activate(); + } + }); + }); + + document.querySelectorAll('[data-skin-preview-switch]').forEach((button) => { + button.addEventListener('click', () => { + const card = button.closest('[data-candidate-skin]'); + const skin = normalizeCandidateSkin(card?.dataset.candidateSkin); + const current = resolvedPreviewTheme(skin); + previewOverrides[skin] = current === 'light' ? 'dark' : 'light'; + applySkinPreviewTheme(skin, previewOverrides[skin]!); + }); }); } diff --git a/ui-html/webview2/settings/ime-settings/src/modules/theme.ts b/ui-html/webview2/settings/ime-settings/src/modules/theme.ts index 6f69aaa4..c69efc48 100644 --- a/ui-html/webview2/settings/ime-settings/src/modules/theme.ts +++ b/ui-html/webview2/settings/ime-settings/src/modules/theme.ts @@ -179,8 +179,8 @@ function updateSkinThemeCard( `全局 ${modeLabel} · 设置 ${settingsResolved === 'light' ? '浅色' : '深色'} · 候选 ${candResolved === 'light' ? '浅色' : '深色'}`; } - // Skin gallery follows the global theme; the switch button can preview the other one. - syncSkinPreviewTheme(resolveTheme(mode, 'follow')); + // Fluent preview follows the same resolved theme as the real candidate surface. + syncSkinPreviewTheme(candResolved); } export function setThemeMode(value: string | undefined): void { diff --git a/ui-html/webview2/settings/ime-settings/src/partials/skin.html b/ui-html/webview2/settings/ime-settings/src/partials/skin.html index e2166c48..80509c89 100644 --- a/ui-html/webview2/settings/ime-settings/src/partials/skin.html +++ b/ui-html/webview2/settings/ime-settings/src/partials/skin.html @@ -1,33 +1,56 @@
皮肤
-
+
Fluent 主题(Dark)
当前默认主题
-
+
- +
-
+
-
-
+
-
-
+
+
+
+
+ +
+
+
+
微信绿主题(Dark)
+
微信绿候选窗与悬浮工具栏
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
diff --git a/ui-html/webview2/settings/ime-settings/src/styles/modules/skin.css b/ui-html/webview2/settings/ime-settings/src/styles/modules/skin.css index ade69e85..67dc52c6 100644 --- a/ui-html/webview2/settings/ime-settings/src/styles/modules/skin.css +++ b/ui-html/webview2/settings/ime-settings/src/styles/modules/skin.css @@ -1,4 +1,5 @@ .skin-theme-card { padding: 0; overflow: hidden; } +.skin-theme-card + .skin-theme-card { margin-top: 18px; } .skin-theme-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 20px 24px; } .skin-theme-description { margin-top: 5px; color: var(--text-muted); font-size: 13px; } .skin-theme-actions { @@ -8,7 +9,7 @@ gap: 8px; flex-shrink: 0; } -.skin-theme-toggle { cursor: default; } +.skin-theme-toggle { cursor: pointer; } .skin-preview-switch { height: 24px; padding: 0 10px; @@ -21,17 +22,12 @@ line-height: 1; cursor: pointer; } -.skin-preview-switch:hover { - color: var(--text-color); - background: var(--button-secondary-bg); -} +.skin-preview-switch:hover { color: var(--text-color); background: var(--button-secondary-bg); } .skin-preview-list { padding: 9px 0; background: var(--skin-preview-stage-bg); } .skin-preview-row { min-width: 0; } .skin-preview-stage { display: flex; align-items: flex-start; justify-content: flex-start; min-width: 0; padding: 9px 24px; overflow: hidden; } .skin-preview-row .candidate { flex: 0 0 auto; align-items: flex-start; margin-top: 0; transform-origin: left center; } .skin-preview-horizontal .container { - /* Match the real candidate window: shrink to content, but wrap when the - available preview width is too small. */ width: fit-content; max-width: 100%; min-width: 0; @@ -40,3 +36,44 @@ word-break: break-word; } .skin-preview-toolbar .ftb-preview-host { display: flex; align-items: center; justify-content: flex-start; padding: 0; } + +/* Preview equivalents of the standalone WeChat skin HTML files. */ +.candidate.skin-wechat.theme-dark { + --accent-strong: #07c160; + --cand-bg: #151515; + --cand-border: #292929; + --cand-shadow: 0 6px 18px rgba(0, 0, 0, .42); + --cand-text: #b7b7b7; + --cand-num: #858585; + --cand-hover: rgba(7, 193, 96, .22); + --cand-selected: #07c160; +} +.candidate.skin-wechat.theme-light { + --accent-strong: #07c160; + --cand-bg: #f7f7f7; + --cand-border: #dedede; + --cand-shadow: 0 5px 15px rgba(0, 0, 0, .14); + --cand-text: #333333; + --cand-num: #757575; + --cand-hover: rgba(7, 193, 96, .14); + --cand-selected: #07c160; +} +.candidate.skin-wechat .container { padding: 2px; border-radius: 5px; } +.candidate.skin-wechat .cand { border-radius: 4px; } +.candidate.skin-wechat .first { border-radius: 5px; } +.candidate.skin-wechat .first::before { display: none; } +.candidate.skin-wechat .first .text, +.candidate.skin-wechat .first .num, +.candidate.skin-wechat .first .cand-no { color: #fff; } + +.ftb-preview-host.skin-wechat.theme-dark .status-bar { + background-color: #151515; + border-color: #292929; + box-shadow: 0 5px 14px rgba(0, 0, 0, .38); +} +.ftb-preview-host.skin-wechat.theme-light .status-bar { + background-color: #f7f7f7; + border-color: #dedede; + box-shadow: 0 5px 14px rgba(0, 0, 0, .12); +} +.ftb-preview-host.skin-wechat .drag-handle { background: #07c160; }