From eea1cccf16942f2d38e5f18dfe0d8757b91a9ae4 Mon Sep 17 00:00:00 2001 From: nicwands Date: Thu, 19 Mar 2026 17:25:43 -0400 Subject: [PATCH] total restructure: isolate functionality to core so web works --- electron.vite.config.mjs | 1 + out/main/index.js | 222 ++++++++++++------ out/renderer/index.html | 4 +- src/core/Config.js | 148 ++++++++++++ .../src/libs => }/core/IpcAdapter.js | 0 src/{renderer/src/libs => }/core/NotesAPI.js | 46 +++- src/core/PluginManager.js | 56 +++++ src/{main => }/core/PluginRegistry.js | 8 +- src/core/index.js | 77 ++++++ src/core/types.ts | 4 + src/main/NodeStorage.js | 79 +++++++ src/main/core/Config.js | 86 ------- src/main/index.js | 47 ++-- src/renderer/src/composables/useConfig.js | 14 +- src/renderer/src/composables/useNotes.js | 9 +- src/renderer/src/libs/core/getNotesAPI.js | 47 ---- src/renderer/src/libs/getNotesAPI.js | 18 ++ src/renderer/src/main.js | 3 +- src/renderer/src/plugins/core.js | 12 + vite.config.js | 1 + 20 files changed, 635 insertions(+), 247 deletions(-) create mode 100644 src/core/Config.js rename src/{renderer/src/libs => }/core/IpcAdapter.js (100%) rename src/{renderer/src/libs => }/core/NotesAPI.js (84%) create mode 100644 src/core/PluginManager.js rename src/{main => }/core/PluginRegistry.js (70%) create mode 100644 src/core/index.js create mode 100644 src/core/types.ts create mode 100644 src/main/NodeStorage.js delete mode 100644 src/main/core/Config.js delete mode 100644 src/renderer/src/libs/core/getNotesAPI.js create mode 100644 src/renderer/src/libs/getNotesAPI.js create mode 100644 src/renderer/src/plugins/core.js diff --git a/electron.vite.config.mjs b/electron.vite.config.mjs index f047e15..08cc29e 100644 --- a/electron.vite.config.mjs +++ b/electron.vite.config.mjs @@ -10,6 +10,7 @@ export default defineConfig({ resolve: { alias: { '@': resolve('src/renderer/src'), + '@core': resolve('src/core'), }, }, }, diff --git a/out/main/index.js b/out/main/index.js index d107034..a6b22de 100644 --- a/out/main/index.js +++ b/out/main/index.js @@ -3,6 +3,9 @@ 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 __cjs_mod__ from "node:module"; @@ -10,13 +13,18 @@ const __filename = import.meta.filename; const __dirname = import.meta.dirname; const require2 = __cjs_mod__.createRequire(import.meta.url); class PluginRegistry { - constructor() { + constructor(environment = "web") { 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) { @@ -31,72 +39,146 @@ class PluginRegistry { })); } } -const USER_DATA_STRING = "__DEFAULT_USER_DATA__"; -class Config { - constructor(defaultPlugin) { - this.defaultPlugin = defaultPlugin; - this.configPath = path.join(app.getPath("userData"), "config.json"); - } - // Helper to replace placeholders with dynamic values - _resolveDefaults(config) { - if (Array.isArray(config)) { - return config.map((item) => this._resolveDefaults(item)); - } else if (config && typeof config === "object") { - const resolved = {}; - for (const [key, value] of Object.entries(config)) { - resolved[key] = this._resolveDefaults(value); +function createIpcStorage() { + return { + async load() { + return await window.api.getConfig(); + }, + async save(data) { + await window.api.setConfig(data); + } + }; +} +function getDefaultConfig() { + return { + activeAdapter: "supabase", + adapters: { + supabase: { + supabaseUrl: "https://example.supabase.co", + supabaseKey: "", + bucket: "notes" } - return resolved; - } else if (typeof config === "string" && config.includes(USER_DATA_STRING)) { - return config.replace(USER_DATA_STRING, app.getPath("userData")); - } else { + }, + 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; } - } - async load() { - let parsed; - try { - const raw = await fs.readFile(this.configPath, "utf8"); - parsed = JSON.parse(raw); - } catch (err) { - parsed = null; + const stored = await storage.load(); + config = stored || getDefaultConfig(); + if (!stored) { + await storage.save(config); } - if (!parsed || !parsed.activeAdapter) { - const defaultConfig = {}; - for (const field of this.defaultPlugin.configSchema) { - defaultConfig[field.key] = field.default ?? null; + 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 + }; +} +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); } - parsed = { - ...parsed ? parsed : {}, - activeAdapter: this.defaultPlugin.id, - adapters: {} - }; - parsed.adapters[this.defaultPlugin.id] = defaultConfig; - parsed[theme] = "dark"; - await this.write(parsed); - } else { - parsed.adapters = this._resolveDefaults(parsed.adapters); + return resolved; + } else if (typeof obj === "string" && obj.includes(USER_DATA_STRING)) { + return obj.replace(USER_DATA_STRING, app.getPath("userData")); } - return parsed; - } - async write(configObject) { - const dir = path.dirname(this.configPath); - await fs.mkdir(dir, { recursive: true }); - const resolvedConfig = { - ...configObject, - adapters: this._resolveDefaults(configObject.adapters) - }; - await fs.writeFile( - this.configPath, - JSON.stringify(resolvedConfig, null, 2), - "utf8" - ); + 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; + }, + 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" + ); + } + }; } 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, @@ -152,34 +234,38 @@ app.whenReady().then(async () => { win.webContents.send(event, data); }); }; - const registry = new PluginRegistry(); + const registry = new PluginRegistry("electron"); registry.register(filesystemPlugin); registry.register(supabasePlugin); - const config = new Config(filesystemPlugin); - const initialConfig = await config.load(); + const nodeStorage = createNodeStorage(filesystemPlugin); + const configManager = createConfigManager("electron", nodeStorage); + const initialConfig = await configManager.loadConfig(); const setActivePlugin = async (pluginId) => { - const currentConfig = await config.load(); - await config.write({ ...currentConfig, activeAdapter: pluginId }); + const currentConfig = await configManager.loadConfig(); + await configManager.setConfig({ + ...currentConfig, + activeAdapter: pluginId + }); const plugin = registry.get(pluginId); const adapterConfig = currentConfig.adapters[pluginId] || {}; - const adapter = plugin.createAdapter(adapterConfig); - await adapter.init(); + activeAdapter = plugin.createAdapter(adapterConfig); + await activeAdapter.init(); ipcMain.removeHandler("adapter:call"); ipcMain.handle("adapter:call", async (_, method, args) => { - if (!adapter[method]) { + if (!activeAdapter[method]) { throw new Error(`Invalid adapter method: ${method}`); } - return await adapter[method](...args); + return await activeAdapter[method](...args); }); broadcastNoteChange("plugin-changed", pluginId); return true; }; await setActivePlugin(initialConfig.activeAdapter); ipcMain.handle("getConfig", async () => { - return await config.load(); + return await configManager.loadConfig(); }); ipcMain.handle("setConfig", async (_, newConfig) => { - await config.write(newConfig); + await configManager.setConfig(newConfig); }); ipcMain.handle("listPlugins", async () => { return registry.list(); @@ -211,8 +297,8 @@ app.whenReady().then(async () => { } }); electronApp.setAppUserModelId("com.electron"); - app.on("browser-window-created", (_, window) => { - optimizer.watchWindowShortcuts(window); + app.on("browser-window-created", (_, window2) => { + optimizer.watchWindowShortcuts(window2); }); createWindow(); app.on("activate", function() { diff --git a/out/renderer/index.html b/out/renderer/index.html index 26bff44..c1b7715 100644 --- a/out/renderer/index.html +++ b/out/renderer/index.html @@ -2,13 +2,13 @@ - Taker of Notes + Electron - + diff --git a/src/core/Config.js b/src/core/Config.js new file mode 100644 index 0000000..a57e482 --- /dev/null +++ b/src/core/Config.js @@ -0,0 +1,148 @@ +const DB_NAME = 'takerofnotes' +const DB_VERSION = 1 +const STORE_NAME = 'config' +const CONFIG_KEY = 'app_config' + +let db = null + +function openDB() { + return new Promise((resolve, reject) => { + if (db) { + resolve(db) + return + } + + 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.result + if (!database.objectStoreNames.contains(STORE_NAME)) { + database.createObjectStore(STORE_NAME, { keyPath: 'id' }) + } + } + }) +} + +async function getFromDB() { + const database = await openDB() + return new Promise((resolve, reject) => { + const transaction = database.transaction(STORE_NAME, 'readonly') + const store = transaction.objectStore(STORE_NAME) + const request = store.get(CONFIG_KEY) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve(request.result?.data || null) + }) +} + +async function saveToDB(data) { + const database = await openDB() + return new Promise((resolve, reject) => { + const transaction = database.transaction(STORE_NAME, 'readwrite') + const store = transaction.objectStore(STORE_NAME) + const request = store.put({ id: CONFIG_KEY, data }) + + request.onerror = () => reject(request.error) + request.onsuccess = () => resolve() + }) +} + +function createIndexedDBStorage() { + return { + async load() { + const stored = await getFromDB() + return stored || null + }, + async save(data) { + await saveToDB(data) + }, + } +} + +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 diff --git a/src/renderer/src/libs/core/IpcAdapter.js b/src/core/IpcAdapter.js similarity index 100% rename from src/renderer/src/libs/core/IpcAdapter.js rename to src/core/IpcAdapter.js diff --git a/src/renderer/src/libs/core/NotesAPI.js b/src/core/NotesAPI.js similarity index 84% rename from src/renderer/src/libs/core/NotesAPI.js rename to src/core/NotesAPI.js index 95fac5b..a072f10 100644 --- a/src/renderer/src/libs/core/NotesAPI.js +++ b/src/core/NotesAPI.js @@ -1,7 +1,6 @@ import sodium from 'libsodium-wrappers' import { v4 as uuidv4 } from 'uuid' import { Index } from 'flexsearch' -import * as uint from '@/libs/uint' export default class NotesAPI { constructor(adapter, encryptionKey = null) { @@ -26,12 +25,44 @@ export default class NotesAPI { } } + _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 = uint.hexToUint8Array(this.encryptionKey) + const key = this._hexToUint8Array(this.encryptionKey) if (key.length !== 32) { throw new Error( 'Encryption key must be 64 hex characters (32 bytes)', @@ -47,8 +78,8 @@ export default class NotesAPI { key, ) - const combined = uint.concatUint8Arrays(nonce, ciphertext) - return uint.uint8ArrayToBase64(combined) + const combined = this._concatUint8Arrays(nonce, ciphertext) + return this._uint8ArrayToBase64(combined) } _decrypt(encryptedData) { @@ -56,7 +87,7 @@ export default class NotesAPI { throw new Error('Encryption key not set') } - const key = uint.hexToUint8Array(this.encryptionKey) + const key = this._hexToUint8Array(this.encryptionKey) if (key.length !== 32) { throw new Error( 'Encryption key must be 64 hex characters (32 bytes)', @@ -65,7 +96,7 @@ export default class NotesAPI { let combined try { - combined = uint.base64ToUint8Array(encryptedData) + combined = this._base64ToUint8Array(encryptedData) } catch (e) { throw new Error('Invalid encrypted data: not valid base64') } @@ -141,9 +172,6 @@ export default class NotesAPI { return extractText(content) } - /* ----------------------- - Public API - ------------------------*/ getCategories() { const categories = new Set() diff --git a/src/core/PluginManager.js b/src/core/PluginManager.js new file mode 100644 index 0000000..5b3d47e --- /dev/null +++ b/src/core/PluginManager.js @@ -0,0 +1,56 @@ +import PluginRegistry from './PluginRegistry.js' +import IpcAdapter from './IpcAdapter.js' + +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) + } + + return { + listPlugins() { + return registry.list() + }, + + getPlugin(pluginId) { + return registry.get(pluginId) + }, + + getAdapter(pluginId, adapterConfig = {}) { + const plugin = registry.get(pluginId) + + if (environment === 'electron') { + return new IpcAdapter() + } + + 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 + }, + + getActiveAdapter() { + return adapter + }, + + getActivePluginId() { + return activePluginId + }, + + getEnvironment() { + return environment + }, + } +} diff --git a/src/main/core/PluginRegistry.js b/src/core/PluginRegistry.js similarity index 70% rename from src/main/core/PluginRegistry.js rename to src/core/PluginRegistry.js index 9a681fe..064fe60 100644 --- a/src/main/core/PluginRegistry.js +++ b/src/core/PluginRegistry.js @@ -1,6 +1,7 @@ export default class PluginRegistry { - constructor() { + constructor(environment = 'web') { this.plugins = new Map() + this.environment = environment } register(plugin) { @@ -8,6 +9,11 @@ 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/index.js b/src/core/index.js new file mode 100644 index 0000000..6b06702 --- /dev/null +++ b/src/core/index.js @@ -0,0 +1,77 @@ +import createPluginManager from './PluginManager.js' +import createConfigManager from './Config.js' +import NotesAPI from './NotesAPI.js' + +const generateEncryptionKey = () => { + const array = new Uint8Array(32) + crypto.getRandomValues(array) + return Array.from(array) + .map((b) => b.toString(16).padStart(2, '0')) + .join('') +} + +let coreInstance = null + +export function initializeCore(environment, plugins = []) { + const pluginManager = createPluginManager(environment, plugins) + const configManager = createConfigManager(environment) + + let notesAPI = null + let initPromise = null + + const getNotesAPI = async () => { + if (notesAPI) return notesAPI + + if (!initPromise) { + initPromise = (async () => { + const config = await configManager.loadConfig() + + let encryptionKey = config?.encryptionKey + + if (!encryptionKey) { + encryptionKey = generateEncryptionKey() + await configManager.setConfig({ ...config, encryptionKey }) + } + + const pluginId = config?.activeAdapter || 'filesystem' + const adapterConfig = + config.adapters?.[config.activeAdapter] || {} + const adapter = pluginManager.getAdapter( + pluginId, + adapterConfig, + ) + + notesAPI = new NotesAPI(adapter, encryptionKey) + await notesAPI.init() + + return notesAPI + })() + } + + return initPromise + } + + coreInstance = { + environment, + 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 new file mode 100644 index 0000000..e8fd446 --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,4 @@ +export enum ENVIRONMENTS { + ELECTRON = 'electron', + WEB = 'web', +} diff --git a/src/main/NodeStorage.js b/src/main/NodeStorage.js new file mode 100644 index 0000000..3c2af97 --- /dev/null +++ b/src/main/NodeStorage.js @@ -0,0 +1,79 @@ +import fs from 'fs/promises' +import path from 'path' +import { app } from 'electron' + +const USER_DATA_STRING = '__DEFAULT_USER_DATA__' + +export 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) + } + 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 + }, + + 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', + ) + }, + } +} + +export default createNodeStorage diff --git a/src/main/core/Config.js b/src/main/core/Config.js deleted file mode 100644 index 57abdfd..0000000 --- a/src/main/core/Config.js +++ /dev/null @@ -1,86 +0,0 @@ -import fs from 'fs/promises' -import path from 'path' -import { app } from 'electron' - -const USER_DATA_STRING = '__DEFAULT_USER_DATA__' - -export default class Config { - constructor(defaultPlugin) { - this.defaultPlugin = defaultPlugin - this.configPath = path.join(app.getPath('userData'), 'config.json') - } - - // Helper to replace placeholders with dynamic values - _resolveDefaults(config) { - if (Array.isArray(config)) { - return config.map((item) => this._resolveDefaults(item)) - } else if (config && typeof config === 'object') { - const resolved = {} - for (const [key, value] of Object.entries(config)) { - resolved[key] = this._resolveDefaults(value) - } - return resolved - } else if ( - typeof config === 'string' && - config.includes(USER_DATA_STRING) - ) { - return config.replace(USER_DATA_STRING, app.getPath('userData')) - } else { - return config - } - } - - async load() { - let parsed - - try { - const raw = await fs.readFile(this.configPath, 'utf8') - parsed = JSON.parse(raw) - } catch (err) { - parsed = null - } - - if (!parsed || !parsed.activeAdapter) { - const defaultConfig = {} - - for (const field of this.defaultPlugin.configSchema) { - defaultConfig[field.key] = field.default ?? null - } - - parsed = { - ...(parsed ? parsed : {}), - activeAdapter: this.defaultPlugin.id, - adapters: {}, - } - parsed.adapters[this.defaultPlugin.id] = defaultConfig - - parsed[theme] = 'dark' - - await this.write(parsed) - } else { - // Ensure any "__DEFAULT_USER_DATA__" values are resolved on load - parsed.adapters = this._resolveDefaults(parsed.adapters) - } - - return parsed - } - - async write(configObject) { - const dir = path.dirname(this.configPath) - - // Ensure directory exists - await fs.mkdir(dir, { recursive: true }) - - // Resolve defaults before writing - const resolvedConfig = { - ...configObject, - adapters: this._resolveDefaults(configObject.adapters), - } - - await fs.writeFile( - this.configPath, - JSON.stringify(resolvedConfig, null, 2), - 'utf8', - ) - } -} diff --git a/src/main/index.js b/src/main/index.js index 10bda5c..9aa21f3 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -3,8 +3,8 @@ 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 from './core/PluginRegistry.js' -import Config from './core/Config.js' +import { PluginRegistry, createConfigManager } from '../core/index.js' +import { createNodeStorage } from './NodeStorage.js' import { join } from 'path' const DEFAULT_WINDOW_SIZE = { width: 354, height: 549 } @@ -13,7 +13,8 @@ const DEFAULT_MOVE_WINDOW_SIZE = { width: 708, height: 549 } const preloadPath = join(__dirname, '../preload/index.mjs') const rendererPath = join(__dirname, '../renderer/index.html') -// Main window +let activeAdapter = null + function createWindow() { const mainWindow = new BrowserWindow({ width: DEFAULT_WINDOW_SIZE.width, @@ -42,7 +43,6 @@ function createWindow() { } } -// Open note in new window function createNoteWindow(noteId) { const noteWindow = new BrowserWindow({ width: DEFAULT_WINDOW_SIZE.width, @@ -68,48 +68,44 @@ function createNoteWindow(noteId) { } app.whenReady().then(async () => { - // Open note in new window ipcMain.on('open-note-window', (_, noteId) => { createNoteWindow(noteId) }) - // Broadcast note changes to all windows const broadcastNoteChange = (event, data) => { BrowserWindow.getAllWindows().forEach((win) => { win.webContents.send(event, data) }) } - // Create plugin registry - const registry = new PluginRegistry() - - // Register built-in plugins + const registry = new PluginRegistry('electron') registry.register(filesystemPlugin) registry.register(supabasePlugin) - // Pull plugin config - const config = new Config(filesystemPlugin) - const initialConfig = await config.load() + const nodeStorage = createNodeStorage(filesystemPlugin) + const configManager = createConfigManager('electron', nodeStorage) + const initialConfig = await configManager.loadConfig() const setActivePlugin = async (pluginId) => { - const currentConfig = await config.load() - await config.write({ ...currentConfig, activeAdapter: pluginId }) + const currentConfig = await configManager.loadConfig() + await configManager.setConfig({ + ...currentConfig, + activeAdapter: pluginId, + }) const plugin = registry.get(pluginId) const adapterConfig = currentConfig.adapters[pluginId] || {} - const adapter = plugin.createAdapter(adapterConfig) + activeAdapter = plugin.createAdapter(adapterConfig) - // Initialize adapter - await adapter.init() + await activeAdapter.init() - // Handle adapter methods via IPC ipcMain.removeHandler('adapter:call') ipcMain.handle('adapter:call', async (_, method, args) => { - if (!adapter[method]) { + if (!activeAdapter[method]) { throw new Error(`Invalid adapter method: ${method}`) } - return await adapter[method](...args) + return await activeAdapter[method](...args) }) broadcastNoteChange('plugin-changed', pluginId) @@ -117,18 +113,15 @@ app.whenReady().then(async () => { return true } - // Set active plugin await setActivePlugin(initialConfig.activeAdapter) - // Get/set config ipcMain.handle('getConfig', async () => { - return await config.load() + return await configManager.loadConfig() }) ipcMain.handle('setConfig', async (_, newConfig) => { - await config.write(newConfig) + await configManager.setConfig(newConfig) }) - // Get/set plugins ipcMain.handle('listPlugins', async () => { return registry.list() }) @@ -136,12 +129,10 @@ app.whenReady().then(async () => { return await setActivePlugin(pluginId) }) - // Handle note change events from renderer ipcMain.on('note-changed', (_, event, data) => { broadcastNoteChange(event, data) }) - // Handle resizing for note "move" functionality ipcMain.handle('move-opened', (_) => { const activeWindow = BrowserWindow.getFocusedWindow() const windowSize = activeWindow.getSize() diff --git a/src/renderer/src/composables/useConfig.js b/src/renderer/src/composables/useConfig.js index 492d849..8315fe2 100644 --- a/src/renderer/src/composables/useConfig.js +++ b/src/renderer/src/composables/useConfig.js @@ -1,4 +1,5 @@ import { ref, watch, toRaw, onMounted } from 'vue' +import { getCore } from '@core/index.js' const config = ref() let configResolve = null @@ -7,19 +8,24 @@ const configPromise = new Promise((resolve) => { }) export default () => { + const core = getCore() + const { configManager } = core + onMounted(async () => { if (config.value) { configResolve() return } - config.value = await window.api.getConfig() + + const loadedConfig = await configManager.loadConfig() + config.value = loadedConfig configResolve() }) watch( config, async (newValue) => { - await window.api.setConfig(toRaw(newValue)) + await configManager.setConfig(toRaw(newValue)) }, { deep: true }, ) @@ -30,7 +36,9 @@ export default () => { } const refreshConfig = async () => { - config.value = await window.api.getConfig() + await configManager.refreshConfig() + const newConfig = await configManager.getConfig() + config.value = newConfig configResolve() } diff --git a/src/renderer/src/composables/useNotes.js b/src/renderer/src/composables/useNotes.js index f9d1728..1c7210d 100644 --- a/src/renderer/src/composables/useNotes.js +++ b/src/renderer/src/composables/useNotes.js @@ -1,6 +1,7 @@ import _omit from 'lodash/omit' import { ref } from 'vue' -import { getNotesAPI } from '@/libs/core/getNotesAPI' +import { getNotesAPI } from '@/libs/getNotesAPI' +import { useEnvironment } from '@/composables/useEnvironment.js' const categories = ref([]) const searchResults = ref([]) @@ -8,6 +9,8 @@ const notesChangeCount = ref(0) let listenersInitialized = false +const environment = useEnvironment() + const setupListeners = () => { if (listenersInitialized || typeof window === 'undefined') return listenersInitialized = true @@ -41,7 +44,9 @@ const broadcastChange = (event, data) => { window.api.notifyNoteChanged(event, data) } -setupListeners() +if (environment === 'electron') { + setupListeners() +} export default () => { /* ------------------------- diff --git a/src/renderer/src/libs/core/getNotesAPI.js b/src/renderer/src/libs/core/getNotesAPI.js deleted file mode 100644 index 5ca7efa..0000000 --- a/src/renderer/src/libs/core/getNotesAPI.js +++ /dev/null @@ -1,47 +0,0 @@ -import NotesAPI from '@/libs/core/NotesAPI.js' -import IpcAdapter from '@/libs/core/IpcAdapter.js' -import useConfig from '@/composables/useConfig.js' - -// Singleton pattern to make sure only one instance of NotesAPI exists -let notesAPI = null -let initPromise = null - -const generateEncryptionKey = () => { - const array = new Uint8Array(32) - crypto.getRandomValues(array) - return Array.from(array) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') -} - -const createInstance = async () => { - const { config, ensureConfig } = useConfig() - await ensureConfig() - - let encryptionKey = config.value?.encryptionKey - - if (!encryptionKey) { - encryptionKey = generateEncryptionKey() - config.value.encryptionKey = encryptionKey - } - - const adapter = new IpcAdapter() - const api = new NotesAPI(adapter, encryptionKey) - - await api.init() - - return api -} - -export const getNotesAPI = async () => { - if (notesAPI) return notesAPI - - if (!initPromise) { - initPromise = createInstance().then((api) => { - notesAPI = api - return api - }) - } - - return initPromise -} diff --git a/src/renderer/src/libs/getNotesAPI.js b/src/renderer/src/libs/getNotesAPI.js new file mode 100644 index 0000000..e81e166 --- /dev/null +++ b/src/renderer/src/libs/getNotesAPI.js @@ -0,0 +1,18 @@ +import { getCore } from '@core/index.js' + +let notesAPI = null +let initPromise = null + +export const getNotesAPI = async () => { + if (notesAPI) return notesAPI + + if (!initPromise) { + initPromise = (async () => { + const core = getCore() + notesAPI = await core.getNotesAPI() + return notesAPI + })() + } + + return initPromise +} diff --git a/src/renderer/src/main.js b/src/renderer/src/main.js index 78cac79..9ff2615 100644 --- a/src/renderer/src/main.js +++ b/src/renderer/src/main.js @@ -1,11 +1,12 @@ import './styles/main.scss' import { createApp } from 'vue' import App from './App.vue' +import { initCore } from './plugins/core' import { router } from './plugins/router' const app = createApp(App) -// Plugins +app.use(initCore) app.use(router) app.mount('#app') diff --git a/src/renderer/src/plugins/core.js b/src/renderer/src/plugins/core.js new file mode 100644 index 0000000..96af256 --- /dev/null +++ b/src/renderer/src/plugins/core.js @@ -0,0 +1,12 @@ +import { useEnvironment } from '@/composables/useEnvironment' +import { initializeCore } from '@core/index.js' +import supabasePlugin from '@takerofnotes/plugin-supabase' + +export const initCore = () => { + const environment = useEnvironment() + + // Plugins that are valid for web (electron uses IPC) + const plugins = [supabasePlugin] + + initializeCore(environment, plugins) +} diff --git a/vite.config.js b/vite.config.js index 47df5db..4b6b374 100644 --- a/vite.config.js +++ b/vite.config.js @@ -9,6 +9,7 @@ export default defineConfig({ resolve: { alias: { '@': fileURLToPath(new URL('./src/renderer/src', import.meta.url)), + '@core': fileURLToPath(new URL('./src/core', import.meta.url)), }, }, server: {