Compare commits
2 Commits
555cfd4a93
...
ad961fd31a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad961fd31a | ||
|
|
a0a5cde33a |
@@ -5,6 +5,7 @@ import filesystemPlugin from "@takerofnotes/plugin-filesystem";
|
|||||||
import supabasePlugin from "@takerofnotes/plugin-supabase";
|
import supabasePlugin from "@takerofnotes/plugin-supabase";
|
||||||
import s3Plugin from "@takerofnotes/plugin-s3";
|
import s3Plugin from "@takerofnotes/plugin-s3";
|
||||||
import postgresPlugin from "@takerofnotes/plugin-postgre-sql";
|
import postgresPlugin from "@takerofnotes/plugin-postgre-sql";
|
||||||
|
import Ajv from "ajv";
|
||||||
import sodium from "libsodium-wrappers";
|
import sodium from "libsodium-wrappers";
|
||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { Index } from "flexsearch";
|
import { Index } from "flexsearch";
|
||||||
@@ -74,13 +75,60 @@ class PluginRegistry {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
||||||
const getDefaultConfig = () => {
|
const getDefaultConfig = () => {
|
||||||
return {
|
return {
|
||||||
activeAdapter: "browser",
|
activeAdapter: "browser",
|
||||||
theme: "dark"
|
theme: "dark"
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const createConfigManager = (storage) => {
|
const CONFIG_SCHEMA = {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
activeAdapter: { type: "string" },
|
||||||
|
theme: { type: "string", enum: ["dark", "light"] },
|
||||||
|
encryptionKey: { type: "string" },
|
||||||
|
adapters: { type: "object" }
|
||||||
|
},
|
||||||
|
required: ["activeAdapter"],
|
||||||
|
additionalProperties: true
|
||||||
|
};
|
||||||
|
const validateConfig = ajv.compile(CONFIG_SCHEMA);
|
||||||
|
const convertSchemaToJson = (schemaArray) => {
|
||||||
|
const properties = {};
|
||||||
|
const required = [];
|
||||||
|
for (const field of schemaArray) {
|
||||||
|
properties[field.key] = { type: "string" };
|
||||||
|
if (field.required) {
|
||||||
|
required.push(field.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "object",
|
||||||
|
properties,
|
||||||
|
required,
|
||||||
|
additionalProperties: false
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const validateAdapterConfigs = (adapters, pluginManager) => {
|
||||||
|
if (!adapters || !pluginManager) return { valid: true, errors: [] };
|
||||||
|
const errors = [];
|
||||||
|
for (const [adapterId, adapterConfig] of Object.entries(adapters)) {
|
||||||
|
const plugin = pluginManager.getPlugin(adapterId);
|
||||||
|
if (!plugin?.configSchema) continue;
|
||||||
|
const schema = convertSchemaToJson(plugin.configSchema);
|
||||||
|
const validate = ajv.compile(schema);
|
||||||
|
const valid = validate(adapterConfig);
|
||||||
|
if (!valid) {
|
||||||
|
const adapterErrors = validate.errors.map(
|
||||||
|
(e) => `${adapterId}${e.instancePath}: ${e.message}`
|
||||||
|
);
|
||||||
|
errors.push(...adapterErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { valid: errors.length === 0, errors };
|
||||||
|
};
|
||||||
|
const createConfigManager = (storage, pluginManager) => {
|
||||||
let config = null;
|
let config = null;
|
||||||
return {
|
return {
|
||||||
async loadConfig() {
|
async loadConfig() {
|
||||||
@@ -96,6 +144,20 @@ const createConfigManager = (storage) => {
|
|||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
async setConfig(newConfig) {
|
async setConfig(newConfig) {
|
||||||
|
const valid = validateConfig(newConfig);
|
||||||
|
if (!valid) {
|
||||||
|
const errors = validateConfig.errors.map((e) => `${e.instancePath || "root"}: ${e.message}`).join("; ");
|
||||||
|
throw new Error(`Config validation failed: ${errors}`);
|
||||||
|
}
|
||||||
|
const adapterValidation = validateAdapterConfigs(
|
||||||
|
newConfig.adapters,
|
||||||
|
pluginManager
|
||||||
|
);
|
||||||
|
if (!adapterValidation.valid) {
|
||||||
|
throw new Error(
|
||||||
|
`Adapter config validation failed: ${adapterValidation.errors.join("; ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
config = newConfig;
|
config = newConfig;
|
||||||
await storage.save(newConfig);
|
await storage.save(newConfig);
|
||||||
},
|
},
|
||||||
@@ -111,8 +173,10 @@ class NotesAPI {
|
|||||||
}
|
}
|
||||||
this.adapter = adapter;
|
this.adapter = adapter;
|
||||||
this.notesCache = /* @__PURE__ */ new Map();
|
this.notesCache = /* @__PURE__ */ new Map();
|
||||||
|
this.categories = /* @__PURE__ */ new Set();
|
||||||
this.encryptionKey = encryptionKey;
|
this.encryptionKey = encryptionKey;
|
||||||
this._sodiumReady = false;
|
this._sodiumReady = false;
|
||||||
|
this._decryptionFailures = [];
|
||||||
this.index = new Index({
|
this.index = new Index({
|
||||||
tokenize: "forward"
|
tokenize: "forward"
|
||||||
});
|
});
|
||||||
@@ -124,11 +188,10 @@ class NotesAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_hexToUint8Array(hex) {
|
_hexToUint8Array(hex) {
|
||||||
const bytes = new Uint8Array(hex.length / 2);
|
return Uint8Array.from(
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
hex.match(/.{1,2}/g),
|
||||||
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
(byte) => parseInt(byte, 16)
|
||||||
}
|
);
|
||||||
return bytes;
|
|
||||||
}
|
}
|
||||||
_concatUint8Arrays(a, b) {
|
_concatUint8Arrays(a, b) {
|
||||||
const result = new Uint8Array(a.length + b.length);
|
const result = new Uint8Array(a.length + b.length);
|
||||||
@@ -137,19 +200,11 @@ class NotesAPI {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
_uint8ArrayToBase64(bytes) {
|
_uint8ArrayToBase64(bytes) {
|
||||||
let binary = "";
|
return btoa(String.fromCharCode(...bytes));
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
|
||||||
binary += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
|
||||||
return btoa(binary);
|
|
||||||
}
|
}
|
||||||
_base64ToUint8Array(base64) {
|
_base64ToUint8Array(base64) {
|
||||||
const binary = atob(base64);
|
const binary = atob(base64);
|
||||||
const bytes = new Uint8Array(binary.length);
|
return Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
||||||
for (let i = 0; i < binary.length; i++) {
|
|
||||||
bytes[i] = binary.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
}
|
||||||
_encrypt(note) {
|
_encrypt(note) {
|
||||||
if (!this.encryptionKey) {
|
if (!this.encryptionKey) {
|
||||||
@@ -218,18 +273,28 @@ class NotesAPI {
|
|||||||
await this._initSodium();
|
await this._initSodium();
|
||||||
await this.adapter.init();
|
await this.adapter.init();
|
||||||
this.notesCache.clear();
|
this.notesCache.clear();
|
||||||
|
this.categories.clear();
|
||||||
|
this._decryptionFailures = [];
|
||||||
const encryptedNotes = await this.adapter.getAll();
|
const encryptedNotes = await this.adapter.getAll();
|
||||||
for (const encryptedNote of encryptedNotes) {
|
for (const encryptedNote of encryptedNotes) {
|
||||||
|
const noteId = encryptedNote.id || "unknown";
|
||||||
try {
|
try {
|
||||||
const note = this._decrypt(encryptedNote.data || encryptedNote);
|
const note = this._decrypt(encryptedNote.data || encryptedNote);
|
||||||
this.notesCache.set(note.id, note);
|
this.notesCache.set(note.id, note);
|
||||||
|
if (note.category) {
|
||||||
|
this.categories.add(note.category);
|
||||||
|
}
|
||||||
const searchText = note.plainText || this._extractPlainText(note.content);
|
const searchText = note.plainText || this._extractPlainText(note.content);
|
||||||
this.index.add(note.id, note.title + "\n" + searchText);
|
this.index.add(note.id, note.title + "\n" + searchText);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to decrypt note:", error);
|
console.error("Failed to decrypt note:", error);
|
||||||
|
this._decryptionFailures.push(noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
getDecryptionFailures() {
|
||||||
|
return [...this._decryptionFailures];
|
||||||
|
}
|
||||||
_extractPlainText(content) {
|
_extractPlainText(content) {
|
||||||
if (!content) return "";
|
if (!content) return "";
|
||||||
if (typeof content === "string") return content;
|
if (typeof content === "string") return content;
|
||||||
@@ -241,19 +306,14 @@ class NotesAPI {
|
|||||||
return extractText(content);
|
return extractText(content);
|
||||||
}
|
}
|
||||||
getCategories() {
|
getCategories() {
|
||||||
const categories = /* @__PURE__ */ new Set();
|
return Array.from(this.categories).sort();
|
||||||
for (const note of this.notesCache.values()) {
|
|
||||||
if (note.category) {
|
|
||||||
categories.add(note.category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(categories).sort();
|
|
||||||
}
|
}
|
||||||
getCategoryNotes(categoryName = null) {
|
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 }));
|
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) {
|
getNote(id) {
|
||||||
const note = this.notesCache.get(id);
|
const note = this.notesCache.get(id);
|
||||||
|
console.log(id, this.notesCache);
|
||||||
return note ? { ...note } : null;
|
return note ? { ...note } : null;
|
||||||
}
|
}
|
||||||
async createNote(metadata = {}, content = "", plainText = "") {
|
async createNote(metadata = {}, content = "", plainText = "") {
|
||||||
@@ -273,14 +333,32 @@ class NotesAPI {
|
|||||||
data: this._encrypt(note)
|
data: this._encrypt(note)
|
||||||
};
|
};
|
||||||
this.notesCache.set(id, note);
|
this.notesCache.set(id, note);
|
||||||
|
if (note.category) {
|
||||||
|
this.categories.add(note.category);
|
||||||
|
}
|
||||||
this.index.add(id, note.title + "\n" + plainText);
|
this.index.add(id, note.title + "\n" + plainText);
|
||||||
await this.adapter.create(encryptedNote);
|
await this.adapter.create(encryptedNote);
|
||||||
return note;
|
return note;
|
||||||
}
|
}
|
||||||
async deleteNote(id) {
|
async deleteNote(id) {
|
||||||
|
const note = this.notesCache.get(id);
|
||||||
|
const category = note?.category;
|
||||||
await this.adapter.delete(id);
|
await this.adapter.delete(id);
|
||||||
this.notesCache.delete(id);
|
this.notesCache.delete(id);
|
||||||
this.index.remove(id);
|
this.index.remove(id);
|
||||||
|
if (category) {
|
||||||
|
const notesWithCategory = Array.from(
|
||||||
|
this.notesCache.values()
|
||||||
|
).filter((n) => n.category === category);
|
||||||
|
if (notesWithCategory.length === 0) {
|
||||||
|
this.categories.delete(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this._decryptionFailures.includes(id)) {
|
||||||
|
this._decryptionFailures = this._decryptionFailures.filter(
|
||||||
|
(id2) => id2 !== id2
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async updateNote(id, updates = {}) {
|
async updateNote(id, updates = {}) {
|
||||||
const note = this.notesCache.get(id);
|
const note = this.notesCache.get(id);
|
||||||
@@ -291,6 +369,7 @@ class NotesAPI {
|
|||||||
throw new Error(`Invalid update field: ${key}`);
|
throw new Error(`Invalid update field: ${key}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const oldCategory = note.category;
|
||||||
const updatedNote = {
|
const updatedNote = {
|
||||||
...note,
|
...note,
|
||||||
...updates,
|
...updates,
|
||||||
@@ -301,6 +380,19 @@ class NotesAPI {
|
|||||||
data: this._encrypt(updatedNote)
|
data: this._encrypt(updatedNote)
|
||||||
};
|
};
|
||||||
this.notesCache.set(id, updatedNote);
|
this.notesCache.set(id, updatedNote);
|
||||||
|
if (updates.category !== void 0) {
|
||||||
|
if (oldCategory) {
|
||||||
|
const notesWithOldCategory = Array.from(
|
||||||
|
this.notesCache.values()
|
||||||
|
).filter((n) => n.category === oldCategory && n.id !== id);
|
||||||
|
if (notesWithOldCategory.length === 0) {
|
||||||
|
this.categories.delete(oldCategory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updatedNote.category) {
|
||||||
|
this.categories.add(updatedNote.category);
|
||||||
|
}
|
||||||
|
}
|
||||||
const searchText = updatedNote.plainText || this._extractPlainText(updatedNote.content);
|
const searchText = updatedNote.plainText || this._extractPlainText(updatedNote.content);
|
||||||
this.index.update(id, updatedNote.title + "\n" + searchText);
|
this.index.update(id, updatedNote.title + "\n" + searchText);
|
||||||
await this.adapter.update(encryptedNote);
|
await this.adapter.update(encryptedNote);
|
||||||
@@ -319,46 +411,45 @@ const generateEncryptionKey = () => {
|
|||||||
crypto.getRandomValues(array);
|
crypto.getRandomValues(array);
|
||||||
return Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
|
return Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||||
};
|
};
|
||||||
const initPluginManager = (runtime, plugins, config) => {
|
const initPluginManager = (runtime, plugins) => {
|
||||||
const registry = new PluginRegistry();
|
const registry = new PluginRegistry();
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
registry.register(plugin);
|
registry.register(plugin);
|
||||||
}
|
}
|
||||||
const manager = createPluginManager(registry);
|
return createPluginManager(registry);
|
||||||
const activeConfig = config.adapters?.[config.activeAdapter] || {};
|
|
||||||
manager.setActivePlugin(config.activeAdapter, activeConfig);
|
|
||||||
return manager;
|
|
||||||
};
|
};
|
||||||
const initConfigManager = async (runtime) => {
|
const initConfigManager = async (runtime, pluginManager) => {
|
||||||
let storage;
|
let storage;
|
||||||
{
|
{
|
||||||
const { createNodeStorage } = await import("./NodeStorage-B8VFtrTS.js");
|
const { createNodeStorage } = await import("./NodeStorage-B8VFtrTS.js");
|
||||||
const filesystemPlugin2 = (await import("@takerofnotes/plugin-filesystem")).default;
|
const filesystemPlugin2 = (await import("@takerofnotes/plugin-filesystem")).default;
|
||||||
storage = createNodeStorage(filesystemPlugin2);
|
storage = createNodeStorage(filesystemPlugin2);
|
||||||
}
|
}
|
||||||
return createConfigManager(storage);
|
return createConfigManager(storage, pluginManager);
|
||||||
};
|
};
|
||||||
const initializeCore = async (runtime, { plugins }) => {
|
const initializeCore = async (runtime, { plugins }) => {
|
||||||
const configManager = await initConfigManager();
|
const pluginManager = initPluginManager(runtime, plugins);
|
||||||
|
const configManager = await initConfigManager(runtime, pluginManager);
|
||||||
const config = await configManager.loadConfig();
|
const config = await configManager.loadConfig();
|
||||||
const pluginManager = initPluginManager(runtime, plugins, config);
|
const activeConfig = config.adapters?.[config.activeAdapter] || {};
|
||||||
|
pluginManager.setActivePlugin(config.activeAdapter, activeConfig);
|
||||||
let notesAPI = null;
|
let notesAPI = null;
|
||||||
let initPromise = null;
|
let initPromise = null;
|
||||||
const getNotesAPI = async () => {
|
const getNotesAPI = async () => {
|
||||||
if (notesAPI) return notesAPI;
|
if (notesAPI) return notesAPI;
|
||||||
if (!initPromise) {
|
if (!initPromise) {
|
||||||
initPromise = (async () => {
|
initPromise = (async () => {
|
||||||
const config2 = await configManager.loadConfig();
|
const latestConfig = await configManager.loadConfig();
|
||||||
let encryptionKey = config2?.encryptionKey;
|
let encryptionKey = latestConfig?.encryptionKey;
|
||||||
if (!encryptionKey) {
|
if (!encryptionKey) {
|
||||||
encryptionKey = generateEncryptionKey();
|
encryptionKey = generateEncryptionKey();
|
||||||
await configManager.setConfig({
|
await configManager.setConfig({
|
||||||
...config2,
|
...latestConfig,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const pluginId = config2?.activeAdapter || "filesystem";
|
const pluginId = latestConfig?.activeAdapter || "filesystem";
|
||||||
const adapterConfig = config2?.adapters?.[pluginId] || {};
|
const adapterConfig = latestConfig?.adapters?.[pluginId] || {};
|
||||||
const adapter = pluginManager.getAdapter(
|
const adapter = pluginManager.getAdapter(
|
||||||
pluginId,
|
pluginId,
|
||||||
adapterConfig
|
adapterConfig
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"@tiptap/starter-kit": "^3.19.0",
|
"@tiptap/starter-kit": "^3.19.0",
|
||||||
"@tiptap/vue-3": "^3.19.0",
|
"@tiptap/vue-3": "^3.19.0",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
|
"ajv": "^6.14.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
@@ -4917,7 +4918,6 @@
|
|||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
@@ -7245,7 +7245,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-fifo": {
|
"node_modules/fast-fifo": {
|
||||||
@@ -7258,7 +7257,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-xml-builder": {
|
"node_modules/fast-xml-builder": {
|
||||||
@@ -8192,7 +8190,6 @@
|
|||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-stringify-safe": {
|
"node_modules/json-stringify-safe": {
|
||||||
@@ -9756,7 +9753,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -11243,7 +11239,6 @@
|
|||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"@tiptap/starter-kit": "^3.19.0",
|
"@tiptap/starter-kit": "^3.19.0",
|
||||||
"@tiptap/vue-3": "^3.19.0",
|
"@tiptap/vue-3": "^3.19.0",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
|
"ajv": "^6.14.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import Ajv from 'ajv'
|
||||||
|
|
||||||
|
const ajv = new Ajv({ allErrors: true, strict: false })
|
||||||
|
|
||||||
const getDefaultConfig = () => {
|
const getDefaultConfig = () => {
|
||||||
return {
|
return {
|
||||||
activeAdapter: 'browser',
|
activeAdapter: 'browser',
|
||||||
@@ -5,7 +9,65 @@ const getDefaultConfig = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createConfigManager = (storage) => {
|
const CONFIG_SCHEMA = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
activeAdapter: { type: 'string' },
|
||||||
|
theme: { type: 'string', enum: ['dark', 'light'] },
|
||||||
|
encryptionKey: { type: 'string' },
|
||||||
|
adapters: { type: 'object' },
|
||||||
|
},
|
||||||
|
required: ['activeAdapter'],
|
||||||
|
additionalProperties: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateConfig = ajv.compile(CONFIG_SCHEMA)
|
||||||
|
|
||||||
|
const convertSchemaToJson = (schemaArray) => {
|
||||||
|
const properties = {}
|
||||||
|
const required = []
|
||||||
|
|
||||||
|
for (const field of schemaArray) {
|
||||||
|
properties[field.key] = { type: 'string' }
|
||||||
|
|
||||||
|
if (field.required) {
|
||||||
|
required.push(field.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'object',
|
||||||
|
properties,
|
||||||
|
required,
|
||||||
|
additionalProperties: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAdapterConfigs = (adapters, pluginManager) => {
|
||||||
|
if (!adapters || !pluginManager) return { valid: true, errors: [] }
|
||||||
|
|
||||||
|
const errors = []
|
||||||
|
|
||||||
|
for (const [adapterId, adapterConfig] of Object.entries(adapters)) {
|
||||||
|
const plugin = pluginManager.getPlugin(adapterId)
|
||||||
|
if (!plugin?.configSchema) continue
|
||||||
|
|
||||||
|
const schema = convertSchemaToJson(plugin.configSchema)
|
||||||
|
const validate = ajv.compile(schema)
|
||||||
|
const valid = validate(adapterConfig)
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
const adapterErrors = validate.errors.map(
|
||||||
|
(e) => `${adapterId}${e.instancePath}: ${e.message}`,
|
||||||
|
)
|
||||||
|
errors.push(...adapterErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: errors.length === 0, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createConfigManager = (storage, pluginManager) => {
|
||||||
let config = null
|
let config = null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -27,6 +89,24 @@ export const createConfigManager = (storage) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async setConfig(newConfig) {
|
async setConfig(newConfig) {
|
||||||
|
const valid = validateConfig(newConfig)
|
||||||
|
if (!valid) {
|
||||||
|
const errors = validateConfig.errors
|
||||||
|
.map((e) => `${e.instancePath || 'root'}: ${e.message}`)
|
||||||
|
.join('; ')
|
||||||
|
throw new Error(`Config validation failed: ${errors}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapterValidation = validateAdapterConfigs(
|
||||||
|
newConfig.adapters,
|
||||||
|
pluginManager,
|
||||||
|
)
|
||||||
|
if (!adapterValidation.valid) {
|
||||||
|
throw new Error(
|
||||||
|
`Adapter config validation failed: ${adapterValidation.errors.join('; ')}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
config = newConfig
|
config = newConfig
|
||||||
await storage.save(newConfig)
|
await storage.save(newConfig)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
export default class IpcAdapter {
|
export default class IpcAdapter {
|
||||||
constructor() {
|
constructor() {}
|
||||||
this._methods = ['init', 'getAll', 'create', 'update', 'delete']
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
return await window.api.adapterCall('init')
|
return await window.api.adapterCall('init')
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ export default class NotesAPI {
|
|||||||
|
|
||||||
this.adapter = adapter
|
this.adapter = adapter
|
||||||
this.notesCache = new Map()
|
this.notesCache = new Map()
|
||||||
|
this.categories = new Set()
|
||||||
this.encryptionKey = encryptionKey
|
this.encryptionKey = encryptionKey
|
||||||
this._sodiumReady = false
|
this._sodiumReady = false
|
||||||
|
this._decryptionFailures = []
|
||||||
|
|
||||||
this.index = new Index({
|
this.index = new Index({
|
||||||
tokenize: 'forward',
|
tokenize: 'forward',
|
||||||
@@ -26,11 +28,9 @@ export default class NotesAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_hexToUint8Array(hex) {
|
_hexToUint8Array(hex) {
|
||||||
const bytes = new Uint8Array(hex.length / 2)
|
return Uint8Array.from(hex.match(/.{1,2}/g), (byte) =>
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
parseInt(byte, 16),
|
||||||
bytes[i] = parseInt(hex.substr(i * 2, 2), 16)
|
)
|
||||||
}
|
|
||||||
return bytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_concatUint8Arrays(a, b) {
|
_concatUint8Arrays(a, b) {
|
||||||
@@ -41,20 +41,12 @@ export default class NotesAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_uint8ArrayToBase64(bytes) {
|
_uint8ArrayToBase64(bytes) {
|
||||||
let binary = ''
|
return btoa(String.fromCharCode(...bytes))
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
|
||||||
binary += String.fromCharCode(bytes[i])
|
|
||||||
}
|
|
||||||
return btoa(binary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_base64ToUint8Array(base64) {
|
_base64ToUint8Array(base64) {
|
||||||
const binary = atob(base64)
|
const binary = atob(base64)
|
||||||
const bytes = new Uint8Array(binary.length)
|
return Uint8Array.from(binary, (c) => c.charCodeAt(0))
|
||||||
for (let i = 0; i < binary.length; i++) {
|
|
||||||
bytes[i] = binary.charCodeAt(i)
|
|
||||||
}
|
|
||||||
return bytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_encrypt(note) {
|
_encrypt(note) {
|
||||||
@@ -142,23 +134,34 @@ export default class NotesAPI {
|
|||||||
await this._initSodium()
|
await this._initSodium()
|
||||||
await this.adapter.init()
|
await this.adapter.init()
|
||||||
this.notesCache.clear()
|
this.notesCache.clear()
|
||||||
|
this.categories.clear()
|
||||||
|
this._decryptionFailures = []
|
||||||
|
|
||||||
const encryptedNotes = await this.adapter.getAll()
|
const encryptedNotes = await this.adapter.getAll()
|
||||||
|
|
||||||
for (const encryptedNote of encryptedNotes) {
|
for (const encryptedNote of encryptedNotes) {
|
||||||
|
const noteId = encryptedNote.id || 'unknown'
|
||||||
try {
|
try {
|
||||||
const note = this._decrypt(encryptedNote.data || encryptedNote)
|
const note = this._decrypt(encryptedNote.data || encryptedNote)
|
||||||
|
|
||||||
this.notesCache.set(note.id, note)
|
this.notesCache.set(note.id, note)
|
||||||
|
if (note.category) {
|
||||||
|
this.categories.add(note.category)
|
||||||
|
}
|
||||||
const searchText =
|
const searchText =
|
||||||
note.plainText || this._extractPlainText(note.content)
|
note.plainText || this._extractPlainText(note.content)
|
||||||
this.index.add(note.id, note.title + '\n' + searchText)
|
this.index.add(note.id, note.title + '\n' + searchText)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to decrypt note:', error)
|
console.error('Failed to decrypt note:', error)
|
||||||
|
this._decryptionFailures.push(noteId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDecryptionFailures() {
|
||||||
|
return [...this._decryptionFailures]
|
||||||
|
}
|
||||||
|
|
||||||
_extractPlainText(content) {
|
_extractPlainText(content) {
|
||||||
if (!content) return ''
|
if (!content) return ''
|
||||||
if (typeof content === 'string') return content
|
if (typeof content === 'string') return content
|
||||||
@@ -173,15 +176,7 @@ export default class NotesAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCategories() {
|
getCategories() {
|
||||||
const categories = new Set()
|
return Array.from(this.categories).sort()
|
||||||
|
|
||||||
for (const note of this.notesCache.values()) {
|
|
||||||
if (note.category) {
|
|
||||||
categories.add(note.category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(categories).sort()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategoryNotes(categoryName = null) {
|
getCategoryNotes(categoryName = null) {
|
||||||
@@ -193,6 +188,7 @@ export default class NotesAPI {
|
|||||||
|
|
||||||
getNote(id) {
|
getNote(id) {
|
||||||
const note = this.notesCache.get(id)
|
const note = this.notesCache.get(id)
|
||||||
|
|
||||||
return note ? { ...note } : null
|
return note ? { ...note } : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +212,9 @@ export default class NotesAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.notesCache.set(id, note)
|
this.notesCache.set(id, note)
|
||||||
|
if (note.category) {
|
||||||
|
this.categories.add(note.category)
|
||||||
|
}
|
||||||
this.index.add(id, note.title + '\n' + plainText)
|
this.index.add(id, note.title + '\n' + plainText)
|
||||||
|
|
||||||
await this.adapter.create(encryptedNote)
|
await this.adapter.create(encryptedNote)
|
||||||
@@ -224,10 +223,30 @@ export default class NotesAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteNote(id) {
|
async deleteNote(id) {
|
||||||
|
const note = this.notesCache.get(id)
|
||||||
|
const category = note?.category
|
||||||
|
|
||||||
await this.adapter.delete(id)
|
await this.adapter.delete(id)
|
||||||
|
|
||||||
this.notesCache.delete(id)
|
this.notesCache.delete(id)
|
||||||
this.index.remove(id)
|
this.index.remove(id)
|
||||||
|
|
||||||
|
// Delete category if needed
|
||||||
|
if (category) {
|
||||||
|
const notesWithCategory = Array.from(
|
||||||
|
this.notesCache.values(),
|
||||||
|
).filter((n) => n.category === category)
|
||||||
|
if (notesWithCategory.length === 0) {
|
||||||
|
this.categories.delete(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update decryption failures
|
||||||
|
if (this._decryptionFailures.includes(id)) {
|
||||||
|
this._decryptionFailures = this._decryptionFailures.filter(
|
||||||
|
(id) => id !== id,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateNote(id, updates = {}) {
|
async updateNote(id, updates = {}) {
|
||||||
@@ -242,6 +261,8 @@ export default class NotesAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldCategory = note.category
|
||||||
|
|
||||||
const updatedNote = {
|
const updatedNote = {
|
||||||
...note,
|
...note,
|
||||||
...updates,
|
...updates,
|
||||||
@@ -255,6 +276,20 @@ export default class NotesAPI {
|
|||||||
|
|
||||||
this.notesCache.set(id, updatedNote)
|
this.notesCache.set(id, updatedNote)
|
||||||
|
|
||||||
|
if (updates.category !== undefined) {
|
||||||
|
if (oldCategory) {
|
||||||
|
const notesWithOldCategory = Array.from(
|
||||||
|
this.notesCache.values(),
|
||||||
|
).filter((n) => n.category === oldCategory && n.id !== id)
|
||||||
|
if (notesWithOldCategory.length === 0) {
|
||||||
|
this.categories.delete(oldCategory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updatedNote.category) {
|
||||||
|
this.categories.add(updatedNote.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const searchText =
|
const searchText =
|
||||||
updatedNote.plainText || this._extractPlainText(updatedNote.content)
|
updatedNote.plainText || this._extractPlainText(updatedNote.content)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const generateEncryptionKey = () => {
|
|||||||
.join('')
|
.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const initPluginManager = (runtime, plugins, config) => {
|
const initPluginManager = (runtime, plugins) => {
|
||||||
if (runtime === 'electron-renderer') return createPluginManagerClient()
|
if (runtime === 'electron-renderer') return createPluginManagerClient()
|
||||||
|
|
||||||
const registry = new PluginRegistry()
|
const registry = new PluginRegistry()
|
||||||
@@ -27,14 +27,10 @@ const initPluginManager = (runtime, plugins, config) => {
|
|||||||
registry.register(plugin)
|
registry.register(plugin)
|
||||||
}
|
}
|
||||||
|
|
||||||
const manager = createPluginManager(registry)
|
return createPluginManager(registry)
|
||||||
const activeConfig = config.adapters?.[config.activeAdapter] || {}
|
|
||||||
manager.setActivePlugin(config.activeAdapter, activeConfig)
|
|
||||||
|
|
||||||
return manager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initConfigManager = async (runtime) => {
|
const initConfigManager = async (runtime, pluginManager) => {
|
||||||
if (runtime === 'electron-renderer') return createConfigManagerClient()
|
if (runtime === 'electron-renderer') return createConfigManagerClient()
|
||||||
|
|
||||||
let storage
|
let storage
|
||||||
@@ -48,37 +44,41 @@ const initConfigManager = async (runtime) => {
|
|||||||
storage = createWebStorage()
|
storage = createWebStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return createConfigManager(storage)
|
return createConfigManager(storage, pluginManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initializeCore = async (runtime, { plugins }) => {
|
export const initializeCore = async (runtime, { plugins }) => {
|
||||||
const configManager = await initConfigManager(runtime)
|
const pluginManager = initPluginManager(runtime, plugins)
|
||||||
|
const configManager = await initConfigManager(runtime, pluginManager)
|
||||||
const config = await configManager.loadConfig()
|
const config = await configManager.loadConfig()
|
||||||
const pluginManager = initPluginManager(runtime, plugins, config)
|
|
||||||
|
|
||||||
// NotesAPI bootstrap
|
// Set active plugin
|
||||||
|
const activeConfig = config.adapters?.[config.activeAdapter] || {}
|
||||||
|
pluginManager.setActivePlugin(config.activeAdapter, activeConfig)
|
||||||
|
|
||||||
|
// Create API instance
|
||||||
let notesAPI = null
|
let notesAPI = null
|
||||||
let initPromise = null
|
let initPromise = null
|
||||||
|
|
||||||
const getNotesAPI = async () => {
|
const getNotesAPI = async () => {
|
||||||
if (notesAPI) return notesAPI
|
if (notesAPI) return notesAPI
|
||||||
|
|
||||||
if (!initPromise) {
|
if (!initPromise) {
|
||||||
initPromise = (async () => {
|
initPromise = (async () => {
|
||||||
const config = await configManager.loadConfig()
|
// Get fresh config to ensure adapters are populated from main process
|
||||||
|
const latestConfig = await configManager.loadConfig()
|
||||||
|
|
||||||
let encryptionKey = config?.encryptionKey
|
let encryptionKey = latestConfig?.encryptionKey
|
||||||
|
|
||||||
if (!encryptionKey) {
|
if (!encryptionKey) {
|
||||||
encryptionKey = generateEncryptionKey()
|
encryptionKey = generateEncryptionKey()
|
||||||
await configManager.setConfig({
|
await configManager.setConfig({
|
||||||
...config,
|
...latestConfig,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginId = config?.activeAdapter || 'filesystem'
|
const pluginId = latestConfig?.activeAdapter || 'filesystem'
|
||||||
const adapterConfig = config?.adapters?.[pluginId] || {}
|
const adapterConfig = latestConfig?.adapters?.[pluginId] || {}
|
||||||
|
|
||||||
const adapter = pluginManager.getAdapter(
|
const adapter = pluginManager.getAdapter(
|
||||||
pluginId,
|
pluginId,
|
||||||
|
|||||||
103
src/renderer/src/components/DecryptionWarning.vue
Normal file
103
src/renderer/src/components/DecryptionWarning.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="failedIds.length && visible" class="decryption-warning">
|
||||||
|
<div class="warning">
|
||||||
|
<button class="warning-text" @click="detailsOpen = !detailsOpen">
|
||||||
|
<strong>{{ failedIds.length }} </strong>
|
||||||
|
<span>
|
||||||
|
{{ failedIds.length === 1 ? 'note could' : 'notes could' }}
|
||||||
|
not be decrypted
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="close-button" @click="visible = false">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="detailsOpen" class="details">
|
||||||
|
<span>Failed note IDs:</span>
|
||||||
|
|
||||||
|
<ul class="id-list">
|
||||||
|
<li v-for="id in failedIds" :key="id">{{ id }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="instructions">
|
||||||
|
These notes may have been encrypted with a different key. Either
|
||||||
|
change your encryption key in preferences, or delete these
|
||||||
|
notes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button class="delete-button" @click="onDeleteNotes">
|
||||||
|
Delete Notes (Permanent)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import useNotes from '@/composables/useNotes'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
failedIds: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const visible = ref(true)
|
||||||
|
const detailsOpen = ref(false)
|
||||||
|
|
||||||
|
const { deleteNote } = useNotes()
|
||||||
|
|
||||||
|
const onDeleteNotes = async () => {
|
||||||
|
await Promise.all(props.failedIds.map((id) => deleteNote(id)))
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decryption-warning {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--theme-bg);
|
||||||
|
border-top: 1px solid var(--grey-100);
|
||||||
|
padding: 10px var(--layout-margin);
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.warning-text {
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.details {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--grey-100);
|
||||||
|
}
|
||||||
|
.instructions {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.delete-button {
|
||||||
|
display: block;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
border: 1px solid var(--theme-fg);
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-accent);
|
||||||
|
border-color: var(--theme-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<router-link class="menu-item" to="/instructions">
|
<router-link class="menu-item" to="/instructions">
|
||||||
Instructio
|
Instructio
|
||||||
</router-link>
|
</router-link>
|
||||||
<button class="menu-item">Import</button>
|
<!-- <button class="menu-item">Import</button> -->
|
||||||
<button class="menu-item" @click="handleExport">Export</button>
|
<button class="menu-item" @click="handleExport">Export</button>
|
||||||
<router-link class="menu-item" to="/preferences">
|
<router-link class="menu-item" to="/preferences">
|
||||||
Preferences
|
Preferences
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import useOpenNote from '@/composables/useOpenNote'
|
import useOpenNote from '@/composables/useOpenNote'
|
||||||
import useNotes from '@/composables/useNotes'
|
import useNotes from '@/composables/useNotes'
|
||||||
|
import { useMagicKeys } from '@vueuse/core'
|
||||||
|
import { watchEffect } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({ category: String })
|
const props = defineProps({ category: String })
|
||||||
const emit = defineEmits(['noteOpened'])
|
const emit = defineEmits(['noteOpened'])
|
||||||
@@ -22,9 +24,12 @@ const onClick = async () => {
|
|||||||
openNote(note.id)
|
openNote(note.id)
|
||||||
emit('noteOpened')
|
emit('noteOpened')
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
// Add keyboard shortcut
|
||||||
.new-note {
|
const { ctrl, n } = useMagicKeys()
|
||||||
}
|
watchEffect(() => {
|
||||||
</style>
|
if (ctrl.value && n.value) {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const moveActive = computed(() => route.query.move === props.note.id)
|
|||||||
.title {
|
.title {
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
@include line-clamp(3);
|
||||||
}
|
}
|
||||||
.action {
|
.action {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ watch(
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
border-left: 1px solid var(--grey-100);
|
border-left: 1px solid var(--grey-100);
|
||||||
|
background: var(--theme-bg);
|
||||||
|
|
||||||
.inner {
|
.inner {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -61,8 +61,11 @@ const onUpdate = _debounce(async ({ editor }) => {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const note = await loadNote(props.id)
|
const note = await loadNote(props.id)
|
||||||
|
if (!note) return
|
||||||
|
|
||||||
|
if (note.title !== 'Untitled') {
|
||||||
title.value = note.title || ''
|
title.value = note.title || ''
|
||||||
|
}
|
||||||
|
|
||||||
editor.value = new Editor({
|
editor.value = new Editor({
|
||||||
extensions,
|
extensions,
|
||||||
@@ -76,6 +79,7 @@ onMounted(async () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
if (titleInput.value) {
|
if (titleInput.value) {
|
||||||
titleInput.value.textContent = title.value
|
titleInput.value.textContent = title.value
|
||||||
|
titleInput.value.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -101,6 +105,11 @@ const onTitleChange = (e) => {
|
|||||||
updateTitle(title.value)
|
updateTitle(title.value)
|
||||||
}
|
}
|
||||||
const onTitleKeydown = (e) => {
|
const onTitleKeydown = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
editor.value?.commands?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
if (title.value.length >= TITLE_CHAR_LIMIT && e.key.length === 1) {
|
if (title.value.length >= TITLE_CHAR_LIMIT && e.key.length === 1) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/renderer/src/composables/useNoteListeners.js
Normal file
52
src/renderer/src/composables/useNoteListeners.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { useEnvironment } from '@/composables/useEnvironment.js'
|
||||||
|
import useCore from '@/composables/useCore'
|
||||||
|
import _omit from 'lodash/omit'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
let listenersInitialized = false
|
||||||
|
const changeCount = ref(0)
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const environment = useEnvironment()
|
||||||
|
const { getNotesAPI } = useCore()
|
||||||
|
|
||||||
|
const setupListeners = () => {
|
||||||
|
if (environment !== 'electron' || listenersInitialized) return
|
||||||
|
listenersInitialized = true
|
||||||
|
|
||||||
|
const updateCacheCount = async (note) => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
await api.updateNote(
|
||||||
|
note.id,
|
||||||
|
_omit(note, ['id', 'createdAt', 'updatedAt']),
|
||||||
|
)
|
||||||
|
|
||||||
|
changeCount.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
window.api.onNoteCreated(updateCacheCount)
|
||||||
|
window.api.onNoteUpdated(updateCacheCount)
|
||||||
|
window.api.onPluginChanged(async () => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
await api.init()
|
||||||
|
|
||||||
|
changeCount.value++
|
||||||
|
})
|
||||||
|
|
||||||
|
// Todo update cache
|
||||||
|
window.api.onNoteDeleted(() => {
|
||||||
|
changeCount.value++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcastChange = (event, data) => {
|
||||||
|
if (environment !== 'electron') return
|
||||||
|
window.api.notifyNoteChanged(event, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setupListeners,
|
||||||
|
broadcastChange,
|
||||||
|
changeCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +1,40 @@
|
|||||||
import _omit from 'lodash/omit'
|
import useNoteListeners from '@/composables/useNoteListeners'
|
||||||
|
import useCore from '@/composables/useCore'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { getNotesAPI } from '@/libs/getNotesAPI'
|
|
||||||
import { useEnvironment } from '@/composables/useEnvironment.js'
|
|
||||||
|
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
const searchResults = ref([])
|
const searchResults = ref([])
|
||||||
const notesChangeCount = ref(0)
|
const decryptionFailures = ref([])
|
||||||
|
|
||||||
let listenersInitialized = false
|
|
||||||
|
|
||||||
const environment = useEnvironment()
|
|
||||||
|
|
||||||
const setupListeners = () => {
|
|
||||||
if (listenersInitialized || typeof window === 'undefined') return
|
|
||||||
listenersInitialized = true
|
|
||||||
|
|
||||||
const updateCacheCount = async (note) => {
|
|
||||||
const api = await getNotesAPI()
|
|
||||||
await api.updateNote(
|
|
||||||
note.id,
|
|
||||||
_omit(note, ['id', 'createdAt', 'updatedAt']),
|
|
||||||
)
|
|
||||||
|
|
||||||
notesChangeCount.value++
|
|
||||||
}
|
|
||||||
|
|
||||||
window.api.onNoteCreated(updateCacheCount)
|
|
||||||
window.api.onNoteUpdated(updateCacheCount)
|
|
||||||
window.api.onPluginChanged(async () => {
|
|
||||||
const api = await getNotesAPI()
|
|
||||||
await api.init()
|
|
||||||
|
|
||||||
notesChangeCount.value++
|
|
||||||
})
|
|
||||||
|
|
||||||
// Todo update cache
|
|
||||||
window.api.onNoteDeleted(() => {
|
|
||||||
notesChangeCount.value++
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const broadcastChange = (event, data) => {
|
|
||||||
if (environment === 'electron') {
|
|
||||||
window.api.notifyNoteChanged(event, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (environment === 'electron') {
|
|
||||||
setupListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
/* -------------------------
|
const { getNotesAPI } = useCore()
|
||||||
Initialization
|
|
||||||
--------------------------*/
|
// Change listeners for electron
|
||||||
|
const { setupListeners, broadcastChange, changeCount } = useNoteListeners()
|
||||||
|
setupListeners()
|
||||||
|
|
||||||
|
const getDecryptionFailures = async () => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
decryptionFailures.value = api.getDecryptionFailures()
|
||||||
|
}
|
||||||
|
getDecryptionFailures()
|
||||||
|
|
||||||
|
// Load
|
||||||
const loadCategories = async () => {
|
const loadCategories = async () => {
|
||||||
const api = await getNotesAPI()
|
const api = await getNotesAPI()
|
||||||
categories.value = api.getCategories()
|
categories.value = api.getCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadCategoryNotes = async (category = null) => {
|
const loadCategoryNotes = async (category = null) => {
|
||||||
const api = await getNotesAPI()
|
const api = await getNotesAPI()
|
||||||
return api.getCategoryNotes(category)
|
return api.getCategoryNotes(category)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadNote = async (id) => {
|
const loadNote = async (id) => {
|
||||||
const api = await getNotesAPI()
|
const api = await getNotesAPI()
|
||||||
|
console.log(id, api)
|
||||||
return api.getNote(id)
|
return api.getNote(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
// Create
|
||||||
Create
|
|
||||||
--------------------------*/
|
|
||||||
const createNote = async (metadata, content, plainText = '') => {
|
const createNote = async (metadata, content, plainText = '') => {
|
||||||
const api = await getNotesAPI()
|
const api = await getNotesAPI()
|
||||||
const note = await api.createNote(metadata, content, plainText)
|
const note = await api.createNote(metadata, content, plainText)
|
||||||
@@ -80,9 +43,7 @@ export default () => {
|
|||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
// Update
|
||||||
Update
|
|
||||||
--------------------------*/
|
|
||||||
const updateNote = async (id, updates) => {
|
const updateNote = async (id, updates) => {
|
||||||
const api = await getNotesAPI()
|
const api = await getNotesAPI()
|
||||||
|
|
||||||
@@ -96,7 +57,6 @@ export default () => {
|
|||||||
|
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCategory = async (category, update) => {
|
const updateCategory = async (category, update) => {
|
||||||
const notes = await loadCategoryNotes(category)
|
const notes = await loadCategoryNotes(category)
|
||||||
|
|
||||||
@@ -107,19 +67,16 @@ export default () => {
|
|||||||
await loadCategories()
|
await loadCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
// Delete
|
||||||
Delete
|
|
||||||
--------------------------*/
|
|
||||||
const deleteNote = async (id) => {
|
const deleteNote = async (id) => {
|
||||||
const api = await getNotesAPI()
|
const api = await getNotesAPI()
|
||||||
await api.deleteNote(id)
|
await api.deleteNote(id)
|
||||||
await loadCategories()
|
await loadCategories()
|
||||||
broadcastChange('note-deleted', { id })
|
broadcastChange('note-deleted', { id })
|
||||||
|
getDecryptionFailures()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
// Search
|
||||||
Search
|
|
||||||
--------------------------*/
|
|
||||||
const search = async (query) => {
|
const search = async (query) => {
|
||||||
const api = await getNotesAPI()
|
const api = await getNotesAPI()
|
||||||
|
|
||||||
@@ -134,7 +91,8 @@ export default () => {
|
|||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
searchResults,
|
searchResults,
|
||||||
notesChangeCount,
|
decryptionFailures,
|
||||||
|
changeCount,
|
||||||
|
|
||||||
loadCategories,
|
loadCategories,
|
||||||
loadCategoryNotes,
|
loadCategoryNotes,
|
||||||
|
|||||||
@@ -11,12 +11,9 @@ Tabula = Index/Overview
|
|||||||
|
|
||||||
Program Key Commands
|
Program Key Commands
|
||||||
|
|
||||||
cmd + s = save\
|
|
||||||
cmd + t = new capitulum\
|
cmd + t = new capitulum\
|
||||||
cmd + n = new nota\
|
cmd + n = new nota\
|
||||||
cmd + x = close window\
|
cmd + x = close window
|
||||||
dbl click = change name / open nota\
|
|
||||||
paste hyperlink twice = activated url
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -25,4 +22,9 @@ Text Markdowns
|
|||||||
cmd + b = Bold\
|
cmd + b = Bold\
|
||||||
cmd + u = underline\
|
cmd + u = underline\
|
||||||
--- = ---------- (ruled line break)\
|
--- = ---------- (ruled line break)\
|
||||||
/_text_/ = Desaturated text
|
\# = Heading\
|
||||||
|
\> = Blockquote\
|
||||||
|
\- = Unordered List\
|
||||||
|
\1. = Ordered List\
|
||||||
|
\` = Inline Code\
|
||||||
|
\``` = Code Block
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import useCore from '@/composables/useCore'
|
|
||||||
|
|
||||||
let notesAPI = null
|
|
||||||
let initPromise = null
|
|
||||||
|
|
||||||
export const getNotesAPI = async () => {
|
|
||||||
if (notesAPI) return notesAPI
|
|
||||||
|
|
||||||
if (!initPromise) {
|
|
||||||
initPromise = (async () => {
|
|
||||||
const { getNotesAPI } = useCore()
|
|
||||||
notesAPI = await getNotesAPI()
|
|
||||||
return notesAPI
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
|
|
||||||
return initPromise
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
createWebHistory,
|
||||||
|
} from 'vue-router'
|
||||||
|
import { useEnvironment } from '@/composables/useEnvironment.ts'
|
||||||
|
|
||||||
import Directory from '@/views/Directory.vue'
|
import Directory from '@/views/Directory.vue'
|
||||||
import Note from '@/views/Note.vue'
|
import Note from '@/views/Note.vue'
|
||||||
@@ -8,6 +13,8 @@ import Instructions from '@/views/Instructions.vue'
|
|||||||
import Search from '@/views/Search.vue'
|
import Search from '@/views/Search.vue'
|
||||||
import Preferences from '@/views/Preferences.vue'
|
import Preferences from '@/views/Preferences.vue'
|
||||||
|
|
||||||
|
const environment = useEnvironment()
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', name: 'directory', component: Directory },
|
{ path: '/', name: 'directory', component: Directory },
|
||||||
{ path: '/note/:id', name: 'note', component: Note },
|
{ path: '/note/:id', name: 'note', component: Note },
|
||||||
@@ -19,6 +26,7 @@ const routes = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history:
|
||||||
|
environment === 'web' ? createWebHistory() : createWebHashHistory(),
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,6 +18,6 @@
|
|||||||
@mixin p {
|
@mixin p {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1.4;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,22 +27,22 @@ const route = useRoute()
|
|||||||
const id = route.params?.id
|
const id = route.params?.id
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const {
|
|
||||||
categories,
|
|
||||||
loadCategoryNotes,
|
|
||||||
updateCategory,
|
|
||||||
notesChangeCount,
|
|
||||||
loadCategories,
|
|
||||||
} = useNotes()
|
|
||||||
|
|
||||||
const notes = ref()
|
const notes = ref()
|
||||||
|
|
||||||
async function refreshNotes() {
|
const refreshNotes = async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
notes.value = await loadCategoryNotes(id)
|
notes.value = await loadCategoryNotes(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
categories,
|
||||||
|
loadCategoryNotes,
|
||||||
|
updateCategory,
|
||||||
|
loadCategories,
|
||||||
|
changeCount,
|
||||||
|
} = useNotes(refreshNotes)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await refreshNotes()
|
await refreshNotes()
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(notesChangeCount, async () => {
|
watch(changeCount, async () => {
|
||||||
await refreshNotes()
|
await refreshNotes()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -14,21 +14,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<new-note />
|
<new-note />
|
||||||
|
|
||||||
|
<decryption-warning :failedIds="decryptionFailures" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<page-loading v-else />
|
<page-loading v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import useNotes from '@/composables/useNotes'
|
import DecryptionWarning from '@/components/DecryptionWarning.vue'
|
||||||
import { onMounted, ref, watch } from 'vue'
|
import { onMounted, ref, watchEffect, watch } from 'vue'
|
||||||
import CategoryRow from '@/components/CategoryRow.vue'
|
import CategoryRow from '@/components/CategoryRow.vue'
|
||||||
import NoteRow from '@/components/NoteRow.vue'
|
|
||||||
import PageLoading from '@/components/PageLoading.vue'
|
import PageLoading from '@/components/PageLoading.vue'
|
||||||
|
import NoteRow from '@/components/NoteRow.vue'
|
||||||
|
import useNotes from '@/composables/useNotes'
|
||||||
import NewNote from '@/components/NewNote.vue'
|
import NewNote from '@/components/NewNote.vue'
|
||||||
|
import { useMagicKeys } from '@vueuse/core'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
|
const router = useRouter()
|
||||||
useNotes()
|
const {
|
||||||
|
categories,
|
||||||
|
loadCategories,
|
||||||
|
loadCategoryNotes,
|
||||||
|
changeCount,
|
||||||
|
decryptionFailures,
|
||||||
|
} = useNotes()
|
||||||
|
|
||||||
const notes = ref()
|
const notes = ref()
|
||||||
const loaded = ref(false)
|
const loaded = ref(false)
|
||||||
@@ -44,15 +55,23 @@ onMounted(async () => {
|
|||||||
await refreshNotes()
|
await refreshNotes()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(notesChangeCount, async () => {
|
watch(changeCount, async () => {
|
||||||
await refreshNotes()
|
await refreshNotes()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// New category keyboard shortcut
|
||||||
|
const { ctrl, t } = useMagicKeys()
|
||||||
|
watchEffect(() => {
|
||||||
|
if (ctrl.value && t.value) {
|
||||||
|
router.push('/category')
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
main.directory {
|
main.directory {
|
||||||
padding-top: var(--nav-height);
|
padding-top: var(--nav-height);
|
||||||
padding-bottom: 30px;
|
padding-bottom: 60px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|||||||
Reference in New Issue
Block a user