diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json index 41be646d3..2182b9c03 100644 --- a/src-tauri/capabilities/desktop.json +++ b/src-tauri/capabilities/desktop.json @@ -12,6 +12,8 @@ "core:webview:allow-set-webview-zoom", "core:window:allow-start-dragging", "core:window:allow-set-theme", + "core:window:allow-get-all-windows", + "core:event:allow-listen", "shell:allow-spawn", "shell:allow-open", "core:app:allow-set-app-theme", diff --git a/src-tauri/capabilities/log-app-window.json b/src-tauri/capabilities/log-app-window.json index 9f95d1bb9..1bc329ab4 100644 --- a/src-tauri/capabilities/log-app-window.json +++ b/src-tauri/capabilities/log-app-window.json @@ -1,14 +1,18 @@ { "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "logs-app-window", + "identifier": "log-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:webview:allow-get-all-webviews", "core:window:allow-set-focus" ] } diff --git a/src-tauri/capabilities/logs-window.json b/src-tauri/capabilities/logs-window.json index ef56e6f75..1a166f503 100644 --- a/src-tauri/capabilities/logs-window.json +++ b/src-tauri/capabilities/logs-window.json @@ -3,12 +3,16 @@ "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:webview:allow-get-all-webviews", "core:window:allow-set-focus" ] } diff --git a/src-tauri/capabilities/system-monitor-window.json b/src-tauri/capabilities/system-monitor-window.json index 68a75e9fb..cec43f8d8 100644 --- a/src-tauri/capabilities/system-monitor-window.json +++ b/src-tauri/capabilities/system-monitor-window.json @@ -8,13 +8,28 @@ "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:webview:allow-get-all-webviews", "core:window:allow-set-focus", "hardware:allow-get-system-info", "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": [] + } ] } diff --git a/src-tauri/src/core/setup.rs b/src-tauri/src/core/setup.rs index 6564d3609..7ba8f2f74 100644 --- a/src-tauri/src/core/setup.rs +++ b/src-tauri/src/core/setup.rs @@ -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 { }) .build(app) } + +pub fn setup_theme_listener(app: &App) -> 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( + app_handle: tauri::AppHandle, + window: tauri::WebviewWindow, +) { + 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); + } + }); +} diff --git a/src-tauri/src/core/system/commands.rs b/src-tauri/src/core/system/commands.rs index 938e6f8bf..9c72fd4da 100644 --- a/src-tauri/src/core/system/commands.rs +++ b/src-tauri/src/core/system/commands.rs @@ -117,3 +117,4 @@ pub fn is_library_available(library: &str) -> bool { } } } + diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 628f22b08..8ca44d9a9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -193,6 +193,7 @@ pub fn run() { } setup_mcp(app); + setup::setup_theme_listener(app)?; Ok(()) }) .build(tauri::generate_context!()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b0df3fc2f..fb1b1950b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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:", diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 02fa8cdf6..32f6068a2 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -1,7 +1,12 @@ { "app": { "security": { - "capabilities": ["desktop", "system-monitor-window"] + "capabilities": [ + "desktop", + "system-monitor-window", + "log-app-window", + "logs-window" + ] } }, "bundle": { diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index 92f937f0f..5c5b493fa 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -1,7 +1,12 @@ { "app": { "security": { - "capabilities": ["desktop", "system-monitor-window"] + "capabilities": [ + "desktop", + "system-monitor-window", + "log-app-window", + "logs-window" + ] } }, "bundle": { diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 91e2eb374..5efc39c75 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,7 +1,12 @@ { "app": { "security": { - "capabilities": ["desktop"] + "capabilities": [ + "desktop", + "system-monitor-window", + "log-app-window", + "logs-window" + ] } }, diff --git a/web-app/index.html b/web-app/index.html index dd2e76ee6..55625d33c 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -17,7 +17,7 @@ Jan diff --git a/web-app/src/components/ui/dropdown-menu.tsx b/web-app/src/components/ui/dropdown-menu.tsx index 7a527aaca..e4782cf12 100644 --- a/web-app/src/components/ui/dropdown-menu.tsx +++ b/web-app/src/components/ui/dropdown-menu.tsx @@ -229,7 +229,7 @@ function DropdownMenuSubContent({ { + return showAlphaSlider ? defaultAlpha : 1 + } const predefineAppBgColor: RgbaColor[] = [ isDark @@ -21,55 +27,64 @@ 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), }, ] + // Check if a color is the default color (considering both dark and light themes) + const isColorDefault = (color: RgbaColor): boolean => { + const isDarkDefault = color.r === 25 && color.g === 25 && color.b === 25 + const isLightDefault = color.r === 255 && color.g === 255 && color.b === 255 + // Accept both 0.4 and 1 as valid default alpha values (handles blur detection timing) + const hasDefaultAlpha = Math.abs(color.a - 0.4) < 0.01 || Math.abs(color.a - 1) < 0.01 + return (isDarkDefault || isLightDefault) && hasDefaultAlpha + } + return (
{predefineAppBgColor.map((item, i) => { const isSelected = (item.r === appBgColor.r && - item.g === appBgColor.g && - item.b === appBgColor.b && - item.a === appBgColor.a) || - (isDefaultColor(appBgColor) && isDefaultColor(item)) + item.g === appBgColor.g && + item.b === appBgColor.b && + Math.abs(item.a - appBgColor.a) < 0.01) || + (isColorDefault(appBgColor) && isColorDefault(item)) return (
{ diff --git a/web-app/src/containers/DropdownAssistant.tsx b/web-app/src/containers/DropdownAssistant.tsx index a75925002..e474df9ed 100644 --- a/web-app/src/containers/DropdownAssistant.tsx +++ b/web-app/src/containers/DropdownAssistant.tsx @@ -33,7 +33,7 @@ const DropdownAssistant = () => { return ( <> -
+
+ )} +
+
+ )} + {folders.length === 0 ? (
@@ -123,9 +163,19 @@ function ProjectContent() { {t('projects.noProjectsYetDesc')}

+ ) : filteredProjects.length === 0 ? ( +
+ +

+ {t('projects.noProjectsFound')} +

+

+ {t('projects.tryDifferentSearch')} +

+
) : (
- {folders + {filteredProjects .slice() .sort((a, b) => b.updated_at - a.updated_at) .map((folder) => { @@ -172,8 +222,8 @@ function ProjectContent() { className="size-8 cursor-pointer flex items-center justify-center rounded-md hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out mr-1" title={ isExpanded - ? t('projects.collapseThreads') - : t('projects.expandThreads') + ? t('projects.collapseProject') + : t('projects.expandProject') } onClick={() => toggleProjectExpansion(folder.id)} > @@ -218,7 +268,9 @@ function ProjectContent() { {/* Thread List */} {isExpanded && projectThreads.length > 0 && ( -
+
{ 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: WebviewWindow[] = 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 => { return this.setTheme(theme) - } + }, } } } diff --git a/web-app/src/services/window/tauri.ts b/web-app/src/services/window/tauri.ts index a6dd643c7..a0e976ced 100644 --- a/web-app/src/services/window/tauri.ts +++ b/web-app/src/services/window/tauri.ts @@ -7,8 +7,39 @@ import type { WindowConfig, WebviewWindowInstance } from './types' import { DefaultWindowService } from './default' export class TauriWindowService extends DefaultWindowService { - async createWebviewWindow(config: WindowConfig): Promise { + async createWebviewWindow( + config: WindowConfig + ): Promise { 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 { + async getWebviewWindowByLabel( + label: string + ): Promise { 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('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) + }) + } } diff --git a/web-app/src/utils/blurSupport.ts b/web-app/src/utils/blurSupport.ts new file mode 100644 index 000000000..6a87be598 --- /dev/null +++ b/web-app/src/utils/blurSupport.ts @@ -0,0 +1,65 @@ +/** + * Utility to check if the system supports blur/acrylic effects + * based on OS information from hardware data + */ + +import type { HardwareData } from '@/hooks/useHardware' + +/** + * Check if Windows supports blur effects based on build number + * Windows 10 build 17134 (version 1803) and later support acrylic effects + */ +function checkWindowsBlurSupport(osName: string): boolean { + // os_name format: "Windows 10 Pro (build 22631)" or similar + const buildMatch = osName.match(/build\s+(\d+)/i) + + if (buildMatch && buildMatch[1]) { + const build = parseInt(buildMatch[1], 10) + return build >= 17134 + } + + // If we can't detect build number, assume modern Windows supports blur + return true +} + +/** + * Check if Linux supports blur effects based on desktop environment + */ +function checkLinuxBlurSupport(): boolean { + // Check environment variables (only available in Tauri) + if (typeof window === 'undefined') return false + + // These checks would need to be done on the backend + // For now, we'll assume Linux with common DEs supports blur + return true +} + +/** + * Check if the system supports blur/acrylic effects + * + * @param hardwareData - Hardware data from the hardware plugin + * @returns true if blur effects are supported + */ +export function supportsBlurEffects(hardwareData: HardwareData | null): boolean { + if (!hardwareData) return false + + const { os_type, os_name } = hardwareData + + // macOS always supports blur/vibrancy effects + if (os_type === 'macos') { + return true + } + + // Windows: Check build number + if (os_type === 'windows') { + return checkWindowsBlurSupport(os_name) + } + + // Linux: Check desktop environment (simplified for now) + if (os_type === 'linux') { + return checkLinuxBlurSupport() + } + + // Unknown platforms: assume no blur support + return false +}