feat(backend): integrate color palettes and improve history suggestions
WebView & Settings: - Add ColorPalette and ColorPalettes structs to webkit config - Inject color palette tokens from Go config as user script at document start - Apply palette based on light/dark theme with CSS custom properties - Support related webview creation with color config - Update AV1 user agent to Chrome 141.0.0.0 Messaging Handler: - Remove automatic search shortcuts from query results (rely on explicit prefix commands) - Use favicon_url from database when available for history suggestions - Simplify query handler by removing shortcut expansion logic Browser App: - Add buildColorPalettes() helper to convert config palettes to webkit format
- commit
- 5b002daf7d49f73f91e689765edef3a071324b7d
- author
- bamen · · 2025-10-06 17:24 UTC
- committer
- bamen · · 2025-10-06 17:46 UTC
- parent
- 135779d
- signature
- unsigned
4 files · +104 -40
M internal/app/browser/webview.go +20 -0
| @@ -45,6 +45,25 @@ func buildMemoryConfig(cfg config.WebkitMemoryConfig) webkit.MemoryConfig { | |||
| 45 | 45 | return mc | |
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | + | func buildColorPalettes(cfg config.AppearanceConfig) webkit.ColorPalettes { | |
| 49 | + | toPalette := func(src config.ColorPalette) webkit.ColorPalette { | |
| 50 | + | return webkit.ColorPalette{ | |
| 51 | + | Background: src.Background, | |
| 52 | + | Surface: src.Surface, | |
| 53 | + | SurfaceVariant: src.SurfaceVariant, | |
| 54 | + | Text: src.Text, | |
| 55 | + | Muted: src.Muted, | |
| 56 | + | Accent: src.Accent, | |
| 57 | + | Border: src.Border, | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | return webkit.ColorPalettes{ | |
| 62 | + | Light: toPalette(cfg.LightPalette), | |
| 63 | + | Dark: toPalette(cfg.DarkPalette), | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | ||
| 48 | 67 | func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { | |
| 49 | 68 | dataDir, err := config.GetDataDir() | |
| 50 | 69 | if err != nil { | |
| @@ -93,6 +112,7 @@ func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { | |||
| 93 | 112 | CustomUserAgent: app.config.CodecPreferences.CustomUserAgent, | |
| 94 | 113 | DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl, | |
| 95 | 114 | }, | |
| 115 | + | Colors: buildColorPalettes(app.config.Appearance), | |
| 96 | 116 | } | |
| 97 | 117 | ||
| 98 | 118 | return cfg, nil | |
| @@ -45,6 +45,25 @@ func buildMemoryConfig(cfg config.WebkitMemoryConfig) webkit.MemoryConfig { | |||||
| 45 | return mc | 45 | return mc | ||
| 46 | } | 46 | } | ||
| 47 | 47 | ||||
| 48 | + | func buildColorPalettes(cfg config.AppearanceConfig) webkit.ColorPalettes { | |||
| 49 | + | toPalette := func(src config.ColorPalette) webkit.ColorPalette { | |||
| 50 | + | return webkit.ColorPalette{ | |||
| 51 | + | Background: src.Background, | |||
| 52 | + | Surface: src.Surface, | |||
| 53 | + | SurfaceVariant: src.SurfaceVariant, | |||
| 54 | + | Text: src.Text, | |||
| 55 | + | Muted: src.Muted, | |||
| 56 | + | Accent: src.Accent, | |||
| 57 | + | Border: src.Border, | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | return webkit.ColorPalettes{ | |||
| 62 | + | Light: toPalette(cfg.LightPalette), | |||
| 63 | + | Dark: toPalette(cfg.DarkPalette), | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | ||||
| 48 | func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { | 67 | func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { | ||
| 49 | dataDir, err := config.GetDataDir() | 68 | dataDir, err := config.GetDataDir() | ||
| 50 | if err != nil { | 69 | if err != nil { | ||
| @@ -93,6 +112,7 @@ func (app *BrowserApp) buildWebkitConfig() (*webkit.Config, error) { | |||||
| 93 | CustomUserAgent: app.config.CodecPreferences.CustomUserAgent, | 112 | CustomUserAgent: app.config.CodecPreferences.CustomUserAgent, | ||
| 94 | DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl, | 113 | DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl, | ||
| 95 | }, | 114 | }, | ||
| 115 | + | Colors: buildColorPalettes(app.config.Appearance), | |||
| 96 | } | 116 | } | ||
| 97 | 117 | ||||
| 98 | return cfg, nil | 118 | return cfg, nil | ||
M internal/app/messaging/handler.go +9 -39
| @@ -4,8 +4,6 @@ import ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "encoding/json" | |
| 6 | 6 | "log" | |
| 7 | - | neturl "net/url" | |
| 8 | - | "sort" | |
| 9 | 7 | "strconv" | |
| 10 | 8 | "strings" | |
| 11 | 9 | "time" | |
| @@ -273,45 +271,10 @@ func (h *Handler) handleQuery(msg Message) { | |||
| 273 | 271 | return scheme + "://" + host + "/favicon.ico" | |
| 274 | 272 | } | |
| 275 | 273 | ||
| 276 | - | expandShortcut := func(tpl string, query string) string { | |
| 277 | - | esc := neturl.QueryEscape(query) | |
| 278 | - | u := strings.ReplaceAll(tpl, "{query}", esc) | |
| 279 | - | u = strings.ReplaceAll(u, "%s", esc) | |
| 280 | - | return u | |
| 281 | - | } | |
| 282 | - | ||
| 283 | 274 | results := make([]suggestion, 0, limit) | |
| 284 | 275 | seen := make(map[string]struct{}, limit*2) | |
| 285 | 276 | ||
| 286 | - | // Shortcuts | |
| 287 | - | if shortcuts, err := h.browserService.GetSearchShortcuts(ctx); err != nil { | |
| 288 | - | log.Printf("[ERROR] Failed to get search shortcuts: %v", err) | |
| 289 | - | } else if len(shortcuts) > 0 { | |
| 290 | - | log.Printf("[DEBUG] Found %d search shortcuts", len(shortcuts)) | |
| 291 | - | keys := make([]string, 0, len(shortcuts)) | |
| 292 | - | for k := range shortcuts { | |
| 293 | - | keys = append(keys, k) | |
| 294 | - | } | |
| 295 | - | sort.Strings(keys) | |
| 296 | - | for _, k := range keys { | |
| 297 | - | sc := shortcuts[k] | |
| 298 | - | url := expandShortcut(sc.URL, q) | |
| 299 | - | log.Printf("[DEBUG] Shortcut %s -> %s", k, url) | |
| 300 | - | if url == "" { | |
| 301 | - | continue | |
| 302 | - | } | |
| 303 | - | if _, ok := seen[url]; ok { | |
| 304 | - | continue | |
| 305 | - | } | |
| 306 | - | results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)}) | |
| 307 | - | seen[url] = struct{}{} | |
| 308 | - | if len(results) >= limit { | |
| 309 | - | break | |
| 310 | - | } | |
| 311 | - | } | |
| 312 | - | } else { | |
| 313 | - | log.Printf("[DEBUG] No search shortcuts found") | |
| 314 | - | } | |
| 277 | + | // Shortcuts intentionally omitted; rely on explicit prefix commands (e.g., gh:) or history results | |
| 315 | 278 | ||
| 316 | 279 | // History | |
| 317 | 280 | if len(results) < limit { | |
| @@ -339,7 +302,14 @@ func (h *Handler) handleQuery(msg Message) { | |||
| 339 | 302 | if _, ok := seen[url]; ok { | |
| 340 | 303 | continue | |
| 341 | 304 | } | |
| 342 | - | results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)}) | |
| 305 | + | // Use favicon_url from database if available, otherwise build it | |
| 306 | + | favicon := "" | |
| 307 | + | if s, ok := m["favicon_url"].(string); ok && s != "" { | |
| 308 | + | favicon = s | |
| 309 | + | } else { | |
| 310 | + | favicon = buildFavicon(url) | |
| 311 | + | } | |
| 312 | + | results = append(results, suggestion{URL: url, Favicon: favicon}) | |
| 343 | 313 | seen[url] = struct{}{} | |
| 344 | 314 | if len(results) >= limit { | |
| 345 | 315 | break | |
| @@ -4,8 +4,6 @@ import ( | |||||
| 4 | "context" | 4 | "context" | ||
| 5 | "encoding/json" | 5 | "encoding/json" | ||
| 6 | "log" | 6 | "log" | ||
| 7 | - | neturl "net/url" | |||
| 8 | - | "sort" | |||
| 9 | "strconv" | 7 | "strconv" | ||
| 10 | "strings" | 8 | "strings" | ||
| 11 | "time" | 9 | "time" | ||
| @@ -273,45 +271,10 @@ func (h *Handler) handleQuery(msg Message) { | |||||
| 273 | return scheme + "://" + host + "/favicon.ico" | 271 | return scheme + "://" + host + "/favicon.ico" | ||
| 274 | } | 272 | } | ||
| 275 | 273 | ||||
| 276 | - | expandShortcut := func(tpl string, query string) string { | |||
| 277 | - | esc := neturl.QueryEscape(query) | |||
| 278 | - | u := strings.ReplaceAll(tpl, "{query}", esc) | |||
| 279 | - | u = strings.ReplaceAll(u, "%s", esc) | |||
| 280 | - | return u | |||
| 281 | - | } | |||
| 282 | - | ||||
| 283 | results := make([]suggestion, 0, limit) | 274 | results := make([]suggestion, 0, limit) | ||
| 284 | seen := make(map[string]struct{}, limit*2) | 275 | seen := make(map[string]struct{}, limit*2) | ||
| 285 | 276 | ||||
| 286 | - | // Shortcuts | 277 | + | // Shortcuts intentionally omitted; rely on explicit prefix commands (e.g., gh:) or history results |
| 287 | - | if shortcuts, err := h.browserService.GetSearchShortcuts(ctx); err != nil { | |||
| 288 | - | log.Printf("[ERROR] Failed to get search shortcuts: %v", err) | |||
| 289 | - | } else if len(shortcuts) > 0 { | |||
| 290 | - | log.Printf("[DEBUG] Found %d search shortcuts", len(shortcuts)) | |||
| 291 | - | keys := make([]string, 0, len(shortcuts)) | |||
| 292 | - | for k := range shortcuts { | |||
| 293 | - | keys = append(keys, k) | |||
| 294 | - | } | |||
| 295 | - | sort.Strings(keys) | |||
| 296 | - | for _, k := range keys { | |||
| 297 | - | sc := shortcuts[k] | |||
| 298 | - | url := expandShortcut(sc.URL, q) | |||
| 299 | - | log.Printf("[DEBUG] Shortcut %s -> %s", k, url) | |||
| 300 | - | if url == "" { | |||
| 301 | - | continue | |||
| 302 | - | } | |||
| 303 | - | if _, ok := seen[url]; ok { | |||
| 304 | - | continue | |||
| 305 | - | } | |||
| 306 | - | results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)}) | |||
| 307 | - | seen[url] = struct{}{} | |||
| 308 | - | if len(results) >= limit { | |||
| 309 | - | break | |||
| 310 | - | } | |||
| 311 | - | } | |||
| 312 | - | } else { | |||
| 313 | - | log.Printf("[DEBUG] No search shortcuts found") | |||
| 314 | - | } | |||
| 315 | 278 | ||||
| 316 | // History | 279 | // History | ||
| 317 | if len(results) < limit { | 280 | if len(results) < limit { | ||
| @@ -339,7 +302,14 @@ func (h *Handler) handleQuery(msg Message) { | |||||
| 339 | if _, ok := seen[url]; ok { | 302 | if _, ok := seen[url]; ok { | ||
| 340 | continue | 303 | continue | ||
| 341 | } | 304 | } | ||
| 342 | - | results = append(results, suggestion{URL: url, Favicon: buildFavicon(url)}) | 305 | + | // Use favicon_url from database if available, otherwise build it |
| 306 | + | favicon := "" | |||
| 307 | + | if s, ok := m["favicon_url"].(string); ok && s != "" { | |||
| 308 | + | favicon = s | |||
| 309 | + | } else { | |||
| 310 | + | favicon = buildFavicon(url) | |||
| 311 | + | } | |||
| 312 | + | results = append(results, suggestion{URL: url, Favicon: favicon}) | |||
| 343 | seen[url] = struct{}{} | 313 | seen[url] = struct{}{} | ||
| 344 | if len(results) >= limit { | 314 | if len(results) >= limit { | ||
| 345 | break | 315 | break | ||
M pkg/webkit/settings.go +20 -0
| @@ -48,11 +48,31 @@ type Config struct { | |||
| 48 | 48 | // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom. | |
| 49 | 49 | UseDomZoom bool | |
| 50 | 50 | ||
| 51 | + | // Colors defines the light/dark palette tokens for the GUI. | |
| 52 | + | Colors ColorPalettes | |
| 53 | + | ||
| 51 | 54 | // CreateWindow controls whether to create a window during WebView initialization. | |
| 52 | 55 | // Set to false when creating WebViews for workspace panes that will be reparented. | |
| 53 | 56 | CreateWindow bool | |
| 54 | 57 | } | |
| 55 | 58 | ||
| 59 | + | // ColorPalette holds semantic UI tokens for a theme. | |
| 60 | + | type ColorPalette struct { | |
| 61 | + | Background string | |
| 62 | + | Surface string | |
| 63 | + | SurfaceVariant string | |
| 64 | + | Text string | |
| 65 | + | Muted string | |
| 66 | + | Accent string | |
| 67 | + | Border string | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // ColorPalettes hosts both light and dark color palettes. | |
| 71 | + | type ColorPalettes struct { | |
| 72 | + | Light ColorPalette | |
| 73 | + | Dark ColorPalette | |
| 74 | + | } | |
| 75 | + | ||
| 56 | 76 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | |
| 57 | 77 | type CodecPreferencesConfig struct { | |
| 58 | 78 | PreferredCodecs []string | |
| @@ -48,11 +48,31 @@ type Config struct { | |||||
| 48 | // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom. | 48 | // UseDomZoom toggles CSS/DOM-based zoom instead of WebKit's native zoom. | ||
| 49 | UseDomZoom bool | 49 | UseDomZoom bool | ||
| 50 | 50 | ||||
| 51 | + | // Colors defines the light/dark palette tokens for the GUI. | |||
| 52 | + | Colors ColorPalettes | |||
| 53 | + | ||||
| 51 | // CreateWindow controls whether to create a window during WebView initialization. | 54 | // CreateWindow controls whether to create a window during WebView initialization. | ||
| 52 | // Set to false when creating WebViews for workspace panes that will be reparented. | 55 | // Set to false when creating WebViews for workspace panes that will be reparented. | ||
| 53 | CreateWindow bool | 56 | CreateWindow bool | ||
| 54 | } | 57 | } | ||
| 55 | 58 | ||||
| 59 | + | // ColorPalette holds semantic UI tokens for a theme. | |||
| 60 | + | type ColorPalette struct { | |||
| 61 | + | Background string | |||
| 62 | + | Surface string | |||
| 63 | + | SurfaceVariant string | |||
| 64 | + | Text string | |||
| 65 | + | Muted string | |||
| 66 | + | Accent string | |||
| 67 | + | Border string | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // ColorPalettes hosts both light and dark color palettes. | |||
| 71 | + | type ColorPalettes struct { | |||
| 72 | + | Light ColorPalette | |||
| 73 | + | Dark ColorPalette | |||
| 74 | + | } | |||
| 75 | + | ||||
| 56 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | 76 | // CodecPreferencesConfig holds codec preferences for WebKit media playback | ||
| 57 | type CodecPreferencesConfig struct { | 77 | type CodecPreferencesConfig struct { | ||
| 58 | PreferredCodecs []string | 78 | PreferredCodecs []string | ||
M pkg/webkit/webview_cgo.go +55 -1
| @@ -1305,7 +1305,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||
| 1305 | 1305 | log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent) | |
| 1306 | 1306 | } else if cfg.CodecPreferences.ForceAV1 { | |
| 1307 | 1307 | // Use modern Chrome UA that signals AV1 support | |
| 1308 | - | av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" | |
| 1308 | + | av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" | |
| 1309 | 1309 | cAV1UA := C.CString(av1UA) | |
| 1310 | 1310 | C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA)) | |
| 1311 | 1311 | C.free(unsafe.Pointer(cAV1UA)) | |
| @@ -1844,6 +1844,7 @@ func (w *WebView) CreateRelatedView() *WebView { | |||
| 1844 | 1844 | VideoAcceleration: w.config.VideoAcceleration, | |
| 1845 | 1845 | Memory: w.config.Memory, | |
| 1846 | 1846 | CodecPreferences: w.config.CodecPreferences, | |
| 1847 | + | Colors: w.config.Colors, | |
| 1847 | 1848 | CreateWindow: false, | |
| 1848 | 1849 | } | |
| 1849 | 1850 | related, err := NewWebViewWithRelated(cfg, w) | |
| @@ -2060,6 +2061,59 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 2060 | 2061 | C.webkit_user_script_unref(schemeScript) | |
| 2061 | 2062 | } | |
| 2062 | 2063 | ||
| 2064 | + | // Inject color palette tokens sourced from Go configuration | |
| 2065 | + | if cfg != nil { | |
| 2066 | + | if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 { | |
| 2067 | + | paletteScript := fmt.Sprintf(`(function(){try{ | |
| 2068 | + | window.__dumber_palette=%s; | |
| 2069 | + | var normalized={}; | |
| 2070 | + | if(window.__dumber_palette){ | |
| 2071 | + | for (var key in window.__dumber_palette){ | |
| 2072 | + | if(Object.prototype.hasOwnProperty.call(window.__dumber_palette,key)){ | |
| 2073 | + | normalized[key.toLowerCase()] = window.__dumber_palette[key]; | |
| 2074 | + | } | |
| 2075 | + | } | |
| 2076 | + | window.__dumber_palette = normalized; | |
| 2077 | + | } | |
| 2078 | + | var tokenMap={Background:'--color-browser-bg',Surface:'--color-browser-surface',SurfaceVariant:'--color-browser-surface-variant',Text:'--color-browser-text',Muted:'--color-browser-muted',Accent:'--color-browser-accent',Border:'--color-browser-border'}; | |
| 2079 | + | var dynamicMap={Background:'--dynamic-bg',Surface:'--dynamic-surface',SurfaceVariant:'--dynamic-surface-variant',Text:'--dynamic-text',Muted:'--dynamic-muted',Accent:'--dynamic-accent',Border:'--dynamic-border'}; | |
| 2080 | + | var apply=function(theme){ | |
| 2081 | + | var palette=(window.__dumber_palette && (window.__dumber_palette[theme] || window.__dumber_palette.light)) || null; | |
| 2082 | + | if(!palette){return;} | |
| 2083 | + | var root=document.documentElement; | |
| 2084 | + | for(var key in tokenMap){ | |
| 2085 | + | if(Object.prototype.hasOwnProperty.call(tokenMap,key) && palette[key]){ | |
| 2086 | + | root.style.setProperty(tokenMap[key], palette[key]); | |
| 2087 | + | if(Object.prototype.hasOwnProperty.call(dynamicMap,key)){ | |
| 2088 | + | root.style.setProperty(dynamicMap[key], palette[key]); | |
| 2089 | + | } | |
| 2090 | + | } | |
| 2091 | + | } | |
| 2092 | + | document.dispatchEvent(new CustomEvent('dumber:palette-updated',{detail:{theme:theme}})); | |
| 2093 | + | }; | |
| 2094 | + | window.__dumber_applyPalette=function(theme){ | |
| 2095 | + | apply(theme==='dark'?'dark':'light'); | |
| 2096 | + | }; | |
| 2097 | + | var initial=document.documentElement.classList.contains('dark')?'dark':'light'; | |
| 2098 | + | if(window.__dumber_initial_theme){ | |
| 2099 | + | initial=window.__dumber_initial_theme; | |
| 2100 | + | } | |
| 2101 | + | apply(initial); | |
| 2102 | + | }catch(err){console.error('[dumber] Failed to apply palette',err);}})();`, string(paletteJSON)) | |
| 2103 | + | cPalette := C.CString(paletteScript) | |
| 2104 | + | defer C.free(unsafe.Pointer(cPalette)) | |
| 2105 | + | paletteUserScript := C.webkit_user_script_new((*C.gchar)(cPalette), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |
| 2106 | + | if paletteUserScript != nil { | |
| 2107 | + | C.webkit_user_content_manager_add_script(w.native.ucm, paletteUserScript) | |
| 2108 | + | C.webkit_user_script_unref(paletteUserScript) | |
| 2109 | + | } else { | |
| 2110 | + | log.Printf("[webkit] Failed to create palette user script") | |
| 2111 | + | } | |
| 2112 | + | } else if err != nil { | |
| 2113 | + | log.Printf("[webkit] Failed to marshal color palettes: %v", err) | |
| 2114 | + | } | |
| 2115 | + | } | |
| 2116 | + | ||
| 2063 | 2117 | // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls) | |
| 2064 | 2118 | if cfg != nil && cfg.Assets != nil { | |
| 2065 | 2119 | log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js") | |
| @@ -1305,7 +1305,7 @@ func NewWebView(cfg *Config) (*WebView, error) { | |||||
| 1305 | log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent) | 1305 | log.Printf("[webkit] Set custom User-Agent: %s", cfg.CodecPreferences.CustomUserAgent) | ||
| 1306 | } else if cfg.CodecPreferences.ForceAV1 { | 1306 | } else if cfg.CodecPreferences.ForceAV1 { | ||
| 1307 | // Use modern Chrome UA that signals AV1 support | 1307 | // Use modern Chrome UA that signals AV1 support | ||
| 1308 | - | av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" | 1308 | + | av1UA := "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" |
| 1309 | cAV1UA := C.CString(av1UA) | 1309 | cAV1UA := C.CString(av1UA) | ||
| 1310 | C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA)) | 1310 | C.webkit_settings_set_user_agent(settings, (*C.gchar)(cAV1UA)) | ||
| 1311 | C.free(unsafe.Pointer(cAV1UA)) | 1311 | C.free(unsafe.Pointer(cAV1UA)) | ||
| @@ -1844,6 +1844,7 @@ func (w *WebView) CreateRelatedView() *WebView { | |||||
| 1844 | VideoAcceleration: w.config.VideoAcceleration, | 1844 | VideoAcceleration: w.config.VideoAcceleration, | ||
| 1845 | Memory: w.config.Memory, | 1845 | Memory: w.config.Memory, | ||
| 1846 | CodecPreferences: w.config.CodecPreferences, | 1846 | CodecPreferences: w.config.CodecPreferences, | ||
| 1847 | + | Colors: w.config.Colors, | |||
| 1847 | CreateWindow: false, | 1848 | CreateWindow: false, | ||
| 1848 | } | 1849 | } | ||
| 1849 | related, err := NewWebViewWithRelated(cfg, w) | 1850 | related, err := NewWebViewWithRelated(cfg, w) | ||
| @@ -2060,6 +2061,59 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 2060 | C.webkit_user_script_unref(schemeScript) | 2061 | C.webkit_user_script_unref(schemeScript) | ||
| 2061 | } | 2062 | } | ||
| 2062 | 2063 | ||||
| 2064 | + | // Inject color palette tokens sourced from Go configuration | |||
| 2065 | + | if cfg != nil { | |||
| 2066 | + | if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 { | |||
| 2067 | + | paletteScript := fmt.Sprintf(`(function(){try{ | |||
| 2068 | + | window.__dumber_palette=%s; | |||
| 2069 | + | var normalized={}; | |||
| 2070 | + | if(window.__dumber_palette){ | |||
| 2071 | + | for (var key in window.__dumber_palette){ | |||
| 2072 | + | if(Object.prototype.hasOwnProperty.call(window.__dumber_palette,key)){ | |||
| 2073 | + | normalized[key.toLowerCase()] = window.__dumber_palette[key]; | |||
| 2074 | + | } | |||
| 2075 | + | } | |||
| 2076 | + | window.__dumber_palette = normalized; | |||
| 2077 | + | } | |||
| 2078 | + | var tokenMap={Background:'--color-browser-bg',Surface:'--color-browser-surface',SurfaceVariant:'--color-browser-surface-variant',Text:'--color-browser-text',Muted:'--color-browser-muted',Accent:'--color-browser-accent',Border:'--color-browser-border'}; | |||
| 2079 | + | var dynamicMap={Background:'--dynamic-bg',Surface:'--dynamic-surface',SurfaceVariant:'--dynamic-surface-variant',Text:'--dynamic-text',Muted:'--dynamic-muted',Accent:'--dynamic-accent',Border:'--dynamic-border'}; | |||
| 2080 | + | var apply=function(theme){ | |||
| 2081 | + | var palette=(window.__dumber_palette && (window.__dumber_palette[theme] || window.__dumber_palette.light)) || null; | |||
| 2082 | + | if(!palette){return;} | |||
| 2083 | + | var root=document.documentElement; | |||
| 2084 | + | for(var key in tokenMap){ | |||
| 2085 | + | if(Object.prototype.hasOwnProperty.call(tokenMap,key) && palette[key]){ | |||
| 2086 | + | root.style.setProperty(tokenMap[key], palette[key]); | |||
| 2087 | + | if(Object.prototype.hasOwnProperty.call(dynamicMap,key)){ | |||
| 2088 | + | root.style.setProperty(dynamicMap[key], palette[key]); | |||
| 2089 | + | } | |||
| 2090 | + | } | |||
| 2091 | + | } | |||
| 2092 | + | document.dispatchEvent(new CustomEvent('dumber:palette-updated',{detail:{theme:theme}})); | |||
| 2093 | + | }; | |||
| 2094 | + | window.__dumber_applyPalette=function(theme){ | |||
| 2095 | + | apply(theme==='dark'?'dark':'light'); | |||
| 2096 | + | }; | |||
| 2097 | + | var initial=document.documentElement.classList.contains('dark')?'dark':'light'; | |||
| 2098 | + | if(window.__dumber_initial_theme){ | |||
| 2099 | + | initial=window.__dumber_initial_theme; | |||
| 2100 | + | } | |||
| 2101 | + | apply(initial); | |||
| 2102 | + | }catch(err){console.error('[dumber] Failed to apply palette',err);}})();`, string(paletteJSON)) | |||
| 2103 | + | cPalette := C.CString(paletteScript) | |||
| 2104 | + | defer C.free(unsafe.Pointer(cPalette)) | |||
| 2105 | + | paletteUserScript := C.webkit_user_script_new((*C.gchar)(cPalette), C.WEBKIT_USER_CONTENT_INJECT_TOP_FRAME, C.WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, nil, nil) | |||
| 2106 | + | if paletteUserScript != nil { | |||
| 2107 | + | C.webkit_user_content_manager_add_script(w.native.ucm, paletteUserScript) | |||
| 2108 | + | C.webkit_user_script_unref(paletteUserScript) | |||
| 2109 | + | } else { | |||
| 2110 | + | log.Printf("[webkit] Failed to create palette user script") | |||
| 2111 | + | } | |||
| 2112 | + | } else if err != nil { | |||
| 2113 | + | log.Printf("[webkit] Failed to marshal color palettes: %v", err) | |||
| 2114 | + | } | |||
| 2115 | + | } | |||
| 2116 | + | ||||
| 2063 | // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls) | 2117 | // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls) | ||
| 2064 | if cfg != nil && cfg.Assets != nil { | 2118 | if cfg != nil && cfg.Assets != nil { | ||
| 2065 | log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js") | 2119 | log.Printf("[webkit] Attempting to load GUI bundle from assets/gui/gui.min.js") | ||