Migrate from IndexedDB to file-based storage with cross-instance sync
This commit is contained in:
@@ -2,6 +2,7 @@ import { app, BrowserWindow, dialog, ipcMain } from "electron"
|
|||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { createApplicationMenu } from "./menu"
|
import { createApplicationMenu } from "./menu"
|
||||||
import { setupInstanceIPC } from "./ipc"
|
import { setupInstanceIPC } from "./ipc"
|
||||||
|
import { setupStorageIPC } from "./storage"
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ function createWindow() {
|
|||||||
|
|
||||||
createApplicationMenu(mainWindow)
|
createApplicationMenu(mainWindow)
|
||||||
setupInstanceIPC(mainWindow)
|
setupInstanceIPC(mainWindow)
|
||||||
|
setupStorageIPC()
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
mainWindow.on("closed", () => {
|
||||||
mainWindow = null
|
mainWindow = null
|
||||||
|
|||||||
121
electron/main/storage.ts
Normal file
121
electron/main/storage.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { app, ipcMain } from "electron"
|
||||||
|
import { join } from "path"
|
||||||
|
import { readFile, writeFile, mkdir, unlink, stat } from "fs/promises"
|
||||||
|
import { existsSync } from "fs"
|
||||||
|
|
||||||
|
const CONFIG_DIR = join(app.getPath("home"), ".config", "opencode-client")
|
||||||
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json")
|
||||||
|
const INSTANCES_DIR = join(CONFIG_DIR, "instances")
|
||||||
|
|
||||||
|
// File watching for config changes
|
||||||
|
let configWatchers = new Set<number>()
|
||||||
|
let configLastModified = 0
|
||||||
|
let configCache: string | null = null
|
||||||
|
|
||||||
|
async function ensureDirectories() {
|
||||||
|
try {
|
||||||
|
await mkdir(CONFIG_DIR, { recursive: true })
|
||||||
|
await mkdir(INSTANCES_DIR, { recursive: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create directories:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readConfigWithCache(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const stats = await stat(CONFIG_FILE)
|
||||||
|
const currentModified = stats.mtime.getTime()
|
||||||
|
|
||||||
|
// If file hasn't been modified since last read, return cache
|
||||||
|
if (configCache && configLastModified >= currentModified) {
|
||||||
|
return configCache
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await readFile(CONFIG_FILE, "utf-8")
|
||||||
|
configCache = content
|
||||||
|
configLastModified = currentModified
|
||||||
|
return content
|
||||||
|
} catch (error) {
|
||||||
|
// File doesn't exist or can't be read
|
||||||
|
configCache = null
|
||||||
|
configLastModified = 0
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateConfigCache() {
|
||||||
|
configCache = null
|
||||||
|
configLastModified = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupStorageIPC() {
|
||||||
|
ensureDirectories()
|
||||||
|
|
||||||
|
ipcMain.handle("storage:getConfigPath", () => CONFIG_FILE)
|
||||||
|
ipcMain.handle("storage:getInstancesDir", () => INSTANCES_DIR)
|
||||||
|
|
||||||
|
ipcMain.handle("storage:readConfigFile", async () => {
|
||||||
|
try {
|
||||||
|
return await readConfigWithCache()
|
||||||
|
} catch (error) {
|
||||||
|
// Return empty config if file doesn't exist
|
||||||
|
return JSON.stringify({ preferences: { showThinkingBlocks: false }, recentFolders: [] }, null, 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle("storage:writeConfigFile", async (_, content: string) => {
|
||||||
|
try {
|
||||||
|
await writeFile(CONFIG_FILE, content, "utf-8")
|
||||||
|
invalidateConfigCache()
|
||||||
|
|
||||||
|
// Notify other renderer processes about config change
|
||||||
|
const windows = require("electron").BrowserWindow.getAllWindows()
|
||||||
|
windows.forEach((win: any) => {
|
||||||
|
if (win.webContents && !win.webContents.isDestroyed()) {
|
||||||
|
win.webContents.send("storage:configChanged")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to write config file:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle("storage:readInstanceFile", async (_, filename: string) => {
|
||||||
|
const instanceFile = join(INSTANCES_DIR, `${filename}.json`)
|
||||||
|
try {
|
||||||
|
return await readFile(instanceFile, "utf-8")
|
||||||
|
} catch (error) {
|
||||||
|
// Return empty instance data if file doesn't exist
|
||||||
|
return JSON.stringify({ messageHistory: [] }, null, 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle("storage:writeInstanceFile", async (_, filename: string, content: string) => {
|
||||||
|
const instanceFile = join(INSTANCES_DIR, `${filename}.json`)
|
||||||
|
try {
|
||||||
|
await writeFile(instanceFile, content, "utf-8")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to write instance file for ${filename}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle("storage:deleteInstanceFile", async (_, filename: string) => {
|
||||||
|
const instanceFile = join(INSTANCES_DIR, `${filename}.json`)
|
||||||
|
try {
|
||||||
|
if (existsSync(instanceFile)) {
|
||||||
|
await unlink(instanceFile)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete instance file for ${filename}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up on app quit
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
configCache = null
|
||||||
|
configLastModified = 0
|
||||||
|
})
|
||||||
@@ -15,6 +15,14 @@ export interface ElectronAPI {
|
|||||||
) => void
|
) => void
|
||||||
onNewInstance: (callback: () => void) => void
|
onNewInstance: (callback: () => void) => void
|
||||||
scanDirectory: (workspaceFolder: string) => Promise<string[]>
|
scanDirectory: (workspaceFolder: string) => Promise<string[]>
|
||||||
|
// Storage operations
|
||||||
|
getConfigPath: () => string
|
||||||
|
getInstancesDir: () => string
|
||||||
|
readConfigFile: () => Promise<string>
|
||||||
|
writeConfigFile: (content: string) => Promise<void>
|
||||||
|
readInstanceFile: (instanceId: string) => Promise<string>
|
||||||
|
writeInstanceFile: (instanceId: string, content: string) => Promise<void>
|
||||||
|
deleteInstanceFile: (instanceId: string) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const electronAPI: ElectronAPI = {
|
const electronAPI: ElectronAPI = {
|
||||||
@@ -37,6 +45,18 @@ const electronAPI: ElectronAPI = {
|
|||||||
ipcRenderer.on("menu:newInstance", () => callback())
|
ipcRenderer.on("menu:newInstance", () => callback())
|
||||||
},
|
},
|
||||||
scanDirectory: (workspaceFolder: string) => ipcRenderer.invoke("fs:scanDirectory", workspaceFolder),
|
scanDirectory: (workspaceFolder: string) => ipcRenderer.invoke("fs:scanDirectory", workspaceFolder),
|
||||||
|
// Storage operations
|
||||||
|
getConfigPath: () => ipcRenderer.invoke("storage:getConfigPath"),
|
||||||
|
getInstancesDir: () => ipcRenderer.invoke("storage:getInstancesDir"),
|
||||||
|
readConfigFile: () => ipcRenderer.invoke("storage:readConfigFile"),
|
||||||
|
writeConfigFile: (content: string) => ipcRenderer.invoke("storage:writeConfigFile", content),
|
||||||
|
readInstanceFile: (filename: string) => ipcRenderer.invoke("storage:readInstanceFile", filename),
|
||||||
|
writeInstanceFile: (filename: string, content: string) =>
|
||||||
|
ipcRenderer.invoke("storage:writeInstanceFile", filename, content),
|
||||||
|
deleteInstanceFile: (filename: string) => ipcRenderer.invoke("storage:deleteInstanceFile", filename),
|
||||||
|
onConfigChanged: (callback: () => void) => {
|
||||||
|
ipcRenderer.on("storage:configChanged", () => callback())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", electronAPI)
|
contextBridge.exposeInMainWorld("electronAPI", electronAPI)
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
const DB_NAME = "opencode-client"
|
|
||||||
const DB_VERSION = 1
|
|
||||||
const HISTORY_STORE = "message-history"
|
|
||||||
|
|
||||||
let db: IDBDatabase | null = null
|
|
||||||
|
|
||||||
async function getDB(): Promise<IDBDatabase> {
|
|
||||||
if (db) return db
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
|
||||||
request.onsuccess = () => {
|
|
||||||
db = request.result
|
|
||||||
resolve(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.onupgradeneeded = (event) => {
|
|
||||||
const database = (event.target as IDBOpenDBRequest).result
|
|
||||||
|
|
||||||
if (!database.objectStoreNames.contains(HISTORY_STORE)) {
|
|
||||||
database.createObjectStore(HISTORY_STORE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveHistory(instanceId: string, history: string[]): Promise<void> {
|
|
||||||
const database = await getDB()
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const tx = database.transaction(HISTORY_STORE, "readwrite")
|
|
||||||
const store = tx.objectStore(HISTORY_STORE)
|
|
||||||
const request = store.put(history, instanceId)
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
|
||||||
request.onsuccess = () => resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHistory(instanceId: string): Promise<string[]> {
|
|
||||||
try {
|
|
||||||
const database = await getDB()
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const tx = database.transaction(HISTORY_STORE, "readonly")
|
|
||||||
const store = tx.objectStore(HISTORY_STORE)
|
|
||||||
const request = store.get(instanceId)
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
|
||||||
request.onsuccess = () => resolve(request.result || [])
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to load history from IndexedDB:", error)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteHistory(instanceId: string): Promise<void> {
|
|
||||||
const database = await getDB()
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const tx = database.transaction(HISTORY_STORE, "readwrite")
|
|
||||||
const store = tx.objectStore(HISTORY_STORE)
|
|
||||||
const request = store.delete(instanceId)
|
|
||||||
|
|
||||||
request.onerror = () => reject(request.error)
|
|
||||||
request.onsuccess = () => resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
106
src/lib/storage.ts
Normal file
106
src/lib/storage.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import type { Preferences, RecentFolder } from "../stores/preferences"
|
||||||
|
|
||||||
|
export interface ConfigData {
|
||||||
|
preferences: Preferences
|
||||||
|
recentFolders: RecentFolder[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceData {
|
||||||
|
messageHistory: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileStorage {
|
||||||
|
private configPath: string
|
||||||
|
private instancesDir: string
|
||||||
|
private configChangeListeners: Set<() => void> = new Set()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.configPath = window.electronAPI.getConfigPath()
|
||||||
|
this.instancesDir = window.electronAPI.getInstancesDir()
|
||||||
|
|
||||||
|
// Listen for config changes from other instances
|
||||||
|
window.electronAPI.onConfigChanged(() => {
|
||||||
|
this.configChangeListeners.forEach((listener) => listener())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config operations
|
||||||
|
async loadConfig(): Promise<ConfigData> {
|
||||||
|
try {
|
||||||
|
const content = await window.electronAPI.readConfigFile()
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load config, using defaults:", error)
|
||||||
|
return {
|
||||||
|
preferences: {
|
||||||
|
showThinkingBlocks: false,
|
||||||
|
},
|
||||||
|
recentFolders: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveConfig(config: ConfigData): Promise<void> {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.writeConfigFile(JSON.stringify(config, null, 2))
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save config:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance operations
|
||||||
|
async loadInstanceData(instanceId: string): Promise<InstanceData> {
|
||||||
|
try {
|
||||||
|
const filename = this.instanceIdToFilename(instanceId)
|
||||||
|
const content = await window.electronAPI.readInstanceFile(filename)
|
||||||
|
return JSON.parse(content)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to load instance data for ${instanceId}, using defaults:`, error)
|
||||||
|
return {
|
||||||
|
messageHistory: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveInstanceData(instanceId: string, data: InstanceData): Promise<void> {
|
||||||
|
try {
|
||||||
|
const filename = this.instanceIdToFilename(instanceId)
|
||||||
|
await window.electronAPI.writeInstanceFile(filename, JSON.stringify(data, null, 2))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to save instance data for ${instanceId}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteInstanceData(instanceId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const filename = this.instanceIdToFilename(instanceId)
|
||||||
|
await window.electronAPI.deleteInstanceFile(filename)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete instance data for ${instanceId}:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert folder path to safe filename
|
||||||
|
private instanceIdToFilename(instanceId: string): string {
|
||||||
|
// Convert folder path to safe filename
|
||||||
|
// Replace path separators and other invalid characters
|
||||||
|
return instanceId
|
||||||
|
.replace(/[\\/]/g, "_") // Replace path separators
|
||||||
|
.replace(/[^a-zA-Z0-9_.-]/g, "_") // Replace other invalid chars
|
||||||
|
.replace(/_{2,}/g, "_") // Replace multiple underscores with single
|
||||||
|
.replace(/^_|_$/g, "") // Remove leading/trailing underscores
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config change listeners
|
||||||
|
onConfigChanged(listener: () => void): () => void {
|
||||||
|
this.configChangeListeners.add(listener)
|
||||||
|
return () => this.configChangeListeners.delete(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const storage = new FileStorage()
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createContext, createSignal, useContext, onMount, type JSX } from "solid-js"
|
import { createContext, createSignal, useContext, onMount, type JSX } from "solid-js"
|
||||||
|
import { storage } from "./storage"
|
||||||
|
|
||||||
interface ThemeContextValue {
|
interface ThemeContextValue {
|
||||||
isDark: () => boolean
|
isDark: () => boolean
|
||||||
@@ -10,22 +11,43 @@ const ThemeContext = createContext<ThemeContextValue>()
|
|||||||
|
|
||||||
export function ThemeProvider(props: { children: JSX.Element }) {
|
export function ThemeProvider(props: { children: JSX.Element }) {
|
||||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
const savedTheme = localStorage.getItem("theme")
|
const [isDark, setIsDarkSignal] = createSignal(prefersDark)
|
||||||
const initialDark = savedTheme ? savedTheme === "dark" : prefersDark
|
|
||||||
|
|
||||||
const [isDark, setIsDarkSignal] = createSignal(initialDark)
|
async function loadTheme() {
|
||||||
|
try {
|
||||||
|
const config = await storage.loadConfig()
|
||||||
|
const savedTheme = (config as any).theme
|
||||||
|
const initialDark = savedTheme ? savedTheme === "dark" : prefersDark
|
||||||
|
setIsDarkSignal(initialDark)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load theme from config:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTheme(dark: boolean) {
|
||||||
|
try {
|
||||||
|
const config = await storage.loadConfig()
|
||||||
|
;(config as any).theme = dark ? "dark" : "light"
|
||||||
|
await storage.saveConfig(config)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to save theme to config:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (isDark()) {
|
loadTheme()
|
||||||
document.documentElement.setAttribute("data-theme", "dark")
|
|
||||||
} else {
|
// Listen for config changes from other instances
|
||||||
document.documentElement.removeAttribute("data-theme")
|
const unsubscribe = storage.onConfigChanged(() => {
|
||||||
}
|
loadTheme()
|
||||||
|
})
|
||||||
|
|
||||||
|
return unsubscribe
|
||||||
})
|
})
|
||||||
|
|
||||||
const setTheme = (dark: boolean) => {
|
const setTheme = (dark: boolean) => {
|
||||||
setIsDarkSignal(dark)
|
setIsDarkSignal(dark)
|
||||||
localStorage.setItem("theme", dark ? "dark" : "light")
|
saveTheme(dark)
|
||||||
if (dark) {
|
if (dark) {
|
||||||
document.documentElement.setAttribute("data-theme", "dark")
|
document.documentElement.setAttribute("data-theme", "dark")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { saveHistory, loadHistory, deleteHistory } from "../lib/db"
|
import { storage, type InstanceData } from "../lib/storage"
|
||||||
|
|
||||||
const MAX_HISTORY = 100
|
const MAX_HISTORY = 100
|
||||||
|
|
||||||
@@ -18,9 +18,11 @@ export async function addToHistory(instanceId: string, text: string): Promise<vo
|
|||||||
|
|
||||||
instanceHistories.set(instanceId, history)
|
instanceHistories.set(instanceId, history)
|
||||||
|
|
||||||
saveHistory(instanceId, history).catch((err) => {
|
try {
|
||||||
|
await storage.saveInstanceData(instanceId, { messageHistory: history })
|
||||||
|
} catch (err) {
|
||||||
console.warn("Failed to persist message history:", err)
|
console.warn("Failed to persist message history:", err)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getHistory(instanceId: string): Promise<string[]> {
|
export async function getHistory(instanceId: string): Promise<string[]> {
|
||||||
@@ -31,7 +33,12 @@ export async function getHistory(instanceId: string): Promise<string[]> {
|
|||||||
export async function clearHistory(instanceId: string): Promise<void> {
|
export async function clearHistory(instanceId: string): Promise<void> {
|
||||||
instanceHistories.delete(instanceId)
|
instanceHistories.delete(instanceId)
|
||||||
historyLoaded.delete(instanceId)
|
historyLoaded.delete(instanceId)
|
||||||
await deleteHistory(instanceId)
|
|
||||||
|
try {
|
||||||
|
await storage.saveInstanceData(instanceId, { messageHistory: [] })
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to clear history:", error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureHistoryLoaded(instanceId: string): Promise<void> {
|
async function ensureHistoryLoaded(instanceId: string): Promise<void> {
|
||||||
@@ -40,8 +47,8 @@ async function ensureHistoryLoaded(instanceId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const history = await loadHistory(instanceId)
|
const data = await storage.loadInstanceData(instanceId)
|
||||||
instanceHistories.set(instanceId, history)
|
instanceHistories.set(instanceId, data.messageHistory)
|
||||||
historyLoaded.add(instanceId)
|
historyLoaded.add(instanceId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Failed to load history:", error)
|
console.warn("Failed to load history:", error)
|
||||||
|
|||||||
@@ -1,69 +1,50 @@
|
|||||||
import { createSignal } from "solid-js"
|
import { createSignal, onMount } from "solid-js"
|
||||||
|
import { storage, type ConfigData } from "../lib/storage"
|
||||||
|
|
||||||
const STORAGE_KEY = "opencode-preferences"
|
export interface Preferences {
|
||||||
const RECENT_FOLDERS_KEY = "opencode-recent-folders"
|
|
||||||
const MAX_RECENT_FOLDERS = 10
|
|
||||||
|
|
||||||
interface Preferences {
|
|
||||||
showThinkingBlocks: boolean
|
showThinkingBlocks: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RecentFolder {
|
export interface RecentFolder {
|
||||||
path: string
|
path: string
|
||||||
lastAccessed: number
|
lastAccessed: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_RECENT_FOLDERS = 10
|
||||||
|
|
||||||
const defaultPreferences: Preferences = {
|
const defaultPreferences: Preferences = {
|
||||||
showThinkingBlocks: false,
|
showThinkingBlocks: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPreferences(): Preferences {
|
const [preferences, setPreferences] = createSignal<Preferences>(defaultPreferences)
|
||||||
try {
|
const [recentFolders, setRecentFolders] = createSignal<RecentFolder[]>([])
|
||||||
const stored = localStorage.getItem(STORAGE_KEY)
|
|
||||||
if (stored) {
|
|
||||||
return { ...defaultPreferences, ...JSON.parse(stored) }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load preferences:", error)
|
|
||||||
}
|
|
||||||
return defaultPreferences
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePreferences(prefs: Preferences): void {
|
async function loadConfig(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs))
|
const config = await storage.loadConfig()
|
||||||
|
setPreferences({ ...defaultPreferences, ...config.preferences })
|
||||||
|
setRecentFolders(config.recentFolders)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save preferences:", error)
|
console.error("Failed to load config:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadRecentFolders(): RecentFolder[] {
|
async function saveConfig(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(RECENT_FOLDERS_KEY)
|
const config: ConfigData = {
|
||||||
if (stored) {
|
preferences: preferences(),
|
||||||
return JSON.parse(stored)
|
recentFolders: recentFolders(),
|
||||||
}
|
}
|
||||||
|
await storage.saveConfig(config)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load recent folders:", error)
|
console.error("Failed to save config:", error)
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveRecentFolders(folders: RecentFolder[]): void {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(RECENT_FOLDERS_KEY, JSON.stringify(folders))
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to save recent folders:", error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [preferences, setPreferences] = createSignal<Preferences>(loadPreferences())
|
|
||||||
const [recentFolders, setRecentFolders] = createSignal<RecentFolder[]>(loadRecentFolders())
|
|
||||||
|
|
||||||
function updatePreferences(updates: Partial<Preferences>): void {
|
function updatePreferences(updates: Partial<Preferences>): void {
|
||||||
const updated = { ...preferences(), ...updates }
|
const updated = { ...preferences(), ...updates }
|
||||||
setPreferences(updated)
|
setPreferences(updated)
|
||||||
savePreferences(updated)
|
saveConfig().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleShowThinkingBlocks(): void {
|
function toggleShowThinkingBlocks(): void {
|
||||||
@@ -76,13 +57,26 @@ function addRecentFolder(path: string): void {
|
|||||||
|
|
||||||
const trimmed = folders.slice(0, MAX_RECENT_FOLDERS)
|
const trimmed = folders.slice(0, MAX_RECENT_FOLDERS)
|
||||||
setRecentFolders(trimmed)
|
setRecentFolders(trimmed)
|
||||||
saveRecentFolders(trimmed)
|
saveConfig().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRecentFolder(path: string): void {
|
function removeRecentFolder(path: string): void {
|
||||||
const folders = recentFolders().filter((f) => f.path !== path)
|
const folders = recentFolders().filter((f) => f.path !== path)
|
||||||
setRecentFolders(folders)
|
setRecentFolders(folders)
|
||||||
saveRecentFolders(folders)
|
saveConfig().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load config on mount and listen for changes from other instances
|
||||||
|
onMount(() => {
|
||||||
|
loadConfig()
|
||||||
|
|
||||||
|
// Reload config when changed by another instance
|
||||||
|
const unsubscribe = storage.onConfigChanged(() => {
|
||||||
|
loadConfig()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return unsubscribe
|
||||||
|
})
|
||||||
|
|
||||||
export { preferences, updatePreferences, toggleShowThinkingBlocks, recentFolders, addRecentFolder, removeRecentFolder }
|
export { preferences, updatePreferences, toggleShowThinkingBlocks, recentFolders, addRecentFolder, removeRecentFolder }
|
||||||
|
|||||||
Reference in New Issue
Block a user