diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 34aef1e..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Build Electron App - -on: - push: - tags: - - 'v*' - -jobs: - build: - strategy: - fail-fast: false - matrix: - include: - - os: windows-latest - script: build:win - - os: macos-latest - script: build:mac - - os: ubuntu-latest - script: build:linux - - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Run platform build - run: npm run ${{ matrix.script }} - env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.os }}-build - path: | - dist/** diff --git a/out/main/index.js b/out/main/index.js index a6b22de..2969fc2 100644 --- a/out/main/index.js +++ b/out/main/index.js @@ -3,28 +3,61 @@ import { electronApp, optimizer, is } from "@electron-toolkit/utils"; import { app, ipcMain, BrowserWindow, shell } from "electron"; import filesystemPlugin from "@takerofnotes/plugin-filesystem"; import supabasePlugin from "@takerofnotes/plugin-supabase"; -import "libsodium-wrappers"; -import "uuid"; -import "flexsearch"; -import fs from "fs/promises"; -import path, { join } from "path"; +import sodium from "libsodium-wrappers"; +import { v4 } from "uuid"; +import { Index } from "flexsearch"; +import { join } from "path"; import __cjs_mod__ from "node:module"; const __filename = import.meta.filename; const __dirname = import.meta.dirname; const require2 = __cjs_mod__.createRequire(import.meta.url); +const createPluginManager = (registry) => { + let activePluginId = null; + let adapter = null; + return { + listPlugins() { + return registry.list(); + }, + getPlugin(pluginId) { + return registry.get(pluginId); + }, + getAdapter(pluginId, adapterConfig = {}) { + const plugin = registry.get(pluginId); + if (!plugin) { + throw new Error(`Plugin not found: ${pluginId}`); + } + return plugin.createAdapter(adapterConfig); + }, + setActivePlugin(pluginId, adapterConfig = {}) { + activePluginId = pluginId; + adapter = this.getAdapter(pluginId, adapterConfig); + return adapter; + }, + async testPlugin(pluginId, adapterConfig = {}) { + const plugin = registry.get(pluginId); + if (!plugin) { + throw new Error(`Plugin not found: ${pluginId}`); + } + const adapter2 = this.getAdapter(pluginId, adapterConfig); + await adapter2.init(); + return await adapter2.testConnection(); + }, + getActiveAdapter() { + return adapter; + }, + getActivePluginId() { + return activePluginId; + } + }; +}; class PluginRegistry { - constructor(environment = "web") { + constructor() { this.plugins = /* @__PURE__ */ new Map(); - this.environment = environment; } register(plugin) { if (!plugin.id) { throw new Error("Plugin must have an id"); } - const environments = plugin.environments || ["electron", "web"]; - if (!environments.includes(this.environment)) { - return; - } this.plugins.set(plugin.id, plugin); } get(id) { @@ -39,146 +72,314 @@ class PluginRegistry { })); } } -function createIpcStorage() { - return { - async load() { - return await window.api.getConfig(); - }, - async save(data) { - await window.api.setConfig(data); - } - }; -} -function getDefaultConfig() { +const getDefaultConfig = () => { return { activeAdapter: "supabase", - adapters: { - supabase: { - supabaseUrl: "https://example.supabase.co", - supabaseKey: "", - bucket: "notes" - } - }, - theme: "light" - }; -} -let config = null; -let configResolve = null; -const configPromise = new Promise((resolve) => { - configResolve = resolve; -}); -function createConfigManager(environment, customStorage = null) { - let storage; - if (customStorage) { - storage = customStorage; - } else { - storage = createIpcStorage(); - } - const loadConfig = async () => { - if (config !== null) { - return config; - } - const stored = await storage.load(); - config = stored || getDefaultConfig(); - if (!stored) { - await storage.save(config); - } - configResolve(config); - return config; - }; - const writeConfig = async (newConfig) => { - await storage.save(newConfig); - config = newConfig; - }; - const getConfig = () => { - if (config !== null) { - return config; - } - return configPromise; - }; - const setConfig = async (newConfig) => { - config = newConfig; - await writeConfig(newConfig); - }; - const refreshConfig = async () => { - config = await storage.load(); - configResolve(config); + theme: "dark" }; +}; +const createConfigManager = (storage) => { + let config = null; return { - loadConfig, - getConfig, - setConfig, - refreshConfig - }; -} -const USER_DATA_STRING = "__DEFAULT_USER_DATA__"; -function createNodeStorage(defaultPlugin = null) { - const configPath = path.join(app.getPath("userData"), "config.json"); - function resolveDefaults(obj) { - if (Array.isArray(obj)) { - return obj.map(resolveDefaults); - } else if (obj && typeof obj === "object") { - const resolved = {}; - for (const [key, value] of Object.entries(obj)) { - resolved[key] = resolveDefaults(value); + async loadConfig() { + if (config) return config; + const stored = await storage.load(); + config = stored || getDefaultConfig(); + if (!stored) { + await storage.save(config); } - return resolved; - } else if (typeof obj === "string" && obj.includes(USER_DATA_STRING)) { - return obj.replace(USER_DATA_STRING, app.getPath("userData")); - } - return obj; - } - return { - async load() { - let parsed; - try { - const raw = await fs.readFile(configPath, "utf8"); - parsed = JSON.parse(raw); - } catch { - parsed = null; - } - if (!parsed || !parsed.activeAdapter) { - const defaultConfig = {}; - if (defaultPlugin) { - for (const field of defaultPlugin.configSchema) { - defaultConfig[field.key] = field.default ?? null; - } - } - parsed = { - ...parsed ? parsed : {}, - activeAdapter: defaultPlugin?.id || "supabase", - adapters: {}, - theme: "dark" - }; - if (defaultPlugin) { - parsed.adapters[defaultPlugin.id] = defaultConfig; - } - await this.save(parsed); - } else { - parsed.adapters = resolveDefaults(parsed.adapters); - } - return parsed; + return config; }, - async save(configObject) { - const dir = path.dirname(configPath); - await fs.mkdir(dir, { recursive: true }); - const resolved = { - ...configObject, - adapters: resolveDefaults(configObject.adapters) - }; - await fs.writeFile( - configPath, - JSON.stringify(resolved, null, 2), - "utf8" + getConfig() { + return config; + }, + async setConfig(newConfig) { + config = newConfig; + await storage.save(newConfig); + }, + async refreshConfig() { + config = await storage.load(); + } + }; +}; +class NotesAPI { + constructor(adapter, encryptionKey = null) { + if (!adapter) { + throw new Error("NotesAPI requires a storage adapter"); + } + this.adapter = adapter; + this.notesCache = /* @__PURE__ */ new Map(); + this.encryptionKey = encryptionKey; + this._sodiumReady = false; + this.index = new Index({ + tokenize: "forward" + }); + } + async _initSodium() { + if (!this._sodiumReady) { + await sodium.ready; + this._sodiumReady = true; + } + } + _hexToUint8Array(hex) { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return bytes; + } + _concatUint8Arrays(a, b) { + const result = new Uint8Array(a.length + b.length); + result.set(a, 0); + result.set(b, a.length); + return result; + } + _uint8ArrayToBase64(bytes) { + let binary = ""; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + } + _base64ToUint8Array(base64) { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; + } + _encrypt(note) { + if (!this.encryptionKey) { + throw new Error("Encryption key not set"); + } + const key = this._hexToUint8Array(this.encryptionKey); + if (key.length !== 32) { + throw new Error( + "Encryption key must be 64 hex characters (32 bytes)" ); } - }; + const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES); + const message = JSON.stringify(note); + const ciphertext = sodium.crypto_secretbox_easy( + new TextEncoder().encode(message), + nonce, + key + ); + const combined = this._concatUint8Arrays(nonce, ciphertext); + return this._uint8ArrayToBase64(combined); + } + _decrypt(encryptedData) { + if (!this.encryptionKey) { + throw new Error("Encryption key not set"); + } + const key = this._hexToUint8Array(this.encryptionKey); + if (key.length !== 32) { + throw new Error( + "Encryption key must be 64 hex characters (32 bytes)" + ); + } + let combined; + try { + combined = this._base64ToUint8Array(encryptedData); + } catch (e) { + throw new Error("Invalid encrypted data: not valid base64"); + } + if (combined.length < sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) { + throw new Error("Invalid encrypted data: too short"); + } + const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES); + const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES); + let decrypted; + try { + decrypted = sodium.crypto_secretbox_open_easy( + ciphertext, + nonce, + key + ); + } catch (e) { + throw new Error("Decryption failed: wrong key or corrupted data"); + } + if (!decrypted) { + throw new Error("Decryption failed: no data returned"); + } + const decryptedStr = new TextDecoder().decode(decrypted); + try { + return JSON.parse(decryptedStr); + } catch (e) { + throw new Error( + `Decryption succeeded but invalid JSON: ${decryptedStr}` + ); + } + } + async init() { + await this._initSodium(); + await this.adapter.init(); + this.notesCache.clear(); + const encryptedNotes = await this.adapter.getAll(); + for (const encryptedNote of encryptedNotes) { + try { + const note = this._decrypt(encryptedNote.data || encryptedNote); + this.notesCache.set(note.id, note); + const searchText = note.plainText || this._extractPlainText(note.content); + this.index.add(note.id, note.title + "\n" + searchText); + } catch (error) { + console.error("Failed to decrypt note:", error); + } + } + } + _extractPlainText(content) { + if (!content) return ""; + if (typeof content === "string") return content; + const extractText = (node) => { + if (typeof node === "string") return node; + if (!node || !node.content) return ""; + return node.content.map(extractText).join(" "); + }; + return extractText(content); + } + getCategories() { + const categories = /* @__PURE__ */ new Set(); + for (const note of this.notesCache.values()) { + if (note.category) { + categories.add(note.category); + } + } + return Array.from(categories).sort(); + } + getCategoryNotes(categoryName = null) { + return Array.from(this.notesCache.values()).filter((n) => n.category === categoryName).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)).map((n) => ({ ...n })); + } + getNote(id) { + const note = this.notesCache.get(id); + return note ? { ...note } : null; + } + async createNote(metadata = {}, content = "", plainText = "") { + const id = v4(); + const now = (/* @__PURE__ */ new Date()).toISOString(); + const note = { + id, + title: metadata.title || "Untitled", + category: metadata.category || null, + createdAt: now, + updatedAt: now, + content, + plainText + }; + const encryptedNote = { + id: note.id, + data: this._encrypt(note) + }; + this.notesCache.set(id, note); + this.index.add(id, note.title + "\n" + plainText); + await this.adapter.create(encryptedNote); + return note; + } + async deleteNote(id) { + await this.adapter.delete(id); + this.notesCache.delete(id); + this.index.remove(id); + } + async updateNote(id, updates = {}) { + const note = this.notesCache.get(id); + if (!note) throw new Error("Note not found"); + const allowedFields = ["title", "category", "content", "plainText"]; + for (const key of Object.keys(updates)) { + if (!allowedFields.includes(key)) { + throw new Error(`Invalid update field: ${key}`); + } + } + const updatedNote = { + ...note, + ...updates, + updatedAt: (/* @__PURE__ */ new Date()).toISOString() + }; + const encryptedNote = { + id: updatedNote.id, + data: this._encrypt(updatedNote) + }; + this.notesCache.set(id, updatedNote); + const searchText = updatedNote.plainText || this._extractPlainText(updatedNote.content); + this.index.update(id, updatedNote.title + "\n" + searchText); + await this.adapter.update(encryptedNote); + return updatedNote; + } + search(query) { + const ids = this.index.search(query, { + limit: 50, + suggest: true + }); + return ids.map((id) => this.notesCache.get(id)); + } } +const generateEncryptionKey = () => { + const array = new Uint8Array(32); + crypto.getRandomValues(array); + return Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join(""); +}; +const initPluginManager = (runtime, plugins, config) => { + const registry = new PluginRegistry(); + for (const plugin of plugins) { + registry.register(plugin); + } + const manager = createPluginManager(registry); + manager.setActivePlugin( + config.activeAdapter, + config.adapters[config.activeAdapter] + ); + return manager; +}; +const initConfigManager = async (runtime) => { + let storage; + { + const { createNodeStorage } = await import("./NodeStorage-DrLmsh6l.js"); + storage = createNodeStorage(filesystemPlugin); + } + return createConfigManager(storage); +}; +const initializeCore = async (runtime, { plugins }) => { + const configManager = await initConfigManager(); + const config = await configManager.loadConfig(); + const pluginManager = initPluginManager(runtime, plugins, config); + let notesAPI = null; + let initPromise = null; + const getNotesAPI = async () => { + if (notesAPI) return notesAPI; + if (!initPromise) { + initPromise = (async () => { + const config2 = await configManager.loadConfig(); + let encryptionKey = config2?.encryptionKey; + if (!encryptionKey) { + encryptionKey = generateEncryptionKey(); + await configManager.setConfig({ + ...config2, + encryptionKey + }); + } + const pluginId = config2?.activeAdapter || "filesystem"; + const adapterConfig = config2?.adapters?.[pluginId] || {}; + const adapter = pluginManager.getAdapter( + pluginId, + adapterConfig + ); + notesAPI = new NotesAPI(adapter, encryptionKey); + await notesAPI.init(); + return notesAPI; + })(); + } + return initPromise; + }; + return { + runtime, + pluginManager, + configManager, + getNotesAPI + }; +}; const DEFAULT_WINDOW_SIZE = { width: 354, height: 549 }; const DEFAULT_MOVE_WINDOW_SIZE = { width: 708, height: 549 }; const preloadPath = join(__dirname, "../preload/index.mjs"); const rendererPath = join(__dirname, "../renderer/index.html"); -let activeAdapter = null; function createWindow() { const mainWindow = new BrowserWindow({ width: DEFAULT_WINDOW_SIZE.width, @@ -234,44 +435,28 @@ app.whenReady().then(async () => { win.webContents.send(event, data); }); }; - const registry = new PluginRegistry("electron"); - registry.register(filesystemPlugin); - registry.register(supabasePlugin); - const nodeStorage = createNodeStorage(filesystemPlugin); - const configManager = createConfigManager("electron", nodeStorage); - const initialConfig = await configManager.loadConfig(); - const setActivePlugin = async (pluginId) => { - const currentConfig = await configManager.loadConfig(); - await configManager.setConfig({ - ...currentConfig, - activeAdapter: pluginId - }); - const plugin = registry.get(pluginId); - const adapterConfig = currentConfig.adapters[pluginId] || {}; - activeAdapter = plugin.createAdapter(adapterConfig); - await activeAdapter.init(); - ipcMain.removeHandler("adapter:call"); - ipcMain.handle("adapter:call", async (_, method, args) => { - if (!activeAdapter[method]) { - throw new Error(`Invalid adapter method: ${method}`); - } - return await activeAdapter[method](...args); - }); - broadcastNoteChange("plugin-changed", pluginId); - return true; - }; - await setActivePlugin(initialConfig.activeAdapter); - ipcMain.handle("getConfig", async () => { - return await configManager.loadConfig(); + const { pluginManager, configManager } = await initializeCore( + "electron-main", + { + plugins: [filesystemPlugin, supabasePlugin] + } + ); + ipcMain.handle("pluginManager:call", async (_, method, ...args) => { + const methodCall = await pluginManager[method](...args); + if (method === "setActivePlugin") { + broadcastNoteChange("plugin-changed"); + } + return methodCall; }); - ipcMain.handle("setConfig", async (_, newConfig) => { - await configManager.setConfig(newConfig); + ipcMain.handle("configManager:call", async (_, method, ...args) => { + return await configManager[method](...args); }); - ipcMain.handle("listPlugins", async () => { - return registry.list(); - }); - ipcMain.handle("setActivePlugin", async (_, pluginId) => { - return await setActivePlugin(pluginId); + ipcMain.handle("adapter:call", async (_, method, ...args) => { + const adapter = pluginManager.getActiveAdapter(); + if (!adapter[method]) { + throw new Error(`Invalid adapter method: ${method}`); + } + return await adapter[method](...args); }); ipcMain.on("note-changed", (_, event, data) => { broadcastNoteChange(event, data); @@ -297,8 +482,8 @@ app.whenReady().then(async () => { } }); electronApp.setAppUserModelId("com.electron"); - app.on("browser-window-created", (_, window2) => { - optimizer.watchWindowShortcuts(window2); + app.on("browser-window-created", (_, window) => { + optimizer.watchWindowShortcuts(window); }); createWindow(); app.on("activate", function() { diff --git a/out/preload/index.mjs b/out/preload/index.mjs index fe8debb..b4f58bc 100644 --- a/out/preload/index.mjs +++ b/out/preload/index.mjs @@ -1,9 +1,20 @@ import { contextBridge, ipcRenderer } from "electron"; const api = { - getConfig: () => ipcRenderer.invoke("getConfig"), - setConfig: (config) => ipcRenderer.invoke("setConfig", config), - listPlugins: () => ipcRenderer.invoke("listPlugins"), - setActivePlugin: (pluginId) => ipcRenderer.invoke("setActivePlugin", pluginId), + pluginManagerCall: (method, ...args) => ipcRenderer.invoke( + "pluginManager:call", + method, + ...args.length ? args : [] + ), + configManagerCall: (method, ...args) => ipcRenderer.invoke( + "configManager:call", + method, + ...args.length ? args : [] + ), + adapterCall: (method, ...args) => ipcRenderer.invoke( + "adapter:call", + method, + ...args.length ? args : [] + ), openNoteWindow: (noteId) => { ipcRenderer.send("open-note-window", noteId); }, @@ -29,17 +40,12 @@ const api = { ipcRenderer.invoke("move-closed"); } }; -const adapter = { - call: (method, ...args) => ipcRenderer.invoke("adapter:call", method, args) -}; if (process.contextIsolated) { try { contextBridge.exposeInMainWorld("api", api); - contextBridge.exposeInMainWorld("adapter", adapter); } catch (error) { console.error(error); } } else { window.api = api; - window.adapter = adapter; } diff --git a/out/renderer/assets/__vite-browser-external-2Ng8QIWW.js b/out/renderer/assets/__vite-browser-external-2Ng8QIWW.js deleted file mode 100644 index f2316bd..0000000 --- a/out/renderer/assets/__vite-browser-external-2Ng8QIWW.js +++ /dev/null @@ -1,4 +0,0 @@ -const __viteBrowserExternal = {}; -export { - __viteBrowserExternal as default -}; diff --git a/out/renderer/assets/geist-mono-BzrJhchg.woff2 b/out/renderer/assets/geist-mono-BzrJhchg.woff2 deleted file mode 100644 index acf07fc..0000000 Binary files a/out/renderer/assets/geist-mono-BzrJhchg.woff2 and /dev/null differ diff --git a/out/renderer/assets/geist-mono-OFKGen7b.woff b/out/renderer/assets/geist-mono-OFKGen7b.woff deleted file mode 100644 index 5d71b23..0000000 Binary files a/out/renderer/assets/geist-mono-OFKGen7b.woff and /dev/null differ diff --git a/out/renderer/assets/geist-mono-bold-Bz_UliG4.woff b/out/renderer/assets/geist-mono-bold-Bz_UliG4.woff deleted file mode 100644 index ecaceab..0000000 Binary files a/out/renderer/assets/geist-mono-bold-Bz_UliG4.woff and /dev/null differ diff --git a/out/renderer/assets/geist-mono-bold-CTLtpKvJ.woff2 b/out/renderer/assets/geist-mono-bold-CTLtpKvJ.woff2 deleted file mode 100644 index c44b8fd..0000000 Binary files a/out/renderer/assets/geist-mono-bold-CTLtpKvJ.woff2 and /dev/null differ diff --git a/out/renderer/assets/neuefraktur-A4S1ACH2.woff2 b/out/renderer/assets/neuefraktur-A4S1ACH2.woff2 deleted file mode 100644 index 5c41f61..0000000 Binary files a/out/renderer/assets/neuefraktur-A4S1ACH2.woff2 and /dev/null differ diff --git a/out/renderer/assets/neuefraktur-CwjUIZ0G.woff b/out/renderer/assets/neuefraktur-CwjUIZ0G.woff deleted file mode 100644 index 359f29f..0000000 Binary files a/out/renderer/assets/neuefraktur-CwjUIZ0G.woff and /dev/null differ diff --git a/out/renderer/index.html b/out/renderer/index.html deleted file mode 100644 index c1b7715..0000000 --- a/out/renderer/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Electron - - - - - - - -
- - diff --git a/src/core/ConfigManager.js b/src/core/ConfigManager.js new file mode 100644 index 0000000..8bb5190 --- /dev/null +++ b/src/core/ConfigManager.js @@ -0,0 +1,55 @@ +const getDefaultConfig = () => { + return { + activeAdapter: 'supabase', + theme: 'dark', + } +} + +export const createConfigManager = (storage) => { + let config = null + + return { + async loadConfig() { + if (config) return config + + const stored = await storage.load() + config = stored || getDefaultConfig() + + if (!stored) { + await storage.save(config) + } + + return config + }, + + getConfig() { + return config + }, + + async setConfig(newConfig) { + config = newConfig + await storage.save(newConfig) + }, + + async refreshConfig() { + config = await storage.load() + }, + } +} + +export const createConfigManagerClient = () => { + return { + async loadConfig() { + return await window.api.configManagerCall('loadConfig') + }, + async getConfig() { + return await window.api.configManagerCall('getConfig') + }, + async setConfig(newConfig) { + return await window.api.configManagerCall('setConfig', newConfig) + }, + async refreshConfig() { + return await window.api.configManagerCall('refreshConfig') + }, + } +} diff --git a/src/core/IpcAdapter.js b/src/core/IpcAdapter.js index b2fb5bc..3c7c5b8 100644 --- a/src/core/IpcAdapter.js +++ b/src/core/IpcAdapter.js @@ -4,22 +4,22 @@ export default class IpcAdapter { } async init() { - return await window.adapter.call('init') + return await window.api.adapterCall('init') } async getAll() { - return await window.adapter.call('getAll') + return await window.api.adapterCall('getAll') } async create(note) { - return await window.adapter.call('create', note) + return await window.api.adapterCall('create', note) } async update(note) { - return await window.adapter.call('update', note) + return await window.api.adapterCall('update', note) } async delete(id) { - return await window.adapter.call('delete', id) + return await window.api.adapterCall('delete', id) } } diff --git a/src/main/NodeStorage.js b/src/core/NodeStorage.js similarity index 100% rename from src/main/NodeStorage.js rename to src/core/NodeStorage.js diff --git a/src/core/PluginManager.js b/src/core/PluginManager.js index 5b3d47e..b9c9765 100644 --- a/src/core/PluginManager.js +++ b/src/core/PluginManager.js @@ -1,16 +1,8 @@ -import PluginRegistry from './PluginRegistry.js' -import IpcAdapter from './IpcAdapter.js' +import IpcAdapter from './IpcAdapter' -let registry = null -let activePluginId = null -let adapter = null - -export default function createPluginManager(environment, plugins = []) { - registry = new PluginRegistry(environment) - - for (const plugin of plugins) { - registry.register(plugin) - } +export const createPluginManager = (registry) => { + let activePluginId = null + let adapter = null return { listPlugins() { @@ -24,10 +16,6 @@ export default function createPluginManager(environment, plugins = []) { getAdapter(pluginId, adapterConfig = {}) { const plugin = registry.get(pluginId) - if (environment === 'electron') { - return new IpcAdapter() - } - if (!plugin) { throw new Error(`Plugin not found: ${pluginId}`) } @@ -41,6 +29,19 @@ export default function createPluginManager(environment, plugins = []) { return adapter }, + async testPlugin(pluginId, adapterConfig = {}) { + const plugin = registry.get(pluginId) + + if (!plugin) { + throw new Error(`Plugin not found: ${pluginId}`) + } + + const adapter = this.getAdapter(pluginId, adapterConfig) + await adapter.init() + + return await adapter.testConnection() + }, + getActiveAdapter() { return adapter }, @@ -48,9 +49,30 @@ export default function createPluginManager(environment, plugins = []) { getActivePluginId() { return activePluginId }, + } +} - getEnvironment() { - return environment +// Client for calling manager through IPC +export const createPluginManagerClient = () => { + return { + listPlugins() { + return window.api.pluginManagerCall('listPlugins') + }, + + getAdapter(pluginId, config) { + return new IpcAdapter(pluginId, config) + }, + + setActivePlugin(pluginId, adapterConfig) { + return window.api.pluginManagerCall( + 'setActivePlugin', + pluginId, + adapterConfig, + ) + }, + + async testPlugin(id, config) { + return window.api.pluginManagerCall('testPlugin', { id, config }) }, } } diff --git a/src/core/PluginRegistry.js b/src/core/PluginRegistry.js index 064fe60..9a681fe 100644 --- a/src/core/PluginRegistry.js +++ b/src/core/PluginRegistry.js @@ -1,7 +1,6 @@ export default class PluginRegistry { - constructor(environment = 'web') { + constructor() { this.plugins = new Map() - this.environment = environment } register(plugin) { @@ -9,11 +8,6 @@ export default class PluginRegistry { throw new Error('Plugin must have an id') } - const environments = plugin.environments || ['electron', 'web'] - if (!environments.includes(this.environment)) { - return - } - this.plugins.set(plugin.id, plugin) } diff --git a/src/core/Config.js b/src/core/WebStorage.js similarity index 51% rename from src/core/Config.js rename to src/core/WebStorage.js index a57e482..f25f2f1 100644 --- a/src/core/Config.js +++ b/src/core/WebStorage.js @@ -1,3 +1,4 @@ +const USER_DATA_STRING = '__DEFAULT_USER_DATA__' const DB_NAME = 'takerofnotes' const DB_VERSION = 1 const STORE_NAME = 'config' @@ -53,7 +54,7 @@ async function saveToDB(data) { }) } -function createIndexedDBStorage() { +export function createWebStorage() { return { async load() { const stored = await getFromDB() @@ -65,84 +66,4 @@ function createIndexedDBStorage() { } } -function createIpcStorage() { - return { - async load() { - return await window.api.getConfig() - }, - async save(data) { - await window.api.setConfig(data) - }, - } -} - -function getDefaultConfig() { - return { - activeAdapter: 'supabase', - theme: 'dark', - } -} - -let config = null -let configResolve = null -const configPromise = new Promise((resolve) => { - configResolve = resolve -}) - -export function createConfigManager(environment, customStorage = null) { - let storage - - if (customStorage) { - storage = customStorage - } else if (environment === 'electron') { - storage = createIpcStorage() - } else { - storage = createIndexedDBStorage() - } - - const loadConfig = async () => { - if (config !== null) { - return config - } - - const stored = await storage.load() - config = stored || getDefaultConfig() - if (!stored) { - await storage.save(config) - } - - configResolve(config) - return config - } - - const writeConfig = async (newConfig) => { - await storage.save(newConfig) - config = newConfig - } - - const getConfig = () => { - if (config !== null) { - return config - } - return configPromise - } - - const setConfig = async (newConfig) => { - config = newConfig - await writeConfig(newConfig) - } - - const refreshConfig = async () => { - config = await storage.load() - configResolve(config) - } - - return { - loadConfig, - getConfig, - setConfig, - refreshConfig, - } -} - -export default createConfigManager +export default createWebStorage diff --git a/src/core/index.js b/src/core/index.js index 6b06702..3c7b9d0 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -1,5 +1,14 @@ -import createPluginManager from './PluginManager.js' -import createConfigManager from './Config.js' +import filesystemPlugin from '@takerofnotes/plugin-filesystem' +import { + createPluginManager, + createPluginManagerClient, +} from './PluginManager.js' +import { createWebStorage } from './WebStorage.js' +import PluginRegistry from './PluginRegistry.js' +import { + createConfigManager, + createConfigManagerClient, +} from './ConfigManager.js' import NotesAPI from './NotesAPI.js' const generateEncryptionKey = () => { @@ -10,12 +19,46 @@ const generateEncryptionKey = () => { .join('') } -let coreInstance = null +const initPluginManager = (runtime, plugins, config) => { + if (runtime === 'electron-renderer') return createPluginManagerClient() -export function initializeCore(environment, plugins = []) { - const pluginManager = createPluginManager(environment, plugins) - const configManager = createConfigManager(environment) + const registry = new PluginRegistry() + for (const plugin of plugins) { + registry.register(plugin) + } + + const manager = createPluginManager(registry) + manager.setActivePlugin( + config.activeAdapter, + config.adapters[config.activeAdapter], + ) + + return manager +} + +const initConfigManager = async (runtime) => { + if (runtime === 'electron-renderer') return createConfigManagerClient() + + let storage + if (runtime === 'electron-main') { + const { createNodeStorage } = await import('./NodeStorage.js') + storage = createNodeStorage(filesystemPlugin) + } else if (runtime === 'web') { + storage = createWebStorage() + } + + return createConfigManager(storage) +} + +export const initializeCore = async (runtime, { plugins }) => { + const configManager = await initConfigManager(runtime) + const config = await configManager.loadConfig() + const pluginManager = initPluginManager(runtime, plugins, config) + + // ------------------------- + // NotesAPI bootstrap + // ------------------------- let notesAPI = null let initPromise = null @@ -30,12 +73,15 @@ export function initializeCore(environment, plugins = []) { if (!encryptionKey) { encryptionKey = generateEncryptionKey() - await configManager.setConfig({ ...config, encryptionKey }) + await configManager.setConfig({ + ...config, + encryptionKey, + }) } const pluginId = config?.activeAdapter || 'filesystem' - const adapterConfig = - config.adapters?.[config.activeAdapter] || {} + const adapterConfig = config?.adapters?.[pluginId] || {} + const adapter = pluginManager.getAdapter( pluginId, adapterConfig, @@ -51,27 +97,10 @@ export function initializeCore(environment, plugins = []) { return initPromise } - coreInstance = { - environment, + return { + runtime, pluginManager, configManager, getNotesAPI, } - - return coreInstance } - -export function getCore() { - if (!coreInstance) { - throw new Error('Core not initialized. Call initializeCore() first.') - } - return coreInstance -} - -export { default as PluginRegistry } from './PluginRegistry.js' -export { default as IpcAdapter } from './IpcAdapter.js' -export { default as NotesAPI } from './NotesAPI.js' -export { - default as createConfigManager, - createConfigManager as createConfig, -} from './Config.js' diff --git a/src/core/types.ts b/src/core/types.ts deleted file mode 100644 index e8fd446..0000000 --- a/src/core/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ENVIRONMENTS { - ELECTRON = 'electron', - WEB = 'web', -} diff --git a/src/main/index.js b/src/main/index.js index 9aa21f3..8afddb8 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -3,8 +3,7 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { app, shell, BrowserWindow, ipcMain } from 'electron' import filesystemPlugin from '@takerofnotes/plugin-filesystem' import supabasePlugin from '@takerofnotes/plugin-supabase' -import { PluginRegistry, createConfigManager } from '../core/index.js' -import { createNodeStorage } from './NodeStorage.js' +import { initializeCore } from '../core/index.js' import { join } from 'path' const DEFAULT_WINDOW_SIZE = { width: 354, height: 549 } @@ -13,8 +12,6 @@ const DEFAULT_MOVE_WINDOW_SIZE = { width: 708, height: 549 } const preloadPath = join(__dirname, '../preload/index.mjs') const rendererPath = join(__dirname, '../renderer/index.html') -let activeAdapter = null - function createWindow() { const mainWindow = new BrowserWindow({ width: DEFAULT_WINDOW_SIZE.width, @@ -78,55 +75,32 @@ app.whenReady().then(async () => { }) } - const registry = new PluginRegistry('electron') - registry.register(filesystemPlugin) - registry.register(supabasePlugin) + const { pluginManager, configManager } = await initializeCore( + 'electron-main', + { + plugins: [filesystemPlugin, supabasePlugin], + }, + ) - const nodeStorage = createNodeStorage(filesystemPlugin) - const configManager = createConfigManager('electron', nodeStorage) - const initialConfig = await configManager.loadConfig() + ipcMain.handle('pluginManager:call', async (_, method, ...args) => { + const methodCall = await pluginManager[method](...args) - const setActivePlugin = async (pluginId) => { - const currentConfig = await configManager.loadConfig() - await configManager.setConfig({ - ...currentConfig, - activeAdapter: pluginId, - }) + if (method === 'setActivePlugin') { + broadcastNoteChange('plugin-changed') + } - const plugin = registry.get(pluginId) - const adapterConfig = currentConfig.adapters[pluginId] || {} - activeAdapter = plugin.createAdapter(adapterConfig) - - await activeAdapter.init() - - ipcMain.removeHandler('adapter:call') - ipcMain.handle('adapter:call', async (_, method, args) => { - if (!activeAdapter[method]) { - throw new Error(`Invalid adapter method: ${method}`) - } - - return await activeAdapter[method](...args) - }) - - broadcastNoteChange('plugin-changed', pluginId) - - return true - } - - await setActivePlugin(initialConfig.activeAdapter) - - ipcMain.handle('getConfig', async () => { - return await configManager.loadConfig() + return methodCall }) - ipcMain.handle('setConfig', async (_, newConfig) => { - await configManager.setConfig(newConfig) + ipcMain.handle('configManager:call', async (_, method, ...args) => { + return await configManager[method](...args) }) + ipcMain.handle('adapter:call', async (_, method, ...args) => { + const adapter = pluginManager.getActiveAdapter() + if (!adapter[method]) { + throw new Error(`Invalid adapter method: ${method}`) + } - ipcMain.handle('listPlugins', async () => { - return registry.list() - }) - ipcMain.handle('setActivePlugin', async (_, pluginId) => { - return await setActivePlugin(pluginId) + return await adapter[method](...args) }) ipcMain.on('note-changed', (_, event, data) => { diff --git a/src/preload/index.js b/src/preload/index.js index 8a7c5ba..c5d042a 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -2,11 +2,24 @@ import { contextBridge, ipcRenderer } from 'electron' // Custom APIs for renderer const api = { - getConfig: () => ipcRenderer.invoke('getConfig'), - setConfig: (config) => ipcRenderer.invoke('setConfig', config), - listPlugins: () => ipcRenderer.invoke('listPlugins'), - setActivePlugin: (pluginId) => - ipcRenderer.invoke('setActivePlugin', pluginId), + pluginManagerCall: (method, ...args) => + ipcRenderer.invoke( + 'pluginManager:call', + method, + ...(args.length ? args : []), + ), + configManagerCall: (method, ...args) => + ipcRenderer.invoke( + 'configManager:call', + method, + ...(args.length ? args : []), + ), + adapterCall: (method, ...args) => + ipcRenderer.invoke( + 'adapter:call', + method, + ...(args.length ? args : []), + ), openNoteWindow: (noteId) => { ipcRenderer.send('open-note-window', noteId) }, @@ -33,19 +46,12 @@ const api = { }, } -// Implement adapter API - communicates with plugin adapter in main process -const adapter = { - call: (method, ...args) => ipcRenderer.invoke('adapter:call', method, args), -} - if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('api', api) - contextBridge.exposeInMainWorld('adapter', adapter) } catch (error) { console.error(error) } } else { window.api = api - window.adapter = adapter } diff --git a/src/renderer/index.html b/src/renderer/index.html index 2364bd2..ff85577 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -2,12 +2,12 @@ - Electron + Taker of Notes - + />--> diff --git a/src/renderer/src/components/NoteRow.vue b/src/renderer/src/components/NoteRow.vue index 0a07cb4..7a07cb7 100644 --- a/src/renderer/src/components/NoteRow.vue +++ b/src/renderer/src/components/NoteRow.vue @@ -43,7 +43,6 @@ const onMoveOpened = async () => { move: props.note.id, }, }) - console.log(route.query) } const moveActive = computed(() => route.query.move === props.note.id) diff --git a/src/renderer/src/composables/useConfig.js b/src/renderer/src/composables/useConfig.js index 8315fe2..7c2dbd5 100644 --- a/src/renderer/src/composables/useConfig.js +++ b/src/renderer/src/composables/useConfig.js @@ -1,5 +1,5 @@ import { ref, watch, toRaw, onMounted } from 'vue' -import { getCore } from '@core/index.js' +import useCore from '@/composables/useCore' const config = ref() let configResolve = null @@ -8,8 +8,7 @@ const configPromise = new Promise((resolve) => { }) export default () => { - const core = getCore() - const { configManager } = core + const { configManager } = useCore() onMounted(async () => { if (config.value) { @@ -17,8 +16,7 @@ export default () => { return } - const loadedConfig = await configManager.loadConfig() - config.value = loadedConfig + config.value = await configManager.loadConfig() configResolve() }) diff --git a/src/renderer/src/composables/useCore.js b/src/renderer/src/composables/useCore.js new file mode 100644 index 0000000..e28c5b5 --- /dev/null +++ b/src/renderer/src/composables/useCore.js @@ -0,0 +1,11 @@ +import { inject } from 'vue' + +let core + +export default () => { + if (!core) { + core = inject('core') + } + + return core +} diff --git a/src/renderer/src/composables/useNotes.js b/src/renderer/src/composables/useNotes.js index 1c7210d..bcdfb89 100644 --- a/src/renderer/src/composables/useNotes.js +++ b/src/renderer/src/composables/useNotes.js @@ -41,7 +41,9 @@ const setupListeners = () => { } const broadcastChange = (event, data) => { - window.api.notifyNoteChanged(event, data) + if (environment === 'electron') { + window.api.notifyNoteChanged(event, data) + } } if (environment === 'electron') { diff --git a/src/renderer/src/composables/usePlugins.js b/src/renderer/src/composables/usePlugins.js index e2fe2f8..704dc7b 100644 --- a/src/renderer/src/composables/usePlugins.js +++ b/src/renderer/src/composables/usePlugins.js @@ -1,20 +1,27 @@ -import { ref } from 'vue' +import useCore from '@/composables/useCore' import useConfig from './useConfig' +import { ref } from 'vue' export default async () => { - const { refreshConfig } = useConfig() + const { config } = useConfig() + const { pluginManager } = useCore() const plugins = ref([]) - plugins.value = await window.api.listPlugins() + plugins.value = await pluginManager.listPlugins() - const setActivePlugin = async (pluginId) => { - await window.api.setActivePlugin(pluginId) - await refreshConfig() + const setActivePlugin = async (pluginId, activeConfig = {}) => { + await pluginManager.setActivePlugin(pluginId, { ...activeConfig }) + config.value.activeAdapter = pluginId + } + + const testPlugin = async (pluginId, config = {}) => { + await pluginManager.testPlugin(pluginId, { ...config }) } return { plugins, setActivePlugin, + testPlugin, } } diff --git a/src/renderer/src/libs/getNotesAPI.js b/src/renderer/src/libs/getNotesAPI.js index e81e166..ce9a26d 100644 --- a/src/renderer/src/libs/getNotesAPI.js +++ b/src/renderer/src/libs/getNotesAPI.js @@ -1,4 +1,4 @@ -import { getCore } from '@core/index.js' +import useCore from '@/composables/useCore' let notesAPI = null let initPromise = null @@ -8,8 +8,8 @@ export const getNotesAPI = async () => { if (!initPromise) { initPromise = (async () => { - const core = getCore() - notesAPI = await core.getNotesAPI() + const { getNotesAPI } = useCore() + notesAPI = await getNotesAPI() return notesAPI })() } diff --git a/src/renderer/src/main.js b/src/renderer/src/main.js index 9ff2615..d99e4a3 100644 --- a/src/renderer/src/main.js +++ b/src/renderer/src/main.js @@ -6,7 +6,7 @@ import { router } from './plugins/router' const app = createApp(App) -app.use(initCore) +await initCore(app) app.use(router) app.mount('#app') diff --git a/src/renderer/src/plugins/core.js b/src/renderer/src/plugins/core.js index 96af256..e1214ac 100644 --- a/src/renderer/src/plugins/core.js +++ b/src/renderer/src/plugins/core.js @@ -2,11 +2,18 @@ import { useEnvironment } from '@/composables/useEnvironment' import { initializeCore } from '@core/index.js' import supabasePlugin from '@takerofnotes/plugin-supabase' -export const initCore = () => { +export const initCore = async (app) => { const environment = useEnvironment() + // Set runtime + const runtime = environment === 'electron' ? 'electron-renderer' : 'web' + // Plugins that are valid for web (electron uses IPC) const plugins = [supabasePlugin] - initializeCore(environment, plugins) + const core = await initializeCore(runtime, { + plugins, + }) + + app.provide('core', core) } diff --git a/src/renderer/src/views/Directory.vue b/src/renderer/src/views/Directory.vue index ce62ea7..00eb05a 100644 --- a/src/renderer/src/views/Directory.vue +++ b/src/renderer/src/views/Directory.vue @@ -19,8 +19,6 @@