feat(webui): config page improvements, bridge adaptation, vite build updates
- Extend ConfigPage with new sections and improved layout - Update bridge.ts for internal HTTPS origin compatibility - Update vite-plugin-pages for multi-page build - Add webui config payload type - Refactor webkit scheme_handler (extract shared logic) - Update appearance coordinator for engine-agnostic theming - Add cef-helper binary
- commit
- 7be6b417da991aac50c99d1ff37d8c96fdf3732a
- author
- brice · · 2026-03-24 05:43 UTC
- committer
- brice · · 2026-03-28 04:38 UTC
- parent
- 3aee6b3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +274 -131
A cef-helper +0 -0 binary file: no text diff
binary file: no text diff
A internal/infrastructure/config/webui_payload.go +123 -0
| @@ -0,0 +1,123 @@ | |||
| 1 | + | package config | |
| 2 | + | ||
| 3 | + | import "github.com/bnema/dumber/internal/application/port" | |
| 4 | + | ||
| 5 | + | // WebUIConfigAppearancePayload is the JSON shape expected by dumb://config. | |
| 6 | + | type WebUIConfigAppearancePayload struct { | |
| 7 | + | SansFont string `json:"sans_font"` | |
| 8 | + | SerifFont string `json:"serif_font"` | |
| 9 | + | MonospaceFont string `json:"monospace_font"` | |
| 10 | + | DefaultFontSize int `json:"default_font_size"` | |
| 11 | + | ColorScheme string `json:"color_scheme"` | |
| 12 | + | LightPalette ColorPalette `json:"light_palette"` | |
| 13 | + | DarkPalette ColorPalette `json:"dark_palette"` | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // WebUIConfigHardwarePayload exposes detected hardware to the settings UI. | |
| 17 | + | type WebUIConfigHardwarePayload struct { | |
| 18 | + | CPUCores int `json:"cpu_cores"` | |
| 19 | + | CPUThreads int `json:"cpu_threads"` | |
| 20 | + | TotalRAMMB int `json:"total_ram_mb"` | |
| 21 | + | GPUVendor string `json:"gpu_vendor"` | |
| 22 | + | GPUName string `json:"gpu_name"` | |
| 23 | + | VRAMMB int `json:"vram_mb"` | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // WebUIConfigCustomPerformancePayload holds user-editable custom profile values. | |
| 27 | + | type WebUIConfigCustomPerformancePayload struct { | |
| 28 | + | SkiaCPUThreads int `json:"skia_cpu_threads"` | |
| 29 | + | SkiaGPUThreads int `json:"skia_gpu_threads"` | |
| 30 | + | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |
| 31 | + | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |
| 32 | + | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // WebUIConfigResolvedPerformancePayload holds the resolved effective values. | |
| 36 | + | type WebUIConfigResolvedPerformancePayload struct { | |
| 37 | + | SkiaCPUThreads int `json:"skia_cpu_threads"` | |
| 38 | + | SkiaGPUThreads int `json:"skia_gpu_threads"` | |
| 39 | + | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |
| 40 | + | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |
| 41 | + | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |
| 42 | + | ConservativeThreshold float64 `json:"conservative_threshold"` | |
| 43 | + | StrictThreshold float64 `json:"strict_threshold"` | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | // WebUIConfigPerformancePayload is the performance tab payload expected by the UI. | |
| 47 | + | type WebUIConfigPerformancePayload struct { | |
| 48 | + | Profile string `json:"profile"` | |
| 49 | + | Custom WebUIConfigCustomPerformancePayload `json:"custom"` | |
| 50 | + | Resolved WebUIConfigResolvedPerformancePayload `json:"resolved"` | |
| 51 | + | Hardware WebUIConfigHardwarePayload `json:"hardware"` | |
| 52 | + | } | |
| 53 | + | ||
| 54 | + | // WebUIConfigPayload is the full JSON shape expected by dumb://config. | |
| 55 | + | type WebUIConfigPayload struct { | |
| 56 | + | Appearance WebUIConfigAppearancePayload `json:"appearance"` | |
| 57 | + | Performance WebUIConfigPerformancePayload `json:"performance"` | |
| 58 | + | DefaultUIScale float64 `json:"default_ui_scale"` | |
| 59 | + | DefaultSearchEngine string `json:"default_search_engine"` | |
| 60 | + | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |
| 61 | + | EngineType string `json:"engine_type"` | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // BuildWebUIConfigPayload projects the full config into the stable DTO used by | |
| 65 | + | // the settings page across engines. | |
| 66 | + | func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayload { | |
| 67 | + | if cfg == nil { | |
| 68 | + | return WebUIConfigPayload{} | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | perfCfg := PerformanceConfigFromEngine(&cfg.Engine) | |
| 72 | + | resolved := ResolvePerformanceProfile(&perfCfg, hw) | |
| 73 | + | ||
| 74 | + | return WebUIConfigPayload{ | |
| 75 | + | DefaultUIScale: cfg.DefaultUIScale, | |
| 76 | + | DefaultSearchEngine: cfg.DefaultSearchEngine, | |
| 77 | + | SearchShortcuts: cfg.SearchShortcuts, | |
| 78 | + | EngineType: cfg.Engine.ResolveEngineType(), | |
| 79 | + | Appearance: WebUIConfigAppearancePayload{ | |
| 80 | + | SansFont: cfg.Appearance.SansFont, | |
| 81 | + | SerifFont: cfg.Appearance.SerifFont, | |
| 82 | + | MonospaceFont: cfg.Appearance.MonospaceFont, | |
| 83 | + | DefaultFontSize: cfg.Appearance.DefaultFontSize, | |
| 84 | + | ColorScheme: cfg.Appearance.ColorScheme, | |
| 85 | + | LightPalette: cfg.Appearance.LightPalette, | |
| 86 | + | DarkPalette: cfg.Appearance.DarkPalette, | |
| 87 | + | }, | |
| 88 | + | Performance: WebUIConfigPerformancePayload{ | |
| 89 | + | Profile: string(cfg.Engine.Profile), | |
| 90 | + | Custom: WebUIConfigCustomPerformancePayload{ | |
| 91 | + | SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads, | |
| 92 | + | SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads, | |
| 93 | + | WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB, | |
| 94 | + | NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB, | |
| 95 | + | WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount, | |
| 96 | + | }, | |
| 97 | + | Resolved: WebUIConfigResolvedPerformancePayload{ | |
| 98 | + | SkiaCPUThreads: resolved.SkiaCPUPaintingThreads, | |
| 99 | + | SkiaGPUThreads: resolved.SkiaGPUPaintingThreads, | |
| 100 | + | WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB, | |
| 101 | + | NetworkProcessMemoryMB: resolved.NetworkProcessMemoryLimitMB, | |
| 102 | + | WebViewPoolPrewarm: resolved.WebViewPoolPrewarmCount, | |
| 103 | + | ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold, | |
| 104 | + | StrictThreshold: resolved.WebProcessMemoryStrictThreshold, | |
| 105 | + | }, | |
| 106 | + | Hardware: buildWebUIHardwarePayload(hw), | |
| 107 | + | }, | |
| 108 | + | } | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | func buildWebUIHardwarePayload(hw *port.HardwareInfo) WebUIConfigHardwarePayload { | |
| 112 | + | if hw == nil { | |
| 113 | + | return WebUIConfigHardwarePayload{} | |
| 114 | + | } | |
| 115 | + | return WebUIConfigHardwarePayload{ | |
| 116 | + | CPUCores: hw.CPUCores, | |
| 117 | + | CPUThreads: hw.CPUThreads, | |
| 118 | + | TotalRAMMB: hw.TotalRAMMB(), | |
| 119 | + | GPUVendor: string(hw.GPUVendor), | |
| 120 | + | GPUName: hw.GPUName, | |
| 121 | + | VRAMMB: hw.VRAMMB(), | |
| 122 | + | } | |
| 123 | + | } | |
| @@ -0,0 +1,123 @@ | |||||
| 1 | + | package config | |||
| 2 | + | ||||
| 3 | + | import "github.com/bnema/dumber/internal/application/port" | |||
| 4 | + | ||||
| 5 | + | // WebUIConfigAppearancePayload is the JSON shape expected by dumb://config. | |||
| 6 | + | type WebUIConfigAppearancePayload struct { | |||
| 7 | + | SansFont string `json:"sans_font"` | |||
| 8 | + | SerifFont string `json:"serif_font"` | |||
| 9 | + | MonospaceFont string `json:"monospace_font"` | |||
| 10 | + | DefaultFontSize int `json:"default_font_size"` | |||
| 11 | + | ColorScheme string `json:"color_scheme"` | |||
| 12 | + | LightPalette ColorPalette `json:"light_palette"` | |||
| 13 | + | DarkPalette ColorPalette `json:"dark_palette"` | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // WebUIConfigHardwarePayload exposes detected hardware to the settings UI. | |||
| 17 | + | type WebUIConfigHardwarePayload struct { | |||
| 18 | + | CPUCores int `json:"cpu_cores"` | |||
| 19 | + | CPUThreads int `json:"cpu_threads"` | |||
| 20 | + | TotalRAMMB int `json:"total_ram_mb"` | |||
| 21 | + | GPUVendor string `json:"gpu_vendor"` | |||
| 22 | + | GPUName string `json:"gpu_name"` | |||
| 23 | + | VRAMMB int `json:"vram_mb"` | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // WebUIConfigCustomPerformancePayload holds user-editable custom profile values. | |||
| 27 | + | type WebUIConfigCustomPerformancePayload struct { | |||
| 28 | + | SkiaCPUThreads int `json:"skia_cpu_threads"` | |||
| 29 | + | SkiaGPUThreads int `json:"skia_gpu_threads"` | |||
| 30 | + | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |||
| 31 | + | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |||
| 32 | + | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // WebUIConfigResolvedPerformancePayload holds the resolved effective values. | |||
| 36 | + | type WebUIConfigResolvedPerformancePayload struct { | |||
| 37 | + | SkiaCPUThreads int `json:"skia_cpu_threads"` | |||
| 38 | + | SkiaGPUThreads int `json:"skia_gpu_threads"` | |||
| 39 | + | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |||
| 40 | + | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |||
| 41 | + | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |||
| 42 | + | ConservativeThreshold float64 `json:"conservative_threshold"` | |||
| 43 | + | StrictThreshold float64 `json:"strict_threshold"` | |||
| 44 | + | } | |||
| 45 | + | ||||
| 46 | + | // WebUIConfigPerformancePayload is the performance tab payload expected by the UI. | |||
| 47 | + | type WebUIConfigPerformancePayload struct { | |||
| 48 | + | Profile string `json:"profile"` | |||
| 49 | + | Custom WebUIConfigCustomPerformancePayload `json:"custom"` | |||
| 50 | + | Resolved WebUIConfigResolvedPerformancePayload `json:"resolved"` | |||
| 51 | + | Hardware WebUIConfigHardwarePayload `json:"hardware"` | |||
| 52 | + | } | |||
| 53 | + | ||||
| 54 | + | // WebUIConfigPayload is the full JSON shape expected by dumb://config. | |||
| 55 | + | type WebUIConfigPayload struct { | |||
| 56 | + | Appearance WebUIConfigAppearancePayload `json:"appearance"` | |||
| 57 | + | Performance WebUIConfigPerformancePayload `json:"performance"` | |||
| 58 | + | DefaultUIScale float64 `json:"default_ui_scale"` | |||
| 59 | + | DefaultSearchEngine string `json:"default_search_engine"` | |||
| 60 | + | SearchShortcuts map[string]SearchShortcut `json:"search_shortcuts"` | |||
| 61 | + | EngineType string `json:"engine_type"` | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // BuildWebUIConfigPayload projects the full config into the stable DTO used by | |||
| 65 | + | // the settings page across engines. | |||
| 66 | + | func BuildWebUIConfigPayload(cfg *Config, hw *port.HardwareInfo) WebUIConfigPayload { | |||
| 67 | + | if cfg == nil { | |||
| 68 | + | return WebUIConfigPayload{} | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | perfCfg := PerformanceConfigFromEngine(&cfg.Engine) | |||
| 72 | + | resolved := ResolvePerformanceProfile(&perfCfg, hw) | |||
| 73 | + | ||||
| 74 | + | return WebUIConfigPayload{ | |||
| 75 | + | DefaultUIScale: cfg.DefaultUIScale, | |||
| 76 | + | DefaultSearchEngine: cfg.DefaultSearchEngine, | |||
| 77 | + | SearchShortcuts: cfg.SearchShortcuts, | |||
| 78 | + | EngineType: cfg.Engine.ResolveEngineType(), | |||
| 79 | + | Appearance: WebUIConfigAppearancePayload{ | |||
| 80 | + | SansFont: cfg.Appearance.SansFont, | |||
| 81 | + | SerifFont: cfg.Appearance.SerifFont, | |||
| 82 | + | MonospaceFont: cfg.Appearance.MonospaceFont, | |||
| 83 | + | DefaultFontSize: cfg.Appearance.DefaultFontSize, | |||
| 84 | + | ColorScheme: cfg.Appearance.ColorScheme, | |||
| 85 | + | LightPalette: cfg.Appearance.LightPalette, | |||
| 86 | + | DarkPalette: cfg.Appearance.DarkPalette, | |||
| 87 | + | }, | |||
| 88 | + | Performance: WebUIConfigPerformancePayload{ | |||
| 89 | + | Profile: string(cfg.Engine.Profile), | |||
| 90 | + | Custom: WebUIConfigCustomPerformancePayload{ | |||
| 91 | + | SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads, | |||
| 92 | + | SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads, | |||
| 93 | + | WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB, | |||
| 94 | + | NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB, | |||
| 95 | + | WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount, | |||
| 96 | + | }, | |||
| 97 | + | Resolved: WebUIConfigResolvedPerformancePayload{ | |||
| 98 | + | SkiaCPUThreads: resolved.SkiaCPUPaintingThreads, | |||
| 99 | + | SkiaGPUThreads: resolved.SkiaGPUPaintingThreads, | |||
| 100 | + | WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB, | |||
| 101 | + | NetworkProcessMemoryMB: resolved.NetworkProcessMemoryLimitMB, | |||
| 102 | + | WebViewPoolPrewarm: resolved.WebViewPoolPrewarmCount, | |||
| 103 | + | ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold, | |||
| 104 | + | StrictThreshold: resolved.WebProcessMemoryStrictThreshold, | |||
| 105 | + | }, | |||
| 106 | + | Hardware: buildWebUIHardwarePayload(hw), | |||
| 107 | + | }, | |||
| 108 | + | } | |||
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | func buildWebUIHardwarePayload(hw *port.HardwareInfo) WebUIConfigHardwarePayload { | |||
| 112 | + | if hw == nil { | |||
| 113 | + | return WebUIConfigHardwarePayload{} | |||
| 114 | + | } | |||
| 115 | + | return WebUIConfigHardwarePayload{ | |||
| 116 | + | CPUCores: hw.CPUCores, | |||
| 117 | + | CPUThreads: hw.CPUThreads, | |||
| 118 | + | TotalRAMMB: hw.TotalRAMMB(), | |||
| 119 | + | GPUVendor: string(hw.GPUVendor), | |||
| 120 | + | GPUName: hw.GPUName, | |||
| 121 | + | VRAMMB: hw.VRAMMB(), | |||
| 122 | + | } | |||
| 123 | + | } | |||
M internal/infrastructure/webkit/scheme_handler.go +1 -107
| @@ -74,60 +74,6 @@ type DumbSchemeHandler struct { | |||
| 74 | 74 | ctx context.Context | |
| 75 | 75 | } | |
| 76 | 76 | ||
| 77 | - | type configAppearancePayload struct { | |
| 78 | - | SansFont string `json:"sans_font"` | |
| 79 | - | SerifFont string `json:"serif_font"` | |
| 80 | - | MonospaceFont string `json:"monospace_font"` | |
| 81 | - | DefaultFontSize int `json:"default_font_size"` | |
| 82 | - | ColorScheme string `json:"color_scheme"` | |
| 83 | - | LightPalette config.ColorPalette `json:"light_palette"` | |
| 84 | - | DarkPalette config.ColorPalette `json:"dark_palette"` | |
| 85 | - | } | |
| 86 | - | ||
| 87 | - | type configHardwarePayload struct { | |
| 88 | - | CPUCores int `json:"cpu_cores"` | |
| 89 | - | CPUThreads int `json:"cpu_threads"` | |
| 90 | - | TotalRAMMB int `json:"total_ram_mb"` | |
| 91 | - | GPUVendor string `json:"gpu_vendor"` | |
| 92 | - | GPUName string `json:"gpu_name"` | |
| 93 | - | VRAMMB int `json:"vram_mb"` | |
| 94 | - | } | |
| 95 | - | ||
| 96 | - | type configPerformancePayload struct { | |
| 97 | - | Profile string `json:"profile"` | |
| 98 | - | Custom configCustomPerformance `json:"custom"` | |
| 99 | - | Resolved configResolvedPerformance `json:"resolved"` | |
| 100 | - | Hardware configHardwarePayload `json:"hardware"` | |
| 101 | - | } | |
| 102 | - | ||
| 103 | - | // configCustomPerformance holds user-editable fields for custom profile. | |
| 104 | - | type configCustomPerformance struct { | |
| 105 | - | SkiaCPUThreads int `json:"skia_cpu_threads"` | |
| 106 | - | SkiaGPUThreads int `json:"skia_gpu_threads"` | |
| 107 | - | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |
| 108 | - | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |
| 109 | - | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |
| 110 | - | } | |
| 111 | - | ||
| 112 | - | // configResolvedPerformance shows the actual values that will be applied at startup. | |
| 113 | - | type configResolvedPerformance struct { | |
| 114 | - | SkiaCPUThreads int `json:"skia_cpu_threads"` | |
| 115 | - | SkiaGPUThreads int `json:"skia_gpu_threads"` | |
| 116 | - | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |
| 117 | - | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |
| 118 | - | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |
| 119 | - | ConservativeThreshold float64 `json:"conservative_threshold"` | |
| 120 | - | StrictThreshold float64 `json:"strict_threshold"` | |
| 121 | - | } | |
| 122 | - | ||
| 123 | - | type configPayload struct { | |
| 124 | - | Appearance configAppearancePayload `json:"appearance"` | |
| 125 | - | Performance configPerformancePayload `json:"performance"` | |
| 126 | - | DefaultUIScale float64 `json:"default_ui_scale"` | |
| 127 | - | DefaultSearchEngine string `json:"default_search_engine"` | |
| 128 | - | SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"` | |
| 129 | - | } | |
| 130 | - | ||
| 131 | 77 | // NewDumbSchemeHandler creates a new handler for the dumb:// scheme. | |
| 132 | 78 | func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler { | |
| 133 | 79 | log := logging.FromContext(ctx) | |
| @@ -346,44 +292,7 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo | |||
| 346 | 292 | hw = &hwInfo | |
| 347 | 293 | } | |
| 348 | 294 | ||
| 349 | - | // Resolve performance profile with hardware info | |
| 350 | - | perfCfg := config.PerformanceConfigFromEngine(&cfg.Engine) | |
| 351 | - | resolved := config.ResolvePerformanceProfile(&perfCfg, hw) | |
| 352 | - | ||
| 353 | - | resp := configPayload{ | |
| 354 | - | DefaultUIScale: cfg.DefaultUIScale, | |
| 355 | - | DefaultSearchEngine: cfg.DefaultSearchEngine, | |
| 356 | - | SearchShortcuts: cfg.SearchShortcuts, | |
| 357 | - | Appearance: configAppearancePayload{ | |
| 358 | - | SansFont: cfg.Appearance.SansFont, | |
| 359 | - | SerifFont: cfg.Appearance.SerifFont, | |
| 360 | - | MonospaceFont: cfg.Appearance.MonospaceFont, | |
| 361 | - | DefaultFontSize: cfg.Appearance.DefaultFontSize, | |
| 362 | - | ColorScheme: cfg.Appearance.ColorScheme, | |
| 363 | - | LightPalette: cfg.Appearance.LightPalette, | |
| 364 | - | DarkPalette: cfg.Appearance.DarkPalette, | |
| 365 | - | }, | |
| 366 | - | Performance: configPerformancePayload{ | |
| 367 | - | Profile: string(cfg.Engine.Profile), | |
| 368 | - | Custom: configCustomPerformance{ | |
| 369 | - | SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads, | |
| 370 | - | SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads, | |
| 371 | - | WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB, | |
| 372 | - | NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB, | |
| 373 | - | WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount, | |
| 374 | - | }, | |
| 375 | - | Resolved: configResolvedPerformance{ | |
| 376 | - | SkiaCPUThreads: resolved.SkiaCPUPaintingThreads, | |
| 377 | - | SkiaGPUThreads: resolved.SkiaGPUPaintingThreads, | |
| 378 | - | WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB, | |
| 379 | - | NetworkProcessMemoryMB: resolved.NetworkProcessMemoryLimitMB, | |
| 380 | - | WebViewPoolPrewarm: resolved.WebViewPoolPrewarmCount, | |
| 381 | - | ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold, | |
| 382 | - | StrictThreshold: resolved.WebProcessMemoryStrictThreshold, | |
| 383 | - | }, | |
| 384 | - | Hardware: buildHardwarePayload(hw), | |
| 385 | - | }, | |
| 386 | - | } | |
| 295 | + | resp := config.BuildWebUIConfigPayload(cfg, hw) | |
| 387 | 296 | ||
| 388 | 297 | data, err := json.Marshal(resp) | |
| 389 | 298 | if err != nil { | |
| @@ -401,21 +310,6 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo | |||
| 401 | 310 | } | |
| 402 | 311 | } | |
| 403 | 312 | ||
| 404 | - | // buildHardwarePayload converts HardwareInfo to JSON payload. | |
| 405 | - | func buildHardwarePayload(hw *port.HardwareInfo) configHardwarePayload { | |
| 406 | - | if hw == nil { | |
| 407 | - | return configHardwarePayload{} | |
| 408 | - | } | |
| 409 | - | return configHardwarePayload{ | |
| 410 | - | CPUCores: hw.CPUCores, | |
| 411 | - | CPUThreads: hw.CPUThreads, | |
| 412 | - | TotalRAMMB: hw.TotalRAMMB(), | |
| 413 | - | GPUVendor: string(hw.GPUVendor), | |
| 414 | - | GPUName: hw.GPUName, | |
| 415 | - | VRAMMB: hw.VRAMMB(), | |
| 416 | - | } | |
| 417 | - | } | |
| 418 | - | ||
| 419 | 313 | // RegisterPage registers a handler for a specific path. | |
| 420 | 314 | func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) { | |
| 421 | 315 | h.mu.Lock() | |
| @@ -74,60 +74,6 @@ type DumbSchemeHandler struct { | |||||
| 74 | ctx context.Context | 74 | ctx context.Context | ||
| 75 | } | 75 | } | ||
| 76 | 76 | ||||
| 77 | - | type configAppearancePayload struct { | |||
| 78 | - | SansFont string `json:"sans_font"` | |||
| 79 | - | SerifFont string `json:"serif_font"` | |||
| 80 | - | MonospaceFont string `json:"monospace_font"` | |||
| 81 | - | DefaultFontSize int `json:"default_font_size"` | |||
| 82 | - | ColorScheme string `json:"color_scheme"` | |||
| 83 | - | LightPalette config.ColorPalette `json:"light_palette"` | |||
| 84 | - | DarkPalette config.ColorPalette `json:"dark_palette"` | |||
| 85 | - | } | |||
| 86 | - | ||||
| 87 | - | type configHardwarePayload struct { | |||
| 88 | - | CPUCores int `json:"cpu_cores"` | |||
| 89 | - | CPUThreads int `json:"cpu_threads"` | |||
| 90 | - | TotalRAMMB int `json:"total_ram_mb"` | |||
| 91 | - | GPUVendor string `json:"gpu_vendor"` | |||
| 92 | - | GPUName string `json:"gpu_name"` | |||
| 93 | - | VRAMMB int `json:"vram_mb"` | |||
| 94 | - | } | |||
| 95 | - | ||||
| 96 | - | type configPerformancePayload struct { | |||
| 97 | - | Profile string `json:"profile"` | |||
| 98 | - | Custom configCustomPerformance `json:"custom"` | |||
| 99 | - | Resolved configResolvedPerformance `json:"resolved"` | |||
| 100 | - | Hardware configHardwarePayload `json:"hardware"` | |||
| 101 | - | } | |||
| 102 | - | ||||
| 103 | - | // configCustomPerformance holds user-editable fields for custom profile. | |||
| 104 | - | type configCustomPerformance struct { | |||
| 105 | - | SkiaCPUThreads int `json:"skia_cpu_threads"` | |||
| 106 | - | SkiaGPUThreads int `json:"skia_gpu_threads"` | |||
| 107 | - | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |||
| 108 | - | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |||
| 109 | - | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |||
| 110 | - | } | |||
| 111 | - | ||||
| 112 | - | // configResolvedPerformance shows the actual values that will be applied at startup. | |||
| 113 | - | type configResolvedPerformance struct { | |||
| 114 | - | SkiaCPUThreads int `json:"skia_cpu_threads"` | |||
| 115 | - | SkiaGPUThreads int `json:"skia_gpu_threads"` | |||
| 116 | - | WebProcessMemoryMB int `json:"web_process_memory_mb"` | |||
| 117 | - | NetworkProcessMemoryMB int `json:"network_process_memory_mb"` | |||
| 118 | - | WebViewPoolPrewarm int `json:"webview_pool_prewarm"` | |||
| 119 | - | ConservativeThreshold float64 `json:"conservative_threshold"` | |||
| 120 | - | StrictThreshold float64 `json:"strict_threshold"` | |||
| 121 | - | } | |||
| 122 | - | ||||
| 123 | - | type configPayload struct { | |||
| 124 | - | Appearance configAppearancePayload `json:"appearance"` | |||
| 125 | - | Performance configPerformancePayload `json:"performance"` | |||
| 126 | - | DefaultUIScale float64 `json:"default_ui_scale"` | |||
| 127 | - | DefaultSearchEngine string `json:"default_search_engine"` | |||
| 128 | - | SearchShortcuts map[string]config.SearchShortcut `json:"search_shortcuts"` | |||
| 129 | - | } | |||
| 130 | - | ||||
| 131 | // NewDumbSchemeHandler creates a new handler for the dumb:// scheme. | 77 | // NewDumbSchemeHandler creates a new handler for the dumb:// scheme. | ||
| 132 | func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler { | 78 | func NewDumbSchemeHandler(ctx context.Context) *DumbSchemeHandler { | ||
| 133 | log := logging.FromContext(ctx) | 79 | log := logging.FromContext(ctx) | ||
| @@ -346,44 +292,7 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo | |||||
| 346 | hw = &hwInfo | 292 | hw = &hwInfo | ||
| 347 | } | 293 | } | ||
| 348 | 294 | ||||
| 349 | - | // Resolve performance profile with hardware info | 295 | + | resp := config.BuildWebUIConfigPayload(cfg, hw) |
| 350 | - | perfCfg := config.PerformanceConfigFromEngine(&cfg.Engine) | |||
| 351 | - | resolved := config.ResolvePerformanceProfile(&perfCfg, hw) | |||
| 352 | - | ||||
| 353 | - | resp := configPayload{ | |||
| 354 | - | DefaultUIScale: cfg.DefaultUIScale, | |||
| 355 | - | DefaultSearchEngine: cfg.DefaultSearchEngine, | |||
| 356 | - | SearchShortcuts: cfg.SearchShortcuts, | |||
| 357 | - | Appearance: configAppearancePayload{ | |||
| 358 | - | SansFont: cfg.Appearance.SansFont, | |||
| 359 | - | SerifFont: cfg.Appearance.SerifFont, | |||
| 360 | - | MonospaceFont: cfg.Appearance.MonospaceFont, | |||
| 361 | - | DefaultFontSize: cfg.Appearance.DefaultFontSize, | |||
| 362 | - | ColorScheme: cfg.Appearance.ColorScheme, | |||
| 363 | - | LightPalette: cfg.Appearance.LightPalette, | |||
| 364 | - | DarkPalette: cfg.Appearance.DarkPalette, | |||
| 365 | - | }, | |||
| 366 | - | Performance: configPerformancePayload{ | |||
| 367 | - | Profile: string(cfg.Engine.Profile), | |||
| 368 | - | Custom: configCustomPerformance{ | |||
| 369 | - | SkiaCPUThreads: cfg.Engine.WebKit.SkiaCPUPaintingThreads, | |||
| 370 | - | SkiaGPUThreads: cfg.Engine.WebKit.SkiaGPUPaintingThreads, | |||
| 371 | - | WebProcessMemoryMB: cfg.Engine.WebKit.WebProcessMemoryLimitMB, | |||
| 372 | - | NetworkProcessMemoryMB: cfg.Engine.WebKit.NetworkProcessMemoryLimitMB, | |||
| 373 | - | WebViewPoolPrewarm: cfg.Engine.PoolPrewarmCount, | |||
| 374 | - | }, | |||
| 375 | - | Resolved: configResolvedPerformance{ | |||
| 376 | - | SkiaCPUThreads: resolved.SkiaCPUPaintingThreads, | |||
| 377 | - | SkiaGPUThreads: resolved.SkiaGPUPaintingThreads, | |||
| 378 | - | WebProcessMemoryMB: resolved.WebProcessMemoryLimitMB, | |||
| 379 | - | NetworkProcessMemoryMB: resolved.NetworkProcessMemoryLimitMB, | |||
| 380 | - | WebViewPoolPrewarm: resolved.WebViewPoolPrewarmCount, | |||
| 381 | - | ConservativeThreshold: resolved.WebProcessMemoryConservativeThreshold, | |||
| 382 | - | StrictThreshold: resolved.WebProcessMemoryStrictThreshold, | |||
| 383 | - | }, | |||
| 384 | - | Hardware: buildHardwarePayload(hw), | |||
| 385 | - | }, | |||
| 386 | - | } | |||
| 387 | 296 | ||||
| 388 | data, err := json.Marshal(resp) | 297 | data, err := json.Marshal(resp) | ||
| 389 | if err != nil { | 298 | if err != nil { | ||
| @@ -401,21 +310,6 @@ func (h *DumbSchemeHandler) buildConfigResponse(cfg *config.Config) *SchemeRespo | |||||
| 401 | } | 310 | } | ||
| 402 | } | 311 | } | ||
| 403 | 312 | ||||
| 404 | - | // buildHardwarePayload converts HardwareInfo to JSON payload. | |||
| 405 | - | func buildHardwarePayload(hw *port.HardwareInfo) configHardwarePayload { | |||
| 406 | - | if hw == nil { | |||
| 407 | - | return configHardwarePayload{} | |||
| 408 | - | } | |||
| 409 | - | return configHardwarePayload{ | |||
| 410 | - | CPUCores: hw.CPUCores, | |||
| 411 | - | CPUThreads: hw.CPUThreads, | |||
| 412 | - | TotalRAMMB: hw.TotalRAMMB(), | |||
| 413 | - | GPUVendor: string(hw.GPUVendor), | |||
| 414 | - | GPUName: hw.GPUName, | |||
| 415 | - | VRAMMB: hw.VRAMMB(), | |||
| 416 | - | } | |||
| 417 | - | } | |||
| 418 | - | ||||
| 419 | // RegisterPage registers a handler for a specific path. | 313 | // RegisterPage registers a handler for a specific path. | ||
| 420 | func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) { | 314 | func (h *DumbSchemeHandler) RegisterPage(path string, handler PageHandler) { | ||
| 421 | h.mu.Lock() | 315 | h.mu.Lock() | ||
M internal/ui/coordinator/content/appearance.go +10 -6
| @@ -72,23 +72,27 @@ func buildWebUIThemeScript(prefersDark bool, cssText string) (string, error) { | |||
| 72 | 72 | try { | |
| 73 | 73 | var cssText = %s; | |
| 74 | 74 | var prefersDark = %t; | |
| 75 | + | var root = document.documentElement; | |
| 75 | 76 | ||
| 76 | - | // Keep the global flag in sync | |
| 77 | + | // Keep the global flags in sync for both WebKit and CEF-backed pages. | |
| 77 | 78 | window.__dumber_gtk_prefers_dark = prefersDark; | |
| 79 | + | window.__dumber_cef_prefers_dark = prefersDark; | |
| 78 | 80 | ||
| 79 | 81 | // Update dark/light class | |
| 80 | 82 | if (prefersDark) { | |
| 81 | - | document.documentElement.classList.add('dark'); | |
| 82 | - | document.documentElement.classList.remove('light'); | |
| 83 | + | root.classList.add('dark'); | |
| 84 | + | root.classList.remove('light'); | |
| 83 | 85 | } else { | |
| 84 | - | document.documentElement.classList.add('light'); | |
| 85 | - | document.documentElement.classList.remove('dark'); | |
| 86 | + | root.classList.add('light'); | |
| 87 | + | root.classList.remove('dark'); | |
| 86 | 88 | } | |
| 89 | + | root.style.colorScheme = prefersDark ? 'dark' : 'light'; | |
| 87 | 90 | ||
| 88 | 91 | // Update or insert theme style | |
| 89 | - | var style = document.querySelector('style[data-dumber-theme-vars]'); | |
| 92 | + | var style = document.getElementById('dumber-theme-vars') || document.querySelector('style[data-dumber-theme-vars]'); | |
| 90 | 93 | if (!style) { | |
| 91 | 94 | style = document.createElement('style'); | |
| 95 | + | style.id = 'dumber-theme-vars'; | |
| 92 | 96 | style.setAttribute('data-dumber-theme-vars', ''); | |
| 93 | 97 | (document.head || document.documentElement).appendChild(style); | |
| 94 | 98 | } | |
| @@ -72,23 +72,27 @@ func buildWebUIThemeScript(prefersDark bool, cssText string) (string, error) { | |||||
| 72 | try { | 72 | try { | ||
| 73 | var cssText = %s; | 73 | var cssText = %s; | ||
| 74 | var prefersDark = %t; | 74 | var prefersDark = %t; | ||
| 75 | + | var root = document.documentElement; | |||
| 75 | 76 | ||||
| 76 | - | // Keep the global flag in sync | 77 | + | // Keep the global flags in sync for both WebKit and CEF-backed pages. |
| 77 | window.__dumber_gtk_prefers_dark = prefersDark; | 78 | window.__dumber_gtk_prefers_dark = prefersDark; | ||
| 79 | + | window.__dumber_cef_prefers_dark = prefersDark; | |||
| 78 | 80 | ||||
| 79 | // Update dark/light class | 81 | // Update dark/light class | ||
| 80 | if (prefersDark) { | 82 | if (prefersDark) { | ||
| 81 | - | document.documentElement.classList.add('dark'); | 83 | + | root.classList.add('dark'); |
| 82 | - | document.documentElement.classList.remove('light'); | 84 | + | root.classList.remove('light'); |
| 83 | } else { | 85 | } else { | ||
| 84 | - | document.documentElement.classList.add('light'); | 86 | + | root.classList.add('light'); |
| 85 | - | document.documentElement.classList.remove('dark'); | 87 | + | root.classList.remove('dark'); |
| 86 | } | 88 | } | ||
| 89 | + | root.style.colorScheme = prefersDark ? 'dark' : 'light'; | |||
| 87 | 90 | ||||
| 88 | // Update or insert theme style | 91 | // Update or insert theme style | ||
| 89 | - | var style = document.querySelector('style[data-dumber-theme-vars]'); | 92 | + | var style = document.getElementById('dumber-theme-vars') || document.querySelector('style[data-dumber-theme-vars]'); |
| 90 | if (!style) { | 93 | if (!style) { | ||
| 91 | style = document.createElement('style'); | 94 | style = document.createElement('style'); | ||
| 95 | + | style.id = 'dumber-theme-vars'; | |||
| 92 | style.setAttribute('data-dumber-theme-vars', ''); | 96 | style.setAttribute('data-dumber-theme-vars', ''); | ||
| 93 | (document.head || document.documentElement).appendChild(style); | 97 | (document.head || document.documentElement).appendChild(style); | ||
| 94 | } | 98 | } | ||
M webui/src/lib/bridge.ts +6 -4
| @@ -18,11 +18,13 @@ interface WebKitBridge { | |||
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | 20 | /** | |
| 21 | - | * Get the WebKit message handler bridge, if available. | |
| 21 | + | * Get the native bridge, if available. | |
| 22 | + | * WebKit exposes `window.webkit.messageHandlers.dumber` while CEF injects | |
| 23 | + | * `window.dumber.postMessage`. | |
| 22 | 24 | */ | |
| 23 | 25 | export function getWebKitBridge(): WebKitBridge | null { | |
| 24 | 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| 25 | - | const bridge = (window as any).webkit?.messageHandlers?.dumber; | |
| 27 | + | const bridge = (window as any).webkit?.messageHandlers?.dumber ?? (window as any).dumber; | |
| 26 | 28 | return bridge && typeof bridge.postMessage === "function" ? bridge : null; | |
| 27 | 29 | } | |
| 28 | 30 | ||
| @@ -50,7 +52,7 @@ export function toPlainObject<T>(obj: T): T { | |||
| 50 | 52 | } | |
| 51 | 53 | ||
| 52 | 54 | /** | |
| 53 | - | * Post a message to the Go backend via WebKit bridge. | |
| 55 | + | * Post a message to the Go backend via the native bridge. | |
| 54 | 56 | * | |
| 55 | 57 | * Automatically: | |
| 56 | 58 | * - Converts Svelte 5 Proxy objects to plain objects | |
| @@ -62,7 +64,7 @@ export function toPlainObject<T>(obj: T): T { | |||
| 62 | 64 | export function postMessage(msg: BridgeMessage): void { | |
| 63 | 65 | const bridge = getWebKitBridge(); | |
| 64 | 66 | if (!bridge) { | |
| 65 | - | throw new Error("WebKit bridge not available"); | |
| 67 | + | throw new Error("Dumber bridge not available"); | |
| 66 | 68 | } | |
| 67 | 69 | ||
| 68 | 70 | // Ensure webviewId is set | |
| @@ -18,11 +18,13 @@ interface WebKitBridge { | |||||
| 18 | } | 18 | } | ||
| 19 | 19 | ||||
| 20 | /** | 20 | /** | ||
| 21 | - | * Get the WebKit message handler bridge, if available. | 21 | + | * Get the native bridge, if available. |
| 22 | + | * WebKit exposes `window.webkit.messageHandlers.dumber` while CEF injects | |||
| 23 | + | * `window.dumber.postMessage`. | |||
| 22 | */ | 24 | */ | ||
| 23 | export function getWebKitBridge(): WebKitBridge | null { | 25 | export function getWebKitBridge(): WebKitBridge | null { | ||
| 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| 25 | - | const bridge = (window as any).webkit?.messageHandlers?.dumber; | 27 | + | const bridge = (window as any).webkit?.messageHandlers?.dumber ?? (window as any).dumber; |
| 26 | return bridge && typeof bridge.postMessage === "function" ? bridge : null; | 28 | return bridge && typeof bridge.postMessage === "function" ? bridge : null; | ||
| 27 | } | 29 | } | ||
| 28 | 30 | ||||
| @@ -50,7 +52,7 @@ export function toPlainObject<T>(obj: T): T { | |||||
| 50 | } | 52 | } | ||
| 51 | 53 | ||||
| 52 | /** | 54 | /** | ||
| 53 | - | * Post a message to the Go backend via WebKit bridge. | 55 | + | * Post a message to the Go backend via the native bridge. |
| 54 | * | 56 | * | ||
| 55 | * Automatically: | 57 | * Automatically: | ||
| 56 | * - Converts Svelte 5 Proxy objects to plain objects | 58 | * - Converts Svelte 5 Proxy objects to plain objects | ||
| @@ -62,7 +64,7 @@ export function toPlainObject<T>(obj: T): T { | |||||
| 62 | export function postMessage(msg: BridgeMessage): void { | 64 | export function postMessage(msg: BridgeMessage): void { | ||
| 63 | const bridge = getWebKitBridge(); | 65 | const bridge = getWebKitBridge(); | ||
| 64 | if (!bridge) { | 66 | if (!bridge) { | ||
| 65 | - | throw new Error("WebKit bridge not available"); | 67 | + | throw new Error("Dumber bridge not available"); |
| 66 | } | 68 | } | ||
| 67 | 69 | ||||
| 68 | // Ensure webviewId is set | 70 | // Ensure webviewId is set | ||
M webui/src/pages/ConfigPage.svelte +81 -7
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | <script lang="ts"> | |
| 2 | 2 | import { onMount } from "svelte"; | |
| 3 | - | import { ModeWatcher } from "mode-watcher"; | |
| 3 | + | import { ModeWatcher, resetMode, setMode } from "mode-watcher"; | |
| 4 | 4 | import ConfigShell from "./config/ConfigShell.svelte"; | |
| 5 | 5 | import ShortcutsTable from "./config/ShortcutsTable.svelte"; | |
| 6 | 6 | import KeybindingsTab from "./config/KeybindingsTab.svelte"; | |
| @@ -83,6 +83,7 @@ | |||
| 83 | 83 | default_ui_scale: number; | |
| 84 | 84 | default_search_engine: string; | |
| 85 | 85 | search_shortcuts: Record<string, SearchShortcut>; | |
| 86 | + | engine_type: string; | |
| 86 | 87 | }; | |
| 87 | 88 | ||
| 88 | 89 | const PERFORMANCE_PROFILES = [ | |
| @@ -117,6 +118,49 @@ | |||
| 117 | 118 | let themeEvents = $state(0); | |
| 118 | 119 | let activeTab = $state("appearance"); | |
| 119 | 120 | ||
| 121 | + | function getResolvedDarkPreference(fallback?: boolean): boolean { | |
| 122 | + | if (typeof fallback === "boolean") { | |
| 123 | + | return fallback; | |
| 124 | + | } | |
| 125 | + | if (typeof window.__dumber_cef_prefers_dark === "boolean") { | |
| 126 | + | return window.__dumber_cef_prefers_dark; | |
| 127 | + | } | |
| 128 | + | if (typeof window.__dumber_gtk_prefers_dark === "boolean") { | |
| 129 | + | return window.__dumber_gtk_prefers_dark; | |
| 130 | + | } | |
| 131 | + | return document.documentElement.classList.contains("dark"); | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | function applyResolvedTheme(prefersDark: boolean): void { | |
| 135 | + | const root = document.documentElement; | |
| 136 | + | root.classList.toggle("dark", prefersDark); | |
| 137 | + | root.classList.toggle("light", !prefersDark); | |
| 138 | + | root.style.colorScheme = prefersDark ? "dark" : "light"; | |
| 139 | + | window.__dumber_cef_prefers_dark = prefersDark; | |
| 140 | + | window.__dumber_gtk_prefers_dark = prefersDark; | |
| 141 | + | } | |
| 142 | + | ||
| 143 | + | function syncModeWatcherPreference(colorScheme: string | undefined, prefersDark?: boolean): void { | |
| 144 | + | switch (colorScheme) { | |
| 145 | + | case "prefer-dark": | |
| 146 | + | setMode("dark"); | |
| 147 | + | applyResolvedTheme(true); | |
| 148 | + | break; | |
| 149 | + | case "prefer-light": | |
| 150 | + | setMode("light"); | |
| 151 | + | applyResolvedTheme(false); | |
| 152 | + | break; | |
| 153 | + | default: | |
| 154 | + | resetMode(); | |
| 155 | + | applyResolvedTheme(getResolvedDarkPreference(prefersDark)); | |
| 156 | + | break; | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | function isPerformanceSupported(cfg: ConfigDTO | null): boolean { | |
| 161 | + | return cfg?.engine_type !== "cef"; | |
| 162 | + | } | |
| 163 | + | ||
| 120 | 164 | // Load config from API | |
| 121 | 165 | async function loadConfig() { | |
| 122 | 166 | loading = true; | |
| @@ -125,6 +169,10 @@ | |||
| 125 | 169 | const response = await fetch("/api/config"); | |
| 126 | 170 | if (!response.ok) throw new Error("Failed to fetch config"); | |
| 127 | 171 | config = (await response.json()) as ConfigDTO; | |
| 172 | + | syncModeWatcherPreference(config.appearance.color_scheme); | |
| 173 | + | if (!isPerformanceSupported(config) && activeTab === "performance") { | |
| 174 | + | activeTab = "appearance"; | |
| 175 | + | } | |
| 128 | 176 | } catch (e: unknown) { | |
| 129 | 177 | loadError = getErrorMessage(e); | |
| 130 | 178 | console.error("[config] load failed", e); | |
| @@ -146,6 +194,10 @@ | |||
| 146 | 194 | const response = await fetch("/api/config/default"); | |
| 147 | 195 | if (!response.ok) throw new Error("Failed to fetch default config"); | |
| 148 | 196 | config = (await response.json()) as ConfigDTO; | |
| 197 | + | syncModeWatcherPreference(config.appearance.color_scheme); | |
| 198 | + | if (!isPerformanceSupported(config) && activeTab === "performance") { | |
| 199 | + | activeTab = "appearance"; | |
| 200 | + | } | |
| 149 | 201 | } catch (e: unknown) { | |
| 150 | 202 | console.error("[config] reset defaults failed", e); | |
| 151 | 203 | } | |
| @@ -153,7 +205,7 @@ | |||
| 153 | 205 | } | |
| 154 | 206 | ||
| 155 | 207 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | |
| 156 | - | const bridge = window.webkit?.messageHandlers?.dumber; | |
| 208 | + | const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber; | |
| 157 | 209 | if (bridge && typeof bridge.postMessage === "function") { | |
| 158 | 210 | return bridge; | |
| 159 | 211 | } | |
| @@ -180,7 +232,7 @@ | |||
| 180 | 232 | }); | |
| 181 | 233 | ||
| 182 | 234 | if (!bridge) { | |
| 183 | - | throw new Error("WebKit bridge not available (not running inside Dumber)"); | |
| 235 | + | throw new Error("Dumber bridge not available (not running inside Dumber)"); | |
| 184 | 236 | } | |
| 185 | 237 | if (!config) { | |
| 186 | 238 | throw new Error("Config not loaded"); | |
| @@ -208,10 +260,12 @@ | |||
| 208 | 260 | saving = false; | |
| 209 | 261 | ||
| 210 | 262 | // Show restart warning if performance tab was active | |
| 211 | - | if (activeTab === "performance") { | |
| 263 | + | if (activeTab === "performance" && isPerformanceSupported(config)) { | |
| 212 | 264 | showRestartWarning = true; | |
| 213 | 265 | } | |
| 214 | 266 | ||
| 267 | + | syncModeWatcherPreference(config?.appearance.color_scheme); | |
| 268 | + | ||
| 215 | 269 | // Refresh config from backend (in case watcher normalized values) | |
| 216 | 270 | reloadConfig(); | |
| 217 | 271 | ||
| @@ -253,12 +307,14 @@ | |||
| 253 | 307 | onMount(() => { | |
| 254 | 308 | console.debug("[config] mount", { | |
| 255 | 309 | webviewId: window.__dumber_webview_id, | |
| 256 | - | hasBridge: Boolean(window.webkit?.messageHandlers?.dumber), | |
| 310 | + | hasBridge: Boolean(getWebKitBridge()), | |
| 257 | 311 | }); | |
| 258 | 312 | ||
| 259 | 313 | const onThemeChanged = (e: Event) => { | |
| 260 | 314 | themeEvents += 1; | |
| 261 | - | console.debug("[config] theme changed", (e as CustomEvent).detail); | |
| 315 | + | const detail = (e as CustomEvent<{ prefersDark?: boolean }>).detail; | |
| 316 | + | syncModeWatcherPreference(config?.appearance.color_scheme, detail?.prefersDark); | |
| 317 | + | console.debug("[config] theme changed", detail); | |
| 262 | 318 | }; | |
| 263 | 319 | window.addEventListener("dumber:theme-changed", onThemeChanged); | |
| 264 | 320 | ||
| @@ -305,10 +361,22 @@ | |||
| 305 | 361 | <Tabs.Trigger value="appearance">Appearance</Tabs.Trigger> | |
| 306 | 362 | <Tabs.Trigger value="search">Search</Tabs.Trigger> | |
| 307 | 363 | <Tabs.Trigger value="keybindings">Keybindings</Tabs.Trigger> | |
| 308 | - | <Tabs.Trigger value="performance">Performance</Tabs.Trigger> | |
| 364 | + | <Tabs.Trigger | |
| 365 | + | value="performance" | |
| 366 | + | disabled={!isPerformanceSupported(config)} | |
| 367 | + | title={!isPerformanceSupported(config) ? "Performance tuning is only available with the WebKit engine" : undefined} | |
| 368 | + | > | |
| 369 | + | Performance | |
| 370 | + | </Tabs.Trigger> | |
| 309 | 371 | </Tabs.List> | |
| 310 | 372 | </div> | |
| 311 | 373 | ||
| 374 | + | {#if !isPerformanceSupported(config)} | |
| 375 | + | <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground"> | |
| 376 | + | Performance settings are disabled while using the CEF engine because these options are specific to WebKitGTK. | |
| 377 | + | </div> | |
| 378 | + | {/if} | |
| 379 | + | ||
| 312 | 380 | <Tabs.Content value="appearance"> | |
| 313 | 381 | <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none"> | |
| 314 | 382 | <Card.Header> | |
| @@ -449,6 +517,11 @@ | |||
| 449 | 517 | </Card.Description> | |
| 450 | 518 | </Card.Header> | |
| 451 | 519 | <Card.Content class="space-y-6"> | |
| 520 | + | {#if !isPerformanceSupported(config)} | |
| 521 | + | <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground"> | |
| 522 | + | This section is unavailable under the CEF engine. | |
| 523 | + | </div> | |
| 524 | + | {:else} | |
| 452 | 525 | <!-- Detected Hardware Card --> | |
| 453 | 526 | {#if config.performance?.hardware} | |
| 454 | 527 | {@const hw = config.performance.hardware} | |
| @@ -614,6 +687,7 @@ | |||
| 614 | 687 | </div> | |
| 615 | 688 | </div> | |
| 616 | 689 | {/if} | |
| 690 | + | {/if} | |
| 617 | 691 | </Card.Content> | |
| 618 | 692 | </Card.Root> | |
| 619 | 693 | </Tabs.Content> | |
| @@ -1,6 +1,6 @@ | |||||
| 1 | <script lang="ts"> | 1 | <script lang="ts"> | ||
| 2 | import { onMount } from "svelte"; | 2 | import { onMount } from "svelte"; | ||
| 3 | - | import { ModeWatcher } from "mode-watcher"; | 3 | + | import { ModeWatcher, resetMode, setMode } from "mode-watcher"; |
| 4 | import ConfigShell from "./config/ConfigShell.svelte"; | 4 | import ConfigShell from "./config/ConfigShell.svelte"; | ||
| 5 | import ShortcutsTable from "./config/ShortcutsTable.svelte"; | 5 | import ShortcutsTable from "./config/ShortcutsTable.svelte"; | ||
| 6 | import KeybindingsTab from "./config/KeybindingsTab.svelte"; | 6 | import KeybindingsTab from "./config/KeybindingsTab.svelte"; | ||
| @@ -83,6 +83,7 @@ | |||||
| 83 | default_ui_scale: number; | 83 | default_ui_scale: number; | ||
| 84 | default_search_engine: string; | 84 | default_search_engine: string; | ||
| 85 | search_shortcuts: Record<string, SearchShortcut>; | 85 | search_shortcuts: Record<string, SearchShortcut>; | ||
| 86 | + | engine_type: string; | |||
| 86 | }; | 87 | }; | ||
| 87 | 88 | ||||
| 88 | const PERFORMANCE_PROFILES = [ | 89 | const PERFORMANCE_PROFILES = [ | ||
| @@ -117,6 +118,49 @@ | |||||
| 117 | let themeEvents = $state(0); | 118 | let themeEvents = $state(0); | ||
| 118 | let activeTab = $state("appearance"); | 119 | let activeTab = $state("appearance"); | ||
| 119 | 120 | ||||
| 121 | + | function getResolvedDarkPreference(fallback?: boolean): boolean { | |||
| 122 | + | if (typeof fallback === "boolean") { | |||
| 123 | + | return fallback; | |||
| 124 | + | } | |||
| 125 | + | if (typeof window.__dumber_cef_prefers_dark === "boolean") { | |||
| 126 | + | return window.__dumber_cef_prefers_dark; | |||
| 127 | + | } | |||
| 128 | + | if (typeof window.__dumber_gtk_prefers_dark === "boolean") { | |||
| 129 | + | return window.__dumber_gtk_prefers_dark; | |||
| 130 | + | } | |||
| 131 | + | return document.documentElement.classList.contains("dark"); | |||
| 132 | + | } | |||
| 133 | + | ||||
| 134 | + | function applyResolvedTheme(prefersDark: boolean): void { | |||
| 135 | + | const root = document.documentElement; | |||
| 136 | + | root.classList.toggle("dark", prefersDark); | |||
| 137 | + | root.classList.toggle("light", !prefersDark); | |||
| 138 | + | root.style.colorScheme = prefersDark ? "dark" : "light"; | |||
| 139 | + | window.__dumber_cef_prefers_dark = prefersDark; | |||
| 140 | + | window.__dumber_gtk_prefers_dark = prefersDark; | |||
| 141 | + | } | |||
| 142 | + | ||||
| 143 | + | function syncModeWatcherPreference(colorScheme: string | undefined, prefersDark?: boolean): void { | |||
| 144 | + | switch (colorScheme) { | |||
| 145 | + | case "prefer-dark": | |||
| 146 | + | setMode("dark"); | |||
| 147 | + | applyResolvedTheme(true); | |||
| 148 | + | break; | |||
| 149 | + | case "prefer-light": | |||
| 150 | + | setMode("light"); | |||
| 151 | + | applyResolvedTheme(false); | |||
| 152 | + | break; | |||
| 153 | + | default: | |||
| 154 | + | resetMode(); | |||
| 155 | + | applyResolvedTheme(getResolvedDarkPreference(prefersDark)); | |||
| 156 | + | break; | |||
| 157 | + | } | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | function isPerformanceSupported(cfg: ConfigDTO | null): boolean { | |||
| 161 | + | return cfg?.engine_type !== "cef"; | |||
| 162 | + | } | |||
| 163 | + | ||||
| 120 | // Load config from API | 164 | // Load config from API | ||
| 121 | async function loadConfig() { | 165 | async function loadConfig() { | ||
| 122 | loading = true; | 166 | loading = true; | ||
| @@ -125,6 +169,10 @@ | |||||
| 125 | const response = await fetch("/api/config"); | 169 | const response = await fetch("/api/config"); | ||
| 126 | if (!response.ok) throw new Error("Failed to fetch config"); | 170 | if (!response.ok) throw new Error("Failed to fetch config"); | ||
| 127 | config = (await response.json()) as ConfigDTO; | 171 | config = (await response.json()) as ConfigDTO; | ||
| 172 | + | syncModeWatcherPreference(config.appearance.color_scheme); | |||
| 173 | + | if (!isPerformanceSupported(config) && activeTab === "performance") { | |||
| 174 | + | activeTab = "appearance"; | |||
| 175 | + | } | |||
| 128 | } catch (e: unknown) { | 176 | } catch (e: unknown) { | ||
| 129 | loadError = getErrorMessage(e); | 177 | loadError = getErrorMessage(e); | ||
| 130 | console.error("[config] load failed", e); | 178 | console.error("[config] load failed", e); | ||
| @@ -146,6 +194,10 @@ | |||||
| 146 | const response = await fetch("/api/config/default"); | 194 | const response = await fetch("/api/config/default"); | ||
| 147 | if (!response.ok) throw new Error("Failed to fetch default config"); | 195 | if (!response.ok) throw new Error("Failed to fetch default config"); | ||
| 148 | config = (await response.json()) as ConfigDTO; | 196 | config = (await response.json()) as ConfigDTO; | ||
| 197 | + | syncModeWatcherPreference(config.appearance.color_scheme); | |||
| 198 | + | if (!isPerformanceSupported(config) && activeTab === "performance") { | |||
| 199 | + | activeTab = "appearance"; | |||
| 200 | + | } | |||
| 149 | } catch (e: unknown) { | 201 | } catch (e: unknown) { | ||
| 150 | console.error("[config] reset defaults failed", e); | 202 | console.error("[config] reset defaults failed", e); | ||
| 151 | } | 203 | } | ||
| @@ -153,7 +205,7 @@ | |||||
| 153 | } | 205 | } | ||
| 154 | 206 | ||||
| 155 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | 207 | function getWebKitBridge(): { postMessage: (msg: unknown) => void } | null { | ||
| 156 | - | const bridge = window.webkit?.messageHandlers?.dumber; | 208 | + | const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber; |
| 157 | if (bridge && typeof bridge.postMessage === "function") { | 209 | if (bridge && typeof bridge.postMessage === "function") { | ||
| 158 | return bridge; | 210 | return bridge; | ||
| 159 | } | 211 | } | ||
| @@ -180,7 +232,7 @@ | |||||
| 180 | }); | 232 | }); | ||
| 181 | 233 | ||||
| 182 | if (!bridge) { | 234 | if (!bridge) { | ||
| 183 | - | throw new Error("WebKit bridge not available (not running inside Dumber)"); | 235 | + | throw new Error("Dumber bridge not available (not running inside Dumber)"); |
| 184 | } | 236 | } | ||
| 185 | if (!config) { | 237 | if (!config) { | ||
| 186 | throw new Error("Config not loaded"); | 238 | throw new Error("Config not loaded"); | ||
| @@ -208,10 +260,12 @@ | |||||
| 208 | saving = false; | 260 | saving = false; | ||
| 209 | 261 | ||||
| 210 | // Show restart warning if performance tab was active | 262 | // Show restart warning if performance tab was active | ||
| 211 | - | if (activeTab === "performance") { | 263 | + | if (activeTab === "performance" && isPerformanceSupported(config)) { |
| 212 | showRestartWarning = true; | 264 | showRestartWarning = true; | ||
| 213 | } | 265 | } | ||
| 214 | 266 | ||||
| 267 | + | syncModeWatcherPreference(config?.appearance.color_scheme); | |||
| 268 | + | ||||
| 215 | // Refresh config from backend (in case watcher normalized values) | 269 | // Refresh config from backend (in case watcher normalized values) | ||
| 216 | reloadConfig(); | 270 | reloadConfig(); | ||
| 217 | 271 | ||||
| @@ -253,12 +307,14 @@ | |||||
| 253 | onMount(() => { | 307 | onMount(() => { | ||
| 254 | console.debug("[config] mount", { | 308 | console.debug("[config] mount", { | ||
| 255 | webviewId: window.__dumber_webview_id, | 309 | webviewId: window.__dumber_webview_id, | ||
| 256 | - | hasBridge: Boolean(window.webkit?.messageHandlers?.dumber), | 310 | + | hasBridge: Boolean(getWebKitBridge()), |
| 257 | }); | 311 | }); | ||
| 258 | 312 | ||||
| 259 | const onThemeChanged = (e: Event) => { | 313 | const onThemeChanged = (e: Event) => { | ||
| 260 | themeEvents += 1; | 314 | themeEvents += 1; | ||
| 261 | - | console.debug("[config] theme changed", (e as CustomEvent).detail); | 315 | + | const detail = (e as CustomEvent<{ prefersDark?: boolean }>).detail; |
| 316 | + | syncModeWatcherPreference(config?.appearance.color_scheme, detail?.prefersDark); | |||
| 317 | + | console.debug("[config] theme changed", detail); | |||
| 262 | }; | 318 | }; | ||
| 263 | window.addEventListener("dumber:theme-changed", onThemeChanged); | 319 | window.addEventListener("dumber:theme-changed", onThemeChanged); | ||
| 264 | 320 | ||||
| @@ -305,10 +361,22 @@ | |||||
| 305 | <Tabs.Trigger value="appearance">Appearance</Tabs.Trigger> | 361 | <Tabs.Trigger value="appearance">Appearance</Tabs.Trigger> | ||
| 306 | <Tabs.Trigger value="search">Search</Tabs.Trigger> | 362 | <Tabs.Trigger value="search">Search</Tabs.Trigger> | ||
| 307 | <Tabs.Trigger value="keybindings">Keybindings</Tabs.Trigger> | 363 | <Tabs.Trigger value="keybindings">Keybindings</Tabs.Trigger> | ||
| 308 | - | <Tabs.Trigger value="performance">Performance</Tabs.Trigger> | 364 | + | <Tabs.Trigger |
| 365 | + | value="performance" | |||
| 366 | + | disabled={!isPerformanceSupported(config)} | |||
| 367 | + | title={!isPerformanceSupported(config) ? "Performance tuning is only available with the WebKit engine" : undefined} | |||
| 368 | + | > | |||
| 369 | + | Performance | |||
| 370 | + | </Tabs.Trigger> | |||
| 309 | </Tabs.List> | 371 | </Tabs.List> | ||
| 310 | </div> | 372 | </div> | ||
| 311 | 373 | ||||
| 374 | + | {#if !isPerformanceSupported(config)} | |||
| 375 | + | <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground"> | |||
| 376 | + | Performance settings are disabled while using the CEF engine because these options are specific to WebKitGTK. | |||
| 377 | + | </div> | |||
| 378 | + | {/if} | |||
| 379 | + | ||||
| 312 | <Tabs.Content value="appearance"> | 380 | <Tabs.Content value="appearance"> | ||
| 313 | <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none"> | 381 | <Card.Root class="rounded-none border-0 bg-transparent py-0 shadow-none"> | ||
| 314 | <Card.Header> | 382 | <Card.Header> | ||
| @@ -449,6 +517,11 @@ | |||||
| 449 | </Card.Description> | 517 | </Card.Description> | ||
| 450 | </Card.Header> | 518 | </Card.Header> | ||
| 451 | <Card.Content class="space-y-6"> | 519 | <Card.Content class="space-y-6"> | ||
| 520 | + | {#if !isPerformanceSupported(config)} | |||
| 521 | + | <div class="rounded-md border border-border bg-muted/20 px-4 py-3 text-sm text-muted-foreground"> | |||
| 522 | + | This section is unavailable under the CEF engine. | |||
| 523 | + | </div> | |||
| 524 | + | {:else} | |||
| 452 | <!-- Detected Hardware Card --> | 525 | <!-- Detected Hardware Card --> | ||
| 453 | {#if config.performance?.hardware} | 526 | {#if config.performance?.hardware} | ||
| 454 | {@const hw = config.performance.hardware} | 527 | {@const hw = config.performance.hardware} | ||
| @@ -614,6 +687,7 @@ | |||||
| 614 | </div> | 687 | </div> | ||
| 615 | </div> | 688 | </div> | ||
| 616 | {/if} | 689 | {/if} | ||
| 690 | + | {/if} | |||
| 617 | </Card.Content> | 691 | </Card.Content> | ||
| 618 | </Card.Root> | 692 | </Card.Root> | ||
| 619 | </Tabs.Content> | 693 | </Tabs.Content> | ||
M webui/src/pages/config/KeybindingsTab.svelte +2 -2
| @@ -54,7 +54,7 @@ | |||
| 54 | 54 | error = null; | |
| 55 | 55 | ||
| 56 | 56 | if (!getWebKitBridge()) { | |
| 57 | - | error = "WebKit bridge not available"; | |
| 57 | + | error = "Dumber bridge not available"; | |
| 58 | 58 | loading = false; | |
| 59 | 59 | return; | |
| 60 | 60 | } | |
| @@ -93,7 +93,7 @@ | |||
| 93 | 93 | if (!editingBinding) return; | |
| 94 | 94 | ||
| 95 | 95 | if (!getWebKitBridge()) { | |
| 96 | - | error = "WebKit bridge not available"; | |
| 96 | + | error = "Dumber bridge not available"; | |
| 97 | 97 | return; | |
| 98 | 98 | } | |
| 99 | 99 | ||
| @@ -54,7 +54,7 @@ | |||||
| 54 | error = null; | 54 | error = null; | ||
| 55 | 55 | ||||
| 56 | if (!getWebKitBridge()) { | 56 | if (!getWebKitBridge()) { | ||
| 57 | - | error = "WebKit bridge not available"; | 57 | + | error = "Dumber bridge not available"; |
| 58 | loading = false; | 58 | loading = false; | ||
| 59 | return; | 59 | return; | ||
| 60 | } | 60 | } | ||
| @@ -93,7 +93,7 @@ | |||||
| 93 | if (!editingBinding) return; | 93 | if (!editingBinding) return; | ||
| 94 | 94 | ||||
| 95 | if (!getWebKitBridge()) { | 95 | if (!getWebKitBridge()) { | ||
| 96 | - | error = "WebKit bridge not available"; | 96 | + | error = "Dumber bridge not available"; |
| 97 | return; | 97 | return; | ||
| 98 | } | 98 | } | ||
| 99 | 99 | ||||
M webui/src/pages/homepage/messaging.ts +4 -4
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 2 | - | // Homepage WebKit Bridge | |
| 3 | - | // Communication layer between frontend and Go backend via WebKit message handlers | |
| 2 | + | // Homepage bridge | |
| 3 | + | // Communication layer between frontend and Go backend via WebKit or CEF bridge. | |
| 4 | 4 | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 5 | 5 | ||
| 6 | 6 | import type { | |
| @@ -43,7 +43,7 @@ let callbacksInitialized = false; | |||
| 43 | 43 | // ═══════════════════════════════════════════════════════════════════════════════ | |
| 44 | 44 | ||
| 45 | 45 | function getWebKitBridge(): DumberBridge | null { | |
| 46 | - | const bridge = window.webkit?.messageHandlers?.dumber; | |
| 46 | + | const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber; | |
| 47 | 47 | if (bridge && typeof bridge.postMessage === 'function') { | |
| 48 | 48 | return bridge; | |
| 49 | 49 | } | |
| @@ -178,7 +178,7 @@ async function sendMessage<T = unknown>( | |||
| 178 | 178 | ||
| 179 | 179 | const bridge = getWebKitBridge(); | |
| 180 | 180 | if (!bridge) { | |
| 181 | - | throw new Error('WebKit message handler not available'); | |
| 181 | + | throw new Error('Dumber bridge not available'); | |
| 182 | 182 | } | |
| 183 | 183 | ||
| 184 | 184 | const requestId = generateRequestId(type); | |
| @@ -1,6 +1,6 @@ | |||||
| 1 | // ═══════════════════════════════════════════════════════════════════════════════ | 1 | // ═══════════════════════════════════════════════════════════════════════════════ | ||
| 2 | - | // Homepage WebKit Bridge | 2 | + | // Homepage bridge |
| 3 | - | // Communication layer between frontend and Go backend via WebKit message handlers | 3 | + | // Communication layer between frontend and Go backend via WebKit or CEF bridge. |
| 4 | // ═══════════════════════════════════════════════════════════════════════════════ | 4 | // ═══════════════════════════════════════════════════════════════════════════════ | ||
| 5 | 5 | ||||
| 6 | import type { | 6 | import type { | ||
| @@ -43,7 +43,7 @@ let callbacksInitialized = false; | |||||
| 43 | // ═══════════════════════════════════════════════════════════════════════════════ | 43 | // ═══════════════════════════════════════════════════════════════════════════════ | ||
| 44 | 44 | ||||
| 45 | function getWebKitBridge(): DumberBridge | null { | 45 | function getWebKitBridge(): DumberBridge | null { | ||
| 46 | - | const bridge = window.webkit?.messageHandlers?.dumber; | 46 | + | const bridge = window.webkit?.messageHandlers?.dumber ?? window.dumber; |
| 47 | if (bridge && typeof bridge.postMessage === 'function') { | 47 | if (bridge && typeof bridge.postMessage === 'function') { | ||
| 48 | return bridge; | 48 | return bridge; | ||
| 49 | } | 49 | } | ||
| @@ -178,7 +178,7 @@ async function sendMessage<T = unknown>( | |||||
| 178 | 178 | ||||
| 179 | const bridge = getWebKitBridge(); | 179 | const bridge = getWebKitBridge(); | ||
| 180 | if (!bridge) { | 180 | if (!bridge) { | ||
| 181 | - | throw new Error('WebKit message handler not available'); | 181 | + | throw new Error('Dumber bridge not available'); |
| 182 | } | 182 | } | ||
| 183 | 183 | ||||
| 184 | const requestId = generateRequestId(type); | 184 | const requestId = generateRequestId(type); | ||
M webui/src/types/dumber.d.ts +6 -0
| @@ -77,6 +77,8 @@ declare global { | |||
| 77 | 77 | __dumber_webview_id?: number; | |
| 78 | 78 | /** WebKit message handlers for native bridge */ | |
| 79 | 79 | webkit?: DumberWebKit; | |
| 80 | + | /** CEF bridge shim injected on internal pages */ | |
| 81 | + | dumber?: DumberBridge; | |
| 80 | 82 | ||
| 81 | 83 | // ───────────────────────────────────────────────────────────────────────── | |
| 82 | 84 | // Config Page Callbacks | |
| @@ -139,6 +141,10 @@ declare global { | |||
| 139 | 141 | __dumber_color_scheme_manager?: ColorSchemeManager; | |
| 140 | 142 | /** Legacy theme setter */ | |
| 141 | 143 | __dumber_setTheme?: (theme: ThemeMode) => void; | |
| 144 | + | /** Resolved prefers-dark state injected by the backend */ | |
| 145 | + | __dumber_cef_prefers_dark?: boolean; | |
| 146 | + | /** Resolved prefers-dark state injected by the backend */ | |
| 147 | + | __dumber_gtk_prefers_dark?: boolean; | |
| 142 | 148 | } | |
| 143 | 149 | ||
| 144 | 150 | interface Document { | |
| @@ -77,6 +77,8 @@ declare global { | |||||
| 77 | __dumber_webview_id?: number; | 77 | __dumber_webview_id?: number; | ||
| 78 | /** WebKit message handlers for native bridge */ | 78 | /** WebKit message handlers for native bridge */ | ||
| 79 | webkit?: DumberWebKit; | 79 | webkit?: DumberWebKit; | ||
| 80 | + | /** CEF bridge shim injected on internal pages */ | |||
| 81 | + | dumber?: DumberBridge; | |||
| 80 | 82 | ||||
| 81 | // ───────────────────────────────────────────────────────────────────────── | 83 | // ───────────────────────────────────────────────────────────────────────── | ||
| 82 | // Config Page Callbacks | 84 | // Config Page Callbacks | ||
| @@ -139,6 +141,10 @@ declare global { | |||||
| 139 | __dumber_color_scheme_manager?: ColorSchemeManager; | 141 | __dumber_color_scheme_manager?: ColorSchemeManager; | ||
| 140 | /** Legacy theme setter */ | 142 | /** Legacy theme setter */ | ||
| 141 | __dumber_setTheme?: (theme: ThemeMode) => void; | 143 | __dumber_setTheme?: (theme: ThemeMode) => void; | ||
| 144 | + | /** Resolved prefers-dark state injected by the backend */ | |||
| 145 | + | __dumber_cef_prefers_dark?: boolean; | |||
| 146 | + | /** Resolved prefers-dark state injected by the backend */ | |||
| 147 | + | __dumber_gtk_prefers_dark?: boolean; | |||
| 142 | } | 148 | } | ||
| 143 | 149 | ||||
| 144 | interface Document { | 150 | interface Document { | ||
M webui/vite-plugin-pages.ts +41 -1
| @@ -15,6 +15,44 @@ interface PageConfig { | |||
| 15 | 15 | filename?: string; | |
| 16 | 16 | } | |
| 17 | 17 | ||
| 18 | + | // Bootstrap a unified window.dumber bridge before the page bundle runs. | |
| 19 | + | // On WebKit internal pages we forward to the native message handler. | |
| 20 | + | // On CEF internal pages we fall back to the fetch-based /api/message bridge. | |
| 21 | + | function getBridgeBootstrap(): string { | |
| 22 | + | return `<script> | |
| 23 | + | (function() { | |
| 24 | + | if (window.dumber && typeof window.dumber.postMessage === "function") { | |
| 25 | + | return; | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | window.dumber = { | |
| 29 | + | postMessage: async function(msg) { | |
| 30 | + | const nativeBridge = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumber; | |
| 31 | + | if (nativeBridge && typeof nativeBridge.postMessage === "function") { | |
| 32 | + | return nativeBridge.postMessage(msg); | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | const encoded = btoa(unescape(encodeURIComponent(JSON.stringify(msg)))); | |
| 36 | + | const response = await fetch("/api/message", { | |
| 37 | + | method: "POST", | |
| 38 | + | headers: { | |
| 39 | + | "Content-Type": "application/json", | |
| 40 | + | "X-Dumber-Body": encoded | |
| 41 | + | } | |
| 42 | + | }); | |
| 43 | + | const payload = await response.json(); | |
| 44 | + | if (payload && payload._callback && typeof window[payload._callback] === "function") { | |
| 45 | + | window[payload._callback](payload.data); | |
| 46 | + | } | |
| 47 | + | return payload; | |
| 48 | + | } | |
| 49 | + | }; | |
| 50 | + | ||
| 51 | + | window.dispatchEvent(new CustomEvent("dumber:bridge-ready")); | |
| 52 | + | })(); | |
| 53 | + | </script>`; | |
| 54 | + | } | |
| 55 | + | ||
| 18 | 56 | // Load the favicon SVG from assets | |
| 19 | 57 | function getFaviconSVG(): string { | |
| 20 | 58 | const logoPath = resolve(__dirname, "..", "assets", "logo.svg"); | |
| @@ -26,6 +64,7 @@ function generatePageHTML(config: PageConfig): string { | |||
| 26 | 64 | const cssLink = config.css | |
| 27 | 65 | ? `<link rel="stylesheet" href="./${config.css}">` | |
| 28 | 66 | : ""; | |
| 67 | + | const bridgeBootstrap = getBridgeBootstrap(); | |
| 29 | 68 | ||
| 30 | 69 | return `<!DOCTYPE html> | |
| 31 | 70 | <html lang="en"> | |
| @@ -33,7 +72,8 @@ function generatePageHTML(config: PageConfig): string { | |||
| 33 | 72 | <meta charset="UTF-8"> | |
| 34 | 73 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 35 | 74 | <title>${config.title}</title> | |
| 36 | - | <link rel="icon" type="image/x-icon" href="dumb://homepage/favicon.ico"> | |
| 75 | + | <link rel="icon" type="image/png" href="/favicon.png"> | |
| 76 | + | ${bridgeBootstrap} | |
| 37 | 77 | ${cssLink} | |
| 38 | 78 | </head> | |
| 39 | 79 | <body> | |
| @@ -15,6 +15,44 @@ interface PageConfig { | |||||
| 15 | filename?: string; | 15 | filename?: string; | ||
| 16 | } | 16 | } | ||
| 17 | 17 | ||||
| 18 | + | // Bootstrap a unified window.dumber bridge before the page bundle runs. | |||
| 19 | + | // On WebKit internal pages we forward to the native message handler. | |||
| 20 | + | // On CEF internal pages we fall back to the fetch-based /api/message bridge. | |||
| 21 | + | function getBridgeBootstrap(): string { | |||
| 22 | + | return `<script> | |||
| 23 | + | (function() { | |||
| 24 | + | if (window.dumber && typeof window.dumber.postMessage === "function") { | |||
| 25 | + | return; | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | window.dumber = { | |||
| 29 | + | postMessage: async function(msg) { | |||
| 30 | + | const nativeBridge = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.dumber; | |||
| 31 | + | if (nativeBridge && typeof nativeBridge.postMessage === "function") { | |||
| 32 | + | return nativeBridge.postMessage(msg); | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | const encoded = btoa(unescape(encodeURIComponent(JSON.stringify(msg)))); | |||
| 36 | + | const response = await fetch("/api/message", { | |||
| 37 | + | method: "POST", | |||
| 38 | + | headers: { | |||
| 39 | + | "Content-Type": "application/json", | |||
| 40 | + | "X-Dumber-Body": encoded | |||
| 41 | + | } | |||
| 42 | + | }); | |||
| 43 | + | const payload = await response.json(); | |||
| 44 | + | if (payload && payload._callback && typeof window[payload._callback] === "function") { | |||
| 45 | + | window[payload._callback](payload.data); | |||
| 46 | + | } | |||
| 47 | + | return payload; | |||
| 48 | + | } | |||
| 49 | + | }; | |||
| 50 | + | ||||
| 51 | + | window.dispatchEvent(new CustomEvent("dumber:bridge-ready")); | |||
| 52 | + | })(); | |||
| 53 | + | </script>`; | |||
| 54 | + | } | |||
| 55 | + | ||||
| 18 | // Load the favicon SVG from assets | 56 | // Load the favicon SVG from assets | ||
| 19 | function getFaviconSVG(): string { | 57 | function getFaviconSVG(): string { | ||
| 20 | const logoPath = resolve(__dirname, "..", "assets", "logo.svg"); | 58 | const logoPath = resolve(__dirname, "..", "assets", "logo.svg"); | ||
| @@ -26,6 +64,7 @@ function generatePageHTML(config: PageConfig): string { | |||||
| 26 | const cssLink = config.css | 64 | const cssLink = config.css | ||
| 27 | ? `<link rel="stylesheet" href="./${config.css}">` | 65 | ? `<link rel="stylesheet" href="./${config.css}">` | ||
| 28 | : ""; | 66 | : ""; | ||
| 67 | + | const bridgeBootstrap = getBridgeBootstrap(); | |||
| 29 | 68 | ||||
| 30 | return `<!DOCTYPE html> | 69 | return `<!DOCTYPE html> | ||
| 31 | <html lang="en"> | 70 | <html lang="en"> | ||
| @@ -33,7 +72,8 @@ function generatePageHTML(config: PageConfig): string { | |||||
| 33 | <meta charset="UTF-8"> | 72 | <meta charset="UTF-8"> | ||
| 34 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | 73 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| 35 | <title>${config.title}</title> | 74 | <title>${config.title}</title> | ||
| 36 | - | <link rel="icon" type="image/x-icon" href="dumb://homepage/favicon.ico"> | 75 | + | <link rel="icon" type="image/png" href="/favicon.png"> |
| 76 | + | ${bridgeBootstrap} | |||
| 37 | ${cssLink} | 77 | ${cssLink} | ||
| 38 | </head> | 78 | </head> | ||
| 39 | <body> | 79 | <body> | ||