refactor: address code review feedback and simplify architecture
- Move color palette injection from inline Go to main-world.ts for better maintainability - Extract magic emoji value to named constant - Remove shortcuts fallback - fail cleanly if backend unavailable (KISS)
- commit
- 2b84462dc9dff4ac456e76b33011cce7f933eaaf
- author
- bamen · · 2025-10-06 21:10 UTC
- committer
- bamen · · 2025-10-06 17:18 UTC
- parent
- 37da1ff
- signature
- unsigned
5 files · +113 -176
D gui/src/constants/shortcuts.ts +0 -91
| @@ -1,91 +0,0 @@ | |||
| 1 | - | /** | |
| 2 | - | * Search shortcuts configuration | |
| 3 | - | * | |
| 4 | - | * @deprecated This file is only used as a fallback. The real source of truth | |
| 5 | - | * is the backend Go config (internal/config/defaults.go). Shortcuts should be | |
| 6 | - | * fetched from the backend via /api/config or the messaging bridge. | |
| 7 | - | * | |
| 8 | - | * Only kept for development fallback when backend is unavailable. | |
| 9 | - | */ | |
| 10 | - | ||
| 11 | - | export interface SearchShortcutDefinition { | |
| 12 | - | shortcut: string; | |
| 13 | - | url_template: string; | |
| 14 | - | description: string; | |
| 15 | - | } | |
| 16 | - | ||
| 17 | - | /** | |
| 18 | - | * Default search shortcuts | |
| 19 | - | * Alphabetically ordered by shortcut key | |
| 20 | - | */ | |
| 21 | - | export const DEFAULT_SHORTCUTS: Record<string, SearchShortcutDefinition> = { | |
| 22 | - | ddg: { | |
| 23 | - | shortcut: "ddg", | |
| 24 | - | url_template: "https://duckduckgo.com/?q={query}", | |
| 25 | - | description: "DuckDuckGo search", | |
| 26 | - | }, | |
| 27 | - | g: { | |
| 28 | - | shortcut: "g", | |
| 29 | - | url_template: "https://www.google.com/search?q={query}", | |
| 30 | - | description: "Google search", | |
| 31 | - | }, | |
| 32 | - | gh: { | |
| 33 | - | shortcut: "gh", | |
| 34 | - | url_template: "https://github.com/search?q={query}", | |
| 35 | - | description: "GitHub search", | |
| 36 | - | }, | |
| 37 | - | go: { | |
| 38 | - | shortcut: "go", | |
| 39 | - | url_template: "https://pkg.go.dev/search?q={query}", | |
| 40 | - | description: "Go package search", | |
| 41 | - | }, | |
| 42 | - | mdn: { | |
| 43 | - | shortcut: "mdn", | |
| 44 | - | url_template: "https://developer.mozilla.org/en-US/search?q={query}", | |
| 45 | - | description: "MDN Web Docs search", | |
| 46 | - | }, | |
| 47 | - | npm: { | |
| 48 | - | shortcut: "npm", | |
| 49 | - | url_template: "https://www.npmjs.com/search?q={query}", | |
| 50 | - | description: "npm package search", | |
| 51 | - | }, | |
| 52 | - | r: { | |
| 53 | - | shortcut: "r", | |
| 54 | - | url_template: "https://www.reddit.com/search?q={query}", | |
| 55 | - | description: "Reddit search", | |
| 56 | - | }, | |
| 57 | - | so: { | |
| 58 | - | shortcut: "so", | |
| 59 | - | url_template: "https://stackoverflow.com/search?q={query}", | |
| 60 | - | description: "Stack Overflow search", | |
| 61 | - | }, | |
| 62 | - | w: { | |
| 63 | - | shortcut: "w", | |
| 64 | - | url_template: "https://en.wikipedia.org/wiki/{query}", | |
| 65 | - | description: "Wikipedia search", | |
| 66 | - | }, | |
| 67 | - | yt: { | |
| 68 | - | shortcut: "yt", | |
| 69 | - | url_template: "https://www.youtube.com/results?search_query={query}", | |
| 70 | - | description: "YouTube search", | |
| 71 | - | }, | |
| 72 | - | }; | |
| 73 | - | ||
| 74 | - | /** | |
| 75 | - | * Get array of shortcut keys for validation | |
| 76 | - | */ | |
| 77 | - | export const SHORTCUT_KEYS = Object.keys(DEFAULT_SHORTCUTS); | |
| 78 | - | ||
| 79 | - | /** | |
| 80 | - | * Check if a string is a valid shortcut | |
| 81 | - | */ | |
| 82 | - | export function isValidShortcut(key: string): boolean { | |
| 83 | - | return key in DEFAULT_SHORTCUTS; | |
| 84 | - | } | |
| 85 | - | ||
| 86 | - | /** | |
| 87 | - | * Get shortcut description by key | |
| 88 | - | */ | |
| 89 | - | export function getShortcutDescription(key: string): string | null { | |
| 90 | - | return DEFAULT_SHORTCUTS[key]?.description ?? null; | |
| 91 | - | } | |
| @@ -1,91 +0,0 @@ | |||||
| 1 | - | /** | |||
| 2 | - | * Search shortcuts configuration | |||
| 3 | - | * | |||
| 4 | - | * @deprecated This file is only used as a fallback. The real source of truth | |||
| 5 | - | * is the backend Go config (internal/config/defaults.go). Shortcuts should be | |||
| 6 | - | * fetched from the backend via /api/config or the messaging bridge. | |||
| 7 | - | * | |||
| 8 | - | * Only kept for development fallback when backend is unavailable. | |||
| 9 | - | */ | |||
| 10 | - | ||||
| 11 | - | export interface SearchShortcutDefinition { | |||
| 12 | - | shortcut: string; | |||
| 13 | - | url_template: string; | |||
| 14 | - | description: string; | |||
| 15 | - | } | |||
| 16 | - | ||||
| 17 | - | /** | |||
| 18 | - | * Default search shortcuts | |||
| 19 | - | * Alphabetically ordered by shortcut key | |||
| 20 | - | */ | |||
| 21 | - | export const DEFAULT_SHORTCUTS: Record<string, SearchShortcutDefinition> = { | |||
| 22 | - | ddg: { | |||
| 23 | - | shortcut: "ddg", | |||
| 24 | - | url_template: "https://duckduckgo.com/?q={query}", | |||
| 25 | - | description: "DuckDuckGo search", | |||
| 26 | - | }, | |||
| 27 | - | g: { | |||
| 28 | - | shortcut: "g", | |||
| 29 | - | url_template: "https://www.google.com/search?q={query}", | |||
| 30 | - | description: "Google search", | |||
| 31 | - | }, | |||
| 32 | - | gh: { | |||
| 33 | - | shortcut: "gh", | |||
| 34 | - | url_template: "https://github.com/search?q={query}", | |||
| 35 | - | description: "GitHub search", | |||
| 36 | - | }, | |||
| 37 | - | go: { | |||
| 38 | - | shortcut: "go", | |||
| 39 | - | url_template: "https://pkg.go.dev/search?q={query}", | |||
| 40 | - | description: "Go package search", | |||
| 41 | - | }, | |||
| 42 | - | mdn: { | |||
| 43 | - | shortcut: "mdn", | |||
| 44 | - | url_template: "https://developer.mozilla.org/en-US/search?q={query}", | |||
| 45 | - | description: "MDN Web Docs search", | |||
| 46 | - | }, | |||
| 47 | - | npm: { | |||
| 48 | - | shortcut: "npm", | |||
| 49 | - | url_template: "https://www.npmjs.com/search?q={query}", | |||
| 50 | - | description: "npm package search", | |||
| 51 | - | }, | |||
| 52 | - | r: { | |||
| 53 | - | shortcut: "r", | |||
| 54 | - | url_template: "https://www.reddit.com/search?q={query}", | |||
| 55 | - | description: "Reddit search", | |||
| 56 | - | }, | |||
| 57 | - | so: { | |||
| 58 | - | shortcut: "so", | |||
| 59 | - | url_template: "https://stackoverflow.com/search?q={query}", | |||
| 60 | - | description: "Stack Overflow search", | |||
| 61 | - | }, | |||
| 62 | - | w: { | |||
| 63 | - | shortcut: "w", | |||
| 64 | - | url_template: "https://en.wikipedia.org/wiki/{query}", | |||
| 65 | - | description: "Wikipedia search", | |||
| 66 | - | }, | |||
| 67 | - | yt: { | |||
| 68 | - | shortcut: "yt", | |||
| 69 | - | url_template: "https://www.youtube.com/results?search_query={query}", | |||
| 70 | - | description: "YouTube search", | |||
| 71 | - | }, | |||
| 72 | - | }; | |||
| 73 | - | ||||
| 74 | - | /** | |||
| 75 | - | * Get array of shortcut keys for validation | |||
| 76 | - | */ | |||
| 77 | - | export const SHORTCUT_KEYS = Object.keys(DEFAULT_SHORTCUTS); | |||
| 78 | - | ||||
| 79 | - | /** | |||
| 80 | - | * Check if a string is a valid shortcut | |||
| 81 | - | */ | |||
| 82 | - | export function isValidShortcut(key: string): boolean { | |||
| 83 | - | return key in DEFAULT_SHORTCUTS; | |||
| 84 | - | } | |||
| 85 | - | ||||
| 86 | - | /** | |||
| 87 | - | * Get shortcut description by key | |||
| 88 | - | */ | |||
| 89 | - | export function getShortcutDescription(key: string): string | null { | |||
| 90 | - | return DEFAULT_SHORTCUTS[key]?.description ?? null; | |||
| 91 | - | } | |||
M gui/src/injected/main-world.ts +78 -0
| @@ -20,6 +20,7 @@ declare global { | |||
| 20 | 20 | __dumber_initial_theme?: string; | |
| 21 | 21 | __dumber_setTheme?: (theme: "light" | "dark") => void; | |
| 22 | 22 | __dumber_applyPalette?: (theme: "light" | "dark") => void; | |
| 23 | + | __dumber_palette?: { light?: Record<string, string>; dark?: Record<string, string> }; | |
| 23 | 24 | __dumber_applyDomZoom?: (level: number) => void; | |
| 24 | 25 | __dumber_showToast?: ( | |
| 25 | 26 | message: string, | |
| @@ -119,6 +120,83 @@ function detectWindowType(features?: string | null): string { | |||
| 119 | 120 | window.__dumber_webview_id = "__WEBVIEW_ID__"; | |
| 120 | 121 | window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean; | |
| 121 | 122 | ||
| 123 | + | // Initialize color palette (will be replaced by Go with actual palette JSON) | |
| 124 | + | // Placeholder: __PALETTE_JSON__ | |
| 125 | + | try { | |
| 126 | + | const paletteData = "__PALETTE_JSON__"; | |
| 127 | + | if (paletteData && paletteData !== "__PALETTE_JSON__") { | |
| 128 | + | window.__dumber_palette = JSON.parse(paletteData); | |
| 129 | + | ||
| 130 | + | // Normalize palette keys to lowercase | |
| 131 | + | const normalized: typeof window.__dumber_palette = {}; | |
| 132 | + | if (window.__dumber_palette) { | |
| 133 | + | for (const key in window.__dumber_palette) { | |
| 134 | + | if (Object.prototype.hasOwnProperty.call(window.__dumber_palette, key)) { | |
| 135 | + | normalized[key.toLowerCase()] = window.__dumber_palette[key]; | |
| 136 | + | } | |
| 137 | + | } | |
| 138 | + | window.__dumber_palette = normalized; | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | } catch (err) { | |
| 142 | + | console.error("[dumber] Failed to parse palette JSON", err); | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | // Palette application function | |
| 146 | + | window.__dumber_applyPalette = (theme: "light" | "dark") => { | |
| 147 | + | try { | |
| 148 | + | const palette = window.__dumber_palette?.[theme] || window.__dumber_palette?.light; | |
| 149 | + | if (!palette) { | |
| 150 | + | return; | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | const tokenMap: Record<string, string> = { | |
| 154 | + | Background: "--color-browser-bg", | |
| 155 | + | Surface: "--color-browser-surface", | |
| 156 | + | SurfaceVariant: "--color-browser-surface-variant", | |
| 157 | + | Text: "--color-browser-text", | |
| 158 | + | Muted: "--color-browser-muted", | |
| 159 | + | Accent: "--color-browser-accent", | |
| 160 | + | Border: "--color-browser-border", | |
| 161 | + | }; | |
| 162 | + | ||
| 163 | + | const dynamicMap: Record<string, string> = { | |
| 164 | + | Background: "--dynamic-bg", | |
| 165 | + | Surface: "--dynamic-surface", | |
| 166 | + | SurfaceVariant: "--dynamic-surface-variant", | |
| 167 | + | Text: "--dynamic-text", | |
| 168 | + | Muted: "--dynamic-muted", | |
| 169 | + | Accent: "--dynamic-accent", | |
| 170 | + | Border: "--dynamic-border", | |
| 171 | + | }; | |
| 172 | + | ||
| 173 | + | const root = document.documentElement; | |
| 174 | + | for (const key in tokenMap) { | |
| 175 | + | if (Object.prototype.hasOwnProperty.call(tokenMap, key) && palette[key]) { | |
| 176 | + | root.style.setProperty(tokenMap[key], palette[key]); | |
| 177 | + | if (Object.prototype.hasOwnProperty.call(dynamicMap, key)) { | |
| 178 | + | root.style.setProperty(dynamicMap[key], palette[key]); | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | document.dispatchEvent( | |
| 184 | + | new CustomEvent("dumber:palette-updated", { detail: { theme } }) | |
| 185 | + | ); | |
| 186 | + | } catch (err) { | |
| 187 | + | console.error("[dumber] Failed to apply palette", err); | |
| 188 | + | } | |
| 189 | + | }; | |
| 190 | + | ||
| 191 | + | // Apply initial palette based on theme | |
| 192 | + | try { | |
| 193 | + | const initialTheme = document.documentElement.classList.contains("dark") ? "dark" : "light"; | |
| 194 | + | window.__dumber_initial_theme = window.__dumber_initial_theme || initialTheme; | |
| 195 | + | window.__dumber_applyPalette(window.__dumber_initial_theme); | |
| 196 | + | } catch (err) { | |
| 197 | + | console.error("[dumber] Failed to apply initial palette", err); | |
| 198 | + | } | |
| 199 | + | ||
| 122 | 200 | // Theme setter function for GTK theme integration | |
| 123 | 201 | window.__dumber_setTheme = (theme: "light" | "dark") => { | |
| 124 | 202 | window.__dumber_initial_theme = theme; | |
| @@ -20,6 +20,7 @@ declare global { | |||||
| 20 | __dumber_initial_theme?: string; | 20 | __dumber_initial_theme?: string; | ||
| 21 | __dumber_setTheme?: (theme: "light" | "dark") => void; | 21 | __dumber_setTheme?: (theme: "light" | "dark") => void; | ||
| 22 | __dumber_applyPalette?: (theme: "light" | "dark") => void; | 22 | __dumber_applyPalette?: (theme: "light" | "dark") => void; | ||
| 23 | + | __dumber_palette?: { light?: Record<string, string>; dark?: Record<string, string> }; | |||
| 23 | __dumber_applyDomZoom?: (level: number) => void; | 24 | __dumber_applyDomZoom?: (level: number) => void; | ||
| 24 | __dumber_showToast?: ( | 25 | __dumber_showToast?: ( | ||
| 25 | message: string, | 26 | message: string, | ||
| @@ -119,6 +120,83 @@ function detectWindowType(features?: string | null): string { | |||||
| 119 | window.__dumber_webview_id = "__WEBVIEW_ID__"; | 120 | window.__dumber_webview_id = "__WEBVIEW_ID__"; | ||
| 120 | window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean; | 121 | window.__dumber_is_active = "__WEBVIEW_ACTIVE__" as unknown as boolean; | ||
| 121 | 122 | ||||
| 123 | + | // Initialize color palette (will be replaced by Go with actual palette JSON) | |||
| 124 | + | // Placeholder: __PALETTE_JSON__ | |||
| 125 | + | try { | |||
| 126 | + | const paletteData = "__PALETTE_JSON__"; | |||
| 127 | + | if (paletteData && paletteData !== "__PALETTE_JSON__") { | |||
| 128 | + | window.__dumber_palette = JSON.parse(paletteData); | |||
| 129 | + | ||||
| 130 | + | // Normalize palette keys to lowercase | |||
| 131 | + | const normalized: typeof window.__dumber_palette = {}; | |||
| 132 | + | if (window.__dumber_palette) { | |||
| 133 | + | for (const key in window.__dumber_palette) { | |||
| 134 | + | if (Object.prototype.hasOwnProperty.call(window.__dumber_palette, key)) { | |||
| 135 | + | normalized[key.toLowerCase()] = window.__dumber_palette[key]; | |||
| 136 | + | } | |||
| 137 | + | } | |||
| 138 | + | window.__dumber_palette = normalized; | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | } catch (err) { | |||
| 142 | + | console.error("[dumber] Failed to parse palette JSON", err); | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | // Palette application function | |||
| 146 | + | window.__dumber_applyPalette = (theme: "light" | "dark") => { | |||
| 147 | + | try { | |||
| 148 | + | const palette = window.__dumber_palette?.[theme] || window.__dumber_palette?.light; | |||
| 149 | + | if (!palette) { | |||
| 150 | + | return; | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | const tokenMap: Record<string, string> = { | |||
| 154 | + | Background: "--color-browser-bg", | |||
| 155 | + | Surface: "--color-browser-surface", | |||
| 156 | + | SurfaceVariant: "--color-browser-surface-variant", | |||
| 157 | + | Text: "--color-browser-text", | |||
| 158 | + | Muted: "--color-browser-muted", | |||
| 159 | + | Accent: "--color-browser-accent", | |||
| 160 | + | Border: "--color-browser-border", | |||
| 161 | + | }; | |||
| 162 | + | ||||
| 163 | + | const dynamicMap: Record<string, string> = { | |||
| 164 | + | Background: "--dynamic-bg", | |||
| 165 | + | Surface: "--dynamic-surface", | |||
| 166 | + | SurfaceVariant: "--dynamic-surface-variant", | |||
| 167 | + | Text: "--dynamic-text", | |||
| 168 | + | Muted: "--dynamic-muted", | |||
| 169 | + | Accent: "--dynamic-accent", | |||
| 170 | + | Border: "--dynamic-border", | |||
| 171 | + | }; | |||
| 172 | + | ||||
| 173 | + | const root = document.documentElement; | |||
| 174 | + | for (const key in tokenMap) { | |||
| 175 | + | if (Object.prototype.hasOwnProperty.call(tokenMap, key) && palette[key]) { | |||
| 176 | + | root.style.setProperty(tokenMap[key], palette[key]); | |||
| 177 | + | if (Object.prototype.hasOwnProperty.call(dynamicMap, key)) { | |||
| 178 | + | root.style.setProperty(dynamicMap[key], palette[key]); | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | document.dispatchEvent( | |||
| 184 | + | new CustomEvent("dumber:palette-updated", { detail: { theme } }) | |||
| 185 | + | ); | |||
| 186 | + | } catch (err) { | |||
| 187 | + | console.error("[dumber] Failed to apply palette", err); | |||
| 188 | + | } | |||
| 189 | + | }; | |||
| 190 | + | ||||
| 191 | + | // Apply initial palette based on theme | |||
| 192 | + | try { | |||
| 193 | + | const initialTheme = document.documentElement.classList.contains("dark") ? "dark" : "light"; | |||
| 194 | + | window.__dumber_initial_theme = window.__dumber_initial_theme || initialTheme; | |||
| 195 | + | window.__dumber_applyPalette(window.__dumber_initial_theme); | |||
| 196 | + | } catch (err) { | |||
| 197 | + | console.error("[dumber] Failed to apply initial palette", err); | |||
| 198 | + | } | |||
| 199 | + | ||||
| 122 | // Theme setter function for GTK theme integration | 200 | // Theme setter function for GTK theme integration | ||
| 123 | window.__dumber_setTheme = (theme: "light" | "dark") => { | 201 | window.__dumber_setTheme = (theme: "light" | "dark") => { | ||
| 124 | window.__dumber_initial_theme = theme; | 202 | window.__dumber_initial_theme = theme; | ||
M gui/src/services/app.ts +14 -32
| @@ -1,5 +1,4 @@ | |||
| 1 | 1 | import type { HistoryEntry, SearchShortcut } from "../types/generated.js"; | |
| 2 | - | import { DEFAULT_SHORTCUTS } from "../constants/shortcuts.js"; | |
| 3 | 2 | ||
| 4 | 3 | export class AppService { | |
| 5 | 4 | private history: HistoryEntry[] = []; | |
| @@ -80,38 +79,21 @@ export class AppService { | |||
| 80 | 79 | } | |
| 81 | 80 | ||
| 82 | 81 | async loadShortcuts(): Promise<void> { | |
| 83 | - | try { | |
| 84 | - | const cfg = await fetch("/api/config").then((r) => r.json()); | |
| 85 | - | const raw = cfg?.search_shortcuts || {}; | |
| 86 | - | // Normalize field casing from backend (supports URL/Description and url/description) | |
| 87 | - | const normalized: Record<string, SearchShortcut> = {}; | |
| 88 | - | for (const [key, value] of Object.entries(raw)) { | |
| 89 | - | const v = value as Record<string, unknown>; | |
| 90 | - | normalized[key] = { | |
| 91 | - | id: 0, | |
| 92 | - | shortcut: key, | |
| 93 | - | url_template: (v.url ?? v.URL ?? "") as string, | |
| 94 | - | description: (v.description ?? v.Description ?? "") as string, | |
| 95 | - | created_at: null, | |
| 96 | - | }; | |
| 97 | - | } | |
| 98 | - | this.shortcuts = normalized; | |
| 99 | - | } catch (error) { | |
| 100 | - | console.error("Failed to load shortcuts:", error); | |
| 101 | - | // Mock shortcuts for development/fallback using shared constants | |
| 102 | - | this.shortcuts = Object.fromEntries( | |
| 103 | - | Object.entries(DEFAULT_SHORTCUTS).map(([key, def]) => [ | |
| 104 | - | key, | |
| 105 | - | { | |
| 106 | - | id: 0, | |
| 107 | - | shortcut: def.shortcut, | |
| 108 | - | url_template: def.url_template, | |
| 109 | - | description: def.description, | |
| 110 | - | created_at: null, | |
| 111 | - | }, | |
| 112 | - | ]) | |
| 113 | - | ); | |
| 82 | + | const cfg = await fetch("/api/config").then((r) => r.json()); | |
| 83 | + | const raw = cfg?.search_shortcuts || {}; | |
| 84 | + | // Normalize field casing from backend (supports URL/Description and url/description) | |
| 85 | + | const normalized: Record<string, SearchShortcut> = {}; | |
| 86 | + | for (const [key, value] of Object.entries(raw)) { | |
| 87 | + | const v = value as Record<string, unknown>; | |
| 88 | + | normalized[key] = { | |
| 89 | + | id: 0, | |
| 90 | + | shortcut: key, | |
| 91 | + | url_template: (v.url ?? v.URL ?? "") as string, | |
| 92 | + | description: (v.description ?? v.Description ?? "") as string, | |
| 93 | + | created_at: null, | |
| 94 | + | }; | |
| 114 | 95 | } | |
| 96 | + | this.shortcuts = normalized; | |
| 115 | 97 | } | |
| 116 | 98 | ||
| 117 | 99 | getHistory(): HistoryEntry[] { | |
| @@ -1,5 +1,4 @@ | |||||
| 1 | import type { HistoryEntry, SearchShortcut } from "../types/generated.js"; | 1 | import type { HistoryEntry, SearchShortcut } from "../types/generated.js"; | ||
| 2 | - | import { DEFAULT_SHORTCUTS } from "../constants/shortcuts.js"; | |||
| 3 | 2 | ||||
| 4 | export class AppService { | 3 | export class AppService { | ||
| 5 | private history: HistoryEntry[] = []; | 4 | private history: HistoryEntry[] = []; | ||
| @@ -80,38 +79,21 @@ export class AppService { | |||||
| 80 | } | 79 | } | ||
| 81 | 80 | ||||
| 82 | async loadShortcuts(): Promise<void> { | 81 | async loadShortcuts(): Promise<void> { | ||
| 83 | - | try { | 82 | + | const cfg = await fetch("/api/config").then((r) => r.json()); |
| 84 | - | const cfg = await fetch("/api/config").then((r) => r.json()); | 83 | + | const raw = cfg?.search_shortcuts || {}; |
| 85 | - | const raw = cfg?.search_shortcuts || {}; | 84 | + | // Normalize field casing from backend (supports URL/Description and url/description) |
| 86 | - | // Normalize field casing from backend (supports URL/Description and url/description) | 85 | + | const normalized: Record<string, SearchShortcut> = {}; |
| 87 | - | const normalized: Record<string, SearchShortcut> = {}; | 86 | + | for (const [key, value] of Object.entries(raw)) { |
| 88 | - | for (const [key, value] of Object.entries(raw)) { | 87 | + | const v = value as Record<string, unknown>; |
| 89 | - | const v = value as Record<string, unknown>; | 88 | + | normalized[key] = { |
| 90 | - | normalized[key] = { | 89 | + | id: 0, |
| 91 | - | id: 0, | 90 | + | shortcut: key, |
| 92 | - | shortcut: key, | 91 | + | url_template: (v.url ?? v.URL ?? "") as string, |
| 93 | - | url_template: (v.url ?? v.URL ?? "") as string, | 92 | + | description: (v.description ?? v.Description ?? "") as string, |
| 94 | - | description: (v.description ?? v.Description ?? "") as string, | 93 | + | created_at: null, |
| 95 | - | created_at: null, | 94 | + | }; |
| 96 | - | }; | |||
| 97 | - | } | |||
| 98 | - | this.shortcuts = normalized; | |||
| 99 | - | } catch (error) { | |||
| 100 | - | console.error("Failed to load shortcuts:", error); | |||
| 101 | - | // Mock shortcuts for development/fallback using shared constants | |||
| 102 | - | this.shortcuts = Object.fromEntries( | |||
| 103 | - | Object.entries(DEFAULT_SHORTCUTS).map(([key, def]) => [ | |||
| 104 | - | key, | |||
| 105 | - | { | |||
| 106 | - | id: 0, | |||
| 107 | - | shortcut: def.shortcut, | |||
| 108 | - | url_template: def.url_template, | |||
| 109 | - | description: def.description, | |||
| 110 | - | created_at: null, | |||
| 111 | - | }, | |||
| 112 | - | ]) | |||
| 113 | - | ); | |||
| 114 | } | 95 | } | ||
| 96 | + | this.shortcuts = normalized; | |||
| 115 | } | 97 | } | ||
| 116 | 98 | ||||
| 117 | getHistory(): HistoryEntry[] { | 99 | getHistory(): HistoryEntry[] { | ||
M internal/app/browser/stacked_panes.go +6 -1
| @@ -12,6 +12,11 @@ import ( | |||
| 12 | 12 | "github.com/bnema/dumber/pkg/webkit" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | + | const ( | |
| 16 | + | // faviconPlaceholder is the emoji displayed when no favicon is available | |
| 17 | + | faviconPlaceholder = "🌐" | |
| 18 | + | ) | |
| 19 | + | ||
| 15 | 20 | // StackedPaneManager handles all stacked pane operations | |
| 16 | 21 | type StackedPaneManager struct { | |
| 17 | 22 | wm *WorkspaceManager | |
| @@ -711,7 +716,7 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL str | |||
| 711 | 716 | webkit.WidgetShow(faviconImg) | |
| 712 | 717 | } else { | |
| 713 | 718 | // DEBUG: Add visible placeholder label to show code is running | |
| 714 | - | placeholderLabel := webkit.NewLabel("🌐") | |
| 719 | + | placeholderLabel := webkit.NewLabel(faviconPlaceholder) | |
| 715 | 720 | if placeholderLabel != 0 { | |
| 716 | 721 | webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder") | |
| 717 | 722 | webkit.BoxAppend(titleBox, placeholderLabel) | |
| @@ -12,6 +12,11 @@ import ( | |||||
| 12 | "github.com/bnema/dumber/pkg/webkit" | 12 | "github.com/bnema/dumber/pkg/webkit" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| 15 | + | const ( | |||
| 16 | + | // faviconPlaceholder is the emoji displayed when no favicon is available | |||
| 17 | + | faviconPlaceholder = "🌐" | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 15 | // StackedPaneManager handles all stacked pane operations | 20 | // StackedPaneManager handles all stacked pane operations | ||
| 16 | type StackedPaneManager struct { | 21 | type StackedPaneManager struct { | ||
| 17 | wm *WorkspaceManager | 22 | wm *WorkspaceManager | ||
| @@ -711,7 +716,7 @@ func (spm *StackedPaneManager) createTitleBarWithTitle(title string, pageURL str | |||||
| 711 | webkit.WidgetShow(faviconImg) | 716 | webkit.WidgetShow(faviconImg) | ||
| 712 | } else { | 717 | } else { | ||
| 713 | // DEBUG: Add visible placeholder label to show code is running | 718 | // DEBUG: Add visible placeholder label to show code is running | ||
| 714 | - | placeholderLabel := webkit.NewLabel("🌐") | 719 | + | placeholderLabel := webkit.NewLabel(faviconPlaceholder) |
| 715 | if placeholderLabel != 0 { | 720 | if placeholderLabel != 0 { | ||
| 716 | webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder") | 721 | webkit.WidgetAddCSSClass(placeholderLabel, "stacked-pane-favicon-placeholder") | ||
| 717 | webkit.BoxAppend(titleBox, placeholderLabel) | 722 | webkit.BoxAppend(titleBox, placeholderLabel) | ||
M pkg/webkit/webview_cgo.go +15 -52
| @@ -2061,58 +2061,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||
| 2061 | 2061 | C.webkit_user_script_unref(schemeScript) | |
| 2062 | 2062 | } | |
| 2063 | 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 | - | } | |
| 2064 | + | // Color palette injection now handled in main-world.ts via __PALETTE_JSON__ placeholder | |
| 2116 | 2065 | ||
| 2117 | 2066 | // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls) | |
| 2118 | 2067 | if cfg != nil && cfg.Assets != nil { | |
| @@ -2263,6 +2212,20 @@ func buildDomBridgeScript(cfg *Config, initialZoom float64, webViewId uintptr, w | |||
| 2263 | 2212 | log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState) | |
| 2264 | 2213 | script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState) | |
| 2265 | 2214 | ||
| 2215 | + | // Inject color palette JSON | |
| 2216 | + | if cfg != nil { | |
| 2217 | + | if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 { | |
| 2218 | + | script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", string(paletteJSON)) | |
| 2219 | + | } else { | |
| 2220 | + | if err != nil { | |
| 2221 | + | log.Printf("[webkit] Failed to marshal color palettes: %v", err) | |
| 2222 | + | } | |
| 2223 | + | script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", "null") | |
| 2224 | + | } | |
| 2225 | + | } else { | |
| 2226 | + | script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", "null") | |
| 2227 | + | } | |
| 2228 | + | ||
| 2266 | 2229 | log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes)) | |
| 2267 | 2230 | return script | |
| 2268 | 2231 | } | |
| @@ -2061,58 +2061,7 @@ func (w *WebView) enableUserContentManager(cfg *Config) { | |||||
| 2061 | C.webkit_user_script_unref(schemeScript) | 2061 | C.webkit_user_script_unref(schemeScript) | ||
| 2062 | } | 2062 | } | ||
| 2063 | 2063 | ||||
| 2064 | - | // Inject color palette tokens sourced from Go configuration | 2064 | + | // Color palette injection now handled in main-world.ts via __PALETTE_JSON__ placeholder |
| 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 | 2065 | ||||
| 2117 | // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls) | 2066 | // Add GUI bundle as user script at document-start (contains toast, omnibox, and controls) | ||
| 2118 | if cfg != nil && cfg.Assets != nil { | 2067 | if cfg != nil && cfg.Assets != nil { | ||
| @@ -2263,6 +2212,20 @@ func buildDomBridgeScript(cfg *Config, initialZoom float64, webViewId uintptr, w | |||||
| 2263 | log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState) | 2212 | log.Printf("[webkit] Building script with WebView %s, active state: %s", webViewIdStr, activeState) | ||
| 2264 | script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState) | 2213 | script = strings.ReplaceAll(script, "\"__WEBVIEW_ACTIVE__\"", activeState) | ||
| 2265 | 2214 | ||||
| 2215 | + | // Inject color palette JSON | |||
| 2216 | + | if cfg != nil { | |||
| 2217 | + | if paletteJSON, err := json.Marshal(cfg.Colors); err == nil && len(paletteJSON) > 0 { | |||
| 2218 | + | script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", string(paletteJSON)) | |||
| 2219 | + | } else { | |||
| 2220 | + | if err != nil { | |||
| 2221 | + | log.Printf("[webkit] Failed to marshal color palettes: %v", err) | |||
| 2222 | + | } | |||
| 2223 | + | script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", "null") | |||
| 2224 | + | } | |||
| 2225 | + | } else { | |||
| 2226 | + | script = strings.ReplaceAll(script, "\"__PALETTE_JSON__\"", "null") | |||
| 2227 | + | } | |||
| 2228 | + | ||||
| 2266 | log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes)) | 2229 | log.Printf("[webkit] Loaded main-world script successfully, size: %d bytes", len(scriptBytes)) | ||
| 2267 | return script | 2230 | return script | ||
| 2268 | } | 2231 | } | ||