diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 5b54e205..9fee4f7d 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -12,7 +12,8 @@ export default defineConfig({ renderer: { resolve: { alias: { - '@renderer': resolve('src/renderer/src') + '@renderer': resolve('src/renderer/src'), + '@common': resolve('src/common') } }, plugins: [react()] diff --git a/src/common/constants.ts b/src/common/constants.ts new file mode 100644 index 00000000..fe199054 --- /dev/null +++ b/src/common/constants.ts @@ -0,0 +1,4 @@ +export const StorageKey = { + THEME_SETTING: 'themeSetting', + RELAY_GROUPS: 'relayGroups' +} diff --git a/src/common/types.ts b/src/common/types.ts index ba274908..1521de36 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -24,14 +24,13 @@ export type TElectronWindow = { isEncryptionAvailable: () => Promise } theme: { - onChange: (cb: (theme: TTheme) => void) => void + addChangeListener: (listener: (theme: TTheme) => void) => void + removeChangeListener: () => void current: () => Promise - themeSetting: () => Promise - set: (themeSetting: TThemeSetting) => Promise } storage: { - getRelayGroups: () => Promise - setRelayGroups: (relayGroups: TRelayGroup[]) => Promise + getItem: (key: string) => Promise + setItem: (key: string, value: string) => Promise } nostr: { login: (nsec: string) => Promise<{ diff --git a/src/main/index.ts b/src/main/index.ts index 600e66ad..c203c8fa 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -71,7 +71,7 @@ app.whenReady().then(async () => { const storageService = new StorageService() storageService.init() - const themeService = new ThemeService(storageService, sendToRenderer) + const themeService = new ThemeService(sendToRenderer) themeService.init() const nostrService = new NostrService() diff --git a/src/main/services/storage.service.ts b/src/main/services/storage.service.ts index cf8b105c..cc9dd2ee 100644 --- a/src/main/services/storage.service.ts +++ b/src/main/services/storage.service.ts @@ -1,42 +1,10 @@ -import { TConfig, TRelayGroup, TThemeSetting } from '@common/types' import { app, ipcMain } from 'electron' import { existsSync, readFileSync, writeFileSync } from 'fs' import path from 'path' export class StorageService { - private storage: Storage - - constructor() { - this.storage = new Storage() - } - - init() { - ipcMain.handle('storage:getRelayGroups', () => this.getRelayGroups()) - ipcMain.handle('storage:setRelayGroups', (_, relayGroups: TRelayGroup[]) => - this.setRelayGroups(relayGroups) - ) - } - - getRelayGroups(): TRelayGroup[] | null { - return this.storage.get('relayGroups') ?? null - } - - setRelayGroups(relayGroups: TRelayGroup[]) { - this.storage.set('relayGroups', relayGroups) - } - - getTheme() { - return this.storage.get('theme') ?? 'system' - } - - setTheme(theme: TThemeSetting) { - this.storage.set('theme', theme) - } -} - -class Storage { private path: string - private config: TConfig + private config: Record = {} private writeTimer: NodeJS.Timeout | null = null constructor() { @@ -46,11 +14,20 @@ class Storage { this.config = JSON.parse(json) } - get(key: K): V | undefined { - return this.config[key] as V + init() { + ipcMain.handle('storage:getItem', (_, key: string) => this.getItem(key)) + ipcMain.handle('storage:setItem', (_, key: string, value: string) => this.setItem(key, value)) } - set(key: K, value: TConfig[K]) { + getItem(key: string): string | undefined { + const value = this.config[key] + // backward compatibility + if (value && typeof value !== 'string') return JSON.stringify(value) + + return value + } + + setItem(key: string, value: string) { this.config[key] = value if (this.writeTimer) return diff --git a/src/main/services/theme.service.ts b/src/main/services/theme.service.ts index c4eea421..92310812 100644 --- a/src/main/services/theme.service.ts +++ b/src/main/services/theme.service.ts @@ -1,40 +1,18 @@ -import { TThemeSetting } from '@common/types' import { ipcMain, nativeTheme } from 'electron' import { TSendToRenderer } from '../types' -import { StorageService } from './storage.service' export class ThemeService { - private themeSetting: TThemeSetting = 'system' - - constructor( - private storageService: StorageService, - private sendToRenderer: TSendToRenderer - ) {} + constructor(private sendToRenderer: TSendToRenderer) {} init() { - this.themeSetting = this.storageService.getTheme() - ipcMain.handle('theme:current', () => this.getCurrentTheme()) - ipcMain.handle('theme:themeSetting', () => this.themeSetting) - ipcMain.handle('theme:set', (_, theme: TThemeSetting) => this.setTheme(theme)) nativeTheme.on('updated', () => { - if (this.themeSetting === 'system') { - this.sendCurrentThemeToRenderer() - } + this.sendCurrentThemeToRenderer() }) } getCurrentTheme() { - if (this.themeSetting === 'system') { - return nativeTheme.shouldUseDarkColors ? 'dark' : 'light' - } - return this.themeSetting - } - - private setTheme(theme: TThemeSetting) { - this.themeSetting = theme - this.storageService.setTheme(theme) - this.sendCurrentThemeToRenderer() + return nativeTheme.shouldUseDarkColors ? 'dark' : 'light' } private sendCurrentThemeToRenderer() { diff --git a/src/preload/index.ts b/src/preload/index.ts index 86941b68..70d4646c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,4 +1,4 @@ -import { TDraftEvent, TRelayGroup, TThemeSetting } from '@common/types' +import { TDraftEvent, TTheme } from '@common/types' import { electronAPI } from '@electron-toolkit/preload' import { contextBridge, ipcRenderer } from 'electron' @@ -8,19 +8,19 @@ const api = { isEncryptionAvailable: () => ipcRenderer.invoke('system:isEncryptionAvailable') }, theme: { - onChange: (cb: (theme: 'dark' | 'light') => void) => { + addChangeListener: (listener: (theme: TTheme) => void) => { ipcRenderer.on('theme:change', (_, theme) => { - cb(theme) + listener(theme) }) }, - current: () => ipcRenderer.invoke('theme:current'), - themeSetting: () => ipcRenderer.invoke('theme:themeSetting'), - set: (themeSetting: TThemeSetting) => ipcRenderer.invoke('theme:set', themeSetting) + removeChangeListener: () => { + ipcRenderer.removeAllListeners('theme:change') + }, + current: () => ipcRenderer.invoke('theme:current') }, storage: { - getRelayGroups: () => ipcRenderer.invoke('storage:getRelayGroups'), - setRelayGroups: (relayGroups: TRelayGroup[]) => - ipcRenderer.invoke('storage:setRelayGroups', relayGroups) + getItem: (key: string) => ipcRenderer.invoke('storage:getItem', key), + setItem: (key: string, value: string) => ipcRenderer.invoke('storage:setItem', key, value) }, nostr: { login: (nsec: string) => ipcRenderer.invoke('nostr:login', nsec), diff --git a/src/renderer/src/providers/ThemeProvider.tsx b/src/renderer/src/providers/ThemeProvider.tsx index ff76c8b0..4e2a53b7 100644 --- a/src/renderer/src/providers/ThemeProvider.tsx +++ b/src/renderer/src/providers/ThemeProvider.tsx @@ -1,5 +1,6 @@ import { TTheme, TThemeSetting } from '@common/types' import { isElectron } from '@renderer/lib/env' +import storage from '@renderer/services/storage.service' import { createContext, useContext, useEffect, useState } from 'react' type ThemeProviderProps = { @@ -12,8 +13,10 @@ type ThemeProviderState = { setThemeSetting: (themeSetting: TThemeSetting) => Promise } -// web only -function getSystemTheme() { +async function getSystemTheme() { + if (isElectron(window)) { + return await window.api.theme.current() + } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' } @@ -27,33 +30,28 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) { useEffect(() => { const init = async () => { - // electron - if (isElectron(window)) { - const [themeSetting, theme] = await Promise.all([ - window.api.theme.themeSetting(), - window.api.theme.current() - ]) - setTheme(theme) - setThemeSetting(themeSetting) - - window.api.theme.onChange((theme) => { - setTheme(theme) - }) - } else { - // web - if (themeSetting === 'system') { - setTheme(getSystemTheme()) - return - } - setTheme(themeSetting) + const themeSetting = await storage.getThemeSetting() + if (themeSetting === 'system') { + setTheme(await getSystemTheme()) + return } + setTheme(themeSetting) } init() }, []) useEffect(() => { - if (themeSetting !== 'system' || isElectron(window)) return + if (themeSetting !== 'system') return + + if (isElectron(window)) { + window.api.theme.addChangeListener((theme) => { + setTheme(theme) + }) + return () => { + isElectron(window) && window.api.theme.removeChangeListener() + } + } const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') const handleChange = (e: MediaQueryListEvent) => { @@ -80,14 +78,10 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) { const value = { themeSetting: themeSetting, setThemeSetting: async (themeSetting: TThemeSetting) => { - if (isElectron(window)) { - await window.api.theme.set(themeSetting) - } else { - localStorage.setItem('themeSetting', themeSetting) - } + await storage.setThemeSetting(themeSetting) setThemeSetting(themeSetting) if (themeSetting === 'system') { - setTheme(getSystemTheme()) + setTheme(await getSystemTheme()) return } setTheme(themeSetting) diff --git a/src/renderer/src/services/storage.service.ts b/src/renderer/src/services/storage.service.ts index f10c6250..1bb831ce 100644 --- a/src/renderer/src/services/storage.service.ts +++ b/src/renderer/src/services/storage.service.ts @@ -1,4 +1,5 @@ -import { TRelayGroup } from '@common/types' +import { StorageKey } from '@common/constants' +import { TRelayGroup, TThemeSetting } from '@common/types' import { isElectron } from '@renderer/lib/env' const DEFAULT_RELAY_GROUPS: TRelayGroup[] = [ @@ -15,21 +16,19 @@ const DEFAULT_RELAY_GROUPS: TRelayGroup[] = [ ] class Storage { - async getRelayGroups() { + async getItem(key: string) { if (isElectron(window)) { - const relayGroups = await window.api.storage.getRelayGroups() - return relayGroups ?? DEFAULT_RELAY_GROUPS + return window.api.storage.getItem(key) } else { - const relayGroupsStr = localStorage.getItem('relayGroups') - return relayGroupsStr ? (JSON.parse(relayGroupsStr) as TRelayGroup[]) : DEFAULT_RELAY_GROUPS + return localStorage.getItem(key) } } - async setRelayGroups(relayGroups: TRelayGroup[]) { + async setItem(key: string, value: string) { if (isElectron(window)) { - return window.api.storage.setRelayGroups(relayGroups) + return window.api.storage.setItem(key, value) } else { - localStorage.setItem('relayGroups', JSON.stringify(relayGroups)) + return localStorage.setItem(key, value) } } } @@ -39,6 +38,7 @@ class StorageService { private initPromise!: Promise private relayGroups: TRelayGroup[] = [] + private themeSetting: TThemeSetting = 'system' private storage: Storage = new Storage() constructor() { @@ -50,7 +50,10 @@ class StorageService { } async init() { - this.relayGroups = await this.storage.getRelayGroups() + const relayGroupsStr = await this.storage.getItem(StorageKey.RELAY_GROUPS) + this.relayGroups = relayGroupsStr ? JSON.parse(relayGroupsStr) : DEFAULT_RELAY_GROUPS + this.themeSetting = + ((await this.storage.getItem(StorageKey.THEME_SETTING)) as TThemeSetting) ?? 'system' } async getRelayGroups() { @@ -60,9 +63,20 @@ class StorageService { async setRelayGroups(relayGroups: TRelayGroup[]) { await this.initPromise - await this.storage.setRelayGroups(relayGroups) + await this.storage.setItem(StorageKey.RELAY_GROUPS, JSON.stringify(relayGroups)) this.relayGroups = relayGroups } + + async getThemeSetting() { + await this.initPromise + return this.themeSetting + } + + async setThemeSetting(themeSetting: TThemeSetting) { + await this.initPromise + await this.storage.setItem(StorageKey.THEME_SETTING, themeSetting) + this.themeSetting = themeSetting + } } const instance = new StorageService()