Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
423 changes: 421 additions & 2 deletions openless-all/app/scripts/aura-skin-contract.test.mjs

Large diffs are not rendered by default.

55 changes: 42 additions & 13 deletions openless-all/app/scripts/windows-ui-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const capsuleTsx = await readFile(new URL('../src/components/Capsule.tsx', impor
const capsuleLayoutTs = await readFile(new URL('../src/lib/capsuleLayout.ts', import.meta.url), 'utf-8');
const windowChromeTsx = await readFile(new URL('../src/components/WindowChrome.tsx', import.meta.url), 'utf-8');
const floatingShellTsx = await readFile(new URL('../src/components/FloatingShell.tsx', import.meta.url), 'utf-8');
const tokensCss = await readFile(new URL('../src/styles/tokens.css', import.meta.url), 'utf-8');
const themeModeTs = await readFile(new URL('../src/lib/themeMode.ts', import.meta.url), 'utf-8');
const platformTs = await readFile(new URL('../src/lib/platform.ts', import.meta.url), 'utf-8');

if (!capsuleWindow) {
throw new Error('capsule window config missing');
Expand All @@ -34,13 +35,31 @@ assertEqual(capsuleWindow.width, 220, 'windows capsule config keeps translation-
assertEqual(capsuleWindow.height, 110, 'windows capsule config keeps translation-capable height baseline');
assertEqual(capsuleWindow.transparent, true, 'capsule window should keep transparent visuals');
assertEqual(capsuleWindow.alwaysOnTop, true, 'capsule window should stay above the focused app while recording');
assertEqual(mainWindow.decorations, true, 'shared main window config should keep native macOS traffic lights');
assertEqual(mainWindow.decorations, true, 'windows main window should keep native decorations');
assertEqual(mainWindow.visible, false, 'windows main window should stay hidden until the intended first show point');

assertMatch(
libRs,
/#\[cfg\(target_os = "windows"\)\][\s\S]*?main\.set_decorations\(false\)/,
'windows runtime should disable native chrome before the first show',
/fn apply_windows_caption_theme[\s\S]*?DWMWA_USE_IMMERSIVE_DARK_MODE[\s\S]*?DWMWA_CAPTION_COLOR[\s\S]*?DWMWA_TEXT_COLOR[\s\S]*?DWMWA_BORDER_COLOR/,
'windows runtime should sync immersive dark mode and caption/text/border colors',
);

assertMatch(
libRs,
/#\[tauri::command\][\s\S]*?fn set_windows_caption_theme/,
'windows caption theme should be exposed as a Tauri command',
);

assertMatch(
themeModeTs,
/export function applyThemeMode[\s\S]*?syncWindowsCaptionTheme/,
'applyThemeMode should sync Windows native caption theme',
);

assertMatch(
platformTs,
/export async function syncWindowsCaptionTheme[\s\S]*?set_windows_caption_theme/,
'platform IPC wrapper should invoke set_windows_caption_theme',
);

assertMatch(
Expand All @@ -49,10 +68,18 @@ assertMatch(
'macOS capsule should show without taking the key window',
);

if (!/function WindowsResizeHandles\(\)/.test(windowChromeTsx)) {
throw new Error('windows frameless shell should expose explicit resize handles');
const tokensCss = await readFile(new URL('../src/styles/tokens.css', import.meta.url), 'utf-8');

if (!/os === 'win' \|\| os === 'android' \? 0 : 14/.test(windowChromeTsx)) {
throw new Error('windows main shell should rely on native decorations instead of a frameless chrome shell');
}

assertMatch(
windowChromeTsx,
/\/\/ Windows: decorations:true 时外层不画圆角/,
'windows WindowChrome should defer chrome to native decorations',
);

assertMatch(
windowChromeTsx,
/const MAC_TITLEBAR_HEIGHT = 28;/,
Expand All @@ -66,21 +93,23 @@ assertMatch(
if (/standardWindowButton|setFrameOrigin: origin|tune_macos_main_window_controls/.test(libRs)) {
throw new Error('macOS traffic lights should not be manually repositioned; keep native AppKit button frames visible');
}
if (!/action=\"close\"/.test(windowChromeTsx) || !/tone=\"danger\"/.test(windowChromeTsx)) {
throw new Error('windows titlebar should keep the close button and danger hover treatment');
if (!/className=\"ol-linux-close-btn\"/.test(windowChromeTsx)) {
throw new Error('linux titlebar should keep the close button treatment');
}
assertMatch(
tokensCss,
/--ol-motion-spring:[\s\S]*?--ol-motion-soft:[\s\S]*?--ol-motion-quick:/,
'shared motion tokens should drive shell animations and transitions',
);

if (!/startResizeDragging\(direction\)/.test(windowChromeTsx)) {
throw new Error('windows resize handles should delegate edge dragging to Tauri');
}
assertMatch(
windowChromeTsx,
/function LinuxTitlebar\(\)/,
'linux should keep the custom ol-linux-titlebar shell',
);

if (!/borderRadius:\s*'var\(--ol-window-console-radius\)'/.test(floatingShellTsx)) {
throw new Error('floating shell should consume the shared window-console radius');
if (!/borderRadius:\s*'var\(--ol-r-lg\)'/.test(floatingShellTsx)) {
throw new Error('floating shell should consume the shared radius token');
}

assertMatch(
Expand Down
82 changes: 63 additions & 19 deletions openless-all/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ macro_rules! app_invoke_handler_desktop {
commands::sherpa_onnx_asr_reveal_model_dir,
commands::export_error_log,
restart_app,
set_windows_caption_theme,
]
};
}
Expand Down Expand Up @@ -507,9 +508,9 @@ fn run_desktop() {
if let Err(e) = apply_mica(&main, None) {
log::warn!("[main] mica failed: {e}");
}
// Win11 22H2+: 把原生标题栏底色调成白色,与应用 sidebar 视觉统一
// Win11 22H2+: 同步原生标题栏主题;前端就绪后会再调 set_windows_caption_theme
// 老版 Windows 静默失败,不阻塞。
apply_windows_caption_color(&main);
apply_windows_caption_theme(&main, false);
}
// 静默启动开关:prefs.start_minimized = true → 不弹主窗口,
// 用户从菜单栏 / 托盘点击访问。开机自启时尤其有用,避免每次
Expand Down Expand Up @@ -957,41 +958,84 @@ pub(crate) fn refresh_tray_microphone_menu(_app: &AppHandle) -> tauri::Result<()
Ok(())
}

/// 把 Win11 原生标题栏底色刷成白色,与应用 sidebar 视觉统一。需要 Win11 22H2+
/// (Build 22621+) 才支持 `DWMWA_CAPTION_COLOR`(35);老 Windows 上 DwmSetWindowAttribute
/// 返回错误,仅打 warn 不阻塞启动。
/// Win11 22H2+ (Build 22621+) 同步原生标题栏沉浸式暗色 / caption / text / border 色。
/// 老 Windows 上 DwmSetWindowAttribute 返回错误,仅打 warn 不阻塞启动。
#[cfg(target_os = "windows")]
fn apply_windows_caption_color<R: Runtime>(window: &tauri::WebviewWindow<R>) {
fn apply_windows_caption_theme<R: Runtime>(window: &tauri::WebviewWindow<R>, dark: bool) {
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Dwm::{DwmSetWindowAttribute, DWMWA_CAPTION_COLOR};
use windows::Win32::Graphics::Dwm::{
DwmSetWindowAttribute, DWMWA_BORDER_COLOR, DWMWA_CAPTION_COLOR, DWMWA_TEXT_COLOR,
DWMWA_USE_IMMERSIVE_DARK_MODE,
};

let handle = match window.window_handle().map(|h| h.as_raw()) {
Ok(RawWindowHandle::Win32(handle)) => handle,
Ok(other) => {
log::warn!("[main] unexpected raw window handle for caption color: {other:?}");
log::warn!("[main] unexpected raw window handle for caption theme: {other:?}");
return;
}
Err(e) => {
log::warn!("[main] read raw window handle for caption color failed: {e}");
log::warn!("[main] read raw window handle for caption theme failed: {e}");
return;
}
};
let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void);

// 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;
// COLORREF 0x00BBGGRR — light 对齐 WindowChrome glass 起始色 rgb(245,245,247);
// dark 对齐 tokens.css --ol-surface (#141922) / --ol-ink (#f4f7fb) / --ol-surface-2 (#1a202b)。
let immersive_dark: i32 = i32::from(dark);
let caption_color: u32 = if dark { 0x0022_1914 } else { 0x00F7_F5F5 };
let text_color: u32 = if dark { 0x00FB_F7F4 } else { 0x002A_170F };
let border_color: u32 = if dark { 0x002B_201A } else { 0x00E8_E8E8 };

unsafe {
if let Err(e) = DwmSetWindowAttribute(
set_dwm_window_attribute(
hwnd,
DWMWA_USE_IMMERSIVE_DARK_MODE,
&immersive_dark,
"immersive dark mode",
);
set_dwm_window_attribute(
hwnd,
DWMWA_CAPTION_COLOR,
&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}");
}
&caption_color,
"caption color",
);
set_dwm_window_attribute(hwnd, DWMWA_TEXT_COLOR, &text_color, "text color");
set_dwm_window_attribute(hwnd, DWMWA_BORDER_COLOR, &border_color, "border color");
}
}

#[cfg(target_os = "windows")]
unsafe fn set_dwm_window_attribute<T>(
hwnd: windows::Win32::Foundation::HWND,
attribute: windows::Win32::Graphics::Dwm::DWMWINDOWATTRIBUTE,
value: &T,
label: &str,
) {
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;

if let Err(e) = DwmSetWindowAttribute(
hwnd,
attribute,
value as *const _ as *const core::ffi::c_void,
std::mem::size_of_val(value) as u32,
) {
log::warn!("[main] set {label} failed (likely pre-22H2 Win): {e}");
}
}

/// 前端主题切换时同步主窗口原生标题栏;非 Windows 为 no-op。
#[tauri::command]
fn set_windows_caption_theme(app: AppHandle, dark: bool) {
#[cfg(target_os = "windows")]
if let Some(main) = app.get_webview_window("main") {
apply_windows_caption_theme(&main, dark);
}
#[cfg(not(target_os = "windows"))]
{
let _ = (app, dark);
}
}

Expand Down
45 changes: 27 additions & 18 deletions openless-all/app/src-tauri/src/remote_server/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@
--ink: #0a0a0b;
--ink-2: #2a2a2d;
--ink-3: rgba(10, 10, 11, 0.62);
--ink-4: rgba(10, 10, 11, 0.42);
--ink-4: rgba(10, 10, 11, 0.58);

/* 蓝色强调 */
--blue: #2563eb;
--blue-hover: #1d4ed8;
--blue-soft: #eff4ff;
--blue-ring: rgba(37, 99, 235, 0.22);
--on-accent: #ffffff;
--accent-solid-bg: var(--blue);
--accent-solid-bg-hover: var(--blue-hover);
--accent-solid-ink: var(--on-accent);

/* 状态色 */
--ok: #16a34a;
Expand All @@ -35,8 +39,13 @@
--shadow-lg: 0 20px 60px -20px rgba(15, 17, 22, 0.18), 0 8px 32px -16px rgba(15, 17, 22, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.06);

/* 圆角 */
--control-radius: 8px;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 14px;
--r-xl: 18px;
--bubble-radius: var(--r-lg);
--modal-radius: var(--r-xl);
--r-2xl: 22px;
--r-pill: 999px;

Expand Down Expand Up @@ -179,7 +188,7 @@ body {
padding: 15px 18px;
font-size: 17px;
font-weight: 600;
color: #fff;
color: var(--on-accent);
border: none;
border-radius: var(--r-lg);
cursor: pointer;
Expand All @@ -189,10 +198,10 @@ body {
.btn:disabled { opacity: .5; cursor: default; }

.btn-primary {
background: var(--blue);
background: var(--accent-solid-bg);
box-shadow: 0 6px 18px -6px var(--blue-ring);
}
.btn-primary:active { background: var(--blue-hover); }
.btn-primary:active { background: var(--accent-solid-bg-hover); }

.hint-error {
color: var(--danger);
Expand Down Expand Up @@ -230,10 +239,10 @@ body {
.help-link {
display: inline-block;
padding: 9px 16px;
border-radius: var(--r-md);
border-radius: var(--control-radius);
border: none;
background: var(--blue);
color: #fff;
background: var(--accent-solid-bg);
color: var(--accent-solid-ink);
font-size: 13px;
font-weight: 600;
font-family: inherit;
Expand All @@ -242,7 +251,7 @@ body {
-webkit-appearance: none;
appearance: none;
}
.help-link:active { background: var(--blue-hover); }
.help-link:active { background: var(--accent-solid-bg-hover); }
.help-link-ghost {
background: var(--surface);
color: var(--blue);
Expand All @@ -260,7 +269,7 @@ body {
.app-icon {
width: 26px;
height: 26px;
border-radius: 7px;
border-radius: var(--control-radius);
flex: none;
box-shadow: var(--shadow-sm);
}
Expand All @@ -275,7 +284,7 @@ body {
display: inline-flex;
background: var(--surface-2);
border: 0.5px solid var(--line);
border-radius: 12px;
border-radius: var(--r-lg);
padding: 3px;
gap: 2px;
}
Expand All @@ -288,13 +297,13 @@ body {
font-size: 13px;
font-weight: 600;
padding: 7px 14px;
border-radius: 9px;
border-radius: var(--control-radius);
cursor: pointer;
transition: background .15s ease, color .15s ease;
}
.mode-btn.active {
background: var(--blue);
color: #fff;
background: var(--accent-solid-bg);
color: var(--accent-solid-ink);
}

/* ===== 录音主区 ===== */
Expand All @@ -315,8 +324,8 @@ body {
border-radius: 50%;
border: none;
cursor: pointer;
color: #fff;
background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%);
color: var(--accent-solid-ink);
background: linear-gradient(180deg, var(--accent-solid-bg-hover) 0%, var(--accent-solid-bg) 100%);
box-shadow:
0 16px 36px -10px rgba(37, 99, 235, .5),
inset 0 1px 0 rgba(255, 255, 255, .25);
Expand Down Expand Up @@ -359,7 +368,7 @@ body {
box-shadow: 0 16px 36px -10px rgba(220, 38, 38, .5);
animation: breathe 1.6s ease-in-out infinite;
}
.record-btn.recording .record-btn-label { color: #fff; }
.record-btn.recording .record-btn-label { color: var(--on-accent); }
.record-btn.recording .record-btn-ring {
opacity: 1;
border-color: rgba(220, 38, 38, .4);
Expand Down Expand Up @@ -498,7 +507,7 @@ body {
cursor: pointer;
transition: background .15s ease, color .15s ease;
}
.result-copy:active { background: var(--blue); color: #fff; }
.result-copy:active { background: var(--accent-solid-bg); color: var(--accent-solid-ink); }
.result-copy.copied { background: var(--ok-soft); border-color: var(--ok); color: var(--ok); }

/* ===== 提示文字 ===== */
Expand Down Expand Up @@ -570,7 +579,7 @@ body {
font-size: 12px;
line-height: 1.5;
color: var(--ink-4);
background: rgba(255, 255, 255, .85);
background: rgba(255, 255, 255, .92);
backdrop-filter: blur(12px) saturate(160%);
-webkit-backdrop-filter: blur(12px) saturate(160%);
border-top: 0.5px solid var(--line);
Expand Down
4 changes: 2 additions & 2 deletions openless-all/app/src/components/AutoUpdate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,14 @@ export function UpdateDialog({
const installing = status === 'installing';
return (
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.18)', display: 'grid', placeItems: 'center', zIndex: 40 }}>
<div style={{ width: 360, 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: 18 }}>
<div style={{ width: 360, borderRadius: 'var(--ol-modal-radius)', background: 'var(--ol-surface)', border: '0.5px solid var(--ol-line-strong)', boxShadow: '0 18px 42px rgba(0,0,0,0.18)', padding: 18 }}>
<div style={{ fontSize: 15, fontWeight: 650, marginBottom: 8 }}>{t(`settings.about.updateDialog.${status}.title`)}</div>
<div style={{ fontSize: 12, color: 'var(--ol-ink-3)', lineHeight: 1.6, marginBottom: 14 }}>
{t(`settings.about.updateDialog.${status}.desc`, { version })}
</div>
{(downloading || installing || status === 'downloaded') && (
<div style={{ marginBottom: 14 }}>
<div style={{ height: 8, borderRadius: 999, background: 'var(--ol-surface-2)', overflow: 'hidden', border: '0.5px solid var(--ol-line)' }}>
<div style={{ height: 8, borderRadius: 'var(--ol-pill-radius)', background: 'var(--ol-surface-2)', overflow: 'hidden', border: '0.5px solid var(--ol-line)' }}>
<div style={{ height: '100%', width: `${status === 'downloaded' || installing ? 100 : progress ?? 8}%`, background: 'var(--ol-blue)', transition: 'width 0.18s var(--ol-motion-soft)' }} />
</div>
<div style={{ marginTop: 6, fontSize: 11, color: 'var(--ol-ink-4)' }}>
Expand Down
Loading
Loading