refactor: storage

This commit is contained in:
codytseng
2024-11-24 17:43:07 +08:00
parent 6d4bb00f8b
commit 3aa383ad32
9 changed files with 83 additions and 116 deletions

View File

@@ -12,7 +12,8 @@ export default defineConfig({
renderer: { renderer: {
resolve: { resolve: {
alias: { alias: {
'@renderer': resolve('src/renderer/src') '@renderer': resolve('src/renderer/src'),
'@common': resolve('src/common')
} }
}, },
plugins: [react()] plugins: [react()]

4
src/common/constants.ts Normal file
View File

@@ -0,0 +1,4 @@
export const StorageKey = {
THEME_SETTING: 'themeSetting',
RELAY_GROUPS: 'relayGroups'
}

View File

@@ -24,14 +24,13 @@ export type TElectronWindow = {
isEncryptionAvailable: () => Promise<boolean> isEncryptionAvailable: () => Promise<boolean>
} }
theme: { theme: {
onChange: (cb: (theme: TTheme) => void) => void addChangeListener: (listener: (theme: TTheme) => void) => void
removeChangeListener: () => void
current: () => Promise<TTheme> current: () => Promise<TTheme>
themeSetting: () => Promise<TThemeSetting>
set: (themeSetting: TThemeSetting) => Promise<void>
} }
storage: { storage: {
getRelayGroups: () => Promise<TRelayGroup[]> getItem: (key: string) => Promise<string>
setRelayGroups: (relayGroups: TRelayGroup[]) => Promise<void> setItem: (key: string, value: string) => Promise<void>
} }
nostr: { nostr: {
login: (nsec: string) => Promise<{ login: (nsec: string) => Promise<{

View File

@@ -71,7 +71,7 @@ app.whenReady().then(async () => {
const storageService = new StorageService() const storageService = new StorageService()
storageService.init() storageService.init()
const themeService = new ThemeService(storageService, sendToRenderer) const themeService = new ThemeService(sendToRenderer)
themeService.init() themeService.init()
const nostrService = new NostrService() const nostrService = new NostrService()

View File

@@ -1,42 +1,10 @@
import { TConfig, TRelayGroup, TThemeSetting } from '@common/types'
import { app, ipcMain } from 'electron' import { app, ipcMain } from 'electron'
import { existsSync, readFileSync, writeFileSync } from 'fs' import { existsSync, readFileSync, writeFileSync } from 'fs'
import path from 'path' import path from 'path'
export class StorageService { 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 path: string
private config: TConfig private config: Record<string, string> = {}
private writeTimer: NodeJS.Timeout | null = null private writeTimer: NodeJS.Timeout | null = null
constructor() { constructor() {
@@ -46,11 +14,20 @@ class Storage {
this.config = JSON.parse(json) this.config = JSON.parse(json)
} }
get<K extends keyof TConfig, V extends TConfig[K]>(key: K): V | undefined { init() {
return this.config[key] as V ipcMain.handle('storage:getItem', (_, key: string) => this.getItem(key))
ipcMain.handle('storage:setItem', (_, key: string, value: string) => this.setItem(key, value))
} }
set<K extends keyof TConfig>(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 this.config[key] = value
if (this.writeTimer) return if (this.writeTimer) return

View File

@@ -1,41 +1,19 @@
import { TThemeSetting } from '@common/types'
import { ipcMain, nativeTheme } from 'electron' import { ipcMain, nativeTheme } from 'electron'
import { TSendToRenderer } from '../types' import { TSendToRenderer } from '../types'
import { StorageService } from './storage.service'
export class ThemeService { export class ThemeService {
private themeSetting: TThemeSetting = 'system' constructor(private sendToRenderer: TSendToRenderer) {}
constructor(
private storageService: StorageService,
private sendToRenderer: TSendToRenderer
) {}
init() { init() {
this.themeSetting = this.storageService.getTheme()
ipcMain.handle('theme:current', () => this.getCurrentTheme()) ipcMain.handle('theme:current', () => this.getCurrentTheme())
ipcMain.handle('theme:themeSetting', () => this.themeSetting)
ipcMain.handle('theme:set', (_, theme: TThemeSetting) => this.setTheme(theme))
nativeTheme.on('updated', () => { nativeTheme.on('updated', () => {
if (this.themeSetting === 'system') {
this.sendCurrentThemeToRenderer() this.sendCurrentThemeToRenderer()
}
}) })
} }
getCurrentTheme() { getCurrentTheme() {
if (this.themeSetting === 'system') {
return nativeTheme.shouldUseDarkColors ? 'dark' : 'light' return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'
} }
return this.themeSetting
}
private setTheme(theme: TThemeSetting) {
this.themeSetting = theme
this.storageService.setTheme(theme)
this.sendCurrentThemeToRenderer()
}
private sendCurrentThemeToRenderer() { private sendCurrentThemeToRenderer() {
this.sendToRenderer('theme:change', this.getCurrentTheme()) this.sendToRenderer('theme:change', this.getCurrentTheme())

View File

@@ -1,4 +1,4 @@
import { TDraftEvent, TRelayGroup, TThemeSetting } from '@common/types' import { TDraftEvent, TTheme } from '@common/types'
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge, ipcRenderer } from 'electron' import { contextBridge, ipcRenderer } from 'electron'
@@ -8,19 +8,19 @@ const api = {
isEncryptionAvailable: () => ipcRenderer.invoke('system:isEncryptionAvailable') isEncryptionAvailable: () => ipcRenderer.invoke('system:isEncryptionAvailable')
}, },
theme: { theme: {
onChange: (cb: (theme: 'dark' | 'light') => void) => { addChangeListener: (listener: (theme: TTheme) => void) => {
ipcRenderer.on('theme:change', (_, theme) => { ipcRenderer.on('theme:change', (_, theme) => {
cb(theme) listener(theme)
}) })
}, },
current: () => ipcRenderer.invoke('theme:current'), removeChangeListener: () => {
themeSetting: () => ipcRenderer.invoke('theme:themeSetting'), ipcRenderer.removeAllListeners('theme:change')
set: (themeSetting: TThemeSetting) => ipcRenderer.invoke('theme:set', themeSetting) },
current: () => ipcRenderer.invoke('theme:current')
}, },
storage: { storage: {
getRelayGroups: () => ipcRenderer.invoke('storage:getRelayGroups'), getItem: (key: string) => ipcRenderer.invoke('storage:getItem', key),
setRelayGroups: (relayGroups: TRelayGroup[]) => setItem: (key: string, value: string) => ipcRenderer.invoke('storage:setItem', key, value)
ipcRenderer.invoke('storage:setRelayGroups', relayGroups)
}, },
nostr: { nostr: {
login: (nsec: string) => ipcRenderer.invoke('nostr:login', nsec), login: (nsec: string) => ipcRenderer.invoke('nostr:login', nsec),

View File

@@ -1,5 +1,6 @@
import { TTheme, TThemeSetting } from '@common/types' import { TTheme, TThemeSetting } from '@common/types'
import { isElectron } from '@renderer/lib/env' import { isElectron } from '@renderer/lib/env'
import storage from '@renderer/services/storage.service'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
type ThemeProviderProps = { type ThemeProviderProps = {
@@ -12,8 +13,10 @@ type ThemeProviderState = {
setThemeSetting: (themeSetting: TThemeSetting) => Promise<void> setThemeSetting: (themeSetting: TThemeSetting) => Promise<void>
} }
// web only async function getSystemTheme() {
function getSystemTheme() { if (isElectron(window)) {
return await window.api.theme.current()
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
} }
@@ -27,33 +30,28 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
// electron const themeSetting = await storage.getThemeSetting()
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') { if (themeSetting === 'system') {
setTheme(getSystemTheme()) setTheme(await getSystemTheme())
return return
} }
setTheme(themeSetting) setTheme(themeSetting)
} }
}
init() init()
}, []) }, [])
useEffect(() => { 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 mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = (e: MediaQueryListEvent) => { const handleChange = (e: MediaQueryListEvent) => {
@@ -80,14 +78,10 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const value = { const value = {
themeSetting: themeSetting, themeSetting: themeSetting,
setThemeSetting: async (themeSetting: TThemeSetting) => { setThemeSetting: async (themeSetting: TThemeSetting) => {
if (isElectron(window)) { await storage.setThemeSetting(themeSetting)
await window.api.theme.set(themeSetting)
} else {
localStorage.setItem('themeSetting', themeSetting)
}
setThemeSetting(themeSetting) setThemeSetting(themeSetting)
if (themeSetting === 'system') { if (themeSetting === 'system') {
setTheme(getSystemTheme()) setTheme(await getSystemTheme())
return return
} }
setTheme(themeSetting) setTheme(themeSetting)

View File

@@ -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' import { isElectron } from '@renderer/lib/env'
const DEFAULT_RELAY_GROUPS: TRelayGroup[] = [ const DEFAULT_RELAY_GROUPS: TRelayGroup[] = [
@@ -15,21 +16,19 @@ const DEFAULT_RELAY_GROUPS: TRelayGroup[] = [
] ]
class Storage { class Storage {
async getRelayGroups() { async getItem(key: string) {
if (isElectron(window)) { if (isElectron(window)) {
const relayGroups = await window.api.storage.getRelayGroups() return window.api.storage.getItem(key)
return relayGroups ?? DEFAULT_RELAY_GROUPS
} else { } else {
const relayGroupsStr = localStorage.getItem('relayGroups') return localStorage.getItem(key)
return relayGroupsStr ? (JSON.parse(relayGroupsStr) as TRelayGroup[]) : DEFAULT_RELAY_GROUPS
} }
} }
async setRelayGroups(relayGroups: TRelayGroup[]) { async setItem(key: string, value: string) {
if (isElectron(window)) { if (isElectron(window)) {
return window.api.storage.setRelayGroups(relayGroups) return window.api.storage.setItem(key, value)
} else { } else {
localStorage.setItem('relayGroups', JSON.stringify(relayGroups)) return localStorage.setItem(key, value)
} }
} }
} }
@@ -39,6 +38,7 @@ class StorageService {
private initPromise!: Promise<void> private initPromise!: Promise<void>
private relayGroups: TRelayGroup[] = [] private relayGroups: TRelayGroup[] = []
private themeSetting: TThemeSetting = 'system'
private storage: Storage = new Storage() private storage: Storage = new Storage()
constructor() { constructor() {
@@ -50,7 +50,10 @@ class StorageService {
} }
async init() { 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() { async getRelayGroups() {
@@ -60,9 +63,20 @@ class StorageService {
async setRelayGroups(relayGroups: TRelayGroup[]) { async setRelayGroups(relayGroups: TRelayGroup[]) {
await this.initPromise await this.initPromise
await this.storage.setRelayGroups(relayGroups) await this.storage.setItem(StorageKey.RELAY_GROUPS, JSON.stringify(relayGroups))
this.relayGroups = 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() const instance = new StorageService()