fix: theme native system and check os support blur

This commit is contained in:
Faisal Amir 2025-10-02 12:32:52 +07:00
parent 80ee8fd2b2
commit aa0c4b0d1b
25 changed files with 464 additions and 59 deletions

View File

@ -3,10 +3,13 @@
"identifier": "logs-app-window",
"description": "enables permissions for the logs app window",
"windows": ["logs-app-window"],
"platforms": ["linux", "macOS", "windows"],
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-set-theme",
"core:window:allow-get-all-windows",
"core:event:allow-listen",
"log:default",
"core:webview:allow-create-webview-window",
"core:window:allow-set-focus"

View File

@ -3,10 +3,13 @@
"identifier": "logs-window",
"description": "enables permissions for the logs window",
"windows": ["logs-window-local-api-server"],
"platforms": ["linux", "macOS", "windows"],
"permissions": [
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-set-theme",
"core:window:allow-get-all-windows",
"core:event:allow-listen",
"log:default",
"core:webview:allow-create-webview-window",
"core:window:allow-set-focus"

View File

@ -8,6 +8,8 @@
"core:default",
"core:window:allow-start-dragging",
"core:window:allow-set-theme",
"core:window:allow-get-all-windows",
"core:event:allow-listen",
"log:default",
"core:webview:allow-create-webview-window",
"core:window:allow-set-focus",
@ -15,6 +17,18 @@
"hardware:allow-get-system-usage",
"llamacpp:allow-get-devices",
"llamacpp:allow-read-gguf-metadata",
"deep-link:allow-get-current"
"deep-link:allow-get-current",
{
"identifier": "http:default",
"allow": [
{
"url": "https://*:*"
},
{
"url": "http://*:*"
}
],
"deny": []
}
]
}

View File

@ -7,7 +7,7 @@ use std::{
};
use tar::Archive;
use tauri::{
App, Emitter, Manager, Runtime, Wry
App, Emitter, Manager, Runtime, Wry, WindowEvent
};
#[cfg(desktop)]
@ -270,3 +270,32 @@ pub fn setup_tray(app: &App) -> tauri::Result<TrayIcon> {
})
.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);
}
});
}

View File

@ -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
}
}

View File

@ -78,6 +78,7 @@ pub fn run() {
core::system::commands::factory_reset,
core::system::commands::read_logs,
core::system::commands::is_library_available,
core::system::commands::supports_blur_effects,
// Server commands
core::server::commands::start_server,
core::server::commands::stop_server,
@ -193,6 +194,7 @@ pub fn run() {
}
setup_mcp(app);
setup::setup_theme_listener(app)?;
Ok(())
})
.build(tauri::generate_context!())

View File

