Compare commits
12 Commits
v0.1.1
...
a1b339f668
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1b339f668 | ||
|
|
efc9c73751 | ||
|
|
77b8ad2dcd | ||
|
|
23054d4981 | ||
|
|
a3bb474399 | ||
|
|
e48779e8e0 | ||
|
|
73349444d6 | ||
|
|
e9e0abe380 | ||
|
|
e843b7662d | ||
|
|
cc3ba79df0 | ||
|
|
2609d73bbd | ||
|
|
1c753a6f8f |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -35,6 +35,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Run platform build
|
- name: Run platform build
|
||||||
run: npm run ${{ matrix.script }}
|
run: npm run ${{ matrix.script }}
|
||||||
|
env:
|
||||||
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
3
bin/generateEncryptionKey.js
Normal file
3
bin/generateEncryptionKey.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
console.log(crypto.randomBytes(32).toString('hex'))
|
||||||
@@ -24,9 +24,7 @@ mac:
|
|||||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
notarize: false
|
notarize: true
|
||||||
cscLink: ${{ secrets.CSC_LINK }}
|
|
||||||
cscKeyPassword: ${{ secrets.CSC_KEY_PASSWORD }}
|
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}.${ext}
|
||||||
linux:
|
linux:
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import filesystemPlugin from "@takerofnotes/plugin-filesystem";
|
|||||||
import supabasePlugin from "@takerofnotes/plugin-supabase";
|
import supabasePlugin from "@takerofnotes/plugin-supabase";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path, { join } from "path";
|
import path, { join } from "path";
|
||||||
import { Index } from "flexsearch";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import __cjs_mod__ from "node:module";
|
import __cjs_mod__ from "node:module";
|
||||||
const __filename = import.meta.filename;
|
const __filename = import.meta.filename;
|
||||||
const __dirname = import.meta.dirname;
|
const __dirname = import.meta.dirname;
|
||||||
@@ -25,16 +23,21 @@ class PluginRegistry {
|
|||||||
return this.plugins.get(id);
|
return this.plugins.get(id);
|
||||||
}
|
}
|
||||||
list() {
|
list() {
|
||||||
return Array.from(this.plugins.values());
|
return Array.from(this.plugins.values()).map((plugin) => ({
|
||||||
|
id: plugin.id,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
configSchema: plugin.configSchema
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const USER_DATA_STRING = "__DEFAULT_USER_DATA__";
|
const USER_DATA_STRING = "__DEFAULT_USER_DATA__";
|
||||||
class PluginConfig {
|
class Config {
|
||||||
constructor(defaultPlugin) {
|
constructor(defaultPlugin) {
|
||||||
this.defaultPlugin = defaultPlugin;
|
this.defaultPlugin = defaultPlugin;
|
||||||
this.configPath = path.join(app.getPath("userData"), "config.json");
|
this.configPath = path.join(app.getPath("userData"), "config.json");
|
||||||
}
|
}
|
||||||
// Helper to replace placeholders with dynamic values, recursively
|
// Helper to replace placeholders with dynamic values
|
||||||
_resolveDefaults(config) {
|
_resolveDefaults(config) {
|
||||||
if (Array.isArray(config)) {
|
if (Array.isArray(config)) {
|
||||||
return config.map((item) => this._resolveDefaults(item));
|
return config.map((item) => this._resolveDefaults(item));
|
||||||
@@ -64,9 +67,10 @@ class PluginConfig {
|
|||||||
defaultConfig[field.key] = field.default ?? null;
|
defaultConfig[field.key] = field.default ?? null;
|
||||||
}
|
}
|
||||||
parsed = {
|
parsed = {
|
||||||
activeAdapter: this.defaultPlugin.id,
|
...parsed ? parsed : {},
|
||||||
adapterConfig: defaultConfig
|
activeAdapter: this.defaultPlugin.id
|
||||||
};
|
};
|
||||||
|
parsed.adapters[this.defaultPlugin.id] = defaultConfig;
|
||||||
await this.write(parsed);
|
await this.write(parsed);
|
||||||
} else {
|
} else {
|
||||||
parsed.adapterConfig = this._resolveDefaults(parsed.adapterConfig);
|
parsed.adapterConfig = this._resolveDefaults(parsed.adapterConfig);
|
||||||
@@ -87,103 +91,10 @@ class PluginConfig {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
class NotesAPI {
|
|
||||||
constructor(adapter) {
|
|
||||||
if (!adapter) {
|
|
||||||
throw new Error("NotesAPI requires a storage adapter");
|
|
||||||
}
|
|
||||||
this.adapter = adapter;
|
|
||||||
this.notesCache = /* @__PURE__ */ new Map();
|
|
||||||
this.index = new Index({
|
|
||||||
tokenize: "tolerant",
|
|
||||||
resolution: 9
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async init() {
|
|
||||||
await this.adapter.init();
|
|
||||||
const notes = await this.adapter.getAll();
|
|
||||||
for (const note of notes) {
|
|
||||||
this.notesCache.set(note.id, note);
|
|
||||||
this.index.add(note.id, note.title + "\n" + note.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* -----------------------
|
|
||||||
Public API
|
|
||||||
------------------------*/
|
|
||||||
getCategories() {
|
|
||||||
const categories = /* @__PURE__ */ new Set();
|
|
||||||
for (const note of this.notesCache.values()) {
|
|
||||||
if (note.category) {
|
|
||||||
categories.add(note.category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(categories).sort();
|
|
||||||
}
|
|
||||||
getCategoryNotes(categoryName) {
|
|
||||||
return Array.from(this.notesCache.values()).filter((n) => n.category === categoryName).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
||||||
}
|
|
||||||
getNote(id) {
|
|
||||||
return this.notesCache.get(id) ?? null;
|
|
||||||
}
|
|
||||||
async createNote(metadata = {}, content = "") {
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
||||||
const note = {
|
|
||||||
id,
|
|
||||||
title: metadata.title || "Untitled",
|
|
||||||
category: metadata.category || null,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
content
|
|
||||||
};
|
|
||||||
this.notesCache.set(id, note);
|
|
||||||
this.index.add(id, note.title + "\n" + content);
|
|
||||||
await this.adapter.create(note);
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
async deleteNote(id) {
|
|
||||||
await this.adapter.delete(id);
|
|
||||||
this.notesCache.delete(id);
|
|
||||||
this.index.remove(id);
|
|
||||||
}
|
|
||||||
async updateNote(id, content) {
|
|
||||||
const note = this.notesCache.get(id);
|
|
||||||
if (!note) throw new Error("Note not found");
|
|
||||||
note.content = content;
|
|
||||||
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
||||||
this.index.update(id, note.title + "\n" + content);
|
|
||||||
await this.adapter.update(note);
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
async updateNoteMetadata(id, updates = {}) {
|
|
||||||
const note = this.notesCache.get(id);
|
|
||||||
if (!note) throw new Error("Note not found");
|
|
||||||
const allowedFields = ["title", "category"];
|
|
||||||
for (const key of Object.keys(updates)) {
|
|
||||||
if (!allowedFields.includes(key)) {
|
|
||||||
throw new Error(`Invalid metadata field: ${key}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (updates.title !== void 0) {
|
|
||||||
note.title = updates.title;
|
|
||||||
}
|
|
||||||
if (updates.category !== void 0) {
|
|
||||||
note.category = updates.category;
|
|
||||||
}
|
|
||||||
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
||||||
this.index.update(id, note.title + "\n" + note.content);
|
|
||||||
await this.adapter.update(note);
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
search(query) {
|
|
||||||
const ids = this.index.search(query);
|
|
||||||
return ids.map((id) => this.notesCache.get(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const preloadPath = join(__dirname, "../preload/index.mjs");
|
const preloadPath = join(__dirname, "../preload/index.mjs");
|
||||||
const rendererPath = join(__dirname, "../renderer/index.html");
|
const rendererPath = join(__dirname, "../renderer/index.html");
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const mainWindow2 = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 354,
|
width: 354,
|
||||||
height: 549,
|
height: 549,
|
||||||
show: false,
|
show: false,
|
||||||
@@ -193,17 +104,17 @@ function createWindow() {
|
|||||||
sandbox: false
|
sandbox: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mainWindow2.on("ready-to-show", () => {
|
mainWindow.on("ready-to-show", () => {
|
||||||
mainWindow2.show();
|
mainWindow.show();
|
||||||
});
|
});
|
||||||
mainWindow2.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url);
|
shell.openExternal(details.url);
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
mainWindow2.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
||||||
} else {
|
} else {
|
||||||
mainWindow2.loadFile(rendererPath);
|
mainWindow.loadFile(rendererPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function createNoteWindow(noteId) {
|
function createNoteWindow(noteId) {
|
||||||
@@ -220,11 +131,11 @@ function createNoteWindow(noteId) {
|
|||||||
});
|
});
|
||||||
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
noteWindow.loadURL(
|
noteWindow.loadURL(
|
||||||
`${process.env["ELECTRON_RENDERER_URL"]}/note/${noteId}`
|
`${process.env["ELECTRON_RENDERER_URL"]}/#/note/${noteId}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(rendererPath, {
|
noteWindow.loadFile(rendererPath, {
|
||||||
path: `/notes/${noteId}`
|
hash: `/note/${noteId}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,19 +146,44 @@ app.whenReady().then(async () => {
|
|||||||
const registry = new PluginRegistry();
|
const registry = new PluginRegistry();
|
||||||
registry.register(filesystemPlugin);
|
registry.register(filesystemPlugin);
|
||||||
registry.register(supabasePlugin);
|
registry.register(supabasePlugin);
|
||||||
await new PluginConfig(filesystemPlugin).load();
|
const config = new Config(filesystemPlugin);
|
||||||
const plugin = registry.get(supabasePlugin.id);
|
const initialConfig = await config.load();
|
||||||
const adapter = plugin.createAdapter({
|
const setActivePlugin = async (pluginId) => {
|
||||||
supabaseKey: process.env.SUPABASE_KEY,
|
const currentConfig = await config.load();
|
||||||
supabaseUrl: process.env.SUPABASE_URL
|
await config.write({ ...currentConfig, activeAdapter: pluginId });
|
||||||
});
|
const plugin = registry.get(pluginId);
|
||||||
const notesAPI = new NotesAPI(adapter);
|
const adapterConfig = currentConfig.adapters[pluginId] || {};
|
||||||
await notesAPI.init();
|
const adapter = plugin.createAdapter(adapterConfig);
|
||||||
ipcMain.handle("notesAPI:call", (_, method, args) => {
|
await adapter.init();
|
||||||
if (!notesAPI[method]) {
|
ipcMain.removeHandler("adapter:call");
|
||||||
throw new Error("Invalid method");
|
ipcMain.handle("adapter:call", async (_, method, args) => {
|
||||||
|
if (!adapter[method]) {
|
||||||
|
throw new Error(`Invalid adapter method: ${method}`);
|
||||||
}
|
}
|
||||||
return notesAPI[method](...args);
|
return await adapter[method](...args);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
await setActivePlugin(initialConfig.activeAdapter);
|
||||||
|
ipcMain.handle("getConfig", async () => {
|
||||||
|
return await config.load();
|
||||||
|
});
|
||||||
|
ipcMain.handle("setConfig", async (_, newConfig) => {
|
||||||
|
await config.write(newConfig);
|
||||||
|
});
|
||||||
|
ipcMain.handle("listPlugins", async () => {
|
||||||
|
return registry.list();
|
||||||
|
});
|
||||||
|
ipcMain.handle("setActivePlugin", async (_, pluginId) => {
|
||||||
|
return await setActivePlugin(pluginId);
|
||||||
|
});
|
||||||
|
const broadcastNoteChange = (event, data) => {
|
||||||
|
BrowserWindow.getAllWindows().forEach((win) => {
|
||||||
|
win.webContents.send(event, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
ipcMain.on("note-changed", (_, event, data) => {
|
||||||
|
broadcastNoteChange(event, data);
|
||||||
});
|
});
|
||||||
electronApp.setAppUserModelId("com.electron");
|
electronApp.setAppUserModelId("com.electron");
|
||||||
app.on("browser-window-created", (_, window) => {
|
app.on("browser-window-created", (_, window) => {
|
||||||
|
|||||||
@@ -1,19 +1,36 @@
|
|||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
const api = {
|
const api = {
|
||||||
|
getConfig: () => ipcRenderer.invoke("getConfig"),
|
||||||
|
setConfig: (config) => ipcRenderer.invoke("setConfig", config),
|
||||||
|
listPlugins: () => ipcRenderer.invoke("listPlugins"),
|
||||||
|
setActivePlugin: (pluginId) => ipcRenderer.invoke("setActivePlugin", pluginId),
|
||||||
openNoteWindow: (noteId) => {
|
openNoteWindow: (noteId) => {
|
||||||
ipcRenderer.send("open-note-window", noteId);
|
ipcRenderer.send("open-note-window", noteId);
|
||||||
|
},
|
||||||
|
onNoteCreated: (callback) => {
|
||||||
|
ipcRenderer.on("note-created", (_, data) => callback(data));
|
||||||
|
},
|
||||||
|
onNoteUpdated: (callback) => {
|
||||||
|
ipcRenderer.on("note-updated", (_, data) => callback(data));
|
||||||
|
},
|
||||||
|
onNoteDeleted: (callback) => {
|
||||||
|
ipcRenderer.on("note-deleted", (_, data) => callback(data));
|
||||||
|
},
|
||||||
|
notifyNoteChanged: (event, data) => {
|
||||||
|
ipcRenderer.send("note-changed", event, data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const notesAPI = {
|
const adapter = {
|
||||||
call: (method, ...args) => ipcRenderer.invoke("notesAPI:call", method, args)
|
call: (method, ...args) => ipcRenderer.invoke("adapter:call", method, args)
|
||||||
};
|
};
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld("api", api);
|
contextBridge.exposeInMainWorld("api", api);
|
||||||
contextBridge.exposeInMainWorld("notesAPI", notesAPI);
|
contextBridge.exposeInMainWorld("adapter", adapter);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.api = api;
|
window.api = api;
|
||||||
|
window.adapter = adapter;
|
||||||
}
|
}
|
||||||
|
|||||||
4
out/renderer/assets/__vite-browser-external-2Ng8QIWW.js
Normal file
4
out/renderer/assets/__vite-browser-external-2Ng8QIWW.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const __viteBrowserExternal = {};
|
||||||
|
export {
|
||||||
|
__viteBrowserExternal as default
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
97038
out/renderer/assets/index-CoqDP7Z2.js
Normal file
97038
out/renderer/assets/index-CoqDP7Z2.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
BIN
out/renderer/assets/neuefraktur-A4S1ACH2.woff2
Normal file
BIN
out/renderer/assets/neuefraktur-A4S1ACH2.woff2
Normal file
Binary file not shown.
BIN
out/renderer/assets/neuefraktur-CwjUIZ0G.woff
Normal file
BIN
out/renderer/assets/neuefraktur-CwjUIZ0G.woff
Normal file
Binary file not shown.
@@ -8,8 +8,8 @@
|
|||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
||||||
/>
|
/>
|
||||||
<script type="module" crossorigin src="./assets/index-Dv1qfmwr.js"></script>
|
<script type="module" crossorigin src="./assets/index-CoqDP7Z2.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-CZWw79gc.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-CVyE7-c9.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
334
package-lock.json
generated
334
package-lock.json
generated
@@ -1,23 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "app.takerofnotes.com",
|
"name": "takerofnotes-app",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "app.takerofnotes.com",
|
"name": "takerofnotes-app",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@fuzzco/font-loader": "^1.0.2",
|
"@fuzzco/font-loader": "^1.0.2",
|
||||||
"@takerofnotes/plugin-filesystem": "^0.1.1",
|
"@takerofnotes/plugin-filesystem": "^0.2.0",
|
||||||
"@takerofnotes/plugin-supabase": "^0.1.0",
|
"@takerofnotes/plugin-supabase": "^0.1.0",
|
||||||
|
"@tiptap/extension-code-block-lowlight": "^3.20.0",
|
||||||
"@tiptap/extension-document": "^3.19.0",
|
"@tiptap/extension-document": "^3.19.0",
|
||||||
"@tiptap/extension-image": "^3.19.0",
|
"@tiptap/extension-highlight": "^3.20.0",
|
||||||
"@tiptap/extension-table": "^3.19.0",
|
"@tiptap/extension-list": "^3.20.0",
|
||||||
"@tiptap/markdown": "^3.19.0",
|
|
||||||
"@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",
|
||||||
@@ -26,8 +26,9 @@
|
|||||||
"fecha": "^4.2.3",
|
"fecha": "^4.2.3",
|
||||||
"flexsearch": "^0.8.212",
|
"flexsearch": "^0.8.212",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"lenis": "^1.3.17",
|
"libsodium-wrappers": "^0.8.2",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
"sass": "^1.97.3",
|
"sass": "^1.97.3",
|
||||||
"sass-embedded": "^1.97.3",
|
"sass-embedded": "^1.97.3",
|
||||||
"tempus": "^1.0.0-dev.17",
|
"tempus": "^1.0.0-dev.17",
|
||||||
@@ -2343,19 +2344,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@takerofnotes/plugin-filesystem": {
|
"node_modules/@takerofnotes/plugin-filesystem": {
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-filesystem/-/plugin-filesystem-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-filesystem/-/plugin-filesystem-0.2.0.tgz",
|
||||||
"integrity": "sha512-u3L6HLxN/+t7PTtzzRA2uzgaVh/O1vasngU/tQeC6JsTnlIrypWFVbAlTS5zT9Km4y+8w6C16eEq7tcN8+5lPg==",
|
"integrity": "sha512-BP7HBN0SKAqBiv5pDtXpyVmkW9UrOnPXpKeThTwQTIShHyN5aPaD8ZEyv8+vfTSs3qnXWGplNdEPVjbQmc27+Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@takerofnotes/plugin-sdk": "^0.1.0",
|
"@takerofnotes/plugin-sdk": "^0.3.1"
|
||||||
"gray-matter": "^4.0.3"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@takerofnotes/plugin-sdk": {
|
"node_modules/@takerofnotes/plugin-sdk": {
|
||||||
"version": "0.1.0",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-sdk/-/plugin-sdk-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-sdk/-/plugin-sdk-0.3.1.tgz",
|
||||||
"integrity": "sha512-ofhwwiQ59kNMEg2vvYoNq5JdXHB9/6TkDsbyroM5nsP/VPUvJSQ5g0UWCYBhteSzJ36iFUB+LtUwVt6gOXDClw==",
|
"integrity": "sha512-9GfPKyu1n52N00zYlLK32wdmGdc2uSd0jTj6UEixauW0TXn/7hD6SLpLRGSXcfZOJGGoi3iQk4MfjNsthe2ucw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
@@ -2464,17 +2464,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-code-block": {
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
"version": "3.19.0",
|
"version": "3.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz",
|
||||||
"integrity": "sha512-b/2qR+tMn8MQb+eaFYgVk4qXnLNkkRYmwELQ8LEtEDQPxa5Vl7J3eu8+4OyoIFhZrNDZvvoEp80kHMCP8sI6rg==",
|
"integrity": "sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^3.19.0",
|
"@tiptap/core": "^3.20.0",
|
||||||
"@tiptap/pm": "^3.19.0"
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block-lowlight": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-9lN9rn07lOWkLnByT5C1axtq56MHpOI7MpLaCmX3p+x1bDl6Uvixm6AoBdTLfZUmUYeEFBsf7t5cR+QepMbkiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/extension-code-block": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0",
|
||||||
|
"highlight.js": "^11",
|
||||||
|
"lowlight": "^2 || ^3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-document": {
|
"node_modules/@tiptap/extension-document": {
|
||||||
@@ -2558,6 +2575,19 @@
|
|||||||
"@tiptap/core": "^3.19.0"
|
"@tiptap/core": "^3.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/extension-highlight": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-ANL1wFz0s1ScNR4uBfO0s6Sz+qqGp2u6ynrCVk6TCT3d10CQ+gD1gSDTrVRC3CtlMKtHHH4fYrFAq284+J0gKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
"version": "3.19.0",
|
"version": "3.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz",
|
||||||
@@ -2572,19 +2602,6 @@
|
|||||||
"@tiptap/pm": "^3.19.0"
|
"@tiptap/pm": "^3.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-image": {
|
|
||||||
"version": "3.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.19.0.tgz",
|
|
||||||
"integrity": "sha512-/rGl8nBziBPVJJ/9639eQWFDKcI3RQsDM3s+cqYQMFQfMqc7sQB9h4o4sHCBpmKxk3Y0FV/0NjnjLbBVm8OKdQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@tiptap/core": "^3.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tiptap/extension-italic": {
|
"node_modules/@tiptap/extension-italic": {
|
||||||
"version": "3.19.0",
|
"version": "3.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz",
|
||||||
@@ -2694,20 +2711,6 @@
|
|||||||
"@tiptap/core": "^3.19.0"
|
"@tiptap/core": "^3.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-table": {
|
|
||||||
"version": "3.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.19.0.tgz",
|
|
||||||
"integrity": "sha512-Lg8DlkkDUMYE/CcGOxoCWF98B2i7VWh+AGgqlF+XWrHjhlKHfENLRXm1a0vWuyyP3NknRYILoaaZ1s7QzmXKRA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@tiptap/core": "^3.19.0",
|
|
||||||
"@tiptap/pm": "^3.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tiptap/extension-text": {
|
"node_modules/@tiptap/extension-text": {
|
||||||
"version": "3.19.0",
|
"version": "3.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.19.0.tgz",
|
||||||
@@ -2748,23 +2751,6 @@
|
|||||||
"@tiptap/pm": "^3.19.0"
|
"@tiptap/pm": "^3.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/markdown": {
|
|
||||||
"version": "3.19.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.19.0.tgz",
|
|
||||||
"integrity": "sha512-Pnfacq2FHky1rqwmGwEmUJxuZu8VZ8XjaJIqsQC34S3CQWiOU+PukC9In2odzcooiVncLWT9s97jKuYpbmF1tQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"marked": "^17.0.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@tiptap/core": "^3.19.0",
|
|
||||||
"@tiptap/pm": "^3.19.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tiptap/pm": {
|
"node_modules/@tiptap/pm": {
|
||||||
"version": "3.20.0",
|
"version": "3.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz",
|
||||||
@@ -2890,6 +2876,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/hast": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/unist": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/http-cache-semantics": {
|
"node_modules/@types/http-cache-semantics": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
@@ -2970,6 +2965,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/unist": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/verror": {
|
"node_modules/@types/verror": {
|
||||||
"version": "1.10.11",
|
"version": "1.10.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
||||||
@@ -4422,6 +4423,15 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@@ -4439,6 +4449,19 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/devlop": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dir-compare": {
|
"node_modules/dir-compare": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
|
||||||
@@ -5105,19 +5128,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esprima": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"bin": {
|
|
||||||
"esparse": "bin/esparse.js",
|
|
||||||
"esvalidate": "bin/esvalidate.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
@@ -5137,18 +5147,6 @@
|
|||||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/extend-shallow": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"is-extendable": "^0.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/extract-zip": {
|
"node_modules/extract-zip": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||||
@@ -5625,49 +5623,6 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/gray-matter": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"js-yaml": "^3.13.1",
|
|
||||||
"kind-of": "^6.0.2",
|
|
||||||
"section-matter": "^1.0.0",
|
|
||||||
"strip-bom-string": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter/node_modules/argparse": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"sprintf-js": "~1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter/node_modules/js-yaml": {
|
|
||||||
"version": "3.14.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
|
||||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"argparse": "^1.0.7",
|
|
||||||
"esprima": "^4.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"js-yaml": "bin/js-yaml.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gray-matter/node_modules/sprintf-js": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/gsap": {
|
"node_modules/gsap": {
|
||||||
"version": "3.14.2",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
|
||||||
@@ -5738,6 +5693,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/highlight.js": {
|
||||||
|
"version": "11.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||||
|
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "5.5.3",
|
"version": "5.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||||
@@ -5930,15 +5894,6 @@
|
|||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-extendable": {
|
|
||||||
"version": "0.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
|
||||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -6155,45 +6110,25 @@
|
|||||||
"json-buffer": "3.0.1"
|
"json-buffer": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/kind-of": {
|
|
||||||
"version": "6.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
|
||||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lazy-val": {
|
"node_modules/lazy-val": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lenis": {
|
"node_modules/libsodium": {
|
||||||
"version": "1.3.17",
|
"version": "0.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.17.tgz",
|
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz",
|
||||||
"integrity": "sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw==",
|
"integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==",
|
||||||
"license": "MIT",
|
"license": "ISC"
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/darkroomengineering"
|
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"node_modules/libsodium-wrappers": {
|
||||||
"@nuxt/kit": ">=3.0.0",
|
"version": "0.8.2",
|
||||||
"react": ">=17.0.0",
|
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz",
|
||||||
"vue": ">=3.0.0"
|
"integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==",
|
||||||
},
|
"license": "ISC",
|
||||||
"peerDependenciesMeta": {
|
"dependencies": {
|
||||||
"@nuxt/kit": {
|
"libsodium": "^0.8.0"
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"vue": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/linkify-it": {
|
"node_modules/linkify-it": {
|
||||||
@@ -6273,6 +6208,21 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lowlight": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hast": "^3.0.0",
|
||||||
|
"devlop": "^1.0.0",
|
||||||
|
"highlight.js": "~11.11.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -6359,18 +6309,6 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
|
||||||
"version": "17.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.2.tgz",
|
|
||||||
"integrity": "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"marked": "bin/marked.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/matcher": {
|
"node_modules/matcher": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||||
@@ -8121,19 +8059,6 @@
|
|||||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/section-matter": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"extend-shallow": "^2.0.1",
|
|
||||||
"kind-of": "^6.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
@@ -8416,15 +8341,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strip-bom-string": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sumchecker": {
|
"node_modules/sumchecker": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "app.takerofnotes.com",
|
"name": "takerofnotes-app",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "An Electron application with Vue",
|
"description": "An Electron application with Vue",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -28,12 +28,12 @@
|
|||||||
"@electron-toolkit/preload": "^3.0.2",
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
"@electron-toolkit/utils": "^4.0.0",
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
"@fuzzco/font-loader": "^1.0.2",
|
"@fuzzco/font-loader": "^1.0.2",
|
||||||
"@takerofnotes/plugin-filesystem": "^0.1.1",
|
"@takerofnotes/plugin-filesystem": "^0.2.0",
|
||||||
"@takerofnotes/plugin-supabase": "^0.1.0",
|
"@takerofnotes/plugin-supabase": "^0.1.0",
|
||||||
|
"@tiptap/extension-code-block-lowlight": "^3.20.0",
|
||||||
"@tiptap/extension-document": "^3.19.0",
|
"@tiptap/extension-document": "^3.19.0",
|
||||||
"@tiptap/extension-image": "^3.19.0",
|
"@tiptap/extension-highlight": "^3.20.0",
|
||||||
"@tiptap/extension-table": "^3.19.0",
|
"@tiptap/extension-list": "^3.20.0",
|
||||||
"@tiptap/markdown": "^3.19.0",
|
|
||||||
"@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",
|
||||||
@@ -42,8 +42,9 @@
|
|||||||
"fecha": "^4.2.3",
|
"fecha": "^4.2.3",
|
||||||
"flexsearch": "^0.8.212",
|
"flexsearch": "^0.8.212",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"lenis": "^1.3.17",
|
"libsodium-wrappers": "^0.8.2",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
"sass": "^1.97.3",
|
"sass": "^1.97.3",
|
||||||
"sass-embedded": "^1.97.3",
|
"sass-embedded": "^1.97.3",
|
||||||
"tempus": "^1.0.0-dev.17",
|
"tempus": "^1.0.0-dev.17",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
resources/fonts/neuefraktur.woff
Normal file
BIN
resources/fonts/neuefraktur.woff
Normal file
Binary file not shown.
BIN
resources/fonts/neuefraktur.woff2
Normal file
BIN
resources/fonts/neuefraktur.woff2
Normal file
Binary file not shown.
@@ -4,13 +4,13 @@ import { app } from 'electron'
|
|||||||
|
|
||||||
const USER_DATA_STRING = '__DEFAULT_USER_DATA__'
|
const USER_DATA_STRING = '__DEFAULT_USER_DATA__'
|
||||||
|
|
||||||
export default class PluginConfig {
|
export default class Config {
|
||||||
constructor(defaultPlugin) {
|
constructor(defaultPlugin) {
|
||||||
this.defaultPlugin = defaultPlugin
|
this.defaultPlugin = defaultPlugin
|
||||||
this.configPath = path.join(app.getPath('userData'), 'config.json')
|
this.configPath = path.join(app.getPath('userData'), 'config.json')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to replace placeholders with dynamic values, recursively
|
// Helper to replace placeholders with dynamic values
|
||||||
_resolveDefaults(config) {
|
_resolveDefaults(config) {
|
||||||
if (Array.isArray(config)) {
|
if (Array.isArray(config)) {
|
||||||
return config.map((item) => this._resolveDefaults(item))
|
return config.map((item) => this._resolveDefaults(item))
|
||||||
@@ -48,9 +48,10 @@ export default class PluginConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parsed = {
|
parsed = {
|
||||||
|
...(parsed ? parsed : {}),
|
||||||
activeAdapter: this.defaultPlugin.id,
|
activeAdapter: this.defaultPlugin.id,
|
||||||
adapterConfig: defaultConfig,
|
|
||||||
}
|
}
|
||||||
|
parsed.adapters[this.defaultPlugin.id] = defaultConfig
|
||||||
|
|
||||||
await this.write(parsed)
|
await this.write(parsed)
|
||||||
} else {
|
} else {
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { Index } from 'flexsearch'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
|
|
||||||
export default class NotesAPI {
|
|
||||||
constructor(adapter) {
|
|
||||||
if (!adapter) {
|
|
||||||
throw new Error('NotesAPI requires a storage adapter')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.adapter = adapter
|
|
||||||
this.notesCache = new Map()
|
|
||||||
|
|
||||||
this.index = new Index({
|
|
||||||
tokenize: 'tolerant',
|
|
||||||
resolution: 9,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
await this.adapter.init()
|
|
||||||
|
|
||||||
const notes = await this.adapter.getAll()
|
|
||||||
|
|
||||||
for (const note of notes) {
|
|
||||||
this.notesCache.set(note.id, note)
|
|
||||||
this.index.add(note.id, note.title + '\n' + note.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -----------------------
|
|
||||||
Public API
|
|
||||||
------------------------*/
|
|
||||||
getCategories() {
|
|
||||||
const categories = new Set()
|
|
||||||
|
|
||||||
for (const note of this.notesCache.values()) {
|
|
||||||
if (note.category) {
|
|
||||||
categories.add(note.category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(categories).sort()
|
|
||||||
}
|
|
||||||
|
|
||||||
getCategoryNotes(categoryName) {
|
|
||||||
return Array.from(this.notesCache.values())
|
|
||||||
.filter((n) => n.category === categoryName)
|
|
||||||
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
|
||||||
}
|
|
||||||
|
|
||||||
getNote(id) {
|
|
||||||
return this.notesCache.get(id) ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
async createNote(metadata = {}, content = '') {
|
|
||||||
const id = crypto.randomUUID()
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
|
|
||||||
const note = {
|
|
||||||
id,
|
|
||||||
title: metadata.title || 'Untitled',
|
|
||||||
category: metadata.category || null,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
content,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notesCache.set(id, note)
|
|
||||||
this.index.add(id, note.title + '\n' + content)
|
|
||||||
|
|
||||||
await this.adapter.create(note)
|
|
||||||
|
|
||||||
return note
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNote(id) {
|
|
||||||
await this.adapter.delete(id)
|
|
||||||
|
|
||||||
this.notesCache.delete(id)
|
|
||||||
this.index.remove(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateNote(id, content) {
|
|
||||||
const note = this.notesCache.get(id)
|
|
||||||
if (!note) throw new Error('Note not found')
|
|
||||||
|
|
||||||
note.content = content
|
|
||||||
note.updatedAt = new Date().toISOString()
|
|
||||||
|
|
||||||
this.index.update(id, note.title + '\n' + content)
|
|
||||||
|
|
||||||
await this.adapter.update(note)
|
|
||||||
|
|
||||||
return note
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateNoteMetadata(id, updates = {}) {
|
|
||||||
const note = this.notesCache.get(id)
|
|
||||||
if (!note) throw new Error('Note not found')
|
|
||||||
|
|
||||||
const allowedFields = ['title', 'category']
|
|
||||||
for (const key of Object.keys(updates)) {
|
|
||||||
if (!allowedFields.includes(key)) {
|
|
||||||
throw new Error(`Invalid metadata field: ${key}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.title !== undefined) {
|
|
||||||
note.title = updates.title
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.category !== undefined) {
|
|
||||||
note.category = updates.category
|
|
||||||
}
|
|
||||||
|
|
||||||
note.updatedAt = new Date().toISOString()
|
|
||||||
|
|
||||||
this.index.update(id, note.title + '\n' + note.content)
|
|
||||||
|
|
||||||
await this.adapter.update(note)
|
|
||||||
|
|
||||||
return note
|
|
||||||
}
|
|
||||||
|
|
||||||
search(query) {
|
|
||||||
const ids = this.index.search(query)
|
|
||||||
return ids.map((id) => this.notesCache.get(id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,11 @@ export default class PluginRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
list() {
|
list() {
|
||||||
return Array.from(this.plugins.values())
|
return Array.from(this.plugins.values()).map((plugin) => ({
|
||||||
|
id: plugin.id,
|
||||||
|
name: plugin.name,
|
||||||
|
description: plugin.description,
|
||||||
|
configSchema: plugin.configSchema,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { app, shell, BrowserWindow, ipcMain } from 'electron'
|
|||||||
import filesystemPlugin from '@takerofnotes/plugin-filesystem'
|
import filesystemPlugin from '@takerofnotes/plugin-filesystem'
|
||||||
import supabasePlugin from '@takerofnotes/plugin-supabase'
|
import supabasePlugin from '@takerofnotes/plugin-supabase'
|
||||||
import PluginRegistry from './core/PluginRegistry.js'
|
import PluginRegistry from './core/PluginRegistry.js'
|
||||||
import PluginConfig from './core/PluginConfig.js'
|
import Config from './core/Config.js'
|
||||||
import NotesAPI from './core/NotesAPI.js'
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
const preloadPath = join(__dirname, '../preload/index.mjs')
|
const preloadPath = join(__dirname, '../preload/index.mjs')
|
||||||
@@ -56,11 +55,11 @@ function createNoteWindow(noteId) {
|
|||||||
|
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
noteWindow.loadURL(
|
noteWindow.loadURL(
|
||||||
`${process.env['ELECTRON_RENDERER_URL']}/note/${noteId}`,
|
`${process.env['ELECTRON_RENDERER_URL']}/#/note/${noteId}`,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(rendererPath, {
|
noteWindow.loadFile(rendererPath, {
|
||||||
path: `/notes/${noteId}`,
|
hash: `/note/${noteId}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,27 +78,62 @@ app.whenReady().then(async () => {
|
|||||||
registry.register(supabasePlugin)
|
registry.register(supabasePlugin)
|
||||||
|
|
||||||
// Pull plugin config
|
// Pull plugin config
|
||||||
const config = await new PluginConfig(filesystemPlugin).load()
|
const config = new Config(filesystemPlugin)
|
||||||
|
const initialConfig = await config.load()
|
||||||
|
|
||||||
// Create instance of active adapter
|
const setActivePlugin = async (pluginId) => {
|
||||||
// const plugin = registry.get(config.activeAdapter)
|
const currentConfig = await config.load()
|
||||||
const plugin = registry.get(supabasePlugin.id)
|
await config.write({ ...currentConfig, activeAdapter: pluginId })
|
||||||
// const adapter = plugin.createAdapter(config.adapterConfig)
|
|
||||||
const adapter = plugin.createAdapter({
|
const plugin = registry.get(pluginId)
|
||||||
supabaseKey: process.env.SUPABASE_KEY,
|
const adapterConfig = currentConfig.adapters[pluginId] || {}
|
||||||
supabaseUrl: process.env.SUPABASE_URL,
|
const adapter = plugin.createAdapter(adapterConfig)
|
||||||
|
|
||||||
|
// Initialize adapter
|
||||||
|
await adapter.init()
|
||||||
|
|
||||||
|
// Handle adapter methods via IPC
|
||||||
|
ipcMain.removeHandler('adapter:call')
|
||||||
|
ipcMain.handle('adapter:call', async (_, method, args) => {
|
||||||
|
if (!adapter[method]) {
|
||||||
|
throw new Error(`Invalid adapter method: ${method}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await adapter[method](...args)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Init Notes API
|
return true
|
||||||
const notesAPI = new NotesAPI(adapter)
|
|
||||||
await notesAPI.init()
|
|
||||||
|
|
||||||
// Handle Notes API
|
|
||||||
ipcMain.handle('notesAPI:call', (_, method, args) => {
|
|
||||||
if (!notesAPI[method]) {
|
|
||||||
throw new Error('Invalid method')
|
|
||||||
}
|
}
|
||||||
return notesAPI[method](...args)
|
|
||||||
|
// Set active plugin
|
||||||
|
await setActivePlugin(initialConfig.activeAdapter)
|
||||||
|
|
||||||
|
// Get/set config
|
||||||
|
ipcMain.handle('getConfig', async () => {
|
||||||
|
return await config.load()
|
||||||
|
})
|
||||||
|
ipcMain.handle('setConfig', async (_, newConfig) => {
|
||||||
|
await config.write(newConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get/set plugins
|
||||||
|
ipcMain.handle('listPlugins', async () => {
|
||||||
|
return registry.list()
|
||||||
|
})
|
||||||
|
ipcMain.handle('setActivePlugin', async (_, pluginId) => {
|
||||||
|
return await setActivePlugin(pluginId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Broadcast note changes to all windows
|
||||||
|
const broadcastNoteChange = (event, data) => {
|
||||||
|
BrowserWindow.getAllWindows().forEach((win) => {
|
||||||
|
win.webContents.send(event, data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle note change events from renderer
|
||||||
|
ipcMain.on('note-changed', (_, event, data) => {
|
||||||
|
broadcastNoteChange(event, data)
|
||||||
})
|
})
|
||||||
|
|
||||||
electronApp.setAppUserModelId('com.electron')
|
electronApp.setAppUserModelId('com.electron')
|
||||||
|
|||||||
@@ -2,24 +2,41 @@ import { contextBridge, ipcRenderer } from 'electron'
|
|||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
|
getConfig: () => ipcRenderer.invoke('getConfig'),
|
||||||
|
setConfig: (config) => ipcRenderer.invoke('setConfig', config),
|
||||||
|
listPlugins: () => ipcRenderer.invoke('listPlugins'),
|
||||||
|
setActivePlugin: (pluginId) =>
|
||||||
|
ipcRenderer.invoke('setActivePlugin', pluginId),
|
||||||
openNoteWindow: (noteId) => {
|
openNoteWindow: (noteId) => {
|
||||||
ipcRenderer.send('open-note-window', noteId)
|
ipcRenderer.send('open-note-window', noteId)
|
||||||
},
|
},
|
||||||
|
onNoteCreated: (callback) => {
|
||||||
|
ipcRenderer.on('note-created', (_, data) => callback(data))
|
||||||
|
},
|
||||||
|
onNoteUpdated: (callback) => {
|
||||||
|
ipcRenderer.on('note-updated', (_, data) => callback(data))
|
||||||
|
},
|
||||||
|
onNoteDeleted: (callback) => {
|
||||||
|
ipcRenderer.on('note-deleted', (_, data) => callback(data))
|
||||||
|
},
|
||||||
|
notifyNoteChanged: (event, data) => {
|
||||||
|
ipcRenderer.send('note-changed', event, data)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement notes API
|
// Implement adapter API - communicates with plugin adapter in main process
|
||||||
const notesAPI = {
|
const adapter = {
|
||||||
call: (method, ...args) =>
|
call: (method, ...args) => ipcRenderer.invoke('adapter:call', method, args),
|
||||||
ipcRenderer.invoke('notesAPI:call', method, args),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('api', api)
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
contextBridge.exposeInMainWorld('notesAPI', notesAPI)
|
contextBridge.exposeInMainWorld('adapter', adapter)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.api = api
|
window.api = api
|
||||||
|
window.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,20 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import loadFonts from '@fuzzco/font-loader'
|
import loadFonts from '@fuzzco/font-loader'
|
||||||
import { useWindowSize } from '@vueuse/core'
|
import { useWindowSize } from '@vueuse/core'
|
||||||
import Menu from '@/components/menu/Index.vue'
|
import Menu from '@/components/Menu.vue'
|
||||||
import Nav from '@/components/Nav.vue'
|
import Nav from '@/components/Nav.vue'
|
||||||
import ScrollBar from './components/ScrollBar.vue'
|
import ScrollBar from '@/components/ScrollBar.vue'
|
||||||
|
import useConfig from '@/composables/useConfig'
|
||||||
|
|
||||||
const { height } = useWindowSize()
|
const { height } = useWindowSize()
|
||||||
|
|
||||||
|
// Theme state
|
||||||
|
const { config } = useConfig()
|
||||||
|
|
||||||
const classes = computed(() => [
|
const classes = computed(() => [
|
||||||
'container',
|
'container',
|
||||||
{ 'fonts-ready': !fontsLoading.value },
|
{ 'fonts-ready': !fontsLoading.value },
|
||||||
'theme-dark',
|
`theme-${config.value?.theme || 'dark'}`,
|
||||||
])
|
])
|
||||||
|
|
||||||
const fontsLoading = ref(true)
|
const fontsLoading = ref(true)
|
||||||
@@ -59,7 +63,7 @@ const styles = computed(() => ({
|
|||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
background: var(--theme-bg);
|
background: var(--theme-bg);
|
||||||
color: var(--theme-fg);
|
color: var(--theme-fg);
|
||||||
transition: opacity 1000ms;
|
transition: opacity 400ms;
|
||||||
|
|
||||||
&:not(.fonts-ready) {
|
&:not(.fonts-ready) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -1,41 +1,131 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link class="category-row" :to="`/category/${category}`">
|
<component
|
||||||
|
:class="['category-row', { editable }]"
|
||||||
|
:to="`/category/${category}`"
|
||||||
|
:is="wrapper"
|
||||||
|
>
|
||||||
<span class="index">{{ String(index + 1).padStart(2, '0') }}.</span>
|
<span class="index">{{ String(index + 1).padStart(2, '0') }}.</span>
|
||||||
<span class="title h1">{{ category }}</span>
|
<form v-if="isEditing" @submit.prevent="onSave">
|
||||||
</router-link>
|
<input
|
||||||
|
v-model="categoryInput"
|
||||||
|
class="category-input"
|
||||||
|
type="text"
|
||||||
|
ref="input"
|
||||||
|
@blur="onSave"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<span v-else class="title h1">{{ categoryInput }}</span>
|
||||||
|
|
||||||
|
<button v-if="isEditing" class="save-button" @click="onSave">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button v-else-if="editable" class="edit-button" @click="onEdit">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({ index: Number, category: String })
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
index: Number,
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
default: () => '',
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: () => false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['edited'])
|
||||||
|
|
||||||
|
const isEditing = ref(false)
|
||||||
|
const categoryInput = ref('')
|
||||||
|
const input = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
categoryInput.value = props.category
|
||||||
|
|
||||||
|
if (categoryInput.value === '') {
|
||||||
|
onEdit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onEdit = async () => {
|
||||||
|
isEditing.value = true
|
||||||
|
await new Promise((res) => setTimeout(res, 300))
|
||||||
|
|
||||||
|
input.value?.focus()
|
||||||
|
}
|
||||||
|
const onSave = async () => {
|
||||||
|
isEditing.value = false
|
||||||
|
emit('edited', categoryInput.value)
|
||||||
|
await new Promise((res) => setTimeout(res, 300))
|
||||||
|
|
||||||
|
input.value?.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = computed(() => {
|
||||||
|
return props.editable ? 'div' : RouterLink
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.category-row {
|
.category-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: size-vw(26px) 1fr;
|
grid-template-columns: 26px 1fr auto;
|
||||||
|
align-items: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: size-vw(5px) 0 size-vw(15px);
|
padding: 8px 0 6px;
|
||||||
|
border-bottom: 1px dashed currentColor;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.index {
|
.index {
|
||||||
margin-top: size-vw(19px);
|
margin-top: 19px;
|
||||||
|
@include p;
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@include line-clamp(2);
|
@include line-clamp(2);
|
||||||
}
|
}
|
||||||
&::after {
|
.category-input {
|
||||||
content: '----------------------------------------';
|
display: block;
|
||||||
position: absolute;
|
width: 100%;
|
||||||
bottom: 0;
|
@include h1;
|
||||||
left: 0;
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
&.router-link-exact-active {
|
}
|
||||||
|
.edit-button,
|
||||||
|
.save-button {
|
||||||
|
color: var(--grey-100);
|
||||||
|
cursor: pointer;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
.edit-button {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
&.editable:hover {
|
||||||
|
.edit-button {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.router-link-exact-active,
|
||||||
|
&.editable {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
&:hover:not(.router-link-exact-active) {
|
&:hover:not(.router-link-exact-active):not(.editable) {
|
||||||
color: var(--theme-accent);
|
color: var(--theme-accent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
<div v-if="menuOpen" class="menu" ref="container">
|
<div v-if="menuOpen" class="menu" ref="container">
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<div class="menu-wrap layout-block-inner">
|
<div class="menu-wrap layout-block">
|
||||||
<new-note class="menu-item" @noteOpened="closeMenu" />
|
<new-note class="menu-item" @noteOpened="closeMenu" />
|
||||||
<button class="menu-item">+ New Capitulum</button>
|
<router-link class="menu-item" to="/category"
|
||||||
<button class="menu-item">Change Theme</button>
|
>+ New Capitulum</router-link
|
||||||
|
>
|
||||||
|
<theme-switcher class="menu-item" />
|
||||||
<router-link class="menu-item" to="/instructions"
|
<router-link class="menu-item" to="/instructions"
|
||||||
>Instructio</router-link
|
>Instructio</router-link
|
||||||
>
|
>
|
||||||
@@ -19,9 +21,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import NewNote from '@/components/NewNote.vue'
|
import NewNote from '@/components/NewNote.vue'
|
||||||
|
import ThemeSwitcher from '@/components/ThemeSwitcher.vue'
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import Nav from '../Nav.vue'
|
import Nav from './Nav.vue'
|
||||||
import useMenu from '@/composables/useMenu'
|
import useMenu from '@/composables/useMenu'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
@@ -58,12 +61,11 @@ const openNewCategory = () => {}
|
|||||||
.menu-wrap {
|
.menu-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-top: size-vw(3px);
|
padding-top: 3px;
|
||||||
padding-bottom: size-vw(10px);
|
padding-bottom: 10px;
|
||||||
|
|
||||||
.menu-item {
|
.menu-item {
|
||||||
display: block;
|
padding: 16px 0;
|
||||||
padding: size-vw(16px) 0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="nav layout-block-inner">
|
<nav class="nav layout-block">
|
||||||
<button @click="toggleMenu">Menu</button>
|
<button @click="toggleMenu">Menu</button>
|
||||||
|
|
||||||
|
<router-link to="/search">Search</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -29,7 +31,10 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.nav {
|
.nav {
|
||||||
padding-top: size-vw(9px);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 9px;
|
||||||
color: var(--grey-100);
|
color: var(--grey-100);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ const formatDate = (date) => {
|
|||||||
.note-row {
|
.note-row {
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: size-vw(20px);
|
width: 100%;
|
||||||
|
gap: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
width: size-vw(159px);
|
width: calc(100% - 43.2px);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ watch(
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: size-vw(8px);
|
width: 8px;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
border-left: 1px solid var(--grey-100);
|
border-left: 1px solid var(--grey-100);
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ watch(
|
|||||||
|
|
||||||
.handle {
|
.handle {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: size-vw(388px);
|
height: 388px;
|
||||||
background: var(--grey-100);
|
background: var(--grey-100);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
49
src/renderer/src/components/ThemeSwitcher.vue
Normal file
49
src/renderer/src/components/ThemeSwitcher.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="config" class="theme-switcher">
|
||||||
|
<span>Change Theme</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-for="(value, key) in themes"
|
||||||
|
:class="[`theme-${key}`, { active: config.theme === key }]"
|
||||||
|
@click="setTheme(key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import useConfig from '@/composables/useConfig'
|
||||||
|
import { themes } from '@/libs/theme'
|
||||||
|
|
||||||
|
const { config } = useConfig()
|
||||||
|
|
||||||
|
const setTheme = (value) => {
|
||||||
|
if (!config.value) return
|
||||||
|
config.value.theme = value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.theme-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: var(--theme-bg);
|
||||||
|
display: block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 1px solid var(--theme-fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
color: var(--theme-fg) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
302
src/renderer/src/components/note/Editor.vue
Normal file
302
src/renderer/src/components/note/Editor.vue
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="editor" class="note-editor">
|
||||||
|
<editor-content :editor="editor" class="editor-wrap" />
|
||||||
|
|
||||||
|
<editor-menu :editor="editor" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||||
|
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
||||||
|
import { TaskList, TaskItem } from '@tiptap/extension-list'
|
||||||
|
import { Highlight } from '@tiptap/extension-highlight'
|
||||||
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
|
import Document from '@tiptap/extension-document'
|
||||||
|
import { Placeholder } from '@tiptap/extensions'
|
||||||
|
import { all, createLowlight } from 'lowlight'
|
||||||
|
import useNotes from '@/composables/useNotes'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import _debounce from 'lodash/debounce'
|
||||||
|
import EditorMenu from './Menu.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const editor = shallowRef()
|
||||||
|
|
||||||
|
const { loadNote, updateNote } = useNotes()
|
||||||
|
|
||||||
|
const onUpdate = _debounce(async ({ editor }) => {
|
||||||
|
const json = editor.getJSON()
|
||||||
|
const text = editor.getText()
|
||||||
|
|
||||||
|
// Get doc title
|
||||||
|
let title
|
||||||
|
const doc = editor.state.doc
|
||||||
|
|
||||||
|
const firstNode = doc.firstChild
|
||||||
|
if (!firstNode || firstNode.type.name !== 'heading') title = 'Untitled'
|
||||||
|
|
||||||
|
title = firstNode.textContent.trim() || 'Untitled'
|
||||||
|
|
||||||
|
await updateNote(props.id, {
|
||||||
|
title,
|
||||||
|
content: json,
|
||||||
|
plainText: text,
|
||||||
|
})
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const note = await loadNote(props.id)
|
||||||
|
|
||||||
|
// Lowlight setup
|
||||||
|
const lowlight = createLowlight(all)
|
||||||
|
|
||||||
|
// Force note format
|
||||||
|
const CustomDocument = Document.extend({
|
||||||
|
content: 'heading block*',
|
||||||
|
})
|
||||||
|
|
||||||
|
editor.value = new Editor({
|
||||||
|
extensions: [
|
||||||
|
CustomDocument,
|
||||||
|
StarterKit.configure({
|
||||||
|
document: false,
|
||||||
|
heading: { levels: [1] },
|
||||||
|
trailingNode: {
|
||||||
|
node: 'paragraph',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder: ({ node }) => {
|
||||||
|
if (node.type.name === 'heading') {
|
||||||
|
return 'Title'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TaskList,
|
||||||
|
TaskItem,
|
||||||
|
Highlight,
|
||||||
|
CodeBlockLowlight.configure({
|
||||||
|
lowlight,
|
||||||
|
enableTabIndentation: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
content: note.content || [],
|
||||||
|
onUpdate: onUpdate,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
editor.value?.destroy?.()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.note-editor {
|
||||||
|
h1 {
|
||||||
|
font-weight: 700 !important;
|
||||||
|
@include p;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
@include drop-cap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1.is-editor-empty:first-child::before {
|
||||||
|
color: var(--grey-100);
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
pointer-events: none;
|
||||||
|
@include drop-cap;
|
||||||
|
}
|
||||||
|
p strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
p em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: 1px dashed currentColor;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: list-item;
|
||||||
|
margin-left: 1em;
|
||||||
|
|
||||||
|
*:not(:last-child) {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: list-item;
|
||||||
|
margin-left: 1.5em;
|
||||||
|
|
||||||
|
&::marker {
|
||||||
|
@include p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li:not(:last-child) {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--theme-link);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
border: 1px solid var(--grey-100);
|
||||||
|
color: var(--theme-accent);
|
||||||
|
padding: 0 0.2em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
display: block;
|
||||||
|
color: inherit;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
/* Code styling */
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #616161;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-tag,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class {
|
||||||
|
color: #f98181;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-builtin-name,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-params {
|
||||||
|
color: #fbbc88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet {
|
||||||
|
color: #b9f18d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section {
|
||||||
|
color: #faf594;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag {
|
||||||
|
color: #70cff8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid var(--grey-100);
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
s {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mark {
|
||||||
|
background: var(--theme-accent);
|
||||||
|
color: var(--theme-bg);
|
||||||
|
padding: 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul[data-type='taskList'] {
|
||||||
|
list-style: none;
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
> label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
user-select: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type='checkbox'] {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
border: 1px solid var(--grey-100);
|
||||||
|
border-radius: 0.2em;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '✓';
|
||||||
|
font-size: 1.5em;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
&:checked::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul[data-type='taskList'] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-wrap {
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
src/renderer/src/components/note/Find.vue
Normal file
12
src/renderer/src/components/note/Find.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div class="note-find"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.note-find {
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/renderer/src/components/note/Menu.vue
Normal file
61
src/renderer/src/components/note/Menu.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<bubble-menu v-if="editor" :editor="editor">
|
||||||
|
<div class="note-menu">
|
||||||
|
<button
|
||||||
|
@click="editor.chain().focus().toggleBold().run()"
|
||||||
|
:class="{ active: editor.isActive('bold') }"
|
||||||
|
>
|
||||||
|
Bold
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="editor.chain().focus().toggleItalic().run()"
|
||||||
|
:class="{ active: editor.isActive('italic') }"
|
||||||
|
>
|
||||||
|
Italic
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="editor.chain().focus().toggleHighlight().run()"
|
||||||
|
:class="{ active: editor.isActive('highlight') }"
|
||||||
|
>
|
||||||
|
Highlight
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</bubble-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { BubbleMenu } from '@tiptap/vue-3/menus'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editor: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.note-menu {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
border: 1px solid var(--grey-100);
|
||||||
|
color: var(--grey-100);
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background: var(--theme-bg);
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.2em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--grey-100);
|
||||||
|
color: var(--theme-bg);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background: var(--theme-fg);
|
||||||
|
color: var(--theme-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
36
src/renderer/src/composables/useConfig.js
Normal file
36
src/renderer/src/composables/useConfig.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ref, watch, toRaw, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const config = ref()
|
||||||
|
let configResolve = null
|
||||||
|
const configPromise = new Promise((resolve) => {
|
||||||
|
configResolve = resolve
|
||||||
|
})
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
onMounted(async () => {
|
||||||
|
if (config.value) {
|
||||||
|
configResolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.value = await window.api.getConfig()
|
||||||
|
configResolve()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
config,
|
||||||
|
async (newValue) => {
|
||||||
|
await window.api.setConfig(toRaw(newValue))
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
const ensureConfig = async () => {
|
||||||
|
if (config.value) return config.value
|
||||||
|
return configPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
ensureConfig,
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/renderer/src/composables/useEnvironment.ts
Normal file
41
src/renderer/src/composables/useEnvironment.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export enum ENVIRONMENTS {
|
||||||
|
ELECTRON = 'electron',
|
||||||
|
WEB = 'web',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useEnvironment = (): ENVIRONMENTS => {
|
||||||
|
function isElectron() {
|
||||||
|
// Renderer process
|
||||||
|
if (
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
typeof window.process === 'object' &&
|
||||||
|
window.process.type === 'renderer'
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main process
|
||||||
|
if (
|
||||||
|
typeof process !== 'undefined' &&
|
||||||
|
typeof process.versions === 'object' &&
|
||||||
|
!!process.versions.electron
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect the user agent when the `nodeIntegration` option is set to true
|
||||||
|
if (
|
||||||
|
typeof navigator === 'object' &&
|
||||||
|
typeof navigator.userAgent === 'string' &&
|
||||||
|
navigator.userAgent.indexOf('Electron') >= 0
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const environment = isElectron() ? ENVIRONMENTS.ELECTRON : ENVIRONMENTS.WEB
|
||||||
|
|
||||||
|
return environment
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { inject, onBeforeUnmount } from 'vue'
|
|
||||||
|
|
||||||
export default (callback = () => {}, instanceId) => {
|
|
||||||
const instanceKey = `lenis${instanceId ? `-${instanceId}` : ''}`
|
|
||||||
const lenis = inject(instanceKey)
|
|
||||||
|
|
||||||
if (lenis.value) {
|
|
||||||
lenis.value.on('scroll', callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => lenis.value?.off('scroll', callback))
|
|
||||||
|
|
||||||
return lenis
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ export default () => {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const menuOpen = computed(() => route.query.menuOpen === 'true')
|
const menuOpen = computed(() => route.query?.menuOpen === 'true')
|
||||||
|
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
router.push({
|
router.push({
|
||||||
|
|||||||
@@ -1,74 +1,136 @@
|
|||||||
|
import _omit from 'lodash/omit'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { getNotesAPI } from '@/libs/core/getNotesAPI'
|
||||||
|
|
||||||
const categories = ref([])
|
const categories = ref([])
|
||||||
const searchResults = ref([])
|
const searchResults = ref([])
|
||||||
|
const notesChangeCount = ref(0)
|
||||||
|
|
||||||
|
let listenersInitialized = false
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Todo update cache
|
||||||
|
window.api.onNoteDeleted(() => {
|
||||||
|
notesChangeCount.value++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcastChange = (event, data) => {
|
||||||
|
window.api.notifyNoteChanged(event, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners()
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Initialization
|
Initialization
|
||||||
--------------------------*/
|
--------------------------*/
|
||||||
async function loadCategories() {
|
const loadCategories = async () => {
|
||||||
categories.value = await window.notesAPI.call('getCategories')
|
const api = await getNotesAPI()
|
||||||
|
categories.value = api.getCategories()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadCategoryNotes(category = null) {
|
const loadCategoryNotes = async (category = null) => {
|
||||||
return await window.notesAPI.call('getCategoryNotes', category)
|
const api = await getNotesAPI()
|
||||||
|
return api.getCategoryNotes(category)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadNote(id) {
|
const loadNote = async (id) => {
|
||||||
return await window.notesAPI.call('getNote', id)
|
const api = await getNotesAPI()
|
||||||
|
return api.getNote(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Create
|
Create
|
||||||
--------------------------*/
|
--------------------------*/
|
||||||
async function createNote(metadata, content) {
|
const createNote = async (metadata, content, plainText = '') => {
|
||||||
const note = await window.notesAPI.call('createNote', metadata, content)
|
const api = await getNotesAPI()
|
||||||
|
const note = await api.createNote(metadata, content, plainText)
|
||||||
await loadCategories()
|
await loadCategories()
|
||||||
|
broadcastChange('note-created', note)
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Update
|
Update
|
||||||
--------------------------*/
|
--------------------------*/
|
||||||
async function updateNoteContent(id, content) {
|
const updateNote = async (id, updates) => {
|
||||||
const note = await window.notesAPI.call('updateNote', id, content)
|
const api = await getNotesAPI()
|
||||||
|
|
||||||
|
const note = await api.updateNote(id, updates)
|
||||||
|
|
||||||
|
if (updates.category !== undefined || updates.title !== undefined) {
|
||||||
|
await loadCategories()
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastChange('note-updated', note)
|
||||||
|
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateNoteMetadata(id, updates) {
|
const updateCategory = async (category, update) => {
|
||||||
const note = await window.notesAPI.call(
|
const notes = await loadCategoryNotes(category)
|
||||||
'updateNoteMetadata',
|
|
||||||
id,
|
for (const note of notes) {
|
||||||
updates,
|
await updateNote(note.id, { category: update })
|
||||||
)
|
}
|
||||||
|
|
||||||
await loadCategories()
|
await loadCategories()
|
||||||
return note
|
}
|
||||||
|
|
||||||
|
/* -------------------------
|
||||||
|
Delete
|
||||||
|
--------------------------*/
|
||||||
|
const deleteNote = async (id) => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
await api.deleteNote(id)
|
||||||
|
await loadCategories()
|
||||||
|
broadcastChange('note-deleted', { id })
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
Search
|
Search
|
||||||
--------------------------*/
|
--------------------------*/
|
||||||
async function search(query) {
|
const search = async (query) => {
|
||||||
|
const api = await getNotesAPI()
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
searchResults.value = []
|
searchResults.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
searchResults.value = await window.notesAPI.call('search', query)
|
searchResults.value = api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
searchResults,
|
searchResults,
|
||||||
|
notesChangeCount,
|
||||||
|
|
||||||
loadCategories,
|
loadCategories,
|
||||||
loadCategoryNotes,
|
loadCategoryNotes,
|
||||||
loadNote,
|
loadNote,
|
||||||
|
|
||||||
createNote,
|
createNote,
|
||||||
updateNoteContent,
|
updateNote,
|
||||||
updateNoteMetadata,
|
updateCategory,
|
||||||
|
deleteNote,
|
||||||
|
|
||||||
search,
|
search,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useEnvironment, ENVIRONMENTS } from './useEnvironment'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -6,13 +7,9 @@ export default () => {
|
|||||||
function openNote(noteId, options = {}) {
|
function openNote(noteId, options = {}) {
|
||||||
const { newWindow = true } = options
|
const { newWindow = true } = options
|
||||||
|
|
||||||
// Electron environment check
|
const environment = useEnvironment()
|
||||||
const isElectron =
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
window.api &&
|
|
||||||
typeof window.api.openNoteWindow === 'function'
|
|
||||||
|
|
||||||
if (newWindow && isElectron) {
|
if (newWindow && environment === ENVIRONMENTS.ELECTRON) {
|
||||||
window.api.openNoteWindow(noteId)
|
window.api.openNoteWindow(noteId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/renderer/src/composables/usePlugins.js
Normal file
18
src/renderer/src/composables/usePlugins.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const plugins = ref([])
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
plugins.value = await window.api.listPlugins()
|
||||||
|
})
|
||||||
|
|
||||||
|
const setActivePlugin = async (pluginId) => {
|
||||||
|
await window.api.setActivePlugin(pluginId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins,
|
||||||
|
setActivePlugin,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useWindowSize } from '@vueuse/core'
|
|
||||||
import { viewports } from '@/libs/theme'
|
|
||||||
|
|
||||||
const { width: wWidth, height: wHeight } = useWindowSize()
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
// Desktop
|
|
||||||
const dvw = (pixels) => {
|
|
||||||
return (pixels / viewports.desktop.width) * wWidth.value
|
|
||||||
}
|
|
||||||
const dvh = (pixels) => {
|
|
||||||
return (pixels / viewports.desktop.height) * wHeight.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile
|
|
||||||
const mvw = (pixels) => {
|
|
||||||
return (pixels / viewports.mobile.width) * wWidth.value
|
|
||||||
}
|
|
||||||
const mvh = (pixels) => {
|
|
||||||
return (pixels / viewports.mobile.height) * wHeight.value
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
dvw,
|
|
||||||
dvh,
|
|
||||||
mvw,
|
|
||||||
mvh,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import {
|
|
||||||
useElementBounding,
|
|
||||||
useIntersectionObserver,
|
|
||||||
useWindowSize,
|
|
||||||
} from '@vueuse/core'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { mapRange, clamp } from '@/libs/math'
|
|
||||||
import useLenis from '@/composables/useLenis'
|
|
||||||
|
|
||||||
const { height: wHeight } = useWindowSize()
|
|
||||||
|
|
||||||
export const useScrollProgress = (el, callback, entry = 0.5, exit = 0.5) => {
|
|
||||||
const isActive = ref(true)
|
|
||||||
const smoothProgress = ref(0)
|
|
||||||
|
|
||||||
const { height, top } = useElementBounding(el)
|
|
||||||
|
|
||||||
const isIntersected = ref(false)
|
|
||||||
const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
|
|
||||||
isIntersected.value = isIntersecting
|
|
||||||
})
|
|
||||||
|
|
||||||
useLenis(({ scroll }) => {
|
|
||||||
if (!isActive.value) return
|
|
||||||
if (!height.value || !wHeight.value) return
|
|
||||||
if (!isIntersected.value) return
|
|
||||||
|
|
||||||
const pageTop = scroll + top.value
|
|
||||||
|
|
||||||
const start = pageTop - wHeight.value * entry
|
|
||||||
const end = pageTop + height.value - wHeight.value * exit
|
|
||||||
|
|
||||||
let rawProgress = mapRange(start, end, scroll, 0, 1)
|
|
||||||
rawProgress = clamp(0, rawProgress, 1)
|
|
||||||
|
|
||||||
smoothProgress.value += (rawProgress - smoothProgress.value) * 0.1
|
|
||||||
callback?.(smoothProgress.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const destroy = () => {
|
|
||||||
isActive.value = false
|
|
||||||
stop?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
return { destroy }
|
|
||||||
}
|
|
||||||
25
src/renderer/src/libs/core/IpcAdapter.js
Normal file
25
src/renderer/src/libs/core/IpcAdapter.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default class IpcAdapter {
|
||||||
|
constructor() {
|
||||||
|
this._methods = ['init', 'getAll', 'create', 'update', 'delete']
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
return await window.adapter.call('init')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
return await window.adapter.call('getAll')
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(note) {
|
||||||
|
return await window.adapter.call('create', note)
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(note) {
|
||||||
|
return await window.adapter.call('update', note)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id) {
|
||||||
|
return await window.adapter.call('delete', id)
|
||||||
|
}
|
||||||
|
}
|
||||||
246
src/renderer/src/libs/core/NotesAPI.js
Normal file
246
src/renderer/src/libs/core/NotesAPI.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
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) {
|
||||||
|
if (!adapter) {
|
||||||
|
throw new Error('NotesAPI requires a storage adapter')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.adapter = adapter
|
||||||
|
this.notesCache = new Map()
|
||||||
|
this.encryptionKey = encryptionKey
|
||||||
|
this._sodiumReady = false
|
||||||
|
|
||||||
|
this.index = new Index({
|
||||||
|
tokenize: 'forward',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initSodium() {
|
||||||
|
if (!this._sodiumReady) {
|
||||||
|
await sodium.ready
|
||||||
|
this._sodiumReady = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_encrypt(note) {
|
||||||
|
if (!this.encryptionKey) {
|
||||||
|
throw new Error('Encryption key not set')
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = uint.hexToUint8Array(this.encryptionKey)
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error(
|
||||||
|
'Encryption key must be 64 hex characters (32 bytes)',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
|
const message = JSON.stringify(note)
|
||||||
|
|
||||||
|
const ciphertext = sodium.crypto_secretbox_easy(
|
||||||
|
new TextEncoder().encode(message),
|
||||||
|
nonce,
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
|
||||||
|
const combined = uint.concatUint8Arrays(nonce, ciphertext)
|
||||||
|
return uint.uint8ArrayToBase64(combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
_decrypt(encryptedData) {
|
||||||
|
if (!this.encryptionKey) {
|
||||||
|
throw new Error('Encryption key not set')
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = uint.hexToUint8Array(this.encryptionKey)
|
||||||
|
if (key.length !== 32) {
|
||||||
|
throw new Error(
|
||||||
|
'Encryption key must be 64 hex characters (32 bytes)',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let combined
|
||||||
|
try {
|
||||||
|
combined = uint.base64ToUint8Array(encryptedData)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Invalid encrypted data: not valid base64')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
combined.length <
|
||||||
|
sodium.crypto_secretbox_NONCEBYTES +
|
||||||
|
sodium.crypto_secretbox_MACBYTES
|
||||||
|
) {
|
||||||
|
throw new Error('Invalid encrypted data: too short')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES)
|
||||||
|
const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
|
|
||||||
|
let decrypted
|
||||||
|
try {
|
||||||
|
decrypted = sodium.crypto_secretbox_open_easy(
|
||||||
|
ciphertext,
|
||||||
|
nonce,
|
||||||
|
key,
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('Decryption failed: wrong key or corrupted data')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!decrypted) {
|
||||||
|
throw new Error('Decryption failed: no data returned')
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedStr = new TextDecoder().decode(decrypted)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(decryptedStr)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`Decryption succeeded but invalid JSON: ${decryptedStr}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this._initSodium()
|
||||||
|
await this.adapter.init()
|
||||||
|
|
||||||
|
const encryptedNotes = await this.adapter.getAll()
|
||||||
|
|
||||||
|
for (const encryptedNote of encryptedNotes) {
|
||||||
|
try {
|
||||||
|
const note = this._decrypt(encryptedNote.data || encryptedNote)
|
||||||
|
|
||||||
|
this.notesCache.set(note.id, note)
|
||||||
|
const searchText =
|
||||||
|
note.plainText || this._extractPlainText(note.content)
|
||||||
|
this.index.add(note.id, note.title + '\n' + searchText)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to decrypt note:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_extractPlainText(content) {
|
||||||
|
if (!content) return ''
|
||||||
|
if (typeof content === 'string') return content
|
||||||
|
|
||||||
|
const extractText = (node) => {
|
||||||
|
if (typeof node === 'string') return node
|
||||||
|
if (!node || !node.content) return ''
|
||||||
|
return node.content.map(extractText).join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractText(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------
|
||||||
|
Public API
|
||||||
|
------------------------*/
|
||||||
|
getCategories() {
|
||||||
|
const categories = new Set()
|
||||||
|
|
||||||
|
for (const note of this.notesCache.values()) {
|
||||||
|
if (note.category) {
|
||||||
|
categories.add(note.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(categories).sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategoryNotes(categoryName = null) {
|
||||||
|
return Array.from(this.notesCache.values())
|
||||||
|
.filter((n) => n.category === categoryName)
|
||||||
|
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
|
||||||
|
.map((n) => ({ ...n }))
|
||||||
|
}
|
||||||
|
|
||||||
|
getNote(id) {
|
||||||
|
const note = this.notesCache.get(id)
|
||||||
|
return note ? { ...note } : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNote(metadata = {}, content = '', plainText = '') {
|
||||||
|
const id = uuidv4()
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
|
||||||
|
const note = {
|
||||||
|
id,
|
||||||
|
title: metadata.title || 'Untitled',
|
||||||
|
category: metadata.category || null,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
content,
|
||||||
|
plainText,
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedNote = {
|
||||||
|
id: note.id,
|
||||||
|
data: this._encrypt(note),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notesCache.set(id, note)
|
||||||
|
this.index.add(id, note.title + '\n' + plainText)
|
||||||
|
|
||||||
|
await this.adapter.create(encryptedNote)
|
||||||
|
|
||||||
|
return note
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNote(id) {
|
||||||
|
await this.adapter.delete(id)
|
||||||
|
|
||||||
|
this.notesCache.delete(id)
|
||||||
|
this.index.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNote(id, updates = {}) {
|
||||||
|
const note = this.notesCache.get(id)
|
||||||
|
if (!note) throw new Error('Note not found')
|
||||||
|
|
||||||
|
const allowedFields = ['title', 'category', 'content', 'plainText']
|
||||||
|
|
||||||
|
for (const key of Object.keys(updates)) {
|
||||||
|
if (!allowedFields.includes(key)) {
|
||||||
|
throw new Error(`Invalid update field: ${key}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedNote = {
|
||||||
|
...note,
|
||||||
|
...updates,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedNote = {
|
||||||
|
id: updatedNote.id,
|
||||||
|
data: this._encrypt(updatedNote),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notesCache.set(id, updatedNote)
|
||||||
|
|
||||||
|
const searchText =
|
||||||
|
updatedNote.plainText || this._extractPlainText(updatedNote.content)
|
||||||
|
|
||||||
|
this.index.update(id, updatedNote.title + '\n' + searchText)
|
||||||
|
|
||||||
|
await this.adapter.update(encryptedNote)
|
||||||
|
|
||||||
|
return updatedNote
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query) {
|
||||||
|
const ids = this.index.search(query, {
|
||||||
|
limit: 50,
|
||||||
|
suggest: true,
|
||||||
|
})
|
||||||
|
return ids.map((id) => this.notesCache.get(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/renderer/src/libs/core/getNotesAPI.js
Normal file
48
src/renderer/src/libs/core/getNotesAPI.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -7,40 +7,23 @@ const colors = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
light: {
|
|
||||||
bg: colors.white,
|
|
||||||
fg: colors.black,
|
|
||||||
accent: colors.green,
|
|
||||||
link: colors.blue,
|
|
||||||
},
|
|
||||||
dark: {
|
dark: {
|
||||||
bg: colors.black,
|
bg: colors.black,
|
||||||
fg: colors.white,
|
fg: colors.white,
|
||||||
accent: colors.green,
|
accent: colors.green,
|
||||||
link: colors.blue,
|
link: colors.blue,
|
||||||
},
|
},
|
||||||
}
|
light: {
|
||||||
|
bg: colors.white,
|
||||||
const breakpoints = {
|
fg: colors.black,
|
||||||
mobile: 800,
|
accent: colors.green,
|
||||||
}
|
link: colors.blue,
|
||||||
|
|
||||||
const viewports = {
|
|
||||||
mobile: {
|
|
||||||
width: 200,
|
|
||||||
height: 956,
|
|
||||||
},
|
|
||||||
desktop: {
|
|
||||||
width: 354,
|
|
||||||
height: 549,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export { colors, themes, breakpoints, viewports }
|
export { colors, themes }
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
colors,
|
colors,
|
||||||
themes,
|
themes,
|
||||||
breakpoints,
|
|
||||||
viewports,
|
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/renderer/src/libs/uint.js
Normal file
35
src/renderer/src/libs/uint.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export const hexToUint8Array = (hex) => {
|
||||||
|
const bytes = new Uint8Array(hex.length / 2)
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const 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
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uint8ArrayToBase64 = (bytes) => {
|
||||||
|
let binary = ''
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i])
|
||||||
|
}
|
||||||
|
return btoa(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const concatUint8Arrays = (...arrays) => {
|
||||||
|
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0)
|
||||||
|
const result = new Uint8Array(totalLength)
|
||||||
|
let offset = 0
|
||||||
|
for (const arr of arrays) {
|
||||||
|
result.set(arr, offset)
|
||||||
|
offset += arr.length
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
import Directory from '@/views/Directory.vue'
|
import Directory from '@/views/Directory.vue'
|
||||||
import Editor from '@/views/Editor.vue'
|
import Note from '@/views/Note.vue'
|
||||||
|
import CreateCategory from '@/views/CreateCategory.vue'
|
||||||
import Category from '@/views/Category.vue'
|
import Category from '@/views/Category.vue'
|
||||||
import Instructions from '@/views/Instructions.vue'
|
import Instructions from '@/views/Instructions.vue'
|
||||||
|
import Search from '@/views/Search.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', name: 'directory', component: Directory },
|
{ path: '/', name: 'directory', component: Directory },
|
||||||
{ path: '/note/:id', name: 'note', component: Editor },
|
{ path: '/note/:id', name: 'note', component: Note },
|
||||||
|
{ path: '/category', name: 'create-category', component: CreateCategory },
|
||||||
{ path: '/category/:id', name: 'category', component: Category },
|
{ path: '/category/:id', name: 'category', component: Category },
|
||||||
{ path: '/instructions', name: 'instructions', component: Instructions },
|
{ path: '/instructions', name: 'instructions', component: Instructions },
|
||||||
|
{ path: '/search', name: 'search', component: Search },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHashHistory(),
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
font-size: size-vw(30px);
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin h1-mono {
|
@mixin h1-mono {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: size-vw(22px);
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
font-size: size-vw(12px);
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src:
|
src:
|
||||||
url('../../../../resources/fonts/leibniz-fraktur.woff2') format('woff2'),
|
url('../../../../resources/fonts/neuefraktur.woff2') format('woff2'),
|
||||||
url('../../../../resources/fonts/leibniz-fraktur.woff') format('woff');
|
url('../../../../resources/fonts/neuefraktur.woff') format('woff');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Geist Mono */
|
/* Geist Mono */
|
||||||
|
|||||||
@@ -1,58 +1,3 @@
|
|||||||
@use 'sass:math';
|
|
||||||
|
|
||||||
/* Breakpoints */
|
|
||||||
$mobile-breakpoint: get('breakpoints.mobile');
|
|
||||||
|
|
||||||
// Viewport Sizes
|
|
||||||
$desktop-width: get('viewports.desktop.width');
|
|
||||||
$desktop-height: get('viewports.desktop.height');
|
|
||||||
|
|
||||||
$mobile-width: get('viewports.mobile.width');
|
|
||||||
$mobile-height: get('viewports.mobile.height');
|
|
||||||
|
|
||||||
// Breakpoint
|
|
||||||
@mixin mobile {
|
|
||||||
@media (max-width: #{$mobile-breakpoint * 1px - 1px}) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin desktop {
|
|
||||||
@media (min-width: #{$mobile-breakpoint * 1px}) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@function mobile-vw($pixels, $base-vw: $mobile-width) {
|
|
||||||
$px: math.div($pixels, $base-vw);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function mobile-vh($pixels, $base-vh: $mobile-height) {
|
|
||||||
$px: math.div($pixels, $base-vh);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function desktop-vw($pixels, $base-vw: $desktop-width) {
|
|
||||||
$px: math.div($pixels, $base-vw);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function desktop-vh($pixels, $base-vh: $desktop-height) {
|
|
||||||
$px: math.div($pixels, $base-vh);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vh);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function size-vw($pixels, $base-vw: 354) {
|
|
||||||
$px: math.div($pixels, $base-vw);
|
|
||||||
$perc: math.div($px, 1px);
|
|
||||||
@return calc($perc * 100vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
@function columns($columns) {
|
@function columns($columns) {
|
||||||
@return calc(
|
@return calc(
|
||||||
(#{$columns} * var(--layout-column-width)) +
|
(#{$columns} * var(--layout-column-width)) +
|
||||||
@@ -72,14 +17,6 @@ $mobile-height: get('viewports.mobile.height');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin fill($position: absolute) {
|
|
||||||
position: #{$position};
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin fade-on-ready($class: 'ready', $duration: 400ms) {
|
@mixin fade-on-ready($class: 'ready', $duration: 400ms) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity $duration ease;
|
transition: opacity $duration ease;
|
||||||
@@ -90,90 +27,12 @@ $mobile-height: get('viewports.mobile.height');
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clamp text block to number of lines
|
// Clamp text block to number of lines
|
||||||
@mixin line-clamp($lines: 3, $mobile-lines: $lines) {
|
@mixin line-clamp($lines: 3) {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: $lines;
|
-webkit-line-clamp: $lines;
|
||||||
|
|
||||||
@include mobile {
|
|
||||||
-webkit-line-clamp: $mobile-lines;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flip animations
|
|
||||||
@keyframes flip-r {
|
|
||||||
50% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-l {
|
|
||||||
50% {
|
|
||||||
transform: translateX(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-d {
|
|
||||||
50% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes flip-u {
|
|
||||||
50% {
|
|
||||||
transform: translateY(-100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
51% {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin flip-animation(
|
|
||||||
$direction: 'r',
|
|
||||||
$duration: 600ms,
|
|
||||||
$easing: var(--ease-out-expo),
|
|
||||||
$iteration-count: 1
|
|
||||||
) {
|
|
||||||
overflow: hidden;
|
|
||||||
animation: flip-#{$direction} $duration $easing $iteration-count forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin link-hover {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 1px;
|
|
||||||
background: var(--theme-fg);
|
|
||||||
transform-origin: left;
|
|
||||||
transform: scaleX(0);
|
|
||||||
transition: transform 300ms var(--ease-out-quad);
|
|
||||||
}
|
|
||||||
@include desktop {
|
|
||||||
&:hover::before {
|
|
||||||
transform: scaleX(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin stagger-animate($stagger: 100, $num-children: 10, $base-delay: 0) {
|
@mixin stagger-animate($stagger: 100, $num-children: 10, $base-delay: 0) {
|
||||||
@@ -185,20 +44,18 @@ $mobile-height: get('viewports.mobile.height');
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin hover {
|
@mixin hover {
|
||||||
@include desktop {
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@content;
|
@content;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin drop-cap() {
|
@mixin drop-cap() {
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: size-vw(12px);
|
font-size: 12px;
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
|
|
||||||
&:first-child::first-letter {
|
&:first-child::first-letter {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: size-vw(42px);
|
font-size: 42px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@
|
|||||||
|
|
||||||
// css classes exposed globally:
|
// css classes exposed globally:
|
||||||
// .layout-block: element takes the whole layout width
|
// .layout-block: element takes the whole layout width
|
||||||
// .layout-block-inner: same as .layout-block but using padding instead of margin
|
|
||||||
// .layout-grid: extends .layout-block with grid behaviour using layout settings
|
// .layout-grid: extends .layout-block with grid behaviour using layout settings
|
||||||
// .layout-grid-inner: same as .layout-grid but using padding instead of margin
|
|
||||||
|
|
||||||
@use 'sass:map';
|
@use 'sass:map';
|
||||||
|
|
||||||
@@ -20,26 +18,21 @@
|
|||||||
// 'variable': (mobile, desktop)
|
// 'variable': (mobile, desktop)
|
||||||
$layout: (
|
$layout: (
|
||||||
'columns-count': (
|
'columns-count': (
|
||||||
5,
|
6,
|
||||||
18,
|
|
||||||
),
|
),
|
||||||
'columns-gap': (
|
'columns-gap': (
|
||||||
20px,
|
10px,
|
||||||
20px,
|
|
||||||
),
|
),
|
||||||
'margin': (
|
'margin': (
|
||||||
30px,
|
20px,
|
||||||
60px,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
//internal process, do not touch
|
//internal process, do not touch
|
||||||
:root {
|
:root {
|
||||||
--layout-column-count: #{list.nth(map.get($layout, 'columns-count'), 1)};
|
--layout-column-count: #{list.nth(map.get($layout, 'columns-count'), 1)};
|
||||||
--layout-column-gap: #{size-vw(
|
--layout-column-gap: #{list.nth(map.get($layout, 'columns-gap'), 1)};
|
||||||
list.nth(map.get($layout, 'columns-gap'), 1)
|
--layout-margin: #{list.nth(map.get($layout, 'margin'), 1)};
|
||||||
)};
|
|
||||||
--layout-margin: #{size-vw(list.nth(map.get($layout, 'margin'), 1))};
|
|
||||||
--layout-width: calc(100vw - (2 * var(--layout-margin)));
|
--layout-width: calc(100vw - (2 * var(--layout-margin)));
|
||||||
--layout-column-width: calc(
|
--layout-column-width: calc(
|
||||||
(
|
(
|
||||||
@@ -54,13 +47,6 @@ $layout: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layout-block {
|
.layout-block {
|
||||||
max-width: var(--layout-width);
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-block-inner {
|
|
||||||
padding-left: var(--layout-margin);
|
padding-left: var(--layout-margin);
|
||||||
padding-right: var(--layout-margin);
|
padding-right: var(--layout-margin);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -73,11 +59,3 @@ $layout: (
|
|||||||
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
||||||
grid-gap: var(--layout-column-gap);
|
grid-gap: var(--layout-column-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-grid-inner {
|
|
||||||
@extend .layout-block-inner;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
|
|
||||||
grid-gap: var(--layout-column-gap);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
*:where(
|
*:where(
|
||||||
:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)
|
:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)
|
||||||
) {
|
) {
|
||||||
all: unset;
|
all: unset;
|
||||||
display: revert;
|
display: revert;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,23 +10,3 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.lenis {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-smooth {
|
|
||||||
scroll-behavior: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-smooth [data-lenis-prevent] {
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-stopped {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lenis.lenis-scrolling iframe {
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,26 +17,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-only {
|
|
||||||
@include desktop {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktop-only {
|
|
||||||
@include mobile {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html:not(.has-scroll-smooth) {
|
html:not(.has-scroll-smooth) {
|
||||||
.hide-on-native-scroll {
|
.hide-on-native-scroll {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html.has-scroll-smooth {
|
|
||||||
.hide-on-smooth-scroll {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ p,
|
|||||||
a,
|
a,
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
pre {
|
pre,
|
||||||
|
span {
|
||||||
@include p;
|
@include p;
|
||||||
}
|
}
|
||||||
.bold {
|
.bold {
|
||||||
@@ -64,9 +65,9 @@ button {
|
|||||||
// Text selection
|
// Text selection
|
||||||
::selection {
|
::selection {
|
||||||
color: var(--theme-bg);
|
color: var(--theme-bg);
|
||||||
background: var(--theme-accent);
|
background: var(--theme-fg);
|
||||||
}
|
}
|
||||||
::-moz-selection {
|
::-moz-selection {
|
||||||
color: var(--theme-bg);
|
color: var(--theme-bg);
|
||||||
background: var(--theme-accent);
|
background: var(--theme-fg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,12 @@
|
|||||||
<main class="category layout-block">
|
<main class="category layout-block">
|
||||||
<router-link class="back" to="/"><- Go Back</router-link>
|
<router-link class="back" to="/"><- Go Back</router-link>
|
||||||
|
|
||||||
<category-row :index="categoryIndex" :category="id" />
|
<category-row
|
||||||
|
:index="categoryIndex"
|
||||||
|
:category="id"
|
||||||
|
editable
|
||||||
|
@edited="onCategoryEdited"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="notes">
|
<div class="notes">
|
||||||
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
||||||
@@ -13,26 +18,44 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import useNotes from '@/composables/useNotes'
|
import useNotes from '@/composables/useNotes'
|
||||||
import NoteRow from '@/components/NoteRow.vue'
|
import NoteRow from '@/components/NoteRow.vue'
|
||||||
import CategoryRow from '@/components/CategoryRow.vue'
|
import CategoryRow from '@/components/CategoryRow.vue'
|
||||||
import NewNote from '@/components/NewNote.vue'
|
import NewNote from '@/components/NewNote.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const id = route.params.id
|
const id = route.params?.id
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const { categories, loadCategoryNotes } = useNotes()
|
const { categories, loadCategoryNotes, updateCategory, notesChangeCount } =
|
||||||
|
useNotes()
|
||||||
|
|
||||||
const notes = ref()
|
const notes = ref()
|
||||||
|
|
||||||
onMounted(async () => {
|
async function refreshNotes() {
|
||||||
|
if (id) {
|
||||||
notes.value = await loadCategoryNotes(id)
|
notes.value = await loadCategoryNotes(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshNotes()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(notesChangeCount, async () => {
|
||||||
|
await refreshNotes()
|
||||||
|
})
|
||||||
|
|
||||||
|
const onCategoryEdited = async (editedCategory) => {
|
||||||
|
await updateCategory(id, editedCategory)
|
||||||
|
|
||||||
|
router.push({ name: 'category', params: { id: editedCategory } })
|
||||||
|
}
|
||||||
|
|
||||||
const categoryIndex = computed(() => {
|
const categoryIndex = computed(() => {
|
||||||
return categories.value?.findIndex((category) => category === id) || 0
|
return categories.value?.findIndex((category) => category === id) || 1
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -41,20 +64,20 @@ main.category {
|
|||||||
.back {
|
.back {
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
margin-top: size-vw(9px);
|
margin-top: 9px;
|
||||||
}
|
}
|
||||||
.category-row {
|
.category-row {
|
||||||
margin-top: size-vw(4px);
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
.notes {
|
.notes {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: size-vw(14px);
|
gap: 14px;
|
||||||
margin-top: size-vw(9px);
|
margin-top: 9px;
|
||||||
}
|
}
|
||||||
.new-note {
|
.new-note {
|
||||||
display: block;
|
display: block;
|
||||||
margin: size-vw(50px) auto 0;
|
margin: 50px auto 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
16
src/renderer/src/views/CreateCategory.vue
Normal file
16
src/renderer/src/views/CreateCategory.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<main class="create-category layout-block">
|
||||||
|
<category-row :index="1" editable @edited="onCategoryEdited" />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CategoryRow from '@/components/CategoryRow.vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const onCategoryEdited = (name) => {
|
||||||
|
router.push({ name: 'category', params: { id: name } })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -12,38 +12,82 @@
|
|||||||
<div class="notes">
|
<div class="notes">
|
||||||
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
<note-row v-for="note in notes" :note="note" :key="note.id" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-for="plugin in plugins" :key="plugin.id">
|
||||||
|
<input
|
||||||
|
v-model="activePlugin"
|
||||||
|
type="radio"
|
||||||
|
name="plugins"
|
||||||
|
:value="plugin.id"
|
||||||
|
:id="plugin.id"
|
||||||
|
/>
|
||||||
|
<label :for="plugin.id">{{ plugin.name }}</label>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import useNotes from '@/composables/useNotes'
|
import useNotes from '@/composables/useNotes'
|
||||||
import { onMounted, ref } from 'vue'
|
import usePlugins from '@/composables/usePlugins'
|
||||||
|
import useConfig from '@/composables/useConfig'
|
||||||
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import CategoryRow from '@/components/CategoryRow.vue'
|
import CategoryRow from '@/components/CategoryRow.vue'
|
||||||
import NoteRow from '@/components/NoteRow.vue'
|
import NoteRow from '@/components/NoteRow.vue'
|
||||||
|
|
||||||
const { categories, loadCategories, loadCategoryNotes } = useNotes()
|
const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
|
||||||
|
useNotes()
|
||||||
|
|
||||||
|
const { config } = useConfig()
|
||||||
|
const { plugins, setActivePlugin } = usePlugins()
|
||||||
|
|
||||||
|
const activePlugin = ref(config.value?.activeAdapter)
|
||||||
|
|
||||||
const notes = ref()
|
const notes = ref()
|
||||||
|
|
||||||
onMounted(async () => {
|
async function refreshNotes() {
|
||||||
await loadCategories()
|
await loadCategories()
|
||||||
notes.value = await loadCategoryNotes()
|
notes.value = await loadCategoryNotes()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshNotes()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(notesChangeCount, async () => {
|
||||||
|
await refreshNotes()
|
||||||
|
})
|
||||||
|
watch(activePlugin, async (pluginId) => {
|
||||||
|
await setActivePlugin(pluginId)
|
||||||
|
await refreshNotes()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
main.directory {
|
main.directory {
|
||||||
padding-top: size-vw(18px);
|
padding-top: 18px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
|
||||||
|
input[type='radio'] {
|
||||||
|
display: block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: 1px solid white;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: size-vw(17px) 0 size-vw(24px);
|
margin: 17px 0 24px;
|
||||||
@include p;
|
@include p;
|
||||||
}
|
}
|
||||||
.notes {
|
.notes {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: size-vw(14px);
|
gap: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,196 +0,0 @@
|
|||||||
<template>
|
|
||||||
<main v-if="editor" class="editor layout-block">
|
|
||||||
<bubble-menu :editor="editor">
|
|
||||||
<div class="bubble-menu">
|
|
||||||
<button
|
|
||||||
@click="editor.chain().focus().toggleBold().run()"
|
|
||||||
:class="{ active: editor.isActive('bold') }"
|
|
||||||
>
|
|
||||||
Bold
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="editor.chain().focus().toggleItalic().run()"
|
|
||||||
:class="{ active: editor.isActive('italic') }"
|
|
||||||
>
|
|
||||||
Italic
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</bubble-menu>
|
|
||||||
|
|
||||||
<editor-content :editor="editor" class="editor-wrap" />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
|
|
||||||
import { Editor, EditorContent } from '@tiptap/vue-3'
|
|
||||||
import { Markdown } from '@tiptap/markdown'
|
|
||||||
import Image from '@tiptap/extension-image'
|
|
||||||
import Document from '@tiptap/extension-document'
|
|
||||||
import { Placeholder } from '@tiptap/extensions'
|
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
|
||||||
import { BubbleMenu } from '@tiptap/vue-3/menus'
|
|
||||||
import useNotes from '@/composables/useNotes'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import _debounce from 'lodash/debounce'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const id = route.params.id
|
|
||||||
|
|
||||||
const { loadNote, updateNoteContent, updateNoteMetadata } = useNotes()
|
|
||||||
|
|
||||||
const CustomDocument = Document.extend({
|
|
||||||
content: 'heading block*',
|
|
||||||
})
|
|
||||||
|
|
||||||
const editor = shallowRef()
|
|
||||||
|
|
||||||
const updateNote = _debounce(async ({ editor }) => {
|
|
||||||
const markdown = editor.getMarkdown()
|
|
||||||
|
|
||||||
await updateNoteContent(id, markdown)
|
|
||||||
|
|
||||||
updateTitle(editor)
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
// Track title updates for file
|
|
||||||
let lastTitle
|
|
||||||
const updateTitle = _debounce(async (editor) => {
|
|
||||||
const doc = editor.state.doc
|
|
||||||
|
|
||||||
const firstNode = doc.firstChild
|
|
||||||
if (!firstNode || firstNode.type.name !== 'heading') return
|
|
||||||
|
|
||||||
const newTitle = firstNode.textContent.trim() || 'Untitled'
|
|
||||||
|
|
||||||
if (newTitle === lastTitle) return
|
|
||||||
lastTitle = newTitle
|
|
||||||
|
|
||||||
await updateNoteMetadata(id, { title: newTitle })
|
|
||||||
}, 300)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const note = await loadNote(id)
|
|
||||||
lastTitle = note.title
|
|
||||||
|
|
||||||
editor.value = new Editor({
|
|
||||||
extensions: [
|
|
||||||
CustomDocument,
|
|
||||||
StarterKit.configure({
|
|
||||||
document: false,
|
|
||||||
heading: { levels: [1] },
|
|
||||||
trailingNode: {
|
|
||||||
node: 'paragraph',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Placeholder.configure({
|
|
||||||
placeholder: ({ node }) => {
|
|
||||||
if (node.type.name === 'heading') {
|
|
||||||
return 'Title'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
Markdown.configure({}),
|
|
||||||
Image,
|
|
||||||
],
|
|
||||||
content: note.content,
|
|
||||||
contentType: 'markdown',
|
|
||||||
onUpdate: updateNote,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
editor.value?.destroy?.()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
main.editor {
|
|
||||||
padding-top: size-vw(8px);
|
|
||||||
padding-bottom: size-vw(20px);
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-weight: 700 !important;
|
|
||||||
@include p;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
@include drop-cap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h1.is-editor-empty:first-child::before {
|
|
||||||
color: var(--grey-100);
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
pointer-events: none;
|
|
||||||
@include drop-cap;
|
|
||||||
}
|
|
||||||
p strong {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
p em {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
hr::before {
|
|
||||||
content: '----------------------------------------';
|
|
||||||
@include p;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: list-item;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: list-item;
|
|
||||||
margin-left: 1.5em;
|
|
||||||
|
|
||||||
&::marker {
|
|
||||||
@include p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--theme-link);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-wrap {
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: size-vw(20px);
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bubble-menu {
|
|
||||||
display: flex;
|
|
||||||
gap: size-vw(5px);
|
|
||||||
border: 1px solid var(--grey-100);
|
|
||||||
color: var(--grey-100);
|
|
||||||
border-radius: 0.2em;
|
|
||||||
background: var(--theme-bg);
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.2em;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--grey-100);
|
|
||||||
color: var(--theme-bg);
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background: var(--theme-fg);
|
|
||||||
color: var(--theme-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="instructions layout-block-inner">
|
<main class="instructions layout-block">
|
||||||
<router-link class="back-link" to="/"><- Go Back</router-link>
|
<router-link class="back-link" to="/"><- Go Back</router-link>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@@ -67,8 +67,8 @@ main.instructions {
|
|||||||
.back-link {
|
.back-link {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: size-vw(9px);
|
margin-top: 9px;
|
||||||
margin-bottom: size-vw(14px);
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
29
src/renderer/src/views/Note.vue
Normal file
29
src/renderer/src/views/Note.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<main class="note layout-block">
|
||||||
|
<note-editor :id="id" />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { watchEffect } from 'vue'
|
||||||
|
import { useMagicKeys } from '@vueuse/core'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import NoteEditor from '@/components/note/Editor.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const id = route.params.id
|
||||||
|
|
||||||
|
const { ctrl, f } = useMagicKeys()
|
||||||
|
watchEffect(() => {
|
||||||
|
if (ctrl.value && f.value) {
|
||||||
|
console.log('find')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
main.note {
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
105
src/renderer/src/views/Search.vue
Normal file
105
src/renderer/src/views/Search.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<main class="search layout-block">
|
||||||
|
<router-link class="back" to="/"><- Back</router-link>
|
||||||
|
|
||||||
|
<form @submit.prevent="onSearch">
|
||||||
|
<div class="input-wrap">
|
||||||
|
<input
|
||||||
|
v-model="query"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
ref="searchInput"
|
||||||
|
@input="onInput"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="results">
|
||||||
|
<note-row
|
||||||
|
v-for="note in searchResults"
|
||||||
|
:key="note.id"
|
||||||
|
:note="note"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import useNotes from '@/composables/useNotes'
|
||||||
|
import NoteRow from '@/components/NoteRow.vue'
|
||||||
|
import _debounce from 'lodash/debounce'
|
||||||
|
|
||||||
|
const query = ref('')
|
||||||
|
const searchInput = ref()
|
||||||
|
|
||||||
|
const { search, searchResults } = useNotes()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
searchInput.value?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSearch = async () => {
|
||||||
|
await search(query.value)
|
||||||
|
}
|
||||||
|
const onInput = _debounce(async () => {
|
||||||
|
await search(query.value)
|
||||||
|
}, 300)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
main.search {
|
||||||
|
.back {
|
||||||
|
display: block;
|
||||||
|
opacity: 0.25;
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
.input-wrap {
|
||||||
|
margin-top: 19px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding: 5px 15px 6px;
|
||||||
|
background: var(--theme-bg);
|
||||||
|
--clip-start: 16px;
|
||||||
|
clip-path: polygon(
|
||||||
|
var(--clip-start) 1px,
|
||||||
|
calc(100% - var(--clip-start)) 1px,
|
||||||
|
calc(100% - 1.5px) 50%,
|
||||||
|
calc(100% - var(--clip-start)) calc(100% - 1px),
|
||||||
|
var(--clip-start) calc(100% - 1px),
|
||||||
|
1.5px 50%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--theme-fg);
|
||||||
|
--clip-start: 15px;
|
||||||
|
clip-path: polygon(
|
||||||
|
var(--clip-start) 0,
|
||||||
|
calc(100% - var(--clip-start)) 0,
|
||||||
|
100% 50%,
|
||||||
|
calc(100% - var(--clip-start)) 100%,
|
||||||
|
var(--clip-start) 100%,
|
||||||
|
0% 50%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user