From ad961fd31a8f8da0822b822eb279ac7c7b8a1968 Mon Sep 17 00:00:00 2001 From: nicwands Date: Thu, 2 Apr 2026 11:45:46 -0400 Subject: [PATCH] refactor + warning for failed decryptions --- out/main/index.js | 165 ++++++++++++++---- package-lock.json | 7 +- package.json | 1 + src/core/ConfigManager.js | 82 ++++++++- src/core/NotesAPI.js | 83 ++++++--- src/core/index.js | 34 ++-- .../src/components/DecryptionWarning.vue | 103 +++++++++++ src/renderer/src/components/NoteRow.vue | 1 + src/renderer/src/components/ScrollBar.vue | 1 + src/renderer/src/components/note/Editor.vue | 1 + .../src/composables/useNoteListeners.js | 52 ++++++ src/renderer/src/composables/useNotes.js | 90 +++------- src/renderer/src/libs/getNotesAPI.js | 18 -- src/renderer/src/plugins/router.js | 12 +- src/renderer/src/views/Category.vue | 20 +-- src/renderer/src/views/Directory.vue | 22 ++- 16 files changed, 505 insertions(+), 187 deletions(-) create mode 100644 src/renderer/src/components/DecryptionWarning.vue create mode 100644 src/renderer/src/composables/useNoteListeners.js delete mode 100644 src/renderer/src/libs/getNotesAPI.js diff --git a/out/main/index.js b/out/main/index.js index e61668b..97fa74e 100644 --- a/out/main/index.js +++ b/out/main/index.js @@ -5,6 +5,7 @@ import filesystemPlugin from "@takerofnotes/plugin-filesystem"; import supabasePlugin from "@takerofnotes/plugin-supabase"; import s3Plugin from "@takerofnotes/plugin-s3"; import postgresPlugin from "@takerofnotes/plugin-postgre-sql"; +import Ajv from "ajv"; import sodium from "libsodium-wrappers"; import { v4 } from "uuid"; import { Index } from "flexsearch"; @@ -74,13 +75,60 @@ class PluginRegistry { })); } } +const ajv = new Ajv({ allErrors: true, strict: false }); const getDefaultConfig = () => { return { activeAdapter: "browser", 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; return { async loadConfig() { @@ -96,6 +144,20 @@ const createConfigManager = (storage) => { return config; }, 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; await storage.save(newConfig); }, @@ -111,8 +173,10 @@ class NotesAPI { } this.adapter = adapter; this.notesCache = /* @__PURE__ */ new Map(); + this.categories = /* @__PURE__ */ new Set(); this.encryptionKey = encryptionKey; this._sodiumReady = false; + this._decryptionFailures = []; this.index = new Index({ tokenize: "forward" }); @@ -124,11 +188,10 @@ 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; + return Uint8Array.from( + hex.match(/.{1,2}/g), + (byte) => parseInt(byte, 16) + ); } _concatUint8Arrays(a, b) { const result = new Uint8Array(a.length + b.length); @@ -137,19 +200,11 @@ class NotesAPI { return result; } _uint8ArrayToBase64(bytes) { - let binary = ""; - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); + return btoa(String.fromCharCode(...bytes)); } _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; + return Uint8Array.from(binary, (c) => c.charCodeAt(0)); } _encrypt(note) { if (!this.encryptionKey) { @@ -218,18 +273,28 @@ class NotesAPI { await this._initSodium(); await this.adapter.init(); this.notesCache.clear(); + this.categories.clear(); + this._decryptionFailures = []; const encryptedNotes = await this.adapter.getAll(); for (const encryptedNote of encryptedNotes) { + const noteId = encryptedNote.id || "unknown"; try { const note = this._decrypt(encryptedNote.data || encryptedNote); this.notesCache.set(note.id, note); + if (note.category) { + this.categories.add(note.category); + } const searchText = note.plainText || this._extractPlainText(note.content); this.index.add(note.id, note.title + "\n" + searchText); } catch (error) { console.error("Failed to decrypt note:", error); + this._decryptionFailures.push(noteId); } } } + getDecryptionFailures() { + return [...this._decryptionFailures]; + } _extractPlainText(content) { if (!content) return ""; if (typeof content === "string") return content; @@ -241,19 +306,14 @@ class NotesAPI { return extractText(content); } getCategories() { - const categories = /* @__PURE__ */ new Set(); - for (const note of this.notesCache.values()) { - if (note.category) { - categories.add(note.category); - } - } - return Array.from(categories).sort(); + return Array.from(this.categories).sort(); } getCategoryNotes(categoryName = null) { return Array.from(this.notesCache.values()).filter((n) => n.category === categoryName).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)).map((n) => ({ ...n })); } getNote(id) { const note = this.notesCache.get(id); + console.log(id, this.notesCache); return note ? { ...note } : null; } async createNote(metadata = {}, content = "", plainText = "") { @@ -273,14 +333,32 @@ class NotesAPI { data: this._encrypt(note) }; this.notesCache.set(id, note); + if (note.category) { + this.categories.add(note.category); + } this.index.add(id, note.title + "\n" + plainText); await this.adapter.create(encryptedNote); return note; } async deleteNote(id) { + const note = this.notesCache.get(id); + const category = note?.category; await this.adapter.delete(id); this.notesCache.delete(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 = {}) { const note = this.notesCache.get(id); @@ -291,6 +369,7 @@ class NotesAPI { throw new Error(`Invalid update field: ${key}`); } } + const oldCategory = note.category; const updatedNote = { ...note, ...updates, @@ -301,6 +380,19 @@ class NotesAPI { data: this._encrypt(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); this.index.update(id, updatedNote.title + "\n" + searchText); await this.adapter.update(encryptedNote); @@ -319,46 +411,45 @@ const generateEncryptionKey = () => { crypto.getRandomValues(array); 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(); for (const plugin of plugins) { registry.register(plugin); } - const manager = createPluginManager(registry); - const activeConfig = config.adapters?.[config.activeAdapter] || {}; - manager.setActivePlugin(config.activeAdapter, activeConfig); - return manager; + return createPluginManager(registry); }; -const initConfigManager = async (runtime) => { +const initConfigManager = async (runtime, pluginManager) => { let storage; { const { createNodeStorage } = await import("./NodeStorage-B8VFtrTS.js"); const filesystemPlugin2 = (await import("@takerofnotes/plugin-filesystem")).default; storage = createNodeStorage(filesystemPlugin2); } - return createConfigManager(storage); + return createConfigManager(storage, pluginManager); }; 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 pluginManager = initPluginManager(runtime, plugins, config); + const activeConfig = config.adapters?.[config.activeAdapter] || {}; + pluginManager.setActivePlugin(config.activeAdapter, activeConfig); let notesAPI = null; let initPromise = null; const getNotesAPI = async () => { if (notesAPI) return notesAPI; if (!initPromise) { initPromise = (async () => { - const config2 = await configManager.loadConfig(); - let encryptionKey = config2?.encryptionKey; + const latestConfig = await configManager.loadConfig(); + let encryptionKey = latestConfig?.encryptionKey; if (!encryptionKey) { encryptionKey = generateEncryptionKey(); await configManager.setConfig({ - ...config2, + ...latestConfig, encryptionKey }); } - const pluginId = config2?.activeAdapter || "filesystem"; - const adapterConfig = config2?.adapters?.[pluginId] || {}; + const pluginId = latestConfig?.activeAdapter || "filesystem"; + const adapterConfig = latestConfig?.adapters?.[pluginId] || {}; const adapter = pluginManager.getAdapter( pluginId, adapterConfig diff --git a/package-lock.json b/package-lock.json index c4f2cb6..a59f245 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@tiptap/starter-kit": "^3.19.0", "@tiptap/vue-3": "^3.19.0", "@vueuse/core": "^14.2.1", + "ajv": "^6.14.0", "archiver": "^7.0.1", "dotenv": "^17.3.1", "electron-updater": "^6.3.9", @@ -4917,7 +4918,6 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -7245,7 +7245,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-fifo": { @@ -7258,7 +7257,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-xml-builder": { @@ -8192,7 +8190,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { @@ -9756,7 +9753,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -11243,7 +11239,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" diff --git a/package.json b/package.json index bd9d7be..bd0a0e4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@tiptap/starter-kit": "^3.19.0", "@tiptap/vue-3": "^3.19.0", "@vueuse/core": "^14.2.1", + "ajv": "^6.14.0", "archiver": "^7.0.1", "dotenv": "^17.3.1", "electron-updater": "^6.3.9", diff --git a/src/core/ConfigManager.js b/src/core/ConfigManager.js index 9ef1606..16e7f29 100644 --- a/src/core/ConfigManager.js +++ b/src/core/ConfigManager.js @@ -1,3 +1,7 @@ +import Ajv from 'ajv' + +const ajv = new Ajv({ allErrors: true, strict: false }) + const getDefaultConfig = () => { return { 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 return { @@ -27,6 +89,24 @@ export const createConfigManager = (storage) => { }, 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 await storage.save(newConfig) }, diff --git a/src/core/NotesAPI.js b/src/core/NotesAPI.js index a072f10..947018d 100644 --- a/src/core/NotesAPI.js +++ b/src/core/NotesAPI.js @@ -10,8 +10,10 @@ export default class NotesAPI { this.adapter = adapter this.notesCache = new Map() + this.categories = new Set() this.encryptionKey = encryptionKey this._sodiumReady = false + this._decryptionFailures = [] this.index = new Index({ tokenize: 'forward', @@ -26,11 +28,9 @@ 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 + return Uint8Array.from(hex.match(/.{1,2}/g), (byte) => + parseInt(byte, 16), + ) } _concatUint8Arrays(a, b) { @@ -41,20 +41,12 @@ export default class NotesAPI { } _uint8ArrayToBase64(bytes) { - let binary = '' - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]) - } - return btoa(binary) + return btoa(String.fromCharCode(...bytes)) } _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 + return Uint8Array.from(binary, (c) => c.charCodeAt(0)) } _encrypt(note) { @@ -142,23 +134,34 @@ export default class NotesAPI { await this._initSodium() await this.adapter.init() this.notesCache.clear() + this.categories.clear() + this._decryptionFailures = [] const encryptedNotes = await this.adapter.getAll() for (const encryptedNote of encryptedNotes) { + const noteId = encryptedNote.id || 'unknown' try { const note = this._decrypt(encryptedNote.data || encryptedNote) this.notesCache.set(note.id, note) + if (note.category) { + this.categories.add(note.category) + } const searchText = note.plainText || this._extractPlainText(note.content) this.index.add(note.id, note.title + '\n' + searchText) } catch (error) { console.error('Failed to decrypt note:', error) + this._decryptionFailures.push(noteId) } } } + getDecryptionFailures() { + return [...this._decryptionFailures] + } + _extractPlainText(content) { if (!content) return '' if (typeof content === 'string') return content @@ -173,15 +176,7 @@ export default class NotesAPI { } getCategories() { - const categories = new Set() - - for (const note of this.notesCache.values()) { - if (note.category) { - categories.add(note.category) - } - } - - return Array.from(categories).sort() + return Array.from(this.categories).sort() } getCategoryNotes(categoryName = null) { @@ -193,6 +188,7 @@ export default class NotesAPI { getNote(id) { const note = this.notesCache.get(id) + return note ? { ...note } : null } @@ -216,6 +212,9 @@ export default class NotesAPI { } this.notesCache.set(id, note) + if (note.category) { + this.categories.add(note.category) + } this.index.add(id, note.title + '\n' + plainText) await this.adapter.create(encryptedNote) @@ -224,10 +223,30 @@ export default class NotesAPI { } async deleteNote(id) { + const note = this.notesCache.get(id) + const category = note?.category + await this.adapter.delete(id) this.notesCache.delete(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 = {}) { @@ -242,6 +261,8 @@ export default class NotesAPI { } } + const oldCategory = note.category + const updatedNote = { ...note, ...updates, @@ -255,6 +276,20 @@ export default class NotesAPI { 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 = updatedNote.plainText || this._extractPlainText(updatedNote.content) diff --git a/src/core/index.js b/src/core/index.js index 79a6978..94ec2f9 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -18,7 +18,7 @@ const generateEncryptionKey = () => { .join('') } -const initPluginManager = (runtime, plugins, config) => { +const initPluginManager = (runtime, plugins) => { if (runtime === 'electron-renderer') return createPluginManagerClient() const registry = new PluginRegistry() @@ -27,14 +27,10 @@ const initPluginManager = (runtime, plugins, config) => { registry.register(plugin) } - const manager = createPluginManager(registry) - const activeConfig = config.adapters?.[config.activeAdapter] || {} - manager.setActivePlugin(config.activeAdapter, activeConfig) - - return manager + return createPluginManager(registry) } -const initConfigManager = async (runtime) => { +const initConfigManager = async (runtime, pluginManager) => { if (runtime === 'electron-renderer') return createConfigManagerClient() let storage @@ -48,35 +44,41 @@ const initConfigManager = async (runtime) => { storage = createWebStorage() } - return createConfigManager(storage) + return createConfigManager(storage, pluginManager) } 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 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 initPromise = null - const getNotesAPI = async () => { if (notesAPI) return notesAPI if (!initPromise) { initPromise = (async () => { - let encryptionKey = config?.encryptionKey + // Get fresh config to ensure adapters are populated from main process + const latestConfig = await configManager.loadConfig() + + let encryptionKey = latestConfig?.encryptionKey if (!encryptionKey) { encryptionKey = generateEncryptionKey() await configManager.setConfig({ - ...config, + ...latestConfig, encryptionKey, }) } - const pluginId = config?.activeAdapter || 'filesystem' - const adapterConfig = config?.adapters?.[pluginId] || {} + const pluginId = latestConfig?.activeAdapter || 'filesystem' + const adapterConfig = latestConfig?.adapters?.[pluginId] || {} const adapter = pluginManager.getAdapter( pluginId, diff --git a/src/renderer/src/components/DecryptionWarning.vue b/src/renderer/src/components/DecryptionWarning.vue new file mode 100644 index 0000000..5364fa9 --- /dev/null +++ b/src/renderer/src/components/DecryptionWarning.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/renderer/src/components/NoteRow.vue b/src/renderer/src/components/NoteRow.vue index a05660b..4d2f9a0 100644 --- a/src/renderer/src/components/NoteRow.vue +++ b/src/renderer/src/components/NoteRow.vue @@ -63,6 +63,7 @@ const moveActive = computed(() => route.query.move === props.note.id) .title { white-space: break-spaces; word-break: break-word; + @include line-clamp(3); } .action { opacity: 0; diff --git a/src/renderer/src/components/ScrollBar.vue b/src/renderer/src/components/ScrollBar.vue index c9934dc..6dde55f 100644 --- a/src/renderer/src/components/ScrollBar.vue +++ b/src/renderer/src/components/ScrollBar.vue @@ -78,6 +78,7 @@ watch( width: 8px; will-change: transform; border-left: 1px solid var(--grey-100); + background: var(--theme-bg); .inner { height: 100%; diff --git a/src/renderer/src/components/note/Editor.vue b/src/renderer/src/components/note/Editor.vue index 57670d9..31286ca 100644 --- a/src/renderer/src/components/note/Editor.vue +++ b/src/renderer/src/components/note/Editor.vue @@ -61,6 +61,7 @@ const onUpdate = _debounce(async ({ editor }) => { onMounted(async () => { const note = await loadNote(props.id) + if (!note) return if (note.title !== 'Untitled') { title.value = note.title || '' diff --git a/src/renderer/src/composables/useNoteListeners.js b/src/renderer/src/composables/useNoteListeners.js new file mode 100644 index 0000000..df17436 --- /dev/null +++ b/src/renderer/src/composables/useNoteListeners.js @@ -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, + } +} diff --git a/src/renderer/src/composables/useNotes.js b/src/renderer/src/composables/useNotes.js index bcdfb89..2fccd80 100644 --- a/src/renderer/src/composables/useNotes.js +++ b/src/renderer/src/composables/useNotes.js @@ -1,77 +1,40 @@ -import _omit from 'lodash/omit' +import useNoteListeners from '@/composables/useNoteListeners' +import useCore from '@/composables/useCore' import { ref } from 'vue' -import { getNotesAPI } from '@/libs/getNotesAPI' -import { useEnvironment } from '@/composables/useEnvironment.js' const categories = ref([]) const searchResults = ref([]) -const notesChangeCount = ref(0) - -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() -} +const decryptionFailures = ref([]) export default () => { - /* ------------------------- - Initialization - --------------------------*/ + const { getNotesAPI } = useCore() + + // 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 api = await getNotesAPI() categories.value = api.getCategories() } - const loadCategoryNotes = async (category = null) => { const api = await getNotesAPI() return api.getCategoryNotes(category) } - const loadNote = async (id) => { const api = await getNotesAPI() + console.log(id, api) return api.getNote(id) } - /* ------------------------- - Create - --------------------------*/ + // Create const createNote = async (metadata, content, plainText = '') => { const api = await getNotesAPI() const note = await api.createNote(metadata, content, plainText) @@ -80,9 +43,7 @@ export default () => { return note } - /* ------------------------- - Update - --------------------------*/ + // Update const updateNote = async (id, updates) => { const api = await getNotesAPI() @@ -96,7 +57,6 @@ export default () => { return note } - const updateCategory = async (category, update) => { const notes = await loadCategoryNotes(category) @@ -107,19 +67,16 @@ export default () => { await loadCategories() } - /* ------------------------- - Delete - --------------------------*/ + // Delete const deleteNote = async (id) => { const api = await getNotesAPI() await api.deleteNote(id) await loadCategories() broadcastChange('note-deleted', { id }) + getDecryptionFailures() } - /* ------------------------- - Search - --------------------------*/ + // Search const search = async (query) => { const api = await getNotesAPI() @@ -134,7 +91,8 @@ export default () => { return { categories, searchResults, - notesChangeCount, + decryptionFailures, + changeCount, loadCategories, loadCategoryNotes, diff --git a/src/renderer/src/libs/getNotesAPI.js b/src/renderer/src/libs/getNotesAPI.js deleted file mode 100644 index ce9a26d..0000000 --- a/src/renderer/src/libs/getNotesAPI.js +++ /dev/null @@ -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 -} diff --git a/src/renderer/src/plugins/router.js b/src/renderer/src/plugins/router.js index a6da9b3..8a0f19d 100644 --- a/src/renderer/src/plugins/router.js +++ b/src/renderer/src/plugins/router.js @@ -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 Note from '@/views/Note.vue' @@ -8,6 +13,8 @@ import Instructions from '@/views/Instructions.vue' import Search from '@/views/Search.vue' import Preferences from '@/views/Preferences.vue' +const environment = useEnvironment() + const routes = [ { path: '/', name: 'directory', component: Directory }, { path: '/note/:id', name: 'note', component: Note }, @@ -19,6 +26,7 @@ const routes = [ ] export const router = createRouter({ - history: createWebHashHistory(), + history: + environment === 'web' ? createWebHistory() : createWebHashHistory(), routes, }) diff --git a/src/renderer/src/views/Category.vue b/src/renderer/src/views/Category.vue index 7e73051..99f4d8f 100644 --- a/src/renderer/src/views/Category.vue +++ b/src/renderer/src/views/Category.vue @@ -27,22 +27,22 @@ const route = useRoute() const id = route.params?.id const router = useRouter() -const { - categories, - loadCategoryNotes, - updateCategory, - notesChangeCount, - loadCategories, -} = useNotes() - const notes = ref() -async function refreshNotes() { +const refreshNotes = async () => { if (id) { notes.value = await loadCategoryNotes(id) } } +const { + categories, + loadCategoryNotes, + updateCategory, + loadCategories, + changeCount, +} = useNotes(refreshNotes) + onMounted(async () => { await refreshNotes() @@ -52,7 +52,7 @@ onMounted(async () => { } }) -watch(notesChangeCount, async () => { +watch(changeCount, async () => { await refreshNotes() }) diff --git a/src/renderer/src/views/Directory.vue b/src/renderer/src/views/Directory.vue index 08bcb33..cf57d8f 100644 --- a/src/renderer/src/views/Directory.vue +++ b/src/renderer/src/views/Directory.vue @@ -14,24 +14,32 @@ + +