diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index bba95d9c..44e5b09e 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -627,14 +627,16 @@ fn apply_windows_caption_color(window: &tauri::WebviewWindow) { }; let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void); - // COLORREF 0x00BBGGRR 编码——白色就是 0x00FFFFFF。 - let white: u32 = 0x00FFFFFF; + // COLORREF 0x00BBGGRR 编码——选用 rgb(245,245,247) 跟 WindowChrome 的 glass linear-gradient + // 起始色一致,减小原生 caption bar 跟应用磨砂玻璃的色差(用户反馈:纯白 caption + 半透灰 glass + // 色差很丑)。R=0xF5 G=0xF5 B=0xF7 → COLORREF = 0x00F7F5F5。 + let glass_match: u32 = 0x00F7F5F5; unsafe { if let Err(e) = DwmSetWindowAttribute( hwnd, DWMWA_CAPTION_COLOR, - &white as *const _ as *const core::ffi::c_void, - std::mem::size_of_val(&white) as u32, + &glass_match as *const _ as *const core::ffi::c_void, + std::mem::size_of_val(&glass_match) as u32, ) { log::warn!("[main] set caption color failed (likely pre-22H2 Win): {e}"); } diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index 373df648..b1373af3 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -285,11 +285,18 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }: ); } +// 与 @keyframes capsule-out 的 0.24s 时长一致——必须同步,否则定时器先于 +// 动画结束就 unmount → 用户看到半截动画被截断。 +const EXIT_ANIM_MS = 240; +// 初始可见 state:Tauri 内运行从 idle 开始(等后端 capsule:state 事件), +// 浏览器 dev 模式从 recording 开始以便直接看到胶囊。 +const INITIAL_VISIBLE_STATE: CapsuleState = isTauri ? 'idle' : 'recording'; + export function Capsule() { const { t } = useTranslation(); const os = detectOS(); const metrics = getCapsulePillMetrics(os); - const [state, setState] = useState(isTauri ? 'idle' : 'recording'); + const [state, setState] = useState(INITIAL_VISIBLE_STATE); const [level, setLevel] = useState(isTauri ? 0 : 0.6); const [insertedChars, setInsertedChars] = useState(0); const [message, setMessage] = useState(); @@ -297,10 +304,10 @@ export function Capsule() { // `leaving` 与 `lastVisibleState` 协同实现「退出动画」: // - 当 state 从非 idle 变成 idle 时,不立即卸载,而是把 leaving 置为 true 并保留 // 最后一帧的可见 state(lastVisibleState),让胶囊用 capsule-out 动画收缩淡出。 - // - 动画结束(~240ms)后再把 leaving 置回 false,组件回到「真正未挂载」分支。 + // - 动画结束(EXIT_ANIM_MS)后再把 leaving 置回 false,组件回到「真正未挂载」分支。 // - 若期间 state 又切回非 idle(例如用户连按热键),立刻中止 leaving 并恢复显示。 const [leaving, setLeaving] = useState(false); - const [lastVisibleState, setLastVisibleState] = useState(isTauri ? 'idle' : 'recording'); + const [lastVisibleState, setLastVisibleState] = useState(INITIAL_VISIBLE_STATE); // Windows 端 host 在翻译模式从 84 长到 118;macOS / Linux 上 capsuleLayout 已固定 42 忽略此参数。 const hostMetrics = getCapsuleHostMetrics(os, translation); @@ -327,12 +334,12 @@ export function Capsule() { }; }, []); - // 退出动画调度:在 state 真正进入 idle 时,先用 capsule-out 播放 ~240ms,再卸载。 + // 退出动画调度:在 state 真正进入 idle 时,先用 capsule-out 播放 EXIT_ANIM_MS,再卸载。 // 设计要点: // 1. 进入非 idle:清掉 leaving,记录最新可见 state; // 2. 进入 idle 且之前可见:开启 leaving 并启动定时器; - // 3. 期间又被打回非 idle:定时器仍会触发,但 effect 内会读到 state !== idle, - // 所以 cleanup 时直接 clearTimeout,避免错误地把可见状态切到 idle。 + // 3. 期间又被打回非 idle:cleanup 直接 clearTimeout,定时器不会触发, + // 新一轮 effect 会立即恢复可见态,避免错误地把可见状态切到 idle。 useEffect(() => { if (state !== 'idle') { // 立即恢复可见,并取消上一轮可能挂着的离场。 @@ -346,7 +353,7 @@ export function Capsule() { const timer = setTimeout(() => { setLeaving(false); setLastVisibleState('idle'); - }, 240); + }, EXIT_ANIM_MS); return () => clearTimeout(timer); // 故意只依赖 state —— lastVisibleState / leaving 是内部派生量, // 把它们加进依赖会让定时器被反复重建。 diff --git a/openless-all/app/src/components/Icon.tsx b/openless-all/app/src/components/Icon.tsx index a5bdbe42..8bcc6ca6 100644 --- a/openless-all/app/src/components/Icon.tsx +++ b/openless-all/app/src/components/Icon.tsx @@ -4,8 +4,8 @@ import type { CSSProperties } from 'react'; export const ICONS: Record = { - // overview — 仪表盘式分屏布局(顶部摘要条 + 左右两块小卡),传达"概览/总览" - overview: 'M4 5h16v5H4zM4 13h7v6H4zM13 13h7v6h-7z', + // overview — 仪表盘三卡 + 顶卡内 sparkline 数据线,区分纯三块拼图(layout) + overview: 'M4 5h16v5H4zM4 13h7v6H4zM13 13h7v6h-7zM6 7.8l2 1 2-1.5 2 1', // history — 时钟表盘 + 左上角逆时针回拨箭头,强调"过去/回看" history: 'M12 8v4l3 2M3.5 12a8.5 8.5 0 1 0 2.8-6.3L3 8M3 4v4h4', // vocab — Feather 风格 open-book(书脊居中 + 左右两页),相比旧的"带书签的合上书"在 14px 下更易辨识 @@ -13,8 +13,8 @@ export const ICONS: Record = { style: 'M12 3a9 9 0 1 0 0 18 3 3 0 0 0 3-3v-1a2 2 0 0 1 2-2h1a3 3 0 0 0 3-3 9 9 0 0 0-9-9z', // translate — 地球仪(圆 + 赤道 + 经线椭圆),通用的"语言/国际化"符号,比旧版"A+文+三角"在 14px 下更清晰 translate:'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM3 12h18M12 3c2.5 2.5 4 5.5 4 9s-1.5 6.5-4 9c-2.5-2.5-4-5.5-4-9s1.5-6.5 4-9', - // selectionAsk — 三行文本("被选中的内容")+ 右下角对话气泡("追问"),直白表达"对选中文字提问" - selectionAsk:'M3 5h12M3 9h12M3 13h7M14 14h6a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3l-3 3v-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z', + // selectionAsk — 三行文本 + 右下角对话气泡(尾巴拉到 y≈23 防 viewBox 24 底边 stroke-cap 裁切) + selectionAsk:'M3 5h12M3 9h12M3 13h7M14 14h6a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3.5l-2.5 2v-2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2z', settings:'M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1A2 2 0 1 1 7 4.9l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z', help: 'M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3M12 17h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z', mic: 'M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3zM19 11a7 7 0 0 1-14 0M12 18v3M8 21h8', diff --git a/openless-all/app/src/components/WindowChrome.tsx b/openless-all/app/src/components/WindowChrome.tsx index ddfae43c..fdd3d76f 100644 --- a/openless-all/app/src/components/WindowChrome.tsx +++ b/openless-all/app/src/components/WindowChrome.tsx @@ -39,11 +39,12 @@ export function WindowChrome({ // 两个平台用同一份半透明玻璃 background + backdropFilter,让 sidebar 透明地坐在 // 磨砂底板上时有可见的玻璃感。 // Windows: Tauri transparent:true + lib.rs apply_mica 提供 Win11 Mica 透出来; - // macOS: NSVisualEffectView 提供材质。alpha 0.78 比之前的 0.92 更明显但不过透。 + // macOS: NSVisualEffectView 提供材质。 + // alpha 0.92:和原生 Win11 caption(lib.rs 设为 rgb(245,245,247))色差最小,玻璃感仍可见。 const background = ` radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 60%), radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%), - linear-gradient(180deg, rgba(245,245,247,0.78) 0%, rgba(232,232,236,0.78) 100%) + linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%) `; return ( diff --git a/openless-all/app/src/pages/LocalAsr.tsx b/openless-all/app/src/pages/LocalAsr.tsx index 68a3b70a..658a9a7b 100644 --- a/openless-all/app/src/pages/LocalAsr.tsx +++ b/openless-all/app/src/pages/LocalAsr.tsx @@ -772,6 +772,10 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { )}