fix: theme native system and check os support blur
This commit is contained in:
parent
80ee8fd2b2
commit
aa0c4b0d1b
@ -3,10 +3,13 @@
|
|||||||
"identifier": "logs-app-window",
|
"identifier": "logs-app-window",
|
||||||
"description": "enables permissions for the logs app window",
|
"description": "enables permissions for the logs app window",
|
||||||
"windows": ["logs-app-window"],
|
"windows": ["logs-app-window"],
|
||||||
|
"platforms": ["linux", "macOS", "windows"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
"core:window:allow-get-all-windows",
|
||||||
|
"core:event:allow-listen",
|
||||||
"log:default",
|
"log:default",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
"core:window:allow-set-focus"
|
"core:window:allow-set-focus"
|
||||||
|
|||||||
@ -3,10 +3,13 @@
|
|||||||
"identifier": "logs-window",
|
"identifier": "logs-window",
|
||||||
"description": "enables permissions for the logs window",
|
"description": "enables permissions for the logs window",
|
||||||
"windows": ["logs-window-local-api-server"],
|
"windows": ["logs-window-local-api-server"],
|
||||||
|
"platforms": ["linux", "macOS", "windows"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
"core:window:allow-get-all-windows",
|
||||||
|
"core:event:allow-listen",
|
||||||
"log:default",
|
"log:default",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
"core:window:allow-set-focus"
|
"core:window:allow-set-focus"
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"core:window:allow-set-theme",
|
"core:window:allow-set-theme",
|
||||||
|
"core:window:allow-get-all-windows",
|
||||||
|
"core:event:allow-listen",
|
||||||
"log:default",
|
"log:default",
|
||||||
"core:webview:allow-create-webview-window",
|
"core:webview:allow-create-webview-window",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
@ -15,6 +17,18 @@
|
|||||||
"hardware:allow-get-system-usage",
|
"hardware:allow-get-system-usage",
|
||||||
"llamacpp:allow-get-devices",
|
"llamacpp:allow-get-devices",
|
||||||
"llamacpp:allow-read-gguf-metadata",
|
"llamacpp:allow-read-gguf-metadata",
|
||||||
"deep-link:allow-get-current"
|
"deep-link:allow-get-current",
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [
|
||||||
|
{
|
||||||
|
"url": "https://*:*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "http://*:*"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use tar::Archive;
|
use tar::Archive;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
App, Emitter, Manager, Runtime, Wry
|
App, Emitter, Manager, Runtime, Wry, WindowEvent
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
@ -270,3 +270,32 @@ pub fn setup_tray(app: &App) -> tauri::Result<TrayIcon> {
|
|||||||
})
|
})
|
||||||
.build(app)
|
.build(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setup_theme_listener<R: Runtime>(app: &App<R>) -> tauri::Result<()> {
|
||||||
|
// Setup theme listener for main window
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
setup_window_theme_listener(app.handle().clone(), window);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_window_theme_listener<R: Runtime>(
|
||||||
|
app_handle: tauri::AppHandle<R>,
|
||||||
|
window: tauri::WebviewWindow<R>,
|
||||||
|
) {
|
||||||
|
let window_label = window.label().to_string();
|
||||||
|
let app_handle_clone = app_handle.clone();
|
||||||
|
|
||||||
|
window.on_window_event(move |event| {
|
||||||
|
if let WindowEvent::ThemeChanged(theme) = event {
|
||||||
|
let theme_str = match theme {
|
||||||
|
tauri::Theme::Light => "light",
|
||||||
|
tauri::Theme::Dark => "dark",
|
||||||
|
_ => "auto",
|
||||||
|
};
|
||||||
|
log::info!("System theme changed to: {} for window: {}", theme_str, window_label);
|
||||||
|
let _ = app_handle_clone.emit("theme-changed", theme_str);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -117,3 +117,108 @@ pub fn is_library_available(library: &str) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the system supports blur/acrylic effects
|
||||||
|
// - Windows: Checks build version (17134+ for acrylic support)
|
||||||
|
// - Linux: Checks for KWin (KDE) or compositor with blur support
|
||||||
|
// - macOS: Always supported
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn supports_blur_effects() -> bool {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Windows 10 build 17134 (1803) and later support acrylic effects
|
||||||
|
// Windows 11 (build 22000+) has better support
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
if let Ok(output) = Command::new("cmd")
|
||||||
|
.args(&["/C", "ver"])
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
if let Ok(version_str) = String::from_utf8(output.stdout) {
|
||||||
|
// Parse Windows version from output like "Microsoft Windows [Version 10.0.22631.4602]"
|
||||||
|
if let Some(version_part) = version_str.split("Version ").nth(1) {
|
||||||
|
if let Some(build_str) = version_part.split('.').nth(2) {
|
||||||
|
if let Ok(build) = build_str.split(']').next().unwrap_or("0").trim().parse::<u32>() {
|
||||||
|
// Windows 10 build 17134+ or Windows 11 build 22000+ support blur
|
||||||
|
let supports_blur = build >= 17134;
|
||||||
|
if supports_blur {
|
||||||
|
log::info!("✅ Windows build {} detected - Blur/Acrylic effects SUPPORTED", build);
|
||||||
|
} else {
|
||||||
|
log::warn!("❌ Windows build {} detected - Blur/Acrylic effects NOT SUPPORTED (requires build 17134+)", build);
|
||||||
|
}
|
||||||
|
return supports_blur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't detect version, assume it doesn't support blur for safety
|
||||||
|
log::warn!("❌ Could not detect Windows version - Assuming NO blur support for safety");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
// Check for KDE Plasma with KWin (best blur support)
|
||||||
|
if let Ok(output) = Command::new("kwin_x11").arg("--version").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
log::info!("✅ KDE/KWin detected - Blur effects SUPPORTED");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Wayland KWin
|
||||||
|
if let Ok(output) = Command::new("kwin_wayland").arg("--version").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
log::info!("✅ KDE/KWin Wayland detected - Blur effects SUPPORTED");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for GNOME with blur extensions (less reliable)
|
||||||
|
if std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default().contains("GNOME") {
|
||||||
|
log::info!("🔍 GNOME detected - Blur support depends on extensions");
|
||||||
|
// GNOME might have blur through extensions, allow it
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Compiz (older but has blur)
|
||||||
|
if let Ok(_) = Command::new("compiz").arg("--version").output() {
|
||||||
|
log::info!("✅ Compiz compositor detected - Blur effects SUPPORTED");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Picom with blur (common X11 compositor)
|
||||||
|
if let Ok(output) = Command::new("picom").arg("--version").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
log::info!("✅ Picom compositor detected - Blur effects SUPPORTED");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check environment variable for compositor
|
||||||
|
if let Ok(compositor) = std::env::var("COMPOSITOR") {
|
||||||
|
log::info!("🔍 Compositor detected: {} - Assuming blur support", compositor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::warn!("❌ No known blur-capable compositor detected on Linux");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
// macOS always supports blur/vibrancy effects
|
||||||
|
log::info!("✅ macOS detected - Blur/Vibrancy effects SUPPORTED");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
||||||
|
{
|
||||||
|
log::warn!("❌ Unknown platform - Assuming NO blur support");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -78,6 +78,7 @@ pub fn run() {
|
|||||||
core::system::commands::factory_reset,
|
core::system::commands::factory_reset,
|
||||||
core::system::commands::read_logs,
|
core::system::commands::read_logs,
|
||||||
core::system::commands::is_library_available,
|
core::system::commands::is_library_available,
|
||||||
|
core::system::commands::supports_blur_effects,
|
||||||
// Server commands
|
// Server commands
|
||||||
core::server::commands::start_server,
|
core::server::commands::start_server,
|
||||||
core::server::commands::stop_server,
|
core::server::commands::stop_server,
|
||||||
@ -193,6 +194,7 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setup_mcp(app);
|
setup_mcp(app);
|
||||||
|
setup::setup_theme_listener(app)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"capabilities": ["default"],
|
"capabilities": ["default", "logs-app-window", "logs-window", "system-monitor-window"],
|
||||||
"csp": {
|
"csp": {
|
||||||
"default-src": "'self' customprotocol: asset: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*",
|
"default-src": "'self' customprotocol: asset: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*",
|
||||||
"connect-src": "ipc: http://ipc.localhost http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https: http:",
|
"connect-src": "ipc: http://ipc.localhost http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https: http:",
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
<link rel="apple-touch-icon" href="/images/jan-logo.png" />
|
<link rel="apple-touch-icon" href="/images/jan-logo.png" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover, interactive-widget=resizes-visual"
|
content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover"
|
||||||
/>
|
/>
|
||||||
<title>Jan</title>
|
<title>Jan</title>
|
||||||
<!-- INJECT_GOOGLE_ANALYTICS -->
|
<!-- INJECT_GOOGLE_ANALYTICS -->
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useAppearance, isDefaultColor } from '@/hooks/useAppearance'
|
import { useAppearance, isDefaultColor, useBlurSupport } from '@/hooks/useAppearance'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { RgbaColor, RgbaColorPicker } from 'react-colorful'
|
import { RgbaColor, RgbaColorPicker } from 'react-colorful'
|
||||||
import { IconColorPicker } from '@tabler/icons-react'
|
import { IconColorPicker } from '@tabler/icons-react'
|
||||||
@ -14,6 +14,12 @@ export function ColorPickerAppBgColor() {
|
|||||||
const { appBgColor, setAppBgColor } = useAppearance()
|
const { appBgColor, setAppBgColor } = useAppearance()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const showAlphaSlider = useBlurSupport()
|
||||||
|
|
||||||
|
// Helper to get alpha value based on blur support
|
||||||
|
const getAlpha = (defaultAlpha: number) => {
|
||||||
|
return showAlphaSlider ? defaultAlpha : 1
|
||||||
|
}
|
||||||
|
|
||||||
const predefineAppBgColor: RgbaColor[] = [
|
const predefineAppBgColor: RgbaColor[] = [
|
||||||
isDark
|
isDark
|
||||||
@ -21,38 +27,38 @@ export function ColorPickerAppBgColor() {
|
|||||||
r: 25,
|
r: 25,
|
||||||
g: 25,
|
g: 25,
|
||||||
b: 25,
|
b: 25,
|
||||||
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4,
|
a: getAlpha(0.4),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 255,
|
g: 255,
|
||||||
b: 255,
|
b: 255,
|
||||||
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4,
|
a: getAlpha(0.4),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
r: 70,
|
r: 70,
|
||||||
g: 79,
|
g: 79,
|
||||||
b: 229,
|
b: 229,
|
||||||
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.5,
|
a: getAlpha(0.5),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
r: 238,
|
r: 238,
|
||||||
g: 130,
|
g: 130,
|
||||||
b: 238,
|
b: 238,
|
||||||
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.5,
|
a: getAlpha(0.5),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 99,
|
g: 99,
|
||||||
b: 71,
|
b: 71,
|
||||||
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.5,
|
a: getAlpha(0.5),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 165,
|
g: 165,
|
||||||
b: 0,
|
b: 0,
|
||||||
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.5,
|
a: getAlpha(0.5),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -61,9 +67,9 @@ export function ColorPickerAppBgColor() {
|
|||||||
{predefineAppBgColor.map((item, i) => {
|
{predefineAppBgColor.map((item, i) => {
|
||||||
const isSelected =
|
const isSelected =
|
||||||
(item.r === appBgColor.r &&
|
(item.r === appBgColor.r &&
|
||||||
item.g === appBgColor.g &&
|
item.g === appBgColor.g &&
|
||||||
item.b === appBgColor.b &&
|
item.b === appBgColor.b &&
|
||||||
item.a === appBgColor.a) ||
|
item.a === appBgColor.a) ||
|
||||||
(isDefaultColor(appBgColor) && isDefaultColor(item))
|
(isDefaultColor(appBgColor) && isDefaultColor(item))
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const DropdownAssistant = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||||
<div className="flex items-center justify-between gap-2 bg-main-view-fg/5 py-1 hover:bg-main-view-fg/8 px-2 rounded-sm">
|
<div className="inline-flex items-center justify-between gap-2 bg-main-view-fg/5 py-1 hover:bg-main-view-fg/8 px-2 rounded-sm">
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40">
|
<button className="font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40">
|
||||||
<div className="text-main-view-fg/80 flex items-center gap-1">
|
<div className="text-main-view-fg/80 flex items-center gap-1">
|
||||||
|
|||||||
@ -154,7 +154,6 @@ const LeftPanel = () => {
|
|||||||
}
|
}
|
||||||
}, [setLeftPanel, open])
|
}, [setLeftPanel, open])
|
||||||
|
|
||||||
|
|
||||||
const currentPath = useRouterState({
|
const currentPath = useRouterState({
|
||||||
select: (state) => state.location.pathname,
|
select: (state) => state.location.pathname,
|
||||||
})
|
})
|
||||||
@ -640,7 +639,7 @@ const LeftPanel = () => {
|
|||||||
data-test-id={`menu-${menu.title}`}
|
data-test-id={`menu-${menu.title}`}
|
||||||
activeOptions={{ exact: true }}
|
activeOptions={{ exact: true }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded',
|
'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 my-0.5 px-1 rounded',
|
||||||
isActive && 'bg-left-panel-fg/10'
|
isActive && 'bg-left-panel-fg/10'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -31,6 +31,8 @@ vi.mock('zustand/middleware', () => ({
|
|||||||
// Mock global constants
|
// Mock global constants
|
||||||
Object.defineProperty(global, 'IS_WINDOWS', { value: false, writable: true })
|
Object.defineProperty(global, 'IS_WINDOWS', { value: false, writable: true })
|
||||||
Object.defineProperty(global, 'IS_LINUX', { value: false, writable: true })
|
Object.defineProperty(global, 'IS_LINUX', { value: false, writable: true })
|
||||||
|
Object.defineProperty(global, 'IS_MACOS', { value: false, writable: true })
|
||||||
|
Object.defineProperty(global, 'IS_TAURI', { value: false, writable: true })
|
||||||
Object.defineProperty(global, 'IS_WEB_APP', { value: false, writable: true })
|
Object.defineProperty(global, 'IS_WEB_APP', { value: false, writable: true })
|
||||||
|
|
||||||
describe('useAppearance', () => {
|
describe('useAppearance', () => {
|
||||||
@ -212,24 +214,55 @@ describe('useAppearance', () => {
|
|||||||
|
|
||||||
const { result } = renderHook(() => useAppearance())
|
const { result } = renderHook(() => useAppearance())
|
||||||
const testColor = { r: 128, g: 64, b: 192, a: 0.8 }
|
const testColor = { r: 128, g: 64, b: 192, a: 0.8 }
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.setAppBgColor(testColor)
|
result.current.setAppBgColor(testColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.current.appBgColor).toEqual(testColor)
|
// In web environment (IS_TAURI=false), alpha is forced to 1
|
||||||
|
expect(result.current.appBgColor).toEqual({ ...testColor, a: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle transparent colors', () => {
|
it('should handle transparent colors', () => {
|
||||||
const { result } = renderHook(() => useAppearance())
|
const { result } = renderHook(() => useAppearance())
|
||||||
const transparentColor = { r: 100, g: 100, b: 100, a: 0 }
|
const transparentColor = { r: 100, g: 100, b: 100, a: 0 }
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.setAppAccentBgColor(transparentColor)
|
result.current.setAppAccentBgColor(transparentColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(result.current.appAccentBgColor).toEqual(transparentColor)
|
expect(result.current.appAccentBgColor).toEqual(transparentColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should preserve alpha when blur is supported (macOS)', () => {
|
||||||
|
// Mock macOS environment
|
||||||
|
Object.defineProperty(global, 'IS_MACOS', { value: true, writable: true })
|
||||||
|
Object.defineProperty(global, 'IS_TAURI', { value: true, writable: true })
|
||||||
|
Object.defineProperty(global, 'IS_WINDOWS', { value: false, writable: true })
|
||||||
|
Object.defineProperty(global, 'IS_LINUX', { value: false, writable: true })
|
||||||
|
|
||||||
|
const setPropertySpy = vi.fn()
|
||||||
|
Object.defineProperty(document.documentElement, 'style', {
|
||||||
|
value: {
|
||||||
|
setProperty: setPropertySpy,
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAppearance())
|
||||||
|
const testColor = { r: 128, g: 64, b: 192, a: 0.5 }
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setAppBgColor(testColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
// On macOS with Tauri, alpha should be preserved
|
||||||
|
expect(result.current.appBgColor).toEqual(testColor)
|
||||||
|
|
||||||
|
// Reset for other tests
|
||||||
|
Object.defineProperty(global, 'IS_MACOS', { value: false, writable: true })
|
||||||
|
Object.defineProperty(global, 'IS_TAURI', { value: false, writable: true })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Edge cases', () => {
|
describe('Edge cases', () => {
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { localStorageKey } from '@/constants/localStorage'
|
|||||||
import { RgbaColor } from 'react-colorful'
|
import { RgbaColor } from 'react-colorful'
|
||||||
import { rgb, oklch, formatCss } from 'culori'
|
import { rgb, oklch, formatCss } from 'culori'
|
||||||
import { useTheme } from './useTheme'
|
import { useTheme } from './useTheme'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { getServiceHub } from '@/hooks/useServiceHub'
|
||||||
|
|
||||||
export type FontSize = '14px' | '15px' | '16px' | '18px'
|
export type FontSize = '14px' | '15px' | '16px' | '18px'
|
||||||
export type ChatWidth = 'full' | 'compact'
|
export type ChatWidth = 'full' | 'compact'
|
||||||
@ -41,19 +43,37 @@ export const fontSizeOptions = [
|
|||||||
{ label: 'Extra Large', value: '18px' as FontSize },
|
{ label: 'Extra Large', value: '18px' as FontSize },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Helper to determine if blur effects are supported
|
||||||
|
// This will be dynamically checked on Windows and Linux
|
||||||
|
let blurEffectsSupported = true
|
||||||
|
if ((IS_WINDOWS || IS_LINUX) && IS_TAURI) {
|
||||||
|
// Default to false for Windows/Linux, will be checked async
|
||||||
|
blurEffectsSupported = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get the appropriate alpha value
|
||||||
|
const getAlphaValue = () => {
|
||||||
|
// Web always uses alpha = 1
|
||||||
|
if (!IS_TAURI) return 1
|
||||||
|
// Windows/Linux use 1 if blur not supported, 0.4 if supported
|
||||||
|
if ((IS_WINDOWS || IS_LINUX) && !blurEffectsSupported) return 1
|
||||||
|
// macOS and Windows/Linux with blur support use 0.4
|
||||||
|
return 0.4
|
||||||
|
}
|
||||||
|
|
||||||
// Default appearance settings
|
// Default appearance settings
|
||||||
const defaultFontSize: FontSize = '15px'
|
const defaultFontSize: FontSize = '15px'
|
||||||
const defaultAppBgColor: RgbaColor = {
|
const defaultAppBgColor: RgbaColor = {
|
||||||
r: 25,
|
r: 25,
|
||||||
g: 25,
|
g: 25,
|
||||||
b: 25,
|
b: 25,
|
||||||
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4,
|
a: getAlphaValue(),
|
||||||
}
|
}
|
||||||
const defaultLightAppBgColor: RgbaColor = {
|
const defaultLightAppBgColor: RgbaColor = {
|
||||||
r: 255,
|
r: 255,
|
||||||
g: 255,
|
g: 255,
|
||||||
b: 255,
|
b: 255,
|
||||||
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4,
|
a: getAlphaValue(),
|
||||||
}
|
}
|
||||||
const defaultAppMainViewBgColor: RgbaColor = { r: 25, g: 25, b: 25, a: 1 }
|
const defaultAppMainViewBgColor: RgbaColor = { r: 25, g: 25, b: 25, a: 1 }
|
||||||
const defaultLightAppMainViewBgColor: RgbaColor = {
|
const defaultLightAppMainViewBgColor: RgbaColor = {
|
||||||
@ -128,6 +148,45 @@ export const getDefaultTextColor = (isDark: boolean): string => {
|
|||||||
return isDark ? defaultDarkLeftPanelTextColor : defaultLightLeftPanelTextColor
|
return isDark ? defaultDarkLeftPanelTextColor : defaultLightLeftPanelTextColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hook to check if alpha slider should be shown
|
||||||
|
export const useBlurSupport = () => {
|
||||||
|
const [supportsBlur, setSupportsBlur] = useState(
|
||||||
|
IS_MACOS && IS_TAURI // Default to true only for macOS
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkBlurSupport = async () => {
|
||||||
|
if ((IS_WINDOWS || IS_LINUX) && IS_TAURI) {
|
||||||
|
try {
|
||||||
|
const supported = await getServiceHub().app().supportsBlurEffects()
|
||||||
|
blurEffectsSupported = supported
|
||||||
|
setSupportsBlur(supported)
|
||||||
|
|
||||||
|
const platform = IS_WINDOWS ? 'Windows' : 'Linux'
|
||||||
|
if (supported) {
|
||||||
|
console.log(`✅ ${platform} blur effects: SUPPORTED - Alpha slider will be shown`)
|
||||||
|
} else {
|
||||||
|
console.log(`❌ ${platform} blur effects: NOT SUPPORTED - Alpha slider will be hidden, alpha set to 1`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to check ${IS_WINDOWS ? 'Windows' : 'Linux'} blur support:`, error)
|
||||||
|
setSupportsBlur(false)
|
||||||
|
}
|
||||||
|
} else if (IS_MACOS && IS_TAURI) {
|
||||||
|
console.log('🍎 macOS platform: Blur effects supported, alpha slider shown')
|
||||||
|
} else if (!IS_TAURI) {
|
||||||
|
console.log('🌐 Web platform: Alpha slider hidden, alpha set to 1')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBlurSupport()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Return true if alpha slider should be shown
|
||||||
|
// Show on macOS (always), and conditionally on Windows/Linux based on detection
|
||||||
|
return IS_TAURI && (IS_MACOS || supportsBlur)
|
||||||
|
}
|
||||||
|
|
||||||
export const useAppearance = create<AppearanceState>()(
|
export const useAppearance = create<AppearanceState>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => {
|
(set) => {
|
||||||
@ -295,6 +354,11 @@ export const useAppearance = create<AppearanceState>()(
|
|||||||
finalColor = isDark ? defaultAppBgColor : defaultLightAppBgColor
|
finalColor = isDark ? defaultAppBgColor : defaultLightAppBgColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force alpha to 1 if blur effects are not supported
|
||||||
|
if (!blurEffectsSupported && (IS_WINDOWS || IS_LINUX || !IS_TAURI)) {
|
||||||
|
finalColor = { ...finalColor, a: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
// Convert RGBA to a format culori can work with
|
// Convert RGBA to a format culori can work with
|
||||||
const culoriRgb = rgb({
|
const culoriRgb = rgb({
|
||||||
mode: 'rgb',
|
mode: 'rgb',
|
||||||
|
|||||||
@ -32,7 +32,9 @@ export const useTheme = create<ThemeState>()(
|
|||||||
await getServiceHub().theme().setTheme(null)
|
await getServiceHub().theme().setTheme(null)
|
||||||
set(() => ({ activeTheme, isDark: isDarkMode }))
|
set(() => ({ activeTheme, isDark: isDarkMode }))
|
||||||
} else {
|
} else {
|
||||||
await getServiceHub().theme().setTheme(activeTheme as ThemeMode)
|
await getServiceHub()
|
||||||
|
.theme()
|
||||||
|
.setTheme(activeTheme as ThemeMode)
|
||||||
set(() => ({ activeTheme, isDark: activeTheme === 'dark' }))
|
set(() => ({ activeTheme, isDark: activeTheme === 'dark' }))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -56,7 +56,6 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply overflow-hidden;
|
@apply overflow-hidden;
|
||||||
background-color: white;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: -webkit-fill-available;
|
min-height: -webkit-fill-available;
|
||||||
padding-top: env(safe-area-inset-top, 0px);
|
padding-top: env(safe-area-inset-top, 0px);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useAppearance } from '@/hooks/useAppearance'
|
import { useAppearance, useBlurSupport } from '@/hooks/useAppearance'
|
||||||
import { useTheme } from '@/hooks/useTheme'
|
import { useTheme } from '@/hooks/useTheme'
|
||||||
import {
|
import {
|
||||||
isDefaultColor,
|
isDefaultColor,
|
||||||
@ -29,14 +29,15 @@ export function AppearanceProvider() {
|
|||||||
appDestructiveTextColor,
|
appDestructiveTextColor,
|
||||||
} = useAppearance()
|
} = useAppearance()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
|
const showAlphaSlider = useBlurSupport()
|
||||||
|
|
||||||
// Apply appearance settings on mount and when they change
|
// Apply appearance settings on mount and when they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Apply font size
|
// Apply font size
|
||||||
document.documentElement.style.setProperty('--font-size-base', fontSize)
|
document.documentElement.style.setProperty('--font-size-base', fontSize)
|
||||||
|
|
||||||
// Hide alpha slider when IS_LINUX || !IS_TAURI
|
// Hide alpha slider when blur is not supported
|
||||||
const shouldHideAlpha = IS_LINUX || !IS_TAURI
|
const shouldHideAlpha = !showAlphaSlider
|
||||||
let alphaStyleElement = document.getElementById('alpha-slider-style')
|
let alphaStyleElement = document.getElementById('alpha-slider-style')
|
||||||
|
|
||||||
if (shouldHideAlpha) {
|
if (shouldHideAlpha) {
|
||||||
@ -55,12 +56,13 @@ export function AppearanceProvider() {
|
|||||||
// Import culori functions dynamically to avoid SSR issues
|
// Import culori functions dynamically to avoid SSR issues
|
||||||
import('culori').then(({ rgb, oklch, formatCss }) => {
|
import('culori').then(({ rgb, oklch, formatCss }) => {
|
||||||
// Convert RGBA to a format culori can work with
|
// Convert RGBA to a format culori can work with
|
||||||
|
// Use alpha = 1 when blur is not supported
|
||||||
const culoriRgb = rgb({
|
const culoriRgb = rgb({
|
||||||
mode: 'rgb',
|
mode: 'rgb',
|
||||||
r: appBgColor.r / 255,
|
r: appBgColor.r / 255,
|
||||||
g: appBgColor.g / 255,
|
g: appBgColor.g / 255,
|
||||||
b: appBgColor.b / 255,
|
b: appBgColor.b / 255,
|
||||||
alpha: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : appBgColor.a,
|
alpha: showAlphaSlider ? appBgColor.a : 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const culoriRgbMainView = rgb({
|
const culoriRgbMainView = rgb({
|
||||||
@ -176,6 +178,7 @@ export function AppearanceProvider() {
|
|||||||
appAccentTextColor,
|
appAccentTextColor,
|
||||||
appDestructiveBgColor,
|
appDestructiveBgColor,
|
||||||
appDestructiveTextColor,
|
appDestructiveTextColor,
|
||||||
|
showAlphaSlider,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Update appearance when theme changes
|
// Update appearance when theme changes
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useTheme, checkOSDarkMode } from '@/hooks/useTheme'
|
import { useTheme, checkOSDarkMode } from '@/hooks/useTheme'
|
||||||
|
import { isPlatformTauri } from '@/lib/platform/utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ThemeProvider ensures theme settings are applied on every page load
|
* ThemeProvider ensures theme settings are applied on every page load
|
||||||
@ -11,13 +12,22 @@ export function ThemeProvider() {
|
|||||||
|
|
||||||
// Detect OS theme on mount and apply it
|
// Detect OS theme on mount and apply it
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If theme is set to auto, detect OS preference
|
// Force refresh theme on mount to handle Linux startup timing issues
|
||||||
if (activeTheme === 'auto') {
|
const refreshTheme = () => {
|
||||||
const isDarkMode = checkOSDarkMode()
|
if (activeTheme === 'auto') {
|
||||||
setIsDark(isDarkMode)
|
const isDarkMode = checkOSDarkMode()
|
||||||
setTheme('auto')
|
setIsDark(isDarkMode)
|
||||||
|
setTheme('auto')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial refresh
|
||||||
|
refreshTheme()
|
||||||
|
|
||||||
|
// On Linux, desktop environment may not be ready immediately
|
||||||
|
// Add a delayed refresh to catch the correct OS theme
|
||||||
|
const timeoutId = setTimeout(refreshTheme, 100)
|
||||||
|
|
||||||
// Listen for changes in OS theme preference
|
// Listen for changes in OS theme preference
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
@ -30,12 +40,37 @@ export function ThemeProvider() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listener
|
// Add event listener for browser/web
|
||||||
mediaQuery.addEventListener('change', handleThemeChange)
|
mediaQuery.addEventListener('change', handleThemeChange)
|
||||||
|
|
||||||
|
// Listen to Tauri native theme events (uses XDG Desktop Portal on Linux)
|
||||||
|
let unlistenTauri: (() => void) | undefined
|
||||||
|
|
||||||
|
if (isPlatformTauri()) {
|
||||||
|
import('@tauri-apps/api/event')
|
||||||
|
.then(({ listen }) => {
|
||||||
|
return listen<string>('theme-changed', (event) => {
|
||||||
|
if (activeTheme === 'auto') {
|
||||||
|
const isDark = event.payload === 'dark'
|
||||||
|
setIsDark(isDark)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((unlisten) => {
|
||||||
|
unlistenTauri = unlisten
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Failed to setup Tauri theme listener:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
return () => {
|
return () => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
mediaQuery.removeEventListener('change', handleThemeChange)
|
mediaQuery.removeEventListener('change', handleThemeChange)
|
||||||
|
if (unlistenTauri) {
|
||||||
|
unlistenTauri()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [activeTheme, setIsDark, setTheme])
|
}, [activeTheme, setIsDark, setTheme])
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import SetupScreen from '@/containers/SetupScreen'
|
|||||||
import { route } from '@/constants/routes'
|
import { route } from '@/constants/routes'
|
||||||
|
|
||||||
type SearchParams = {
|
type SearchParams = {
|
||||||
model?: {
|
'model'?: {
|
||||||
id: string
|
id: string
|
||||||
provider: string
|
provider: string
|
||||||
}
|
}
|
||||||
@ -33,7 +33,10 @@ export const Route = createFileRoute(route.home as any)({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only include temporary-chat if it's explicitly true
|
// Only include temporary-chat if it's explicitly true
|
||||||
if (search[TEMPORARY_CHAT_QUERY_ID] === 'true' || search[TEMPORARY_CHAT_QUERY_ID] === true) {
|
if (
|
||||||
|
search[TEMPORARY_CHAT_QUERY_ID] === 'true' ||
|
||||||
|
search[TEMPORARY_CHAT_QUERY_ID] === true
|
||||||
|
) {
|
||||||
result['temporary-chat'] = true
|
result['temporary-chat'] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +80,7 @@ function Index() {
|
|||||||
</HeaderPage>
|
</HeaderPage>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-full overflow-y-auto flex flex-col gap-2 justify-center px-3 sm:px-4 md:px-8 py-4 md:py-0'
|
'h-full overflow-y-auto inline-flex flex-col gap-2 justify-center px-3 sm:px-4 md:px-8 py-4 md:py-0'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -110,7 +113,9 @@ function Index() {
|
|||||||
isMobile ? 'text-base' : 'text-lg'
|
isMobile ? 'text-base' : 'text-lg'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isTemporaryChat ? t('chat:temporaryChatDescription') : t('chat:description')}
|
{isTemporaryChat
|
||||||
|
? t('chat:temporaryChatDescription')
|
||||||
|
: t('chat:description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 shrink-0">
|
<div className="flex-1 shrink-0">
|
||||||
|
|||||||
@ -31,17 +31,20 @@ function LogsViewer() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lastLogsLength = 0
|
let lastLogsLength = 0
|
||||||
function updateLogs() {
|
function updateLogs() {
|
||||||
serviceHub.app().readLogs().then((logData) => {
|
serviceHub
|
||||||
let needScroll = false
|
.app()
|
||||||
const filteredLogs = logData.filter(Boolean) as LogEntry[]
|
.readLogs()
|
||||||
if (filteredLogs.length > lastLogsLength) needScroll = true
|
.then((logData) => {
|
||||||
|
let needScroll = false
|
||||||
|
const filteredLogs = logData.filter(Boolean) as LogEntry[]
|
||||||
|
if (filteredLogs.length > lastLogsLength) needScroll = true
|
||||||
|
|
||||||
lastLogsLength = filteredLogs.length
|
lastLogsLength = filteredLogs.length
|
||||||
setLogs(filteredLogs)
|
setLogs(filteredLogs)
|
||||||
|
|
||||||
// Scroll to bottom after initial logs are loaded
|
// Scroll to bottom after initial logs are loaded
|
||||||
if (needScroll) setTimeout(() => scrollToBottom(), 100)
|
if (needScroll) setTimeout(() => scrollToBottom(), 100)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
updateLogs()
|
updateLogs()
|
||||||
|
|
||||||
|
|||||||
@ -39,4 +39,9 @@ export class DefaultAppService implements AppService {
|
|||||||
console.log('readYaml called with path:', path)
|
console.log('readYaml called with path:', path)
|
||||||
throw new Error('readYaml not implemented in default app service')
|
throw new Error('readYaml not implemented in default app service')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async supportsBlurEffects(): Promise<boolean> {
|
||||||
|
// On web/non-Windows platforms, always return false
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,4 +75,8 @@ export class TauriAppService extends DefaultAppService {
|
|||||||
async readYaml<T = unknown>(path: string): Promise<T> {
|
async readYaml<T = unknown>(path: string): Promise<T> {
|
||||||
return await invoke<T>('read_yaml', { path })
|
return await invoke<T>('read_yaml', { path })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async supportsBlurEffects(): Promise<boolean> {
|
||||||
|
return await invoke<boolean>('supports_blur_effects')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,4 +17,5 @@ export interface AppService {
|
|||||||
relocateJanDataFolder(path: string): Promise<void>
|
relocateJanDataFolder(path: string): Promise<void>
|
||||||
getServerStatus(): Promise<boolean>
|
getServerStatus(): Promise<boolean>
|
||||||
readYaml<T = unknown>(path: string): Promise<T>
|
readYaml<T = unknown>(path: string): Promise<T>
|
||||||
|
supportsBlurEffects(): Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,4 +45,9 @@ export class WebAppService implements AppService {
|
|||||||
console.log('YAML reading not available in web mode')
|
console.log('YAML reading not available in web mode')
|
||||||
throw new Error('readYaml not implemented in web app service')
|
throw new Error('readYaml not implemented in web app service')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async supportsBlurEffects(): Promise<boolean> {
|
||||||
|
// Web platforms don't support blur effects
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
* Tauri Theme Service - Desktop implementation
|
* Tauri Theme Service - Desktop implementation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCurrentWindow, Theme } from '@tauri-apps/api/window'
|
import { Theme } from '@tauri-apps/api/window'
|
||||||
|
import { getAllWebviewWindows } from '@tauri-apps/api/webviewWindow'
|
||||||
import type { ThemeMode } from './types'
|
import type { ThemeMode } from './types'
|
||||||
import { DefaultThemeService } from './default'
|
import { DefaultThemeService } from './default'
|
||||||
|
|
||||||
@ -10,7 +11,27 @@ export class TauriThemeService extends DefaultThemeService {
|
|||||||
async setTheme(theme: ThemeMode): Promise<void> {
|
async setTheme(theme: ThemeMode): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const tauriTheme = theme as Theme | null
|
const tauriTheme = theme as Theme | null
|
||||||
await getCurrentWindow().setTheme(tauriTheme)
|
|
||||||
|
// Update all open windows, not just the current one
|
||||||
|
const allWindows = await getAllWebviewWindows()
|
||||||
|
|
||||||
|
// Convert to array if it's not already
|
||||||
|
const windowsArray = Array.isArray(allWindows)
|
||||||
|
? allWindows
|
||||||
|
: Object.values(allWindows)
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
windowsArray.map(async (window) => {
|
||||||
|
try {
|
||||||
|
await window.setTheme(tauriTheme)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to set theme for window ${window.label}:`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting theme in Tauri:', error)
|
console.error('Error setting theme in Tauri:', error)
|
||||||
throw error
|
throw error
|
||||||
@ -21,7 +42,7 @@ export class TauriThemeService extends DefaultThemeService {
|
|||||||
return {
|
return {
|
||||||
setTheme: (theme: ThemeMode): Promise<void> => {
|
setTheme: (theme: ThemeMode): Promise<void> => {
|
||||||
return this.setTheme(theme)
|
return this.setTheme(theme)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,39 @@ import type { WindowConfig, WebviewWindowInstance } from './types'
|
|||||||
import { DefaultWindowService } from './default'
|
import { DefaultWindowService } from './default'
|
||||||
|
|
||||||
export class TauriWindowService extends DefaultWindowService {
|
export class TauriWindowService extends DefaultWindowService {
|
||||||
async createWebviewWindow(config: WindowConfig): Promise<WebviewWindowInstance> {
|
async createWebviewWindow(
|
||||||
|
config: WindowConfig
|
||||||
|
): Promise<WebviewWindowInstance> {
|
||||||
try {
|
try {
|
||||||
|
// Get current theme from localStorage
|
||||||
|
const storedTheme = localStorage.getItem('jan-theme')
|
||||||
|
let theme: 'light' | 'dark' | undefined = undefined
|
||||||
|
|
||||||
|
if (storedTheme) {
|
||||||
|
try {
|
||||||
|
const themeData = JSON.parse(storedTheme)
|
||||||
|
const activeTheme = themeData?.state?.activeTheme
|
||||||
|
const isDark = themeData?.state?.isDark
|
||||||
|
|
||||||
|
// Set theme based on stored preference
|
||||||
|
if (activeTheme === 'auto') {
|
||||||
|
theme = undefined // Let OS decide
|
||||||
|
} else if (
|
||||||
|
activeTheme === 'dark' ||
|
||||||
|
(activeTheme === 'auto' && isDark)
|
||||||
|
) {
|
||||||
|
theme = 'dark'
|
||||||
|
} else if (
|
||||||
|
activeTheme === 'light' ||
|
||||||
|
(activeTheme === 'auto' && !isDark)
|
||||||
|
) {
|
||||||
|
theme = 'light'
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse theme from localStorage:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const webviewWindow = new WebviewWindow(config.label, {
|
const webviewWindow = new WebviewWindow(config.label, {
|
||||||
url: config.url,
|
url: config.url,
|
||||||
title: config.title,
|
title: config.title,
|
||||||
@ -20,8 +51,12 @@ export class TauriWindowService extends DefaultWindowService {
|
|||||||
maximizable: config.maximizable,
|
maximizable: config.maximizable,
|
||||||
closable: config.closable,
|
closable: config.closable,
|
||||||
fullscreen: config.fullscreen,
|
fullscreen: config.fullscreen,
|
||||||
|
theme: theme,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Setup theme listener for this window
|
||||||
|
this.setupThemeListenerForWindow(webviewWindow)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: config.label,
|
label: config.label,
|
||||||
async close() {
|
async close() {
|
||||||
@ -38,7 +73,7 @@ export class TauriWindowService extends DefaultWindowService {
|
|||||||
},
|
},
|
||||||
async setTitle(title: string) {
|
async setTitle(title: string) {
|
||||||
await webviewWindow.setTitle(title)
|
await webviewWindow.setTitle(title)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating Tauri window:', error)
|
console.error('Error creating Tauri window:', error)
|
||||||
@ -46,10 +81,12 @@ export class TauriWindowService extends DefaultWindowService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWebviewWindowByLabel(label: string): Promise<WebviewWindowInstance | null> {
|
async getWebviewWindowByLabel(
|
||||||
|
label: string
|
||||||
|
): Promise<WebviewWindowInstance | null> {
|
||||||
try {
|
try {
|
||||||
const existingWindow = await WebviewWindow.getByLabel(label)
|
const existingWindow = await WebviewWindow.getByLabel(label)
|
||||||
|
|
||||||
if (existingWindow) {
|
if (existingWindow) {
|
||||||
return {
|
return {
|
||||||
label: label,
|
label: label,
|
||||||
@ -67,10 +104,10 @@ export class TauriWindowService extends DefaultWindowService {
|
|||||||
},
|
},
|
||||||
async setTitle(title: string) {
|
async setTitle(title: string) {
|
||||||
await existingWindow.setTitle(title)
|
await existingWindow.setTitle(title)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting Tauri window by label:', error)
|
console.error('Error getting Tauri window by label:', error)
|
||||||
@ -135,8 +172,35 @@ export class TauriWindowService extends DefaultWindowService {
|
|||||||
center: true,
|
center: true,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening local API server logs window in Tauri:', error)
|
console.error(
|
||||||
|
'Error opening local API server logs window in Tauri:',
|
||||||
|
error
|
||||||
|
)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setupThemeListenerForWindow(window: WebviewWindow): void {
|
||||||
|
// Listen to theme change events from Tauri backend
|
||||||
|
import('@tauri-apps/api/event')
|
||||||
|
.then(({ listen }) => {
|
||||||
|
return listen<string>('theme-changed', async (event) => {
|
||||||
|
const theme = event.payload
|
||||||
|
try {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
await window.setTheme('dark')
|
||||||
|
} else if (theme === 'light') {
|
||||||
|
await window.setTheme('light')
|
||||||
|
} else {
|
||||||
|
await window.setTheme(null)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update window theme:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Failed to setup theme listener for window:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user