@ -40,7 +40,7 @@
}
],
"security": {
"capabilities": ["default"],
"capabilities": ["default", "logs-app-window", "logs-window", "system-monitor-window"],
"csp": {
"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:",

View File

@ -17,7 +17,7 @@
<link rel="apple-touch-icon" href="/images/jan-logo.png" />
<meta
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>
<!-- INJECT_GOOGLE_ANALYTICS -->

View File

@ -1,4 +1,4 @@
import { useAppearance, isDefaultColor } from '@/hooks/useAppearance'
import { useAppearance, isDefaultColor, useBlurSupport } from '@/hooks/useAppearance'
import { cn } from '@/lib/utils'
import { RgbaColor, RgbaColorPicker } from 'react-colorful'
import { IconColorPicker } from '@tabler/icons-react'
@ -14,6 +14,12 @@ export function ColorPickerAppBgColor() {
const { appBgColor, setAppBgColor } = useAppearance()
const { isDark } = useTheme()
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[] = [
isDark
@ -21,38 +27,38 @@ export function ColorPickerAppBgColor() {
r: 25,
g: 25,
b: 25,
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4,
a: getAlpha(0.4),
}
: {
r: 255,
g: 255,
b: 255,
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4,
a: getAlpha(0.4),
},
{
r: 70,
g: 79,
b: 229,
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.5,
a: getAlpha(0.5),
},
{
r: 238,
g: 130,
b: 238,
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.5,
a: getAlpha(0.5),
},
{
r: 255,
g: 99,
b: 71,
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.5,
a: getAlpha(0.5),
},
{
r: 255,
g: 165,
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) => {
const isSelected =
(item.r === appBgColor.r &&
item.g === appBgColor.g &&
item.b === appBgColor.b &&
item.a === appBgColor.a) ||
item.g === appBgColor.g &&
item.b === appBgColor.b &&
item.a === appBgColor.a) ||
(isDefaultColor(appBgColor) && isDefaultColor(item))
return (
<div

View File

@ -33,7 +33,7 @@ const DropdownAssistant = () => {
return (
<>
<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>
<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">

View File

@ -154,7 +154,6 @@ const LeftPanel = () => {
}
}, [setLeftPanel, open])
const currentPath = useRouterState({
select: (state) => state.location.pathname,
})
@ -640,7 +639,7 @@ const LeftPanel = () => {
data-test-id={`menu-${menu.title}`}
activeOptions={{ exact: true }}
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'
)}
>

View File

@ -31,6 +31,8 @@ vi.mock('zustand/middleware', () => ({
// Mock global constants
Object.defineProperty(global, 'IS_WINDOWS', { 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 })
describe('useAppearance', () => {
@ -212,24 +214,55 @@ describe('useAppearance', () => {
const { result } = renderHook(() => useAppearance())
const testColor = { r: 128, g: 64, b: 192, a: 0.8 }
act(() => {
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', () => {
const { result } = renderHook(() => useAppearance())
const transparentColor = { r: 100, g: 100, b: 100, a: 0 }
act(() => {
result.current.setAppAccentBgColor(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', () => {

View File

@ -4,6 +4,8 @@ import { localStorageKey } from '@/constants/localStorage'
import { RgbaColor } from 'react-colorful'
import { rgb, oklch, formatCss } from 'culori'
import { useTheme } from './useTheme'
import { useEffect, useState } from 'react'
import { getServiceHub } from '@/hooks/useServiceHub'
export type FontSize = '14px' | '15px' | '16px' | '18px'
export type ChatWidth = 'full' | 'compact'
@ -41,19 +43,37 @@ export const fontSizeOptions = [
{ 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
const defaultFontSize: FontSize = '15px'
const defaultAppBgColor: RgbaColor = {
r: 25,
g: 25,
b: 25,
a: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : 0.4,
a: getAlphaValue(),
}
const defaultLightAppBgColor: RgbaColor = {
r: 255,
g: 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 defaultLightAppMainViewBgColor: RgbaColor = {
@ -128,6 +148,45 @@ export const getDefaultTextColor = (isDark: boolean): string => {
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>()(
persist(
(set) => {
@ -295,6 +354,11 @@ export const useAppearance = create<AppearanceState>()(
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
const culoriRgb = rgb({
mode: 'rgb',

View File

@ -32,7 +32,9 @@ export const useTheme = create<ThemeState>()(
await getServiceHub().theme().setTheme(null)
set(() => ({ activeTheme, isDark: isDarkMode }))
} else {
await getServiceHub().theme().setTheme(activeTheme as ThemeMode)
await getServiceHub()
.theme()
.setTheme(activeTheme as ThemeMode)
set(() => ({ activeTheme, isDark: activeTheme === 'dark' }))
}
},

View File

@ -56,7 +56,6 @@
@layer base {
body {
@apply overflow-hidden;
background-color: white;
min-height: 100vh;
min-height: -webkit-fill-available;
padding-top: env(safe-area-inset-top, 0px);

View File

@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { useAppearance } from '@/hooks/useAppearance'
import { useAppearance, useBlurSupport } from '@/hooks/useAppearance'
import { useTheme } from '@/hooks/useTheme'
import {
isDefaultColor,
@ -29,14 +29,15 @@ export function AppearanceProvider() {
appDestructiveTextColor,
} = useAppearance()
const { isDark } = useTheme()
const showAlphaSlider = useBlurSupport()
// Apply appearance settings on mount and when they change
useEffect(() => {
// Apply font size
document.documentElement.style.setProperty('--font-size-base', fontSize)
// Hide alpha slider when IS_LINUX || !IS_TAURI
const shouldHideAlpha = IS_LINUX || !IS_TAURI
// Hide alpha slider when blur is not supported
const shouldHideAlpha = !showAlphaSlider
let alphaStyleElement = document.getElementById('alpha-slider-style')
if (shouldHideAlpha) {
@ -55,12 +56,13 @@ export function AppearanceProvider() {
// Import culori functions dynamically to avoid SSR issues
import('culori').then(({ rgb, oklch, formatCss }) => {
// Convert RGBA to a format culori can work with
// Use alpha = 1 when blur is not supported
const culoriRgb = rgb({
mode: 'rgb',
r: appBgColor.r / 255,
g: appBgColor.g / 255,
b: appBgColor.b / 255,
alpha: IS_WINDOWS || IS_LINUX || !IS_TAURI ? 1 : appBgColor.a,
alpha: showAlphaSlider ? appBgColor.a : 1,
})
const culoriRgbMainView = rgb({
@ -176,6 +178,7 @@ export function AppearanceProvider() {
appAccentTextColor,
appDestructiveBgColor,
appDestructiveTextColor,
showAlphaSlider,
])
// Update appearance when theme changes

View File

@ -1,5 +1,6 @@
import { useEffect } from 'react'
import { useTheme, checkOSDarkMode } from '@/hooks/useTheme'
import { isPlatformTauri } from '@/lib/platform/utils'
/**
* 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
useEffect(() => {
// If theme is set to auto, detect OS preference
if (activeTheme === 'auto') {
const isDarkMode = checkOSDarkMode()
setIsDark(isDarkMode)
setTheme('auto')
// Force refresh theme on mount to handle Linux startup timing issues
const refreshTheme = () => {
if (activeTheme === 'auto') {
const isDarkMode = checkOSDarkMode()
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
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)
// 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
return () => {
clearTimeout(timeoutId)
mediaQuery.removeEventListener('change', handleThemeChange)
if (unlistenTauri) {
unlistenTauri()
}
}
}, [activeTheme, setIsDark, setTheme])

View File

@ -11,7 +11,7 @@ import SetupScreen from '@/containers/SetupScreen'
import { route } from '@/constants/routes'
type SearchParams = {
model?: {
'model'?: {
id: string
provider: string
}
@ -33,7 +33,10 @@ export const Route = createFileRoute(route.home as any)({
}
// 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
}
@ -77,7 +80,7 @@ function Index() {
</HeaderPage>
<div
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
@ -110,7 +113,9 @@ function Index() {
isMobile ? 'text-base' : 'text-lg'
)}
>
{isTemporaryChat ? t('chat:temporaryChatDescription') : t('chat:description')}
{isTemporaryChat
? t('chat:temporaryChatDescription')
: t('chat:description')}
</p>
</div>
<div className="flex-1 shrink-0">

View File

@ -31,17 +31,20 @@ function LogsViewer() {
useEffect(() => {
let lastLogsLength = 0
function updateLogs() {
serviceHub.app().readLogs().then((logData) => {
let needScroll = false
const filteredLogs = logData.filter(Boolean) as LogEntry[]
if (filteredLogs.length > lastLogsLength) needScroll = true
serviceHub
.app()
.readLogs()
.then((logData) => {
let needScroll = false
const filteredLogs = logData.filter(Boolean) as LogEntry[]
if (filteredLogs.length > lastLogsLength) needScroll = true
lastLogsLength = filteredLogs.length
setLogs(filteredLogs)
lastLogsLength = filteredLogs.length
setLogs(filteredLogs)
// Scroll to bottom after initial logs are loaded
if (needScroll) setTimeout(() => scrollToBottom(), 100)
})
// Scroll to bottom after initial logs are loaded
if (needScroll) setTimeout(() => scrollToBottom(), 100)
})
}
updateLogs()

View File

@ -39,4 +39,9 @@ export class DefaultAppService implements AppService {
console.log('readYaml called with path:', path)
throw new Error('readYaml not implemented in default app service')
}
async supportsBlurEffects(): Promise<boolean> {
// On web/non-Windows platforms, always return false
return false
}
}

View File

@ -75,4 +75,8 @@ export class TauriAppService extends DefaultAppService {
async readYaml<T = unknown>(path: string): Promise<T> {
return await invoke<T>('read_yaml', { path })
}
async supportsBlurEffects(): Promise<boolean> {
return await invoke<boolean>('supports_blur_effects')
}
}

View File

@ -17,4 +17,5 @@ export interface AppService {
relocateJanDataFolder(path: string): Promise<void>
getServerStatus(): Promise<boolean>
readYaml<T = unknown>(path: string): Promise<T>
supportsBlurEffects(): Promise<boolean>
}

View File

@ -45,4 +45,9 @@ export class WebAppService implements AppService {
console.log('YAML reading not available in web mode')
throw new Error('readYaml not implemented in web app service')
}
async supportsBlurEffects(): Promise<boolean> {
// Web platforms don't support blur effects
return false
}
}

View File

@ -2,7 +2,8 @@
* 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 { DefaultThemeService } from './default'
@ -10,7 +11,27 @@ export class TauriThemeService extends DefaultThemeService {
async setTheme(theme: ThemeMode): Promise<void> {
try {
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) {
console.error('Error setting theme in Tauri:', error)
throw error
@ -21,7 +42,7 @@ export class TauriThemeService extends DefaultThemeService {
return {
setTheme: (theme: ThemeMode): Promise<void> => {
return this.setTheme(theme)
}
},
}
}
}

View File

@ -7,8 +7,39 @@ import type { WindowConfig, WebviewWindowInstance } from './types'
import { DefaultWindowService } from './default'
export class TauriWindowService extends DefaultWindowService {
async createWebviewWindow(config: WindowConfig): Promise<WebviewWindowInstance> {
async createWebviewWindow(
config: WindowConfig
): Promise<WebviewWindowInstance> {
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, {
url: config.url,
title: config.title,
@ -20,8 +51,12 @@ export class TauriWindowService extends DefaultWindowService {
maximizable: config.maximizable,
closable: config.closable,
fullscreen: config.fullscreen,
theme: theme,
})
// Setup theme listener for this window
this.setupThemeListenerForWindow(webviewWindow)
return {
label: config.label,
async close() {
@ -38,7 +73,7 @@ export class TauriWindowService extends DefaultWindowService {
},
async setTitle(title: string) {
await webviewWindow.setTitle(title)
}
},
}
} catch (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 {
const existingWindow = await WebviewWindow.getByLabel(label)
if (existingWindow) {
return {
label: label,
@ -67,10 +104,10 @@ export class TauriWindowService extends DefaultWindowService {
},
async setTitle(title: string) {
await existingWindow.setTitle(title)
}
},
}
}
return null
} catch (error) {
console.error('Error getting Tauri window by label:', error)
@ -135,8 +172,35 @@ export class TauriWindowService extends DefaultWindowService {
center: true,
})
} 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
}
}
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)
})
}
}