refine config provider and full replacement flow
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import type {
|
||||
AppConfig,
|
||||
AppConfigUpdateRequest,
|
||||
BinaryCreateRequest,
|
||||
BinaryListResponse,
|
||||
BinaryUpdateRequest,
|
||||
@@ -115,12 +114,6 @@ export const cliApi = {
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
patchConfig(payload: AppConfigUpdateRequest): Promise<AppConfig> {
|
||||
return request<AppConfig>("/api/config/app", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
listBinaries(): Promise<BinaryListResponse> {
|
||||
return request<BinaryListResponse>("/api/config/binaries")
|
||||
},
|
||||
|
||||
@@ -4,21 +4,58 @@ import { cliEvents } from "./cli-events"
|
||||
|
||||
export type ConfigData = AppConfig
|
||||
|
||||
function isDeepEqual(a: unknown, b: unknown): boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
|
||||
try {
|
||||
return JSON.stringify(a) === JSON.stringify(b)
|
||||
} catch (error) {
|
||||
console.warn("Failed to compare config objects", error)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export class ServerStorage {
|
||||
private configChangeListeners: Set<() => void> = new Set()
|
||||
private configChangeListeners: Set<(config: ConfigData) => void> = new Set()
|
||||
private configCache: ConfigData | null = null
|
||||
private loadPromise: Promise<ConfigData> | null = null
|
||||
|
||||
constructor() {
|
||||
cliEvents.on("config.appChanged", () => this.notifyConfigChanged())
|
||||
cliEvents.on("config.appChanged", (event) => {
|
||||
if (event.type !== "config.appChanged") return
|
||||
this.setConfigCache(event.config)
|
||||
})
|
||||
}
|
||||
|
||||
async loadConfig(): Promise<ConfigData> {
|
||||
const config = await cliApi.fetchConfig()
|
||||
return config
|
||||
if (this.configCache) {
|
||||
return this.configCache
|
||||
}
|
||||
|
||||
if (!this.loadPromise) {
|
||||
this.loadPromise = cliApi
|
||||
.fetchConfig()
|
||||
.then((config) => {
|
||||
this.setConfigCache(config)
|
||||
return config
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadPromise = null
|
||||
})
|
||||
}
|
||||
|
||||
return this.loadPromise
|
||||
}
|
||||
|
||||
async saveConfig(config: ConfigData): Promise<void> {
|
||||
await cliApi.updateConfig(config)
|
||||
async updateConfig(next: ConfigData): Promise<ConfigData> {
|
||||
const nextConfig = await cliApi.updateConfig(next)
|
||||
this.setConfigCache(nextConfig)
|
||||
return nextConfig
|
||||
}
|
||||
|
||||
async loadInstanceData(instanceId: string): Promise<InstanceData> {
|
||||
@@ -33,14 +70,26 @@ export class ServerStorage {
|
||||
await cliApi.deleteInstanceData(instanceId)
|
||||
}
|
||||
|
||||
onConfigChanged(listener: () => void): () => void {
|
||||
onConfigChanged(listener: (config: ConfigData) => void): () => void {
|
||||
this.configChangeListeners.add(listener)
|
||||
if (this.configCache) {
|
||||
listener(this.configCache)
|
||||
}
|
||||
return () => this.configChangeListeners.delete(listener)
|
||||
}
|
||||
|
||||
private notifyConfigChanged() {
|
||||
private setConfigCache(config: ConfigData) {
|
||||
if (this.configCache && isDeepEqual(this.configCache, config)) {
|
||||
this.configCache = config
|
||||
return
|
||||
}
|
||||
this.configCache = config
|
||||
this.notifyConfigChanged(config)
|
||||
}
|
||||
|
||||
private notifyConfigChanged(config: ConfigData) {
|
||||
for (const listener of this.configChangeListeners) {
|
||||
listener()
|
||||
listener(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createContext, createSignal, useContext, onMount, createEffect, type JSX } from "solid-js"
|
||||
import { storage, type ConfigData } from "./storage"
|
||||
import { createContext, createEffect, createSignal, onMount, useContext, type JSX } from "solid-js"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
|
||||
interface ThemeContextValue {
|
||||
isDark: () => boolean
|
||||
@@ -20,64 +20,30 @@ function applyTheme(dark: boolean) {
|
||||
|
||||
export function ThemeProvider(props: { children: JSX.Element }) {
|
||||
const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
const [isDark, setIsDarkSignal] = createSignal(true) //systemPrefersDark.matches)
|
||||
let themePreference: "system" | "dark" | "light" = "dark"
|
||||
const { themePreference, setThemePreference } = useConfig()
|
||||
const [isDark, setIsDarkSignal] = createSignal(false)
|
||||
|
||||
applyTheme(true) //systemPrefersDark.matches)
|
||||
|
||||
async function loadTheme() {
|
||||
try {
|
||||
const config = await storage.loadConfig()
|
||||
const savedTheme = config.theme
|
||||
let themeDark: boolean
|
||||
|
||||
if (savedTheme === "system") {
|
||||
themePreference = "system"
|
||||
themeDark = systemPrefersDark.matches
|
||||
} else if (savedTheme === "dark") {
|
||||
themePreference = "dark"
|
||||
themeDark = true
|
||||
} else if (savedTheme === "light") {
|
||||
themePreference = "light"
|
||||
themeDark = false
|
||||
} else {
|
||||
themePreference = "dark"
|
||||
themeDark = true
|
||||
}
|
||||
|
||||
setIsDarkSignal(themeDark)
|
||||
applyTheme(themeDark)
|
||||
} catch (error) {
|
||||
console.warn("Failed to load theme from config:", error)
|
||||
themePreference = "dark"
|
||||
const themeDark = true
|
||||
setIsDarkSignal(themeDark)
|
||||
applyTheme(themeDark)
|
||||
const resolveDarkTheme = () => {
|
||||
const preference = themePreference()
|
||||
if (preference === "system") {
|
||||
return systemPrefersDark.matches
|
||||
}
|
||||
return preference === "dark"
|
||||
}
|
||||
|
||||
async function saveTheme(dark: boolean) {
|
||||
try {
|
||||
const config = await storage.loadConfig()
|
||||
const nextPreference = dark ? "dark" : "light"
|
||||
config.theme = nextPreference
|
||||
themePreference = nextPreference
|
||||
await storage.saveConfig(config)
|
||||
} catch (error) {
|
||||
console.warn("Failed to save theme to config:", error)
|
||||
}
|
||||
const applyResolvedTheme = () => {
|
||||
const dark = resolveDarkTheme()
|
||||
setIsDarkSignal(dark)
|
||||
applyTheme(dark)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
applyResolvedTheme()
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
loadTheme()
|
||||
|
||||
const unsubscribe = storage.onConfigChanged(() => {
|
||||
loadTheme()
|
||||
})
|
||||
|
||||
// Listen for system theme changes
|
||||
const handleSystemThemeChange = (event: MediaQueryListEvent) => {
|
||||
if (themePreference === "system") {
|
||||
if (themePreference() === "system") {
|
||||
setIsDarkSignal(event.matches)
|
||||
applyTheme(event.matches)
|
||||
}
|
||||
@@ -86,19 +52,12 @@ export function ThemeProvider(props: { children: JSX.Element }) {
|
||||
systemPrefersDark.addEventListener("change", handleSystemThemeChange)
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
systemPrefersDark.removeEventListener("change", handleSystemThemeChange)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
applyTheme(isDark())
|
||||
})
|
||||
|
||||
const setTheme = (dark: boolean) => {
|
||||
setIsDarkSignal(dark)
|
||||
applyTheme(dark)
|
||||
saveTheme(dark)
|
||||
setThemePreference(dark ? "dark" : "light")
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
|
||||
Reference in New Issue
Block a user