From 5671805a09895b0b8b36c8aead19538fd2e4c4ca Mon Sep 17 00:00:00 2001 From: baiqing Date: Sat, 13 Jun 2026 13:48:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?revert(ui):=20=E5=9B=9E=E9=80=80=20AURA=20?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E5=A4=A7=E6=94=B9=E5=88=B0=20beta-3=20?= =?UTF-8?q?=E5=A4=96=E8=A7=82=EF=BC=8C=E4=BF=9D=E7=95=99=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=B8=8E=E4=BB=8A=E6=97=A5=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把 #650 引入的 AURA / 玻璃拟态界面大改回退到 beta-3 (v1.3.6-3) 的外观,同时保留 AURA 期间合入的功能与今日两处行为修复。 回退到 beta-3: - 外壳 FloatingShell、概览 / 历史页、卡片按钮 _atoms、设置弹窗、tokens.css / global.css - 移除随 AURA 加入的主题切换(themeMode.ts / ThemeSection.tsx) - 设置关闭按钮去掉圆形底(beta-3 本就透明) 保留:Apple 语音、手机远程录入、安卓、划词追问等功能,后端不动。 今日行为修复: - 风格页切换不再卡顿(prefs:changed 监听只订阅一次 + motion layoutDependency 限制 layout 重测) - ASR 本地引擎不再锁死,可在 provider 页直接切换(Apple 语音 macOS 直接可选) 为划词追问页补回其引用的 6 个 solid / radius token,避免回退后丢色。 --- .../app/src/components/AutoUpdate.tsx | 4 +- openless-all/app/src/components/Capsule.tsx | 24 +- .../app/src/components/FloatingShell.tsx | 271 +++++------ .../app/src/components/GithubLoginModal.tsx | 8 +- .../app/src/components/MarketplaceModal.tsx | 12 +- .../app/src/components/Onboarding.tsx | 34 +- .../app/src/components/SettingsModal.tsx | 127 +++--- .../app/src/components/ShortcutRecorder.tsx | 12 +- openless-all/app/src/components/ui/Modal.tsx | 10 +- openless-all/app/src/components/ui/Row.tsx | 8 +- .../app/src/components/ui/SegSimple.tsx | 39 +- .../app/src/components/ui/SelectLite.tsx | 28 +- .../app/src/components/ui/SwitchLite.tsx | 6 +- openless-all/app/src/i18n/en.ts | 10 +- openless-all/app/src/i18n/ja.ts | 8 - openless-all/app/src/i18n/ko.ts | 8 - openless-all/app/src/i18n/zh-CN.ts | 10 +- openless-all/app/src/i18n/zh-TW.ts | 8 - openless-all/app/src/lib/platform.ts | 18 - openless-all/app/src/lib/themeMode.ts | 52 --- openless-all/app/src/main.tsx | 5 - openless-all/app/src/pages/History.tsx | 134 ++---- .../app/src/pages/LessComputerPanel.tsx | 43 +- openless-all/app/src/pages/LocalAsr.tsx | 22 +- openless-all/app/src/pages/Marketplace.tsx | 34 +- openless-all/app/src/pages/Overview.tsx | 251 +---------- openless-all/app/src/pages/SelectionAsk.tsx | 8 +- openless-all/app/src/pages/Style.tsx | 121 ++--- openless-all/app/src/pages/Translation.tsx | 20 +- openless-all/app/src/pages/Vocab.tsx | 24 +- openless-all/app/src/pages/_atoms.tsx | 105 ++--- .../app/src/pages/settings/AboutSection.tsx | 6 +- .../src/pages/settings/CheckUpdateButton.tsx | 4 +- .../pages/settings/ClaudeConsoleSection.tsx | 2 +- .../src/pages/settings/MarketplaceSection.tsx | 6 +- .../src/pages/settings/ProvidersSection.tsx | 47 +- .../pages/settings/RecordingInputSection.tsx | 10 +- .../src/pages/settings/RemoteInputSection.tsx | 8 +- .../src/pages/settings/ShortcutsSection.tsx | 10 +- .../app/src/pages/settings/ThemeSection.tsx | 36 -- .../app/src/pages/settings/shared.tsx | 14 +- openless-all/app/src/pages/settings/tabs.tsx | 6 +- openless-all/app/src/styles/global.css | 421 +----------------- openless-all/app/src/styles/tokens.css | 98 +--- 44 files changed, 583 insertions(+), 1549 deletions(-) delete mode 100644 openless-all/app/src/lib/themeMode.ts delete mode 100644 openless-all/app/src/pages/settings/ThemeSection.tsx diff --git a/openless-all/app/src/components/AutoUpdate.tsx b/openless-all/app/src/components/AutoUpdate.tsx index d59b9332..0d0cfa0e 100644 --- a/openless-all/app/src/components/AutoUpdate.tsx +++ b/openless-all/app/src/components/AutoUpdate.tsx @@ -200,14 +200,14 @@ export function UpdateDialog({ const installing = status === 'installing'; return (
-
+
{t(`settings.about.updateDialog.${status}.title`)}
{t(`settings.about.updateDialog.${status}.desc`, { version })}
{(downloading || installing || status === 'downloaded') && (
-
+
diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index 091dc104..bb0bd822 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -41,7 +41,7 @@ function AudioBars({ level }: AudioBarsProps) { display: 'inline-block', width: 3, height: base + (max - base) * visualVoice * env, - borderRadius: 'var(--ol-pill-radius)', + borderRadius: 999, background: 'var(--ol-blue)', opacity: 0.82, transformOrigin: 'center', @@ -113,12 +113,12 @@ function CircleButton({ variant, enabled, onClick }: CircleButtonProps) { style={{ width: 28, height: 28, - borderRadius: 'var(--ol-pill-radius)', - background: isCancel ? 'var(--ol-capsule-cancel-bg)' : 'var(--ol-capsule-confirm-bg)', + borderRadius: 999, + background: isCancel ? 'rgba(255, 255, 255, 0.55)' : 'rgba(255, 255, 255, 0.92)', backdropFilter: useBackdrop ? 'blur(12px) saturate(160%)' : 'none', WebkitBackdropFilter: useBackdrop ? 'blur(12px) saturate(160%)' : 'none', - color: isCancel ? 'var(--ol-capsule-cancel-ink)' : 'var(--ol-capsule-confirm-ink)', - border: '0.8px solid var(--ol-glass-border)', + color: 'var(--ol-ink)', + border: '0.8px solid rgba(0, 0, 0, 0.08)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -265,11 +265,11 @@ function Pill({ os, state, level, insertedChars, message, operating, onCancel, o width: metrics.width, height: metrics.height, boxSizing: metrics.boxSizing, - borderRadius: 'var(--ol-pill-radius)', - border: '1px solid var(--ol-glass-border)', + borderRadius: 999, + border: '1px solid rgba(255, 255, 255, 0.55)', boxShadow: os === 'win' - ? `0 10px 24px -14px rgba(0, 0, 0, ${(0.24 + ambient * 0.06).toFixed(3)}), var(--ol-pill-shadow)` - : `0 18px 50px -10px rgba(0, 0, 0, ${shadowAlpha.toFixed(3)}), var(--ol-pill-shadow)`, + ? `0 10px 24px -14px rgba(0, 0, 0, ${(0.24 + ambient * 0.06).toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 1px 0 0 rgba(255, 255, 255, 0.8)` + : `0 18px 50px -10px rgba(0, 0, 0, ${shadowAlpha.toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 1px 0 0 rgba(255, 255, 255, 0.8)`, color: 'var(--ol-ink)', fontFamily: 'var(--ol-font-sans)', transform: `scale(${scale.toFixed(4)})`, @@ -446,11 +446,11 @@ export function Capsule() { alignItems: 'center', gap: 5, padding: '3px 10px', - borderRadius: 'var(--ol-pill-radius)', + borderRadius: 999, fontSize: 10.5, fontWeight: 600, color: 'var(--ol-blue)', - background: 'var(--ol-glass-bg-strong)', + background: 'rgba(255, 255, 255, 0.78)', backdropFilter: 'blur(20px) saturate(180%)', WebkitBackdropFilter: 'blur(20px) saturate(180%)', border: '0.5px solid rgba(37, 99, 235, 0.25)', @@ -465,7 +465,7 @@ export function Capsule() { willChange: 'opacity, transform', }} > - + {t('capsule.translating')}
diff --git a/openless-all/app/src/components/FloatingShell.tsx b/openless-all/app/src/components/FloatingShell.tsx index 266cd3c5..13adbf90 100644 --- a/openless-all/app/src/components/FloatingShell.tsx +++ b/openless-all/app/src/components/FloatingShell.tsx @@ -4,7 +4,7 @@ // // Ported verbatim from design_handoff_openless/variants.jsx::FloatingShell. -import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType, type CSSProperties } from 'react'; +import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentType } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from './Icon'; import { WindowChrome, detectOS, type OS } from './WindowChrome'; @@ -26,14 +26,13 @@ import { shouldShowHotkeyModeMigrationPrompt, } from '../lib/hotkeyMigration'; import { applyFontScale, readFontScale } from '../lib/fontScale'; -import { getCredentials, getPlatformCapabilities } from '../lib/ipc'; +import { getCredentials } from '../lib/ipc'; import { PROVIDER_SETUP_PROMPT_DEFERRED_KEY, shouldShowProviderSetupPrompt, } from '../lib/providerSetup'; import { type SettingsSectionId } from './SettingsModal'; import { useAppState, type AppTab } from '../state/useAppState'; -import { useMobileLayout } from '../lib/useMobileLayout'; interface NavItem { id: AppTab; @@ -72,7 +71,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia const [settingsInitialSection, setSettingsInitialSection] = useState(); const [providerPromptOpen, setProviderPromptOpen] = useState(false); const [hotkeyModePromptOpen, setHotkeyModePromptOpen] = useState(false); - const mobileLayout = useMobileLayout(); // tab 切换的 cross-fade:旧页 blur+fade out(180ms),结束后挂载新页(走 ol-page-slide enter)。 // displayTab 是实际渲染的 tab,currentTab 是用户点中的目标 tab。 @@ -98,17 +96,12 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia [t], ); const Page = (NAV.find((n) => n.id === displayTab) ?? NAV[0]).cmp; - const activeNav = NAV.find(n => n.id === currentTab) ?? NAV[0]; // sidebar nav 滑动指示器:测量当前 active button 的 offsetTop / height, // 用一个 absolute pill 平滑滑过去,而不是每个按钮各自瞬切背景色。 const navItemRefs = useRef>([]); const [pillRect, setPillRect] = useState<{ top: number; height: number } | null>(null); useLayoutEffect(() => { - if (mobileLayout) { - setPillRect(null); - return; - } if (settingsOpen) { setPillRect(null); return; @@ -121,13 +114,11 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia const el = navItemRefs.current[idx]; if (!el) return; setPillRect({ top: el.offsetTop, height: el.offsetHeight }); - }, [currentTab, settingsOpen, NAV, mobileLayout]); + }, [currentTab, settingsOpen, NAV]); useEffect(() => { let cancelled = false; (async () => { - const caps = await getPlatformCapabilities(); - if (cancelled || caps.platform === 'android') return; const credentials = await getCredentials(); const promptDeferredValue = window.sessionStorage.getItem(PROVIDER_SETUP_PROMPT_DEFERRED_KEY); if (!cancelled && shouldShowProviderSetupPrompt(credentials, promptDeferredValue)) { @@ -140,18 +131,11 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia }, []); useEffect(() => { - let cancelled = false; - void getPlatformCapabilities().then((caps) => { - if (cancelled || caps.platform === 'android') return; - const acknowledgedValue = window.localStorage.getItem(HOTKEY_MODE_MIGRATION_ACK_KEY); - const deferredValue = window.sessionStorage.getItem(HOTKEY_MODE_MIGRATION_DEFERRED_KEY); - if (shouldShowHotkeyModeMigrationPrompt(acknowledgedValue, deferredValue)) { - setHotkeyModePromptOpen(true); - } - }); - return () => { - cancelled = true; - }; + const acknowledgedValue = window.localStorage.getItem(HOTKEY_MODE_MIGRATION_ACK_KEY); + const deferredValue = window.sessionStorage.getItem(HOTKEY_MODE_MIGRATION_DEFERRED_KEY); + if (shouldShowHotkeyModeMigrationPrompt(acknowledgedValue, deferredValue)) { + setHotkeyModePromptOpen(true); + } }, []); // 之前监听的 NAVIGATE_LOCAL_ASR_EVENT 已无意义——「模型设置」独立 tab 已下线, @@ -197,69 +181,37 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia }; return ( -
+
{/* Main shell — flush with the frosted backplate (no separate float). */}
- {mobileLayout ? ( -
-
- OpenLess -
-
OpenLess
-
{activeNav.name}
-
-
- -
- ) : ( - /* Sidebar — 透明地坐在外层磨砂底板上,让 LOGO/导航/快捷键/BETA/footer 共用同一片磨砂玻璃 */ - - {/* Main content — Linux 禁用透明窗口后使用不透明面;其他平台保留玻璃层。 */} -
+ {/* Main content — Linux 禁用透明窗口后使用不透明面;其他平台保留玻璃层。 + 悬浮台到右边 / 下边的间距相等(都 8px),左侧贴 sidebar(0)。 */} +
- - {mobileLayout && ( - - )}
{/* Settings modal — rendered inside this window */} @@ -438,6 +403,44 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia /> ) : null} + + {/* tab 切换 + provider prompt + footer popover 公用的入场关键帧 */} +
); } @@ -454,7 +457,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; alignItems: 'center', justifyContent: 'center', padding: 28, - background: 'var(--ol-overlay-bg)', + background: 'rgba(15,17,22,0.28)', backdropFilter: 'blur(6px) saturate(140%)', WebkitBackdropFilter: 'blur(6px) saturate(140%)', animation: 'ol-prompt-fade 0.2s var(--ol-motion-soft)', @@ -463,10 +466,10 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
void; style={{ width: 34, height: 34, - borderRadius: 'var(--ol-control-radius)', - background: 'var(--ol-blue-soft)', + borderRadius: 8, + background: 'rgba(37,99,235,0.10)', color: 'var(--ol-blue)', display: 'inline-flex', alignItems: 'center', @@ -498,7 +501,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; style={{ height: 32, padding: '0 13px', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', color: 'var(--ol-ink-3)', @@ -516,10 +519,10 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void; style={{ height: 32, padding: '0 14px', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: 0, - background: 'var(--ol-primary-solid-bg)', - color: 'var(--ol-primary-solid-ink)', + background: 'var(--ol-ink)', + color: '#fff', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 500, @@ -547,7 +550,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () => alignItems: 'center', justifyContent: 'center', padding: 28, - background: 'var(--ol-overlay-bg)', + background: 'rgba(15,17,22,0.28)', backdropFilter: 'blur(6px) saturate(140%)', WebkitBackdropFilter: 'blur(6px) saturate(140%)', animation: 'ol-prompt-fade 0.2s var(--ol-motion-soft)', @@ -556,10 +559,10 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
style={{ width: 34, height: 34, - borderRadius: 'var(--ol-control-radius)', - background: 'var(--ol-blue-soft)', + borderRadius: 8, + background: 'rgba(37,99,235,0.10)', color: 'var(--ol-blue)', display: 'inline-flex', alignItems: 'center', @@ -591,7 +594,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () => style={{ height: 32, padding: '0 13px', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', color: 'var(--ol-ink-3)', @@ -609,10 +612,10 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () => style={{ height: 32, padding: '0 14px', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: 0, - background: 'var(--ol-primary-solid-bg)', - color: 'var(--ol-primary-solid-ink)', + background: 'var(--ol-ink)', + color: '#fff', fontFamily: 'inherit', fontSize: 12.5, fontWeight: 500, diff --git a/openless-all/app/src/components/GithubLoginModal.tsx b/openless-all/app/src/components/GithubLoginModal.tsx index e2850c49..8dc4c5c3 100644 --- a/openless-all/app/src/components/GithubLoginModal.tsx +++ b/openless-all/app/src/components/GithubLoginModal.tsx @@ -124,7 +124,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps) title={t('common.close')} onClick={close} style={{ - width: 28, height: 28, borderRadius: 'var(--ol-control-radius)', + width: 28, height: 28, borderRadius: 8, display: 'inline-grid', placeItems: 'center', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', @@ -150,7 +150,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps)
@@ -174,7 +174,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps)
{t('marketplace.oauth.waiting')} @@ -197,7 +197,7 @@ export function GithubLoginModal({ onClose, onSuccess }: GithubLoginModalProps) {phase.kind === 'error' && (
{ - (e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-glass-bg-strong)'; + (e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.85)'; (e.currentTarget as HTMLButtonElement).style.color = 'var(--ol-ink-2)'; }} aria-label={t('common.close')} diff --git a/openless-all/app/src/components/Onboarding.tsx b/openless-all/app/src/components/Onboarding.tsx index 8fff0b04..bb518005 100644 --- a/openless-all/app/src/components/Onboarding.tsx +++ b/openless-all/app/src/components/Onboarding.tsx @@ -132,7 +132,7 @@ function AndroidOnboarding({ onComplete }: OnboardingProps) { key={step.id} style={{ height: 4, - borderRadius: 'var(--ol-pill-radius)', + borderRadius: 999, background: index <= stepIndex ? 'var(--ol-blue)' : 'var(--ol-line-soft)', }} /> @@ -143,7 +143,7 @@ function AndroidOnboarding({ onComplete }: OnboardingProps) { style={{ background: 'var(--ol-surface)', border: '0.5px solid var(--ol-line)', - borderRadius: 'var(--ol-bubble-radius)', + borderRadius: 14, boxShadow: 'var(--ol-shadow-lg)', padding: 18, display: 'flex', @@ -347,7 +347,7 @@ function DesktopOnboarding({ padding: 32, boxSizing: 'border-box', background: 'var(--ol-surface)', - borderRadius: 'var(--ol-bubble-radius)', + borderRadius: 14, border: '0.5px solid var(--ol-line)', boxShadow: 'var(--ol-shadow-lg)', }} @@ -470,7 +470,7 @@ function AndroidStepCard({ children }: { children: ReactNode }) { flexDirection: 'column', gap: 12, padding: 14, - borderRadius: 'var(--ol-r-md)', + borderRadius: 10, background: 'var(--ol-surface-2)', border: '0.5px solid var(--ol-line-soft)', minWidth: 0, @@ -488,7 +488,7 @@ function StatusBadge({ granted, label }: { granted: boolean; label: string }) { flexShrink: 0, fontSize: 11, fontWeight: 600, - borderRadius: 'var(--ol-pill-radius)', + borderRadius: 999, padding: '4px 8px', color: granted ? 'var(--ol-ok)' : 'var(--ol-ink-4)', background: granted ? 'rgba(40, 160, 90, 0.12)' : 'rgba(0,0,0,0.06)', @@ -526,9 +526,9 @@ function PermissionStep({ index, title, desc, status, actionLabel, onAction, dis style={{ width: 22, height: 22, - borderRadius: 'var(--ol-pill-radius)', - background: granted ? 'var(--ol-accent-solid-bg)' : 'rgba(0,0,0,0.06)', - color: granted ? 'var(--ol-accent-solid-ink)' : 'var(--ol-ink-3)', + borderRadius: 999, + background: granted ? 'var(--ol-blue)' : 'rgba(0,0,0,0.06)', + color: granted ? '#fff' : 'var(--ol-ink-3)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -558,9 +558,9 @@ function PermissionStep({ index, title, desc, status, actionLabel, onAction, dis fontWeight: 500, fontFamily: 'inherit', border: 0, - borderRadius: 'var(--ol-control-radius)', - background: granted ? 'var(--ol-surface-2)' : 'var(--ol-primary-solid-bg)', - color: granted ? 'var(--ol-ink-3)' : 'var(--ol-primary-solid-ink)', + borderRadius: 8, + background: granted ? 'var(--ol-surface-2)' : 'var(--ol-ink)', + color: granted ? 'var(--ol-ink-3)' : '#fff', cursor: disabled ? 'not-allowed' : 'default', opacity: disabled && !granted ? 0.6 : 1, transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), opacity 0.18s var(--ol-motion-soft), transform 0.12s var(--ol-motion-quick)', @@ -580,9 +580,9 @@ const primaryButtonStyle = { fontWeight: 600, fontFamily: 'inherit', border: 0, - borderRadius: 'var(--ol-r-md)', - background: 'var(--ol-primary-solid-bg)', - color: 'var(--ol-primary-solid-ink)', + borderRadius: 10, + background: 'var(--ol-ink)', + color: '#fff', cursor: 'default', } as const; @@ -594,7 +594,7 @@ const secondaryButtonStyle = { fontWeight: 600, fontFamily: 'inherit', border: '0.5px solid var(--ol-line-strong)', - borderRadius: 'var(--ol-r-md)', + borderRadius: 10, background: 'var(--ol-surface)', color: 'var(--ol-ink-2)', cursor: 'default', @@ -607,7 +607,7 @@ const plainButtonStyle = { fontWeight: 500, fontFamily: 'inherit', border: 0, - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, background: 'transparent', color: 'var(--ol-ink-4)', cursor: 'default', @@ -616,7 +616,7 @@ const plainButtonStyle = { const footerHintStyle = { marginTop: 18, padding: '12px 14px', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, background: 'var(--ol-surface-2)', fontSize: 11.5, color: 'var(--ol-ink-3)', diff --git a/openless-all/app/src/components/SettingsModal.tsx b/openless-all/app/src/components/SettingsModal.tsx index 9b202a93..7fe71d9e 100644 --- a/openless-all/app/src/components/SettingsModal.tsx +++ b/openless-all/app/src/components/SettingsModal.tsx @@ -2,19 +2,20 @@ // // 重构(2026-05):原本是「外层弹窗侧栏 + 设置页内层侧栏」双层嵌套,用户点 // 「设置」还要再面对第二个侧栏。现在拍平成单层 —— 通用 / 服务 / 隐私 / 高级 / -// 个性化 / 关于 六个 tab。每个 tab 的内容见 pages/settings/。 +// 个性化 / 关于 六个 tab + 帮助外链组。每个 tab 的内容见 pages/settings/。 // -// 设计原则:每个可见控件都必须可用。没有后端支撑的占位(如账号)不在此弹窗出现。 +// 设计原则:每个可见控件都必须可用。没有后端支撑的占位(账号 / 主题切换 等) +// 不在此弹窗出现。 import { useLayoutEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from './Icon'; import { SavedToast } from './SavedToast'; import { useSavedToastListener } from '../lib/savedEvent'; +import { openExternal } from '../lib/ipc'; import type { OS } from './WindowChrome'; import { GeneralTab, ServicesTab, PrivacyTab, AdvancedTab } from '../pages/settings/tabs'; import { AboutSection } from '../pages/settings/AboutSection'; -import { useMobileLayout } from '../lib/useMobileLayout'; // 稳定 tab ID(与 i18n key `modal.sections.*` 一致)。 export type SettingsSectionId = @@ -33,8 +34,14 @@ interface SettingsModalProps { interface ModalNavItem { id: string; icon: string; + external?: boolean; + href?: string; } +const HELP_URL = 'https://github.com/appergb/openless#readme'; +const RELEASE_NOTES_URL = 'https://github.com/appergb/openless/releases'; + +// 第一组:可选中的 tab;第二组:外部链接(永远不 active)。 const TAB_ITEMS: ModalNavItem[] = [ { id: 'general', icon: 'settings' }, { id: 'services', icon: 'cloud' }, @@ -42,83 +49,67 @@ const TAB_ITEMS: ModalNavItem[] = [ { id: 'advanced', icon: 'bolt' }, { id: 'about', icon: 'info' }, ]; +const LINK_ITEMS: ModalNavItem[] = [ + { id: 'helpCenter', icon: 'help', external: true, href: HELP_URL }, + { id: 'releaseNotes', icon: 'doc', external: true, href: RELEASE_NOTES_URL }, +]; export function SettingsModal({ os: _os, onClose, initialSettingsSection }: SettingsModalProps) { const { t } = useTranslation(); const [section, setSection] = useState(initialSettingsSection ?? 'general'); const savedToast = useSavedToastListener(); - const mobile = useMobileLayout(); // 与 sidebar nav 一致的滑动指示器:仅 tab 组有 pill;外链组永远不画 pill。 const tabRefs = useRef>([]); const [pillRect, setPillRect] = useState<{ top: number; height: number } | null>(null); useLayoutEffect(() => { - if (mobile) { - setPillRect(null); - return; - } const idx = TAB_ITEMS.findIndex(it => it.id === section); const el = tabRefs.current[idx]; if (!el) return; setPillRect({ top: el.offsetTop, height: el.offsetHeight }); - }, [section, mobile]); + }, [section]); return (
e.stopPropagation()} style={{ - width: '100%', - maxWidth: 920, - height: '100%', - maxHeight: mobile ? 'none' : 620, - display: 'flex', - flexDirection: mobile ? 'column' : 'row', - overflow: 'hidden', + width: '100%', maxWidth: 880, height: '100%', maxHeight: 600, + background: 'var(--ol-surface)', + borderRadius: 14, + border: '0.5px solid rgba(0,0,0,.08)', + boxShadow: '0 30px 80px -20px rgba(15,17,22,.35), 0 0 0 0.5px rgba(0,0,0,.06)', + display: 'flex', overflow: 'hidden', animation: 'ol-modal-card-in 0.24s var(--ol-motion-spring)', position: 'relative', }}> {/* ─── 单层侧栏 ────────────────────────────────────────────── */} {/* ─── 内容区 ────────────────────────────────────────────── 父容器 overflow:hidden + 列向 flex;关闭按钮、section 标题固定在头部, 只有最里层的 scroll wrapper 真正滚动。 */} -
+
{/* "已保存" toast:right:54 避开 28×28 关闭按钮 + 12px gap。 */} -

+

{t(`modal.sections.${section}`)}

+ style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '10px 28px 28px' }}> {/* key=section 让切 tab 时整块重挂载,ol-tab-fade 轻微淡入。 */}
- + {formatComboLabel(value)}
@@ -195,7 +195,7 @@ export function ShortcutRecorder({ tabIndex={-1} onKeyDown={onKeyDown} onKeyUp={onKeyUp} - style={{ padding: '8px 12px', borderRadius: 'var(--ol-control-radius)', background: 'rgba(37,99,235,0.06)', border: '1px solid rgba(37,99,235,0.2)', fontSize: 12, color: 'var(--ol-blue)', outline: 'none' }} + style={{ padding: '8px 12px', borderRadius: 8, background: 'rgba(37,99,235,0.06)', border: '1px solid rgba(37,99,235,0.2)', fontSize: 12, color: 'var(--ol-blue)', outline: 'none' }} ref={el => el?.focus()} > {t('settings.recording.comboRecordHint')} diff --git a/openless-all/app/src/components/ui/Modal.tsx b/openless-all/app/src/components/ui/Modal.tsx index 91594cab..e84d376a 100644 --- a/openless-all/app/src/components/ui/Modal.tsx +++ b/openless-all/app/src/components/ui/Modal.tsx @@ -22,7 +22,7 @@ export function Modal({ children, onClose, zIndex = 50, width = 'min(560px, 100% style={{ position: 'fixed', inset: 0, - background: 'var(--ol-overlay-bg)', + background: 'rgba(0,0,0,0.22)', display: 'grid', placeItems: 'center', zIndex, @@ -36,10 +36,10 @@ export function Modal({ children, onClose, zIndex = 50, width = 'min(560px, 100% width, maxHeight: '85vh', overflow: 'auto', - borderRadius: 'var(--ol-modal-radius)', - background: 'var(--ol-card-bg)', - border: '0.5px solid var(--ol-card-border)', - boxShadow: 'var(--ol-shadow-lg)', + borderRadius: 16, + background: 'var(--ol-surface)', + border: '0.5px solid var(--ol-line-strong)', + boxShadow: '0 18px 42px rgba(0,0,0,0.18)', padding: 22, animation: 'ol-modal-card-in 0.24s var(--ol-motion-spring)', }} diff --git a/openless-all/app/src/components/ui/Row.tsx b/openless-all/app/src/components/ui/Row.tsx index 867693ac..a2723017 100644 --- a/openless-all/app/src/components/ui/Row.tsx +++ b/openless-all/app/src/components/ui/Row.tsx @@ -1,7 +1,6 @@ // Row — two-column row used in the Settings modal sub-sections. import type { ReactNode } from 'react'; -import { useMobileLayout } from '../../lib/useMobileLayout'; interface RowProps { label: string; @@ -10,14 +9,13 @@ interface RowProps { } export function Row({ label, desc, children }: RowProps) { - const mobile = useMobileLayout(); return ( -
-
+
+
{label}
{desc &&
{desc}
}
-
{children}
+
{children}
); } diff --git a/openless-all/app/src/components/ui/SegSimple.tsx b/openless-all/app/src/components/ui/SegSimple.tsx index bb97df07..802df27a 100644 --- a/openless-all/app/src/components/ui/SegSimple.tsx +++ b/openless-all/app/src/components/ui/SegSimple.tsx @@ -1,41 +1,32 @@ // SegSimple — segmented control used in the Settings modal sub-sections. -export type SegOption = { value: string; label: string }; +import { useState } from 'react'; interface SegSimpleProps { - options: SegOption[]; - value: string; - onChange?: (value: string) => void; + options: string[]; + active: string; } -export function SegSimple({ options, value, onChange }: SegSimpleProps) { +export function SegSimple({ options, active }: SegSimpleProps) { + const [v, setV] = useState(active); return ( -
- {options.map((o) => { - const selected = value === o.value; - return ( +
+ {options.map((o) => ( - ); - })} + ))}
); } diff --git a/openless-all/app/src/components/ui/SelectLite.tsx b/openless-all/app/src/components/ui/SelectLite.tsx index 45f87e7f..4822e50a 100644 --- a/openless-all/app/src/components/ui/SelectLite.tsx +++ b/openless-all/app/src/components/ui/SelectLite.tsx @@ -29,7 +29,6 @@ import { } from 'react'; import { createPortal } from 'react-dom'; import { Icon } from '../Icon'; -import { useMobileLayout } from '../../lib/useMobileLayout'; export interface SelectOption { value: string; @@ -60,7 +59,7 @@ const DEFAULT_TRIGGER_STYLE: CSSProperties = { height: 32, fontSize: 12.5, fontFamily: 'inherit', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface-2)', color: 'var(--ol-ink)', @@ -82,7 +81,6 @@ export function SelectLite({ ariaLabel, onOpenChange, }: SelectLiteProps) { - const mobile = useMobileLayout(); const [open, setOpen] = useState(false); // leaving 让 popover 在卸载前播完 exit keyframe(用户报"没有收缩动画"——之前直接 unmount) const [leaving, setLeaving] = useState(false); @@ -266,14 +264,6 @@ export function SelectLite({ const triggerStyle: CSSProperties = { ...DEFAULT_TRIGGER_STYLE, ...style, - boxSizing: 'border-box', - ...(mobile - ? { - width: style?.width ?? '100%', - minWidth: 0, - maxWidth: '100%', - } - : null), opacity: disabled ? 0.5 : 1, cursor: disabled ? 'not-allowed' : 'default', }; @@ -322,12 +312,12 @@ export function SelectLite({ maxHeight: 280, overflowY: 'auto', padding: 4, - borderRadius: 'var(--ol-r-md)', - border: '0.5px solid var(--ol-line-strong)', - background: 'var(--ol-glass-bg-strong)', - backdropFilter: 'blur(var(--ol-glass-blur)) saturate(180%)', - WebkitBackdropFilter: 'blur(var(--ol-glass-blur)) saturate(180%)', - boxShadow: 'var(--ol-shadow-md)', + borderRadius: 10, + border: '0.5px solid rgba(0, 0, 0, 0.10)', + background: 'rgba(252, 252, 254, 0.94)', + backdropFilter: 'blur(20px) saturate(180%)', + WebkitBackdropFilter: 'blur(20px) saturate(180%)', + boxShadow: '0 12px 30px -10px rgba(15, 17, 22, 0.25), 0 0 0 0.5px rgba(0, 0, 0, 0.06)', zIndex: 9999, fontFamily: 'inherit', fontSize: 12.5, @@ -357,11 +347,11 @@ export function SelectLite({ alignItems: 'center', gap: 8, padding: '7px 10px', - borderRadius: 'var(--ol-r-sm)', + borderRadius: 6, cursor: option.disabled ? 'not-allowed' : 'default', opacity: option.disabled ? 0.45 : 1, background: isHighlighted && !option.disabled - ? 'var(--ol-blue-soft)' + ? 'rgba(37, 99, 235, 0.10)' : 'transparent', color: isSelected ? 'var(--ol-blue)' : 'var(--ol-ink)', fontWeight: isSelected ? 600 : 500, diff --git a/openless-all/app/src/components/ui/SwitchLite.tsx b/openless-all/app/src/components/ui/SwitchLite.tsx index 691fb1c7..56181127 100644 --- a/openless-all/app/src/components/ui/SwitchLite.tsx +++ b/openless-all/app/src/components/ui/SwitchLite.tsx @@ -14,8 +14,8 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) { className="ol-focus-ring" onClick={() => setOn(!on)} style={{ - position: 'relative', width: 32, height: 18, borderRadius: 'var(--ol-pill-radius)', border: 0, - background: on ? 'var(--ol-blue)' : 'var(--ol-toggle-off-bg)', + position: 'relative', width: 32, height: 18, borderRadius: 999, border: 0, + background: on ? 'var(--ol-blue)' : 'rgba(0,0,0,0.18)', cursor: 'default', outline: 'none', }} @@ -23,7 +23,7 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) { diff --git a/openless-all/app/src/i18n/en.ts b/openless-all/app/src/i18n/en.ts index 4b98efed..7d45f2b2 100644 --- a/openless-all/app/src/i18n/en.ts +++ b/openless-all/app/src/i18n/en.ts @@ -750,7 +750,7 @@ export const en: typeof zhCN = { volcengineMappingNote: 'Secret Key is not required right now. Resource ID defaults to volc.seedasr.sauc.duration.', localAsrActiveNotice: 'Local ASR ({{name}}) is currently active. Switch or disable it from the Advanced tab.', localAsrTakeoverHint: 'Once "{{name}}" is enabled, the ASR provider will be taken over.', - asrProviderTakenOver: 'ASR provider taken over', + asrProviderTakenOver: 'A local engine is active. Pick another provider in the dropdown above to switch (the local engine stops automatically); manage local models under Advanced → Local models.', localAsrHint: 'Runs on this machine, no API key needed. Download the model from HuggingFace.', foundryLocalAsrHint: 'Runs on this device, no ASR API key needed. First use downloads runtime components and model.', localAsrPerformanceWarning: 'Local inference is slower than cloud ASR with potentially lower Chinese accuracy. Best for offline or privacy-sensitive use.', @@ -958,14 +958,6 @@ export const en: typeof zhCN = { ko: '한국어 (Beta)', restartHint: 'Some native menus (system tray, etc.) may require an app restart to fully switch.', }, - appearance: { - title: 'Appearance', - desc: 'Choose light or dark mode, or follow your system setting.', - label: 'Theme', - followSystem: 'Follow system', - light: 'Light', - dark: 'Dark', - }, remoteInput: { title: 'Remote Input', enableLabel: 'Enable remote input', diff --git a/openless-all/app/src/i18n/ja.ts b/openless-all/app/src/i18n/ja.ts index 445a9068..5942e5a6 100644 --- a/openless-all/app/src/i18n/ja.ts +++ b/openless-all/app/src/i18n/ja.ts @@ -926,14 +926,6 @@ export const ja: typeof zhCN = { ko: '한국어 (Beta)', restartHint: '一部のネイティブメニュー(トレイ等)は再起動後に反映されます。', }, - appearance: { - title: '外観', - desc: 'ライト/ダークモードを選ぶか、システム設定に従います。', - label: 'テーマ', - followSystem: 'システムに従う', - light: 'ライト', - dark: 'ダーク', - }, remoteInput: { title: 'リモート入力', enableLabel: 'リモート入力を有効化', diff --git a/openless-all/app/src/i18n/ko.ts b/openless-all/app/src/i18n/ko.ts index d80bd63f..07030b0c 100644 --- a/openless-all/app/src/i18n/ko.ts +++ b/openless-all/app/src/i18n/ko.ts @@ -926,14 +926,6 @@ export const ko: typeof zhCN = { ko: '한국어 (Beta)', restartHint: '일부 네이티브 메뉴(트레이 등)는 앱 재시작 후 반영될 수 있습니다.', }, - appearance: { - title: '외관', - desc: '라이트/다크 모드를 선택하거나 시스템 설정을 따릅니다.', - label: '테마', - followSystem: '시스템 따라가기', - light: '라이트', - dark: '다크', - }, remoteInput: { title: '원격 입력', enableLabel: '원격 입력 활성화', diff --git a/openless-all/app/src/i18n/zh-CN.ts b/openless-all/app/src/i18n/zh-CN.ts index e26457ab..e476f232 100644 --- a/openless-all/app/src/i18n/zh-CN.ts +++ b/openless-all/app/src/i18n/zh-CN.ts @@ -748,7 +748,7 @@ export const zhCN = { volcengineMappingNote: 'Secret Key 当前无需填写。Resource ID 默认使用 volc.seedasr.sauc.duration。', localAsrActiveNotice: '当前已启用「{{name}}」,可在「高级」中切换或禁用。', localAsrTakeoverHint: '启动「{{name}}」后,ASR 提供商将被接管。', - asrProviderTakenOver: 'ASR 提供商已被接管', + asrProviderTakenOver: '当前用的是本地引擎,在上方下拉直接选其它供应商即可切换(本地引擎会自动停用);本地模型在「高级 → 本地模型」里管理。', localAsrHint: '在本机运行,无需 API Key。从 HuggingFace 下载模型即可使用。', foundryLocalAsrHint: '在本机运行,无需 ASR API Key。首次使用需下载运行组件和模型。', localAsrPerformanceWarning: '本地推理比云端慢,中文准确率可能更低。适合离线或隐私敏感场景。', @@ -956,14 +956,6 @@ export const zhCN = { ko: '한국어 (Beta)', restartHint: '部分原生菜单(系统托盘等)可能需要重启 App 才会切换。', }, - appearance: { - title: '外观', - desc: '选择浅色或深色模式,或跟随系统设置。', - label: '主题', - followSystem: '跟随系统', - light: '浅色', - dark: '深色', - }, remoteInput: { title: '远程输入', enableLabel: '启用远程输入', diff --git a/openless-all/app/src/i18n/zh-TW.ts b/openless-all/app/src/i18n/zh-TW.ts index 55aac730..523716fd 100644 --- a/openless-all/app/src/i18n/zh-TW.ts +++ b/openless-all/app/src/i18n/zh-TW.ts @@ -924,14 +924,6 @@ export const zhTW: typeof zhCN = { ko: '한국어 (Beta)', restartHint: '部分原生菜單(系統托盤等)可能需要重啓 App 纔會切換。', }, - appearance: { - title: '外觀', - desc: '選擇淺色或深色模式,或跟隨系統設置。', - label: '主題', - followSystem: '跟隨系統', - light: '淺色', - dark: '深色', - }, remoteInput: { title: '遠端輸入', enableLabel: '啟用遠端輸入', diff --git a/openless-all/app/src/lib/platform.ts b/openless-all/app/src/lib/platform.ts index 3e91c6d2..03badcdb 100644 --- a/openless-all/app/src/lib/platform.ts +++ b/openless-all/app/src/lib/platform.ts @@ -120,21 +120,3 @@ export async function getPlatformCapabilities(): Promise { export function getCachedPlatformCapabilities(): PlatformCapabilities | null { return cachedCapabilities; } - -/** Win11 原生标题栏暗色同步;非 Windows / 非 Tauri 为 no-op。 */ -export async function syncWindowsCaptionTheme(dark: boolean): Promise { - if (typeof window === 'undefined') return; - if (detectOS() !== 'win') return; - - const isTauri = - globalThis.window !== undefined && - '__TAURI_INTERNALS__' in globalThis.window; - if (!isTauri) return; - - try { - const { invoke } = await import('@tauri-apps/api/core'); - await invoke('set_windows_caption_theme', { dark }); - } catch (err) { - console.warn('[platform] set_windows_caption_theme failed', err); - } -} diff --git a/openless-all/app/src/lib/themeMode.ts b/openless-all/app/src/lib/themeMode.ts deleted file mode 100644 index 5c1138a5..00000000 --- a/openless-all/app/src/lib/themeMode.ts +++ /dev/null @@ -1,52 +0,0 @@ -// 主题模式 — localStorage 持久化,通过 html[data-ol-theme] 切换暗色 token 集。 - -import { syncWindowsCaptionTheme } from './platform'; - -export type ThemeMode = 'system' | 'light' | 'dark'; -export type ResolvedThemeMode = 'light' | 'dark'; - -const THEME_MODE_KEY = 'ol-theme-mode'; - -function systemPrefersDark(): boolean { - return window.matchMedia('(prefers-color-scheme: dark)').matches; -} - -export function resolveThemeMode(mode: ThemeMode): ResolvedThemeMode { - if (mode === 'dark') return 'dark'; - if (mode === 'light') return 'light'; - return systemPrefersDark() ? 'dark' : 'light'; -} - -export function readThemeMode(): ThemeMode { - try { - const v = window.localStorage.getItem(THEME_MODE_KEY); - if (v === 'system' || v === 'light' || v === 'dark') return v; - } catch { /* localStorage 不可用:忽略,落回默认 */ } - return 'system'; -} - -export function applyThemeMode(mode: ThemeMode): void { - const resolved = resolveThemeMode(mode); - const root = document.documentElement; - if (resolved === 'dark') { - root.dataset.olTheme = 'dark'; - } else { - delete root.dataset.olTheme; - } - void syncWindowsCaptionTheme(resolved === 'dark'); -} - -export function setThemeMode(mode: ThemeMode): ThemeMode { - try { window.localStorage.setItem(THEME_MODE_KEY, mode); } catch { /* 忽略 */ } - applyThemeMode(mode); - return mode; -} - -export function subscribeThemeMode(): () => void { - const mq = window.matchMedia('(prefers-color-scheme: dark)'); - const onChange = () => { - if (readThemeMode() === 'system') applyThemeMode('system'); - }; - mq.addEventListener('change', onChange); - return () => mq.removeEventListener('change', onChange); -} diff --git a/openless-all/app/src/main.tsx b/openless-all/app/src/main.tsx index 3fff5d83..73111798 100644 --- a/openless-all/app/src/main.tsx +++ b/openless-all/app/src/main.tsx @@ -3,14 +3,9 @@ import ReactDOM from "react-dom/client"; import { App } from "./App"; import { detectOS } from "./components/WindowChrome"; import i18n from "./i18n"; // 副作用:触发 i18next init -import { applyThemeMode, readThemeMode, subscribeThemeMode } from "./lib/themeMode"; import "./styles/tokens.css"; import "./styles/global.css"; -// 首帧前应用主题,避免暗色偏好下先闪一帧亮色。 -applyThemeMode(readThemeMode()); -subscribeThemeMode(); - import type { OS } from "./components/WindowChrome"; const params = new URLSearchParams(window.location.search); diff --git a/openless-all/app/src/pages/History.tsx b/openless-all/app/src/pages/History.tsx index f51c7ad6..f030fe9b 100644 --- a/openless-all/app/src/pages/History.tsx +++ b/openless-all/app/src/pages/History.tsx @@ -1,16 +1,15 @@ // History.tsx — 接 Tauri 后端 list_history / delete_history_entry / clear_history。 // 真实数据来自 ~/Library/Application Support/OpenLess/history.json。 -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from '../components/Icon'; import { detectOS } from '../components/WindowChrome'; import { formatComboLabel } from '../lib/hotkey'; -import { clearHistory, deleteHistoryEntry, listHistory, readAudioRecording, retranscribeRecording } from '../lib/ipc'; +import { clearHistory, deleteHistoryEntry, listHistory, readAudioRecording } from '../lib/ipc'; import type { DictationSession, PolishMode } from '../lib/types'; import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { Btn, Card, PageHeader, Pill } from './_atoms'; -import { useMobileLayout } from '../lib/useMobileLayout'; function useFilters(): Array<{ id: 'all' | PolishMode; label: string }> { const { t } = useTranslation(); @@ -36,21 +35,15 @@ function useModeLabel(): Record { export function History() { const { t } = useTranslation(); const os = detectOS(); - const mobile = useMobileLayout(); const FILTERS = useFilters(); const MODE_LABEL = useModeLabel(); const [filter, setFilter] = useState<'all' | PolishMode>('all'); - // issue #612:历史页顶部原为静态 div,只显示统计、不可输入。改为真实搜索框。 - const [query, setQuery] = useState(''); - const searchRef = useRef(null); const [items, setItems] = useState([]); const [selectedId, setSelectedId] = useState(null); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const [actionError, setActionError] = useState(null); const [justCopied, setJustCopied] = useState(false); - // issue #613:「重新转录」进行中的条目 id(按钮转 loading、防重复点击)。 - const [retranscribingId, setRetranscribingId] = useState(null); // 录音文件 lazily-detected missing 状态:retention / 条数 cap 清理后磁盘上 wav // 可能已被删,但 history 条目 hasAudioRecording 仍写 true。任一组件 // (播放 / 导出)首次 IPC 拿到 'recording not found' 时把 id 加进来, @@ -87,29 +80,10 @@ export function History() { void refresh(); }, [refresh]); - // ⌘K / Ctrl+K 聚焦搜索框(issue #612 验收可选项)。 - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) { - e.preventDefault(); - searchRef.current?.focus(); - } - }; - window.addEventListener('keydown', onKey); - return () => window.removeEventListener('keydown', onKey); - }, []); - - const filtered = useMemo(() => { - const byMode = filter === 'all' ? items : items.filter(s => s.mode === filter); - const q = query.trim().toLowerCase(); - if (!q) return byMode; - return byMode.filter( - s => - s.finalText.toLowerCase().includes(q) || - s.rawTranscript.toLowerCase().includes(q) || - (s.appName ?? '').toLowerCase().includes(q), - ); - }, [items, filter, query]); + const filtered = useMemo( + () => (filter === 'all' ? items : items.filter(s => s.mode === filter)), + [items, filter], + ); const item = useMemo( () => filtered.find(s => s.id === selectedId) || filtered[0], [filtered, selectedId], @@ -189,28 +163,6 @@ export function History() { } }; - // issue #613:对失败条目的归档录音用当前 provider 重新转录,成功后局部刷新该条。 - const onRetranscribe = async () => { - if (!item || retranscribingId) return; - const targetId = item.id; - setRetranscribingId(targetId); - setActionError(null); - try { - const updated = await retranscribeRecording(targetId); - setItems(prev => prev.map(s => (s.id === targetId ? updated : s))); - } catch (error) { - console.error('[history] failed to retranscribe recording', error); - const msg = errorMessage(error); - // wav 已被清理:隐藏录音相关操作,不当作用户错误。 - if (msg.includes('recording not found') || msg.includes('not found')) { - markAudioMissing(targetId); - } - setActionError(t('history.retranscribeFailed', { err: msg })); - } finally { - setRetranscribingId(null); - } - }; - return (
} /> -
- +
+
- setQuery(e.target.value)} - placeholder={t('history.searchPlaceholder', { shortcut: os === 'mac' ? '⌘K' : 'Ctrl+K' })} - style={{ - flex: 1, minWidth: 0, outline: 'none', border: 0, - background: 'transparent', fontSize: 12, color: 'var(--ol-ink-2)', - fontFamily: 'inherit', - }} - /> -
-
- {t('history.summary', { total: items.length, shown: filtered.length })} + {t('history.summary', { total: items.length, shown: filtered.length })}
{FILTERS.map(f => ( @@ -264,10 +194,10 @@ export function History() { key={f.id} onClick={() => setFilter(f.id)} style={{ - padding: '3px 9px', fontSize: 11, borderRadius: 'var(--ol-pill-radius)', - border: '0.5px solid ' + (filter === f.id ? 'var(--ol-pill-selected-border)' : 'var(--ol-line-strong)'), - background: filter === f.id ? 'var(--ol-pill-selected-bg)' : 'transparent', - color: filter === f.id ? 'var(--ol-pill-selected-ink)' : 'var(--ol-ink-3)', + padding: '3px 9px', fontSize: 11, borderRadius: 999, + border: '0.5px solid ' + (filter === f.id ? 'var(--ol-ink)' : 'var(--ol-line-strong)'), + background: filter === f.id ? 'var(--ol-ink)' : 'transparent', + color: filter === f.id ? '#fff' : 'var(--ol-ink-3)', cursor: 'default', fontFamily: 'inherit', fontWeight: 500, transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)', }} @@ -277,7 +207,7 @@ export function History() {
{actionError && ( -
+
{actionError}
)} @@ -290,9 +220,7 @@ export function History() { )} {!loading && !loadError && filtered.length === 0 && (
- {query.trim() - ? t('history.searchNoMatch', { query: query.trim() }) - : t('history.empty', { trigger: prefs ? formatComboLabel(prefs.dictationHotkey) : '' })} + {t('history.empty', { trigger: prefs ? formatComboLabel(prefs.dictationHotkey) : '' })}
)} {!loadError && filtered.map(s => ( @@ -302,7 +230,7 @@ export function History() { style={{ width: '100%', padding: '10px 12px', textAlign: 'left', display: 'flex', flexDirection: 'column', gap: 4, - border: 0, borderRadius: 'var(--ol-control-radius)', + border: 0, borderRadius: 8, background: selectedId === s.id ? 'rgba(37,99,235,0.06)' : 'transparent', boxShadow: selectedId === s.id ? 'inset 2px 0 0 var(--ol-blue)' : 'none', cursor: 'default', fontFamily: 'inherit', marginBottom: 1, @@ -326,29 +254,17 @@ export function History() {
- + {item ? ( <> -
-
+
+
{formatTime(item.createdAt)} {MODE_LABEL[item.mode]} {formatDuration(item.durationMs, t)}
-
+
void onCopy()}>{justCopied ? t('common.copied') : t('common.copy')} - {/* issue #613:失败条目(有错误码)且录音仍在时,提供「重新转录」。 */} - {item.errorCode && item.hasAudioRecording && !audioMissingIds.has(item.id) && ( - void onRetranscribe()} - > - {retranscribingId === item.id ? t('history.retranscribing') : t('history.retranscribe')} - - )} {item.hasAudioRecording && !audioMissingIds.has(item.id) && ( void onExportAudio()}>{t('history.exportRecording')} )} @@ -362,14 +278,14 @@ export function History() { key={item.id} /> )} -
-
+
+
{t('history.rawLabel')}

{item.rawTranscript || t('history.rawEmpty')}

-
+
{MODE_LABEL[item.mode]}

{item.finalText} diff --git a/openless-all/app/src/pages/LessComputerPanel.tsx b/openless-all/app/src/pages/LessComputerPanel.tsx index fe729b5e..206c0e85 100644 --- a/openless-all/app/src/pages/LessComputerPanel.tsx +++ b/openless-all/app/src/pages/LessComputerPanel.tsx @@ -414,11 +414,11 @@ const shellStyle: CSSProperties = { height: '100vh', display: 'flex', flexDirection: 'column', - borderRadius: 'var(--ol-bubble-radius)', + borderRadius: 14, overflow: 'hidden', - border: '0.5px solid var(--ol-line-strong)', - background: 'var(--ol-panel-bg)', - boxShadow: 'var(--ol-shadow-lg)', + border: '0.5px solid rgba(0, 0, 0, 0.12)', + background: 'rgba(246, 247, 250, 0.88)', + boxShadow: '0 18px 44px -18px rgba(15,17,22,.28), 0 0 0 0.5px rgba(255,255,255,.7) inset', fontFamily: 'var(--ol-font-sans)', color: 'var(--ol-ink)', isolation: 'isolate', @@ -430,8 +430,9 @@ const toolbarStyle: CSSProperties = { alignItems: 'center', padding: '0 8px', borderBottom: '0.5px solid rgba(0, 0, 0, 0.08)', - background: 'var(--ol-panel-bg)', - boxShadow: 'var(--ol-shadow-sm)', + background: + 'linear-gradient(180deg, rgba(255,255,255,0.74), rgba(238,240,245,0.58))', + boxShadow: '0 1px 0 rgba(255,255,255,.55) inset', backdropFilter: 'blur(18px) saturate(150%)', WebkitBackdropFilter: 'blur(18px) saturate(150%)', flexShrink: 0, @@ -443,7 +444,7 @@ const closeBtnStyle: CSSProperties = { width: 22, height: 22, border: 0, - borderRadius: 'var(--ol-r-sm)', + borderRadius: 6, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -478,10 +479,10 @@ const roleLabelStyle: CSSProperties = { const userBubbleStyle: CSSProperties = { maxWidth: '85%', padding: '8px 12px', - borderRadius: 'var(--ol-bubble-radius)', + borderRadius: 14, borderBottomRightRadius: 4, - background: 'var(--ol-accent-solid-bg)', - color: 'var(--ol-accent-solid-ink)', + background: 'var(--ol-blue)', + color: '#fff', fontSize: 13, lineHeight: 1.55, wordBreak: 'break-word', @@ -490,7 +491,7 @@ const userBubbleStyle: CSSProperties = { const assistantBubbleStyle: CSSProperties = { maxWidth: '92%', padding: '8px 12px', - borderRadius: 'var(--ol-bubble-radius)', + borderRadius: 14, borderBottomLeftRadius: 4, background: 'rgba(0,0,0,0.04)', fontSize: 13, @@ -518,7 +519,7 @@ const toolChipStyle: CSSProperties = { color: 'var(--ol-ink-2)', background: 'rgba(0,0,0,0.045)', border: '0.5px solid rgba(0,0,0,0.06)', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, padding: '4px 8px', }; @@ -542,7 +543,7 @@ const approvalCardStyle: CSSProperties = { flexDirection: 'column', gap: 6, padding: '10px 12px', - borderRadius: 'var(--ol-r-lg)', + borderRadius: 12, background: 'rgba(220,38,38,0.05)', border: '0.5px solid rgba(220,38,38,0.20)', }; @@ -552,7 +553,7 @@ const approvalCmdStyle: CSSProperties = { fontSize: 11.5, color: 'var(--ol-ink)', background: 'rgba(0,0,0,0.05)', - borderRadius: 'var(--ol-r-sm)', + borderRadius: 6, padding: '5px 8px', wordBreak: 'break-all', }; @@ -563,25 +564,25 @@ const approvalRerunWarningStyle: CSSProperties = { color: 'rgb(180,83,9)', background: 'rgba(245,158,11,0.10)', border: '0.5px solid rgba(245,158,11,0.25)', - borderRadius: 'var(--ol-r-sm)', + borderRadius: 6, padding: '5px 8px', }; const approveBtnStyle: CSSProperties = { flex: 1, border: 0, - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, padding: '6px 10px', fontSize: 12, fontWeight: 600, cursor: 'default', - background: 'var(--ol-accent-solid-bg)', - color: 'var(--ol-accent-solid-ink)', + background: 'var(--ol-blue)', + color: '#fff', }; const denyBtnStyle: CSSProperties = { flex: 1, - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, padding: '6px 10px', fontSize: 12, fontWeight: 600, @@ -593,7 +594,7 @@ const denyBtnStyle: CSSProperties = { const errorRowStyle: CSSProperties = { padding: '8px 12px', - borderRadius: 'var(--ol-r-md)', + borderRadius: 10, background: 'rgba(220,38,38,0.06)', border: '0.5px solid rgba(220,38,38,0.18)', }; @@ -632,7 +633,7 @@ const globalCss = ` padding: 1px 5px; border-radius: 4px; background: rgba(0,0,0,0.05); } .lc-answer pre { margin: 0 0 6px; padding: 8px 10px; - border-radius: var(--ol-control-radius); background: rgba(0,0,0,0.05); + border-radius: 8px; background: rgba(0,0,0,0.05); overflow-x: auto; } .lc-answer pre code { padding: 0; background: transparent; } .lc-answer a { color: var(--ol-blue); text-decoration: none; } diff --git a/openless-all/app/src/pages/LocalAsr.tsx b/openless-all/app/src/pages/LocalAsr.tsx index 62589498..5c5624a8 100644 --- a/openless-all/app/src/pages/LocalAsr.tsx +++ b/openless-all/app/src/pages/LocalAsr.tsx @@ -1842,7 +1842,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -1903,7 +1903,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -1954,7 +1954,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2266,7 +2266,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { fontSize: 13, height: 31, padding: "0 10px", - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2301,7 +2301,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { fontSize: 13, height: 31, padding: "0 10px", - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2334,7 +2334,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { fontSize: 13, height: 31, padding: "0 10px", - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2613,7 +2613,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2741,7 +2741,7 @@ export function LocalAsr({ embedded = false }: LocalAsrProps = {}) { style={{ fontSize: 13, padding: "6px 10px", - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, border: "0.5px solid rgba(0,0,0,0.12)", background: "var(--ol-surface)", color: "var(--ol-ink)", @@ -2905,7 +2905,7 @@ function FoundryPrepareProgressBlock({

@@ -498,7 +498,7 @@ export function Marketplace() { padding: '6px 10px', fontSize: 12, border: '0.5px solid var(--ol-line-strong)', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, cursor: 'pointer', background: sort === p.id ? 'var(--ol-blue-soft)' : 'var(--ol-surface)', color: sort === p.id ? 'var(--ol-blue)' : 'var(--ol-ink-2)', @@ -567,7 +567,7 @@ export function Marketplace() { style={{ textAlign: 'left', padding: 14, - borderRadius: 'var(--ol-r-lg)', + borderRadius: 12, border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', cursor: 'pointer', @@ -644,7 +644,7 @@ export function Marketplace() { style={{ padding: 12, border: '0.5px solid var(--ol-line)', - borderRadius: 'var(--ol-r-md)', + borderRadius: 10, background: 'var(--ol-surface-2)', marginBottom: 14, maxHeight: 280, @@ -679,7 +679,7 @@ export function Marketplace() { border: 'none', cursor: 'pointer', padding: '4px 8px', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, fontSize: 12, fontWeight: 500, color: 'var(--ol-ink-2)' @@ -744,7 +744,7 @@ export function Marketplace() { textAlign: 'left', padding: 10, border: selected ? '1px solid var(--ol-blue)' : '0.5px solid var(--ol-line-strong)', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, background: selected ? 'var(--ol-blue-soft)' : 'var(--ol-surface)', cursor: 'pointer', display: 'flex', @@ -755,11 +755,11 @@ export function Marketplace() { {/* 选中圈:未选空圆,选中蓝实心 + 白勾 */} {selected && '✓'} @@ -814,7 +814,7 @@ export function Marketplace() { gap: 6, padding: '6px 10px', border: '0.5px solid var(--ol-line-strong)', - borderRadius: 'var(--ol-r-md)', + borderRadius: 10, background: 'var(--ol-surface)', }} > @@ -843,7 +843,7 @@ export function Marketplace() { onClick={() => setShowLogin(true)} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, - padding: '5px 10px', borderRadius: 'var(--ol-control-radius)', + padding: '5px 10px', borderRadius: 9, border: '0.5px solid var(--ol-line-strong)', background: currentLogin ? 'var(--ol-blue-soft)' : 'var(--ol-surface)', color: currentLogin ? 'var(--ol-blue)' : 'var(--ol-ink-3)', @@ -853,7 +853,7 @@ export function Marketplace() { }} > setShowMyPacks(false)} style={{ - width: 30, height: 30, borderRadius: 'var(--ol-control-radius)', + width: 30, height: 30, borderRadius: 9, display: 'inline-grid', placeItems: 'center', border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', @@ -965,7 +965,7 @@ export function Marketplace() { key={pack.id} style={{ padding: 14, - borderRadius: 'var(--ol-r-lg)', + borderRadius: 12, border: '0.5px solid var(--ol-line-strong)', background: 'var(--ol-surface)', display: 'flex', diff --git a/openless-all/app/src/pages/Overview.tsx b/openless-all/app/src/pages/Overview.tsx index 84d5bdcd..118ef703 100644 --- a/openless-all/app/src/pages/Overview.tsx +++ b/openless-all/app/src/pages/Overview.tsx @@ -4,27 +4,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Icon } from '../components/Icon'; import { formatComboLabel } from '../lib/hotkey'; -import { - checkMicrophonePermission, - getCredentials, - getPlatformCapabilities, - listHistory, - startDictation, - stopDictation, -} from '../lib/ipc'; -import type { - CapsulePayload, - CapsuleState, - CredentialsStatus, - DictationSession, - PermissionStatus, - PlatformCapabilities, - PolishMode, -} from '../lib/types'; +import { getCredentials, listHistory } from '../lib/ipc'; +import type { CredentialsStatus, DictationSession, PolishMode } from '../lib/types'; import { useHotkeySettings } from '../state/HotkeySettingsContext'; import { Btn, Card, PageHeader, Pill } from './_atoms'; -import { useMobileLayout } from '../lib/useMobileLayout'; -import { checkAndroidMicrophoneAccess, requestAndroidMicrophoneAccess } from '../lib/androidMicrophonePermission'; function useModeLabels(): Record { const { t } = useTranslation(); @@ -70,7 +53,6 @@ const LLM_NAME_KEY_BY_ID: Record = { export function Overview({ onOpenHistory }: OverviewProps) { const { t } = useTranslation(); - const mobile = useMobileLayout(); const modeLabel = useModeLabels(); const [history, setHistory] = useState([]); const [historyError, setHistoryError] = useState(false); @@ -188,19 +170,9 @@ export function Overview({ onOpenHistory }: OverviewProps) { return ( <> - + - - - -
+
-
+
0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} /> @@ -225,8 +197,8 @@ export function Overview({ onOpenHistory }: OverviewProps) { {/* 底部一行 = flex:1 撑满剩余高度(父 wrapper 是 display:flex/column)。 只有「最近识别」内部允许滚动;其他卡片按内容自然高度,不破裂底部圆角。 issue #243 follow-up:去掉外层 overflow 后底部圆角被裁的视觉问题。 */} -
- +
+
{t('overview.weekTitle')} {t('overview.weekUnit')} @@ -243,12 +215,12 @@ export function Overview({ onOpenHistory }: OverviewProps) {
- +
{t('overview.recentTitle')} {t('overview.recentAll')}
-
+
{historyError ? (
{t('overview.recentLoadFailed')} @@ -282,14 +254,13 @@ interface ProviderCardProps { function ProviderCard({ kind, name, subname, status }: ProviderCardProps) { const { t } = useTranslation(); - const mobile = useMobileLayout(); // ASR 卡用 mic 图标,其他用 sparkle —— 通过比较译文判断会随语言改变,故改用本地化无关的字面量比较。 const isAsr = kind === t('overview.asrKind'); return ( - +
-
+
{kind} {status === 'configured' && ( - + {t('overview.statusConfigured')} )} @@ -331,14 +302,13 @@ interface MetricProps { } function Metric({ icon, label, value, trend, accent }: MetricProps) { - const mobile = useMobileLayout(); return ( - +
{label}
-
{value}
+
{value}
{trend || ' '}
); @@ -373,10 +343,9 @@ function WeekChart({ data }: { data: number[] }) { function RecentRow({ session, modeLabel }: { session: DictationSession; modeLabel: Record }) { const { t } = useTranslation(); - const mobile = useMobileLayout(); return ( -
-
+
+
{formatTime(session.createdAt)} @@ -417,189 +386,3 @@ function weekDayLabels(names: string[]): string[] { } return out; } - -function AndroidMicGrantBanner() { - const { t } = useTranslation(); - const mobile = useMobileLayout(); - const [platformCaps, setPlatformCaps] = useState(null); - const [microphone, setMicrophone] = useState('loading'); - const [busy, setBusy] = useState(false); - - useEffect(() => { - void getPlatformCapabilities().then(setPlatformCaps); - }, []); - - const refreshMic = useCallback(async () => { - if (platformCaps?.platform === 'android') { - setMicrophone(await checkAndroidMicrophoneAccess()); - return; - } - setMicrophone(await checkMicrophonePermission()); - }, [platformCaps?.platform]); - - useEffect(() => { - if (platformCaps?.platform !== 'android') return; - void refreshMic(); - const onFocus = () => { void refreshMic(); }; - window.addEventListener('focus', onFocus); - return () => window.removeEventListener('focus', onFocus); - }, [platformCaps?.platform, refreshMic]); - - if (platformCaps?.platform !== 'android') return null; - if (microphone === 'loading' || microphone === 'granted' || microphone === 'notApplicable') { - return null; - } - - const onGrant = async () => { - setBusy(true); - try { - setMicrophone(await requestAndroidMicrophoneAccess()); - } finally { - setBusy(false); - void refreshMic(); - } - }; - - return ( - -
- -
-
- {t('overview.androidMicBanner.title')} -
-
- {t('overview.androidMicBanner.desc')} -
-
-
- void onGrant()} disabled={busy} style={{ justifyContent: 'center', width: mobile ? '100%' : undefined }}> - {t('overview.androidMicBanner.grant')} - -
- ); -} - -function InAppDictationControl() { - const { t } = useTranslation(); - const mobile = useMobileLayout(); - const [platformCaps, setPlatformCaps] = useState(null); - const [capsuleState, setCapsuleState] = useState('idle'); - const [busy, setBusy] = useState(false); - - useEffect(() => { - void getPlatformCapabilities().then(setPlatformCaps); - }, []); - - useEffect(() => { - if (!platformCaps?.supportsInAppDictation) return; - let cancelled = false; - let unlisten: (() => void) | undefined; - void (async () => { - try { - const { listen } = await import('@tauri-apps/api/event'); - const handle = await listen('capsule:state', (event) => { - if (cancelled) return; - setCapsuleState(event.payload.state); - }); - if (cancelled) { - handle(); - } else { - unlisten = handle; - } - } catch { - // browser dev mock - } - })(); - return () => { - cancelled = true; - unlisten?.(); - }; - }, [platformCaps?.supportsInAppDictation]); - - if (!platformCaps?.supportsInAppDictation) { - return null; - } - - const recording = capsuleState === 'recording'; - const processing = capsuleState === 'transcribing' || capsuleState === 'polishing'; - const statusLabel = recording - ? t('overview.inAppDictation.recording') - : processing - ? t('overview.inAppDictation.processing') - : t('overview.inAppDictation.idle'); - - const onToggle = async () => { - if (busy || processing) return; - setBusy(true); - try { - if (recording) { - await stopDictation(); - } else { - if (platformCaps?.platform === 'android') { - const current = await checkAndroidMicrophoneAccess(); - const status = current === 'granted' - ? current - : await requestAndroidMicrophoneAccess(); - if (status !== 'granted') return; - } - await startDictation(); - } - } catch (error) { - console.error('[overview] in-app dictation toggle failed', error); - } finally { - setBusy(false); - } - }; - - return ( - - -
-
- {t('overview.inAppDictation.title')} -
-
- {statusLabel} -
-
- {!mobile && {statusLabel}} -
- ); -} diff --git a/openless-all/app/src/pages/SelectionAsk.tsx b/openless-all/app/src/pages/SelectionAsk.tsx index 04cc8dae..980fad77 100644 --- a/openless-all/app/src/pages/SelectionAsk.tsx +++ b/openless-all/app/src/pages/SelectionAsk.tsx @@ -100,7 +100,7 @@ export function SelectionAsk() { alignItems: 'center', gap: 12, padding: '8px 14px', - borderRadius: 'var(--ol-r-md)', + borderRadius: 10, background: 'rgba(0,0,0,0.04)', border: '0.5px solid var(--ol-line)', }} @@ -116,7 +116,7 @@ export function SelectionAsk() { position: 'relative', width: 36, height: 20, - borderRadius: 'var(--ol-pill-radius)', + borderRadius: 999, border: 0, background: prefs.qaSaveHistory ? 'var(--ol-blue)' : 'rgba(0,0,0,0.18)', cursor: saving ? 'not-allowed' : 'default', @@ -132,8 +132,8 @@ export function SelectionAsk() { left: prefs.qaSaveHistory ? 18 : 2, width: 16, height: 16, - borderRadius: 'var(--ol-pill-radius)', - background: 'var(--ol-control-solid)', + borderRadius: 999, + background: '#fff', boxShadow: '0 1px 2px rgba(0,0,0,.2)', transition: 'left .16s var(--ol-motion-spring)', }} diff --git a/openless-all/app/src/pages/Style.tsx b/openless-all/app/src/pages/Style.tsx index 43cabc5c..b3f0396d 100644 --- a/openless-all/app/src/pages/Style.tsx +++ b/openless-all/app/src/pages/Style.tsx @@ -127,6 +127,10 @@ export function Style() { const [packs, setPacks] = useState([]); const [selectedId, setSelectedId] = useState(null); + // prefs:changed 监听器用它读「当前选中」,避免把 selectedId 放进 effect 依赖 + // 导致每次切换风格包都 unlisten + 重新 listen(两次 IPC/次点击 → 卡顿)。 + const selectedIdRef = useRef(selectedId); + selectedIdRef.current = selectedId; const [draft, setDraft] = useState(null); const [busy, setBusy] = useState('loading'); const [saveState, setSaveState] = useState('idle'); @@ -192,7 +196,7 @@ export function Style() { try { const { listen } = await import('@tauri-apps/api/event'); unlisten = await listen('prefs:changed', () => { - void loadPacks(selectedId); + void loadPacks(selectedIdRef.current); }); if (cancelled && unlisten) unlisten(); } catch { @@ -203,7 +207,7 @@ export function Style() { cancelled = true; unlisten?.(); }; - }, [selectedId]); + }, []); const selectedPack = packs.find(pack => pack.id === selectedId) ?? null; const activePack = packs.find(pack => pack.active) ?? null; @@ -570,7 +574,7 @@ export function Style() { alignItems: 'center', gap: 6, padding: '6px 12px', - borderRadius: 'var(--ol-pill-radius)', + borderRadius: 999, border: '0.5px solid', borderColor: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-line-strong)', background: rawPack.active ? 'var(--ol-blue-soft)' : 'transparent', @@ -599,6 +603,9 @@ export function Style() {
-
+
{pack.name}
@@ -642,7 +653,7 @@ export function Style() {
@@ -676,11 +687,11 @@ export function Style() { aria-label={t('style.pack.deleteImported')} title={t('style.pack.deleteImported')} style={{ - width: 36, height: 36, borderRadius: 'var(--ol-r-lg)', + width: 36, height: 36, borderRadius: 12, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, - border: '0.5px solid var(--ol-style-delete-border)', - background: 'var(--ol-style-delete-bg)', + border: '0.5px solid rgba(239,68,68,0.32)', + background: 'rgba(254,242,242,0.6)', color: 'var(--ol-red, #ef4444)', cursor: busy === 'deleting' ? 'wait' : 'pointer', opacity: busy === 'deleting' ? 0.55 : 1, @@ -751,11 +762,11 @@ export function Style() { justifyContent: 'center', gap: 8, textAlign: 'center', - border: '0.5px dashed var(--ol-style-add-tile-border)', - borderRadius: 'var(--ol-modal-radius)', + border: '0.5px dashed var(--ol-line-strong)', + borderRadius: 18, padding: 16, - background: 'var(--ol-style-add-tile-bg)', - color: 'var(--ol-style-card-ink-3)', + background: 'transparent', + color: 'var(--ol-ink-3)', cursor: busy === 'creating' ? 'wait' : 'pointer', opacity: busy === 'creating' ? 0.55 : 1, minHeight: 204, @@ -764,16 +775,16 @@ export function Style() { >
-
{t('style.pack.addPackTileTitle')}
-
{t('style.pack.addPackTileHint')}
+
{t('style.pack.addPackTileTitle')}
+
{t('style.pack.addPackTileHint')}
@@ -839,7 +850,7 @@ export function Style() { style={{ width: 28, height: 28, - borderRadius: 'var(--ol-pill-radius)', + borderRadius: 999, border: 0, background: 'transparent', color: 'var(--ol-ink-3)', @@ -986,9 +997,8 @@ export function Style() {
@@ -1054,10 +1064,9 @@ export function Style() {
@@ -1093,9 +1102,7 @@ export function Style() { key={`${draft.id}-example-${index}`} padding={16} style={{ - background: 'var(--ol-style-card-bg)', - border: '0.5px solid var(--ol-style-card-border)', - boxShadow: 'var(--ol-style-card-shadow)', + background: 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(248,250,252,0.98))', }} >
@@ -1113,7 +1120,7 @@ export function Style() { width: 32, height: 32, flexShrink: 0, border: '0.5px solid var(--ol-line-strong)', - borderRadius: 'var(--ol-control-radius)', + borderRadius: 8, background: 'transparent', color: 'var(--ol-ink-2)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -1128,9 +1135,9 @@ export function Style() {
@@ -1140,15 +1147,15 @@ export function Style() {