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: {