Migrate from IndexedDB to file-based storage with cross-instance sync
This commit is contained in:
@@ -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 { storage } from "./storage"
|
||||
|
||||
interface ThemeContextValue {
|
||||
isDark: () => boolean
|
||||
@@ -10,22 +11,43 @@ const ThemeContext = createContext<ThemeContextValue>()
|
||||
|
||||
export function ThemeProvider(props: { children: JSX.Element }) {
|
||||
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
const savedTheme = localStorage.getItem("theme")
|
||||
const initialDark = savedTheme ? savedTheme === "dark" : prefersDark
|
||||
const [isDark, setIsDarkSignal] = createSignal(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(() => {
|
||||
if (isDark()) {
|
||||
document.documentElement.setAttribute("data-theme", "dark")
|
||||
} else {
|
||||
document.documentElement.removeAttribute("data-theme")
|
||||
}
|
||||
loadTheme()
|
||||
|
||||
// Listen for config changes from other instances
|
||||
const unsubscribe = storage.onConfigChanged(() => {
|
||||
loadTheme()
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
})
|
||||
|
||||
const setTheme = (dark: boolean) => {
|
||||
setIsDarkSignal(dark)
|
||||
localStorage.setItem("theme", dark ? "dark" : "light")
|
||||
saveTheme(dark)
|
||||
if (dark) {
|
||||
document.documentElement.setAttribute("data-theme", "dark")
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user