electron: create multiple windows + sync themes

This commit is contained in:
Brendan Allan
2026-03-12 14:28:29 +08:00
parent d8fbe0af01
commit 77ba2ac8da
9 changed files with 59 additions and 14 deletions
+2 -2
View File
@@ -32,7 +32,7 @@ import { initLogging } from "./logging"
import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu"
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
import { createLoadingWindow, createMainWindow, setDockIcon } from "./windows"
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
const initEmitter = new EventEmitter()
let initStep: InitStep = { phase: "server_waiting" }
@@ -156,7 +156,6 @@ async function initialize() {
const globals = {
updaterEnabled: UPDATER_ENABLED,
wsl: getWslConfig().enabled,
deepLinks: pendingDeepLinks,
}
@@ -231,6 +230,7 @@ registerIpcHandlers({
runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
checkUpdate: async () => checkUpdate(),
installUpdate: async () => installUpdate(),
setBackgroundColor: (color) => setBackgroundColor(color),
})
function killSidecar() {
@@ -24,6 +24,7 @@ type Deps = {
runUpdater: (alertOnFail: boolean) => Promise<void> | void
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
installUpdate: () => Promise<void> | void
setBackgroundColor: (color: string) => void
}
export function registerIpcHandlers(deps: Deps) {
@@ -53,6 +54,7 @@ export function registerIpcHandlers(deps: Deps) {
ipcMain.handle("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
ipcMain.handle("check-update", () => deps.checkUpdate())
ipcMain.handle("install-update", () => deps.installUpdate())
ipcMain.handle("set-background-color", (_event: IpcMainInvokeEvent, color: string) => deps.setBackgroundColor(color))
ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
const store = getStore(name)
const value = store.get(key)
@@ -1,6 +1,7 @@
import { BrowserWindow, Menu, shell } from "electron"
import { UPDATER_ENABLED } from "./constants"
import { createMainWindow } from "./windows"
type Deps = {
trigger: (id: string) => void
@@ -48,6 +49,11 @@ export function createMenu(deps: Deps) {
submenu: [
{ label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") },
{ label: "Open Project...", accelerator: "Cmd+O", click: () => deps.trigger("project.open") },
{
label: "New Window",
accelerator: "Cmd+Shift+N",
click: () => createMainWindow({ updaterEnabled: UPDATER_ENABLED }),
},
{ type: "separator" },
{ role: "close" },
],
+12 -2
View File
@@ -6,12 +6,21 @@ import type { TitlebarTheme } from "../preload/types"
type Globals = {
updaterEnabled: boolean
wsl: boolean
deepLinks?: string[]
}
const root = dirname(fileURLToPath(import.meta.url))
let backgroundColor: string | undefined
export function setBackgroundColor(color: string) {
backgroundColor = color
}
export function getBackgroundColor(): string | undefined {
return backgroundColor
}
function iconsDir() {
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
}
@@ -59,6 +68,7 @@ export function createMainWindow(globals: Globals) {
show: true,
title: "OpenCode",
icon: iconPath(),
backgroundColor,
...(process.platform === "darwin"
? {
titleBarStyle: "hidden" as const,
@@ -95,6 +105,7 @@ export function createLoadingWindow(globals: Globals) {
center: true,
show: true,
icon: iconPath(),
backgroundColor,
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
...(process.platform === "win32"
? {
@@ -131,7 +142,6 @@ function injectGlobals(win: BrowserWindow, globals: Globals) {
const deepLinks = globals.deepLinks ?? []
const data = {
updaterEnabled: globals.updaterEnabled,
wsl: globals.wsl,
deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
}
void win.webContents.executeJavaScript(
@@ -62,6 +62,7 @@ const api: ElectronAPI = {
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
checkUpdate: () => ipcRenderer.invoke("check-update"),
installUpdate: () => ipcRenderer.invoke("install-update"),
setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color),
}
contextBridge.exposeInMainWorld("api", api)
@@ -66,4 +66,5 @@ export type ElectronAPI = {
runUpdater: (alertOnFail: boolean) => Promise<void>
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
installUpdate: () => Promise<void>
setBackgroundColor: (color: string) => Promise<void>
}
@@ -10,9 +10,9 @@ import {
useCommand,
} from "@opencode-ai/app"
import type { AsyncStorage } from "@solid-primitives/storage"
import { MemoryRouter } from "@solidjs/router"
import { createResource, onCleanup, onMount, Show } from "solid-js"
import { render } from "solid-js/web"
import { MemoryRouter } from "@solidjs/router"
import pkg from "../../package.json"
import { initI18n, t } from "./i18n"
import { UPDATER_ENABLED } from "./updater"
@@ -226,7 +226,9 @@ const createPlatform = (): Platform => {
const image = await window.api.readClipboardImage().catch(() => null)
if (!image) return null
const blob = new Blob([image.buffer], { type: "image/png" })
return new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" })
return new File([blob], `pasted-image-${Date.now()}.png`, {
type: "image/png",
})
},
}
}
@@ -286,6 +288,21 @@ render(() => {
})
})
// function ThemeSync() {
// const theme = useTheme()
// createEffect(() => {
// theme.themeId()
// theme.mode()
// const bg = getComputedStyle(document.documentElement).getPropertyValue("--background-base").trim()
// if (bg) {
// void window.api.setBackgroundColor(bg)
// }
// })
// return null
// }
return (
<PlatformProvider value={platform}>
<AppBaseProviders>
@@ -298,6 +315,7 @@ render(() => {
router={MemoryRouter}
>
<Inner />
<ThemeSync />
</AppInterface>
)
}}
+3 -4
View File
@@ -1,12 +1,11 @@
import { Menu, MenuItem, PredefinedMenuItem, Submenu } from "@tauri-apps/api/menu"
import { openUrl } from "@tauri-apps/plugin-opener"
import { type as ostype } from "@tauri-apps/plugin-os"
import { relaunch } from "@tauri-apps/plugin-process"
import { openUrl } from "@tauri-apps/plugin-opener"
import { runUpdater, UPDATER_ENABLED } from "./updater"
import { commands } from "./bindings"
import { installCli } from "./cli"
import { initI18n, t } from "./i18n"
import { commands } from "./bindings"
import { runUpdater, UPDATER_ENABLED } from "./updater"
export async function createMenu(trigger: (id: string) => void) {
if (ostype() !== "macos") return
+12 -4
View File
@@ -1,9 +1,9 @@
import { onMount, onCleanup, createEffect } from "solid-js"
import { createEffect, onCleanup, onMount } from "solid-js"
import { createStore } from "solid-js/store"
import type { DesktopTheme } from "./types"
import { resolveThemeVariant, themeToCss } from "./resolve"
import { DEFAULT_THEMES } from "./default-themes"
import { createSimpleContext } from "../context/helper"
import { DEFAULT_THEMES } from "./default-themes"
import { resolveThemeVariant, themeToCss } from "./resolve"
import type { DesktopTheme } from "./types"
export type ColorScheme = "light" | "dark" | "system"
@@ -87,6 +87,14 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
previewScheme: null as ColorScheme | null,
})
window.addEventListener("storage", (e) => {
if (e.key === STORAGE_KEYS.THEME_ID && e.newValue) setStore("themeId", e.newValue)
if (e.key === STORAGE_KEYS.COLOR_SCHEME && e.newValue) {
setStore("colorScheme", e.newValue as ColorScheme)
setStore("mode", e.newValue === "system" ? getSystemMode() : (e.newValue as any))
}
})
onMount(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
const handler = () => {