desktop: multi-window support in electron (#17155)
parent
d9dd33aeeb
commit
84df96eaef
|
|
@ -159,7 +159,7 @@ const effectMinDuration =
|
||||||
<A, E, R>(e: Effect.Effect<A, E, R>) =>
|
<A, E, R>(e: Effect.Effect<A, E, R>) =>
|
||||||
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
|
Effect.all([e, Effect.sleep(duration)], { concurrency: "unbounded" }).pipe(Effect.map((v) => v[0]))
|
||||||
|
|
||||||
function ConnectionGate(props: ParentProps) {
|
function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
const checkServerHealth = useCheckServerHealth()
|
const checkServerHealth = useCheckServerHealth()
|
||||||
|
|
||||||
|
|
@ -168,21 +168,23 @@ function ConnectionGate(props: ParentProps) {
|
||||||
// performs repeated health check with a grace period for
|
// performs repeated health check with a grace period for
|
||||||
// non-http connections, otherwise fails instantly
|
// non-http connections, otherwise fails instantly
|
||||||
const [startupHealthCheck, healthCheckActions] = createResource(() =>
|
const [startupHealthCheck, healthCheckActions] = createResource(() =>
|
||||||
Effect.gen(function* () {
|
props.disableHealthCheck
|
||||||
if (!server.current) return true
|
? true
|
||||||
const { http, type } = server.current
|
: Effect.gen(function* () {
|
||||||
|
if (!server.current) return true
|
||||||
|
const { http, type } = server.current
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const res = yield* Effect.promise(() => checkServerHealth(http))
|
const res = yield* Effect.promise(() => checkServerHealth(http))
|
||||||
if (res.healthy) return true
|
if (res.healthy) return true
|
||||||
if (checkMode() === "background" || type === "http") return false
|
if (checkMode() === "background" || type === "http") return false
|
||||||
}
|
}
|
||||||
}).pipe(
|
}).pipe(
|
||||||
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
effectMinDuration(checkMode() === "blocking" ? "1.2 seconds" : 0),
|
||||||
Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
|
Effect.timeoutOrElse({ duration: "10 seconds", onTimeout: () => Effect.succeed(false) }),
|
||||||
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
Effect.ensuring(Effect.sync(() => setCheckMode("background"))),
|
||||||
Effect.runPromise,
|
Effect.runPromise,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -261,10 +263,11 @@ export function AppInterface(props: {
|
||||||
defaultServer: ServerConnection.Key
|
defaultServer: ServerConnection.Key
|
||||||
servers?: Array<ServerConnection.Any>
|
servers?: Array<ServerConnection.Any>
|
||||||
router?: Component<BaseRouterProps>
|
router?: Component<BaseRouterProps>
|
||||||
|
disableHealthCheck?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
||||||
<ConnectionGate>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<GlobalSDKProvider>
|
<GlobalSDKProvider>
|
||||||
<GlobalSyncProvider>
|
<GlobalSyncProvider>
|
||||||
<Dynamic
|
<Dynamic
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
// @refresh reload
|
// @refresh reload
|
||||||
|
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
|
||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import { AppBaseProviders, AppInterface } from "@/app"
|
import { AppBaseProviders, AppInterface } from "@/app"
|
||||||
import { type Platform, PlatformProvider } from "@/context/platform"
|
import { type Platform, PlatformProvider } from "@/context/platform"
|
||||||
|
|
@ -132,7 +131,11 @@ if (root instanceof HTMLElement) {
|
||||||
() => (
|
() => (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
<AppBaseProviders>
|
<AppBaseProviders>
|
||||||
<AppInterface defaultServer={ServerConnection.Key.make(getDefaultUrl())} servers={[server]} />
|
<AppInterface
|
||||||
|
defaultServer={ServerConnection.Key.make(getDefaultUrl())}
|
||||||
|
servers={[server]}
|
||||||
|
disableHealthCheck
|
||||||
|
/>
|
||||||
</AppBaseProviders>
|
</AppBaseProviders>
|
||||||
</PlatformProvider>
|
</PlatformProvider>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { createServer } from "node:net"
|
||||||
import { homedir } from "node:os"
|
import { homedir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import type { Event } from "electron"
|
import type { Event } from "electron"
|
||||||
import { app, type BrowserWindow, dialog } from "electron"
|
import { app, BrowserWindow, dialog } from "electron"
|
||||||
import pkg from "electron-updater"
|
import pkg from "electron-updater"
|
||||||
|
|
||||||
const APP_NAMES: Record<string, string> = {
|
const APP_NAMES: Record<string, string> = {
|
||||||
|
|
@ -32,7 +32,7 @@ import { initLogging } from "./logging"
|
||||||
import { parseMarkdown } from "./markdown"
|
import { parseMarkdown } from "./markdown"
|
||||||
import { createMenu } from "./menu"
|
import { createMenu } from "./menu"
|
||||||
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
|
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()
|
const initEmitter = new EventEmitter()
|
||||||
let initStep: InitStep = { phase: "server_waiting" }
|
let initStep: InitStep = { phase: "server_waiting" }
|
||||||
|
|
@ -156,12 +156,9 @@ async function initialize() {
|
||||||
|
|
||||||
const globals = {
|
const globals = {
|
||||||
updaterEnabled: UPDATER_ENABLED,
|
updaterEnabled: UPDATER_ENABLED,
|
||||||
wsl: getWslConfig().enabled,
|
|
||||||
deepLinks: pendingDeepLinks,
|
deepLinks: pendingDeepLinks,
|
||||||
}
|
}
|
||||||
|
|
||||||
wireMenu()
|
|
||||||
|
|
||||||
if (needsMigration) {
|
if (needsMigration) {
|
||||||
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
|
const show = await Promise.race([loadingTask.then(() => false), delay(1_000).then(() => true)])
|
||||||
if (show) {
|
if (show) {
|
||||||
|
|
@ -178,6 +175,7 @@ async function initialize() {
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow = createMainWindow(globals)
|
mainWindow = createMainWindow(globals)
|
||||||
|
wireMenu()
|
||||||
|
|
||||||
overlay?.close()
|
overlay?.close()
|
||||||
}
|
}
|
||||||
|
|
@ -231,6 +229,7 @@ registerIpcHandlers({
|
||||||
runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
|
runUpdater: async (alertOnFail) => checkForUpdates(alertOnFail),
|
||||||
checkUpdate: async () => checkUpdate(),
|
checkUpdate: async () => checkUpdate(),
|
||||||
installUpdate: async () => installUpdate(),
|
installUpdate: async () => installUpdate(),
|
||||||
|
setBackgroundColor: (color) => setBackgroundColor(color),
|
||||||
})
|
})
|
||||||
|
|
||||||
function killSidecar() {
|
function killSidecar() {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ type Deps = {
|
||||||
runUpdater: (alertOnFail: boolean) => Promise<void> | void
|
runUpdater: (alertOnFail: boolean) => Promise<void> | void
|
||||||
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
||||||
installUpdate: () => Promise<void> | void
|
installUpdate: () => Promise<void> | void
|
||||||
|
setBackgroundColor: (color: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerIpcHandlers(deps: Deps) {
|
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("run-updater", (_event: IpcMainInvokeEvent, alertOnFail: boolean) => deps.runUpdater(alertOnFail))
|
||||||
ipcMain.handle("check-update", () => deps.checkUpdate())
|
ipcMain.handle("check-update", () => deps.checkUpdate())
|
||||||
ipcMain.handle("install-update", () => deps.installUpdate())
|
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) => {
|
ipcMain.handle("store-get", (_event: IpcMainInvokeEvent, name: string, key: string) => {
|
||||||
const store = getStore(name)
|
const store = getStore(name)
|
||||||
const value = store.get(key)
|
const value = store.get(key)
|
||||||
|
|
@ -140,6 +142,8 @@ export function registerIpcHandlers(deps: Deps) {
|
||||||
new Notification({ title, body }).show()
|
new Notification({ title, body }).show()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle("get-window-count", () => BrowserWindow.getAllWindows().length)
|
||||||
|
|
||||||
ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
|
ipcMain.handle("get-window-focused", (event: IpcMainInvokeEvent) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender)
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
return win?.isFocused() ?? false
|
return win?.isFocused() ?? false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { BrowserWindow, Menu, shell } from "electron"
|
import { BrowserWindow, Menu, shell } from "electron"
|
||||||
|
|
||||||
import { UPDATER_ENABLED } from "./constants"
|
import { UPDATER_ENABLED } from "./constants"
|
||||||
|
import { createMainWindow } from "./windows"
|
||||||
|
|
||||||
type Deps = {
|
type Deps = {
|
||||||
trigger: (id: string) => void
|
trigger: (id: string) => void
|
||||||
|
|
@ -48,6 +49,11 @@ export function createMenu(deps: Deps) {
|
||||||
submenu: [
|
submenu: [
|
||||||
{ label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") },
|
{ label: "New Session", accelerator: "Shift+Cmd+S", click: () => deps.trigger("session.new") },
|
||||||
{ label: "Open Project...", accelerator: "Cmd+O", click: () => deps.trigger("project.open") },
|
{ 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" },
|
{ type: "separator" },
|
||||||
{ role: "close" },
|
{ role: "close" },
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,21 @@ import type { TitlebarTheme } from "../preload/types"
|
||||||
|
|
||||||
type Globals = {
|
type Globals = {
|
||||||
updaterEnabled: boolean
|
updaterEnabled: boolean
|
||||||
wsl: boolean
|
|
||||||
deepLinks?: string[]
|
deepLinks?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = dirname(fileURLToPath(import.meta.url))
|
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() {
|
function iconsDir() {
|
||||||
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
|
return app.isPackaged ? join(process.resourcesPath, "icons") : join(root, "../../resources/icons")
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +68,7 @@ export function createMainWindow(globals: Globals) {
|
||||||
show: true,
|
show: true,
|
||||||
title: "OpenCode",
|
title: "OpenCode",
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
|
backgroundColor,
|
||||||
...(process.platform === "darwin"
|
...(process.platform === "darwin"
|
||||||
? {
|
? {
|
||||||
titleBarStyle: "hidden" as const,
|
titleBarStyle: "hidden" as const,
|
||||||
|
|
@ -95,6 +105,7 @@ export function createLoadingWindow(globals: Globals) {
|
||||||
center: true,
|
center: true,
|
||||||
show: true,
|
show: true,
|
||||||
icon: iconPath(),
|
icon: iconPath(),
|
||||||
|
backgroundColor,
|
||||||
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
|
...(process.platform === "darwin" ? { titleBarStyle: "hidden" as const } : {}),
|
||||||
...(process.platform === "win32"
|
...(process.platform === "win32"
|
||||||
? {
|
? {
|
||||||
|
|
@ -131,7 +142,6 @@ function injectGlobals(win: BrowserWindow, globals: Globals) {
|
||||||
const deepLinks = globals.deepLinks ?? []
|
const deepLinks = globals.deepLinks ?? []
|
||||||
const data = {
|
const data = {
|
||||||
updaterEnabled: globals.updaterEnabled,
|
updaterEnabled: globals.updaterEnabled,
|
||||||
wsl: globals.wsl,
|
|
||||||
deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
|
deepLinks: Array.isArray(deepLinks) ? deepLinks.splice(0) : deepLinks,
|
||||||
}
|
}
|
||||||
void win.webContents.executeJavaScript(
|
void win.webContents.executeJavaScript(
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const api: ElectronAPI = {
|
||||||
storeKeys: (name) => ipcRenderer.invoke("store-keys", name),
|
storeKeys: (name) => ipcRenderer.invoke("store-keys", name),
|
||||||
storeLength: (name) => ipcRenderer.invoke("store-length", name),
|
storeLength: (name) => ipcRenderer.invoke("store-length", name),
|
||||||
|
|
||||||
|
getWindowCount: () => ipcRenderer.invoke("get-window-count"),
|
||||||
onSqliteMigrationProgress: (cb) => {
|
onSqliteMigrationProgress: (cb) => {
|
||||||
const handler = (_: unknown, progress: SqliteMigrationProgress) => cb(progress)
|
const handler = (_: unknown, progress: SqliteMigrationProgress) => cb(progress)
|
||||||
ipcRenderer.on("sqlite-migration-progress", handler)
|
ipcRenderer.on("sqlite-migration-progress", handler)
|
||||||
|
|
@ -62,6 +63,7 @@ const api: ElectronAPI = {
|
||||||
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
|
runUpdater: (alertOnFail) => ipcRenderer.invoke("run-updater", alertOnFail),
|
||||||
checkUpdate: () => ipcRenderer.invoke("check-update"),
|
checkUpdate: () => ipcRenderer.invoke("check-update"),
|
||||||
installUpdate: () => ipcRenderer.invoke("install-update"),
|
installUpdate: () => ipcRenderer.invoke("install-update"),
|
||||||
|
setBackgroundColor: (color: string) => ipcRenderer.invoke("set-background-color", color),
|
||||||
}
|
}
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("api", api)
|
contextBridge.exposeInMainWorld("api", api)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export type ElectronAPI = {
|
||||||
storeKeys: (name: string) => Promise<string[]>
|
storeKeys: (name: string) => Promise<string[]>
|
||||||
storeLength: (name: string) => Promise<number>
|
storeLength: (name: string) => Promise<number>
|
||||||
|
|
||||||
|
getWindowCount: () => Promise<number>
|
||||||
onSqliteMigrationProgress: (cb: (progress: SqliteMigrationProgress) => void) => () => void
|
onSqliteMigrationProgress: (cb: (progress: SqliteMigrationProgress) => void) => () => void
|
||||||
onMenuCommand: (cb: (id: string) => void) => () => void
|
onMenuCommand: (cb: (id: string) => void) => () => void
|
||||||
onDeepLink: (cb: (urls: string[]) => void) => () => void
|
onDeepLink: (cb: (urls: string[]) => void) => () => void
|
||||||
|
|
@ -66,4 +67,5 @@ export type ElectronAPI = {
|
||||||
runUpdater: (alertOnFail: boolean) => Promise<void>
|
runUpdater: (alertOnFail: boolean) => Promise<void>
|
||||||
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
checkUpdate: () => Promise<{ updateAvailable: boolean; version?: string }>
|
||||||
installUpdate: () => Promise<void>
|
installUpdate: () => Promise<void>
|
||||||
|
setBackgroundColor: (color: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,15 @@ import {
|
||||||
useCommand,
|
useCommand,
|
||||||
} from "@opencode-ai/app"
|
} from "@opencode-ai/app"
|
||||||
import type { AsyncStorage } from "@solid-primitives/storage"
|
import type { AsyncStorage } from "@solid-primitives/storage"
|
||||||
import { createResource, onCleanup, onMount, Show } from "solid-js"
|
|
||||||
import { render } from "solid-js/web"
|
|
||||||
import { MemoryRouter } from "@solidjs/router"
|
import { MemoryRouter } from "@solidjs/router"
|
||||||
|
import { createEffect, createResource, onCleanup, onMount, Show } from "solid-js"
|
||||||
|
import { render } from "solid-js/web"
|
||||||
import pkg from "../../package.json"
|
import pkg from "../../package.json"
|
||||||
import { initI18n, t } from "./i18n"
|
import { initI18n, t } from "./i18n"
|
||||||
import { UPDATER_ENABLED } from "./updater"
|
import { UPDATER_ENABLED } from "./updater"
|
||||||
import { webviewZoom } from "./webview-zoom"
|
import { webviewZoom } from "./webview-zoom"
|
||||||
import "./styles.css"
|
import "./styles.css"
|
||||||
|
import { useTheme } from "@opencode-ai/ui/theme"
|
||||||
|
|
||||||
const root = document.getElementById("root")
|
const root = document.getElementById("root")
|
||||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||||
|
|
@ -226,7 +227,9 @@ const createPlatform = (): Platform => {
|
||||||
const image = await window.api.readClipboardImage().catch(() => null)
|
const image = await window.api.readClipboardImage().catch(() => null)
|
||||||
if (!image) return null
|
if (!image) return null
|
||||||
const blob = new Blob([image.buffer], { type: "image/png" })
|
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",
|
||||||
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -240,6 +243,8 @@ listenForDeepLinks()
|
||||||
render(() => {
|
render(() => {
|
||||||
const platform = createPlatform()
|
const platform = createPlatform()
|
||||||
|
|
||||||
|
const [windowCount] = createResource(() => window.api.getWindowCount())
|
||||||
|
|
||||||
// Fetch sidecar credentials (available immediately, before health check)
|
// Fetch sidecar credentials (available immediately, before health check)
|
||||||
const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined))
|
const [sidecar] = createResource(() => window.api.awaitInitialization(() => undefined))
|
||||||
|
|
||||||
|
|
@ -276,6 +281,18 @@ render(() => {
|
||||||
function Inner() {
|
function Inner() {
|
||||||
const cmd = useCommand()
|
const cmd = useCommand()
|
||||||
menuTrigger = (id) => cmd.trigger(id)
|
menuTrigger = (id) => cmd.trigger(id)
|
||||||
|
|
||||||
|
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 null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,13 +306,14 @@ render(() => {
|
||||||
return (
|
return (
|
||||||
<PlatformProvider value={platform}>
|
<PlatformProvider value={platform}>
|
||||||
<AppBaseProviders>
|
<AppBaseProviders>
|
||||||
<Show when={!defaultServer.loading && !sidecar.loading}>
|
<Show when={!defaultServer.loading && !sidecar.loading && !windowCount.loading}>
|
||||||
{(_) => {
|
{(_) => {
|
||||||
return (
|
return (
|
||||||
<AppInterface
|
<AppInterface
|
||||||
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
|
defaultServer={defaultServer.latest ?? ServerConnection.Key.make("sidecar")}
|
||||||
servers={servers()}
|
servers={servers()}
|
||||||
router={MemoryRouter}
|
router={MemoryRouter}
|
||||||
|
disableHealthCheck={(windowCount() ?? 0) > 1}
|
||||||
>
|
>
|
||||||
<Inner />
|
<Inner />
|
||||||
</AppInterface>
|
</AppInterface>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { Menu, MenuItem, PredefinedMenuItem, Submenu } from "@tauri-apps/api/menu"
|
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 { type as ostype } from "@tauri-apps/plugin-os"
|
||||||
import { relaunch } from "@tauri-apps/plugin-process"
|
import { relaunch } from "@tauri-apps/plugin-process"
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener"
|
import { commands } from "./bindings"
|
||||||
|
|
||||||
import { runUpdater, UPDATER_ENABLED } from "./updater"
|
|
||||||
import { installCli } from "./cli"
|
import { installCli } from "./cli"
|
||||||
import { initI18n, t } from "./i18n"
|
import { initI18n, t } from "./i18n"
|
||||||
import { commands } from "./bindings"
|
import { runUpdater, UPDATER_ENABLED } from "./updater"
|
||||||
|
|
||||||
export async function createMenu(trigger: (id: string) => void) {
|
export async function createMenu(trigger: (id: string) => void) {
|
||||||
if (ostype() !== "macos") return
|
if (ostype() !== "macos") return
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 { 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"
|
export type ColorScheme = "light" | "dark" | "system"
|
||||||
|
|
||||||
|
|
@ -87,6 +87,14 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||||
previewScheme: null as ColorScheme | null,
|
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(() => {
|
onMount(() => {
|
||||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue