8 Commits

Author SHA1 Message Date
nicwands
73349444d6 update directory live
Some checks failed
Build Electron App / build (macos-latest, build:mac) (push) Has been cancelled
Build Electron App / build (ubuntu-latest, build:linux) (push) Has been cancelled
Build Electron App / build (windows-latest, build:win) (push) Has been cancelled
2026-03-03 17:21:14 -05:00
nicwands
e9e0abe380 new category flow 2026-03-03 17:09:29 -05:00
nicwands
e843b7662d added notarize: true
Some checks failed
Build Electron App / build (macos-latest, build:mac) (push) Has been cancelled
Build Electron App / build (ubuntu-latest, build:linux) (push) Has been cancelled
Build Electron App / build (windows-latest, build:win) (push) Has been cancelled
2026-03-02 13:03:03 -05:00
nicwands
cc3ba79df0 add notarizing
Some checks failed
Build Electron App / build (macos-latest, build:mac) (push) Has been cancelled
Build Electron App / build (ubuntu-latest, build:linux) (push) Has been cancelled
Build Electron App / build (windows-latest, build:win) (push) Has been cancelled
2026-03-02 12:44:32 -05:00
nicwands
2609d73bbd add encryption 2026-03-02 11:59:52 -05:00
nicwands
1c753a6f8f fix env ref
Some checks failed
Build Electron App / build (macos-latest, build:mac) (push) Has been cancelled
Build Electron App / build (ubuntu-latest, build:linux) (push) Has been cancelled
Build Electron App / build (windows-latest, build:win) (push) Has been cancelled
2026-03-02 10:52:52 -05:00
nicwands
d2b76399bc fix env references
Some checks failed
Build Electron App / build (macos-latest, build:mac) (push) Has been cancelled
Build Electron App / build (ubuntu-latest, build:linux) (push) Has been cancelled
Build Electron App / build (windows-latest, build:win) (push) Has been cancelled
2026-03-02 10:45:50 -05:00
nicwands
1c500ff052 update version 2026-03-02 10:36:12 -05:00
25 changed files with 88980 additions and 39311 deletions

View File

@@ -1,3 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
"recommendations": ["dbaeumer.vscode-eslint"]
}

View File

@@ -0,0 +1,3 @@
import crypto from 'crypto'
console.log(crypto.randomBytes(32).toString('hex'))

View File

@@ -24,9 +24,7 @@ mac:
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
cscLink: $CSC_LINK
cscKeyPassword: $CSC_KEY_PASSWORD
notarize: true
dmg:
artifactName: ${name}-${version}.${ext}
linux:

View File

@@ -7,6 +7,7 @@ import fs from "fs/promises";
import path, { join } from "path";
import { Index } from "flexsearch";
import crypto from "crypto";
import sodium from "libsodium-wrappers";
import __cjs_mod__ from "node:module";
const __filename = import.meta.filename;
const __dirname = import.meta.dirname;
@@ -88,23 +89,100 @@ class PluginConfig {
}
}
class NotesAPI {
constructor(adapter) {
constructor(adapter, encryptionKey = null) {
if (!adapter) {
throw new Error("NotesAPI requires a storage adapter");
}
this.adapter = adapter;
this.notesCache = /* @__PURE__ */ new Map();
this.encryptionKey = encryptionKey || process.env.NOTES_ENCRYPTION_KEY;
this._sodiumReady = false;
this.index = new Index({
tokenize: "tolerant",
resolution: 9
});
}
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 = Buffer.from(this.encryptionKey, "hex");
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(
Buffer.from(message),
nonce,
key
);
const combined = Buffer.concat([nonce, ciphertext]);
return combined.toString("base64");
}
_decrypt(encryptedData) {
if (!this.encryptionKey) {
throw new Error("Encryption key not set");
}
const key = Buffer.from(this.encryptionKey, "hex");
if (key.length !== 32) {
throw new Error(
"Encryption key must be 64 hex characters (32 bytes)"
);
}
let combined;
try {
combined = Buffer.from(encryptedData, "base64");
} 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 = Buffer.from(decrypted).toString("utf8");
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 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);
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);
this.index.add(note.id, note.title + "\n" + note.content);
} catch (error) {
console.error("Failed to decrypt note:", error);
}
}
}
/* -----------------------
@@ -136,9 +214,13 @@ class NotesAPI {
updatedAt: now,
content
};
const encryptedNote = {
id: note.id,
data: this._encrypt(note)
};
this.notesCache.set(id, note);
this.index.add(id, note.title + "\n" + content);
await this.adapter.create(note);
await this.adapter.create(encryptedNote);
return note;
}
async deleteNote(id) {
@@ -151,8 +233,12 @@ class NotesAPI {
if (!note) throw new Error("Note not found");
note.content = content;
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
const encryptedNote = {
id: note.id,
data: this._encrypt(note)
};
this.index.update(id, note.title + "\n" + content);
await this.adapter.update(note);
await this.adapter.update(encryptedNote);
return note;
}
async updateNoteMetadata(id, updates = {}) {
@@ -171,8 +257,12 @@ class NotesAPI {
note.category = updates.category;
}
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
const encryptedNote = {
id: note.id,
data: this._encrypt(note)
};
this.index.update(id, note.title + "\n" + note.content);
await this.adapter.update(note);
await this.adapter.update(encryptedNote);
return note;
}
search(query) {
@@ -235,19 +325,34 @@ app.whenReady().then(async () => {
const registry = new PluginRegistry();
registry.register(filesystemPlugin);
registry.register(supabasePlugin);
await new PluginConfig(filesystemPlugin).load();
const plugin = registry.get(supabasePlugin.id);
const adapter = plugin.createAdapter({
supabaseKey: process.env.SUPABASE_KEY,
supabaseUrl: process.env.SUPABASE_URL
});
const notesAPI = new NotesAPI(adapter);
const config = await new PluginConfig(filesystemPlugin).load();
const plugin = registry.get(config.activeAdapter);
const adapter = plugin.createAdapter(config.adapterConfig);
const notesAPI = new NotesAPI(
adapter,
"729a0d21d783654c68f1a0123e2a0e986350de536b5324f1f35876ea12ffeaf5"
);
await notesAPI.init();
ipcMain.handle("notesAPI:call", (_, method, args) => {
const broadcastNoteChange = (event, data) => {
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send(event, data);
});
};
ipcMain.handle("notesAPI:call", async (_, method, args) => {
if (!notesAPI[method]) {
throw new Error("Invalid method");
}
return notesAPI[method](...args);
const result = await notesAPI[method](...args);
if (method === "createNote") {
broadcastNoteChange("note-created", result);
} else if (method === "updateNote") {
broadcastNoteChange("note-updated", result);
} else if (method === "updateNoteMetadata") {
broadcastNoteChange("note-updated", result);
} else if (method === "deleteNote") {
broadcastNoteChange("note-deleted", { id: args[0] });
}
return result;
});
electronApp.setAppUserModelId("com.electron");
app.on("browser-window-created", (_, window) => {

View File

@@ -2,6 +2,15 @@ import { contextBridge, ipcRenderer } from "electron";
const api = {
openNoteWindow: (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));
}
};
const notesAPI = {

View File

@@ -180,11 +180,7 @@ meter {
all: revert;
}
.lily-cursor {
z-index: 20;
}
.site-header {
.menu {
z-index: 10;
}
@@ -304,13 +300,13 @@ html.has-scroll-smooth .hide-on-smooth-scroll {
grid-gap: var(--layout-column-gap);
}
html:not(.dev),
html:not(.dev) * {
html,
html * {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
html:not(.dev)::-webkit-scrollbar,
html:not(.dev) *::-webkit-scrollbar {
html::-webkit-scrollbar,
html *::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
}
@@ -445,6 +441,10 @@ pre {
min-height: 100vh;
}
button {
cursor: pointer;
}
::selection {
color: var(--theme-bg);
background: var(--theme-accent);
@@ -493,6 +493,223 @@ pre {
transform: translateY(100%);
opacity: 0;
}
}/* Breakpoints */
@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;
}
}
.nav {
padding-top: 2.5423728814vw;
color: var(--grey-100);
}/* Breakpoints */
@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;
}
}
.menu {
position: fixed;
top: 0;
left: 0;
right: 0;
background: var(--theme-bg);
border-bottom: 1px solid var(--grey-100);
}
.menu .menu-wrap {
display: flex;
flex-direction: column;
padding-top: 0.8474576271vw;
padding-bottom: 2.8248587571vw;
}
.menu .menu-wrap .menu-item {
display: block;
padding: 4.5197740113vw 0;
text-align: center;
}
.menu .menu-wrap .menu-item:not(:last-child) {
border-bottom: 1px dashed currentColor;
}
.menu .menu-wrap .menu-item:hover {
color: var(--theme-accent);
}
.menu.menu-enter-active, .menu.menu-leave-active {
transition: transform 300ms var(--ease-out-expo);
}
.menu.menu-enter-from, .menu.menu-leave-to {
transform: translateY(-100%);
}/* Breakpoints */
@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;
}
}
.scroll-bar {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 2.2598870056vw;
will-change: transform;
border-left: 1px solid var(--grey-100);
}
.scroll-bar .inner {
height: 100%;
position: relative;
}
.scroll-bar .inner .handle {
width: 100%;
height: 109.604519774vw;
background: var(--grey-100);
border-radius: 20px;
position: absolute;
right: 0;
will-change: transform;
}/* Breakpoints */
@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;
}
}
.container {
min-height: calc(100 * var(--vh));
@@ -547,7 +764,8 @@ pre {
}
.category-row {
display: grid;
grid-template-columns: 7.3446327684vw 1fr;
grid-template-columns: 7.3446327684vw 1fr auto;
align-items: flex-start;
width: 100%;
position: relative;
padding: 1.4124293785vw 0 4.2372881356vw;
@@ -555,6 +773,10 @@ pre {
}
.category-row .index {
margin-top: 5.3672316384vw;
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 3.3898305085vw;
}
.category-row .title {
display: block;
@@ -570,16 +792,48 @@ pre {
-webkit-line-clamp: 2;
}
}
.category-row .category-input {
display: block;
width: 100%;
font-family: var(--font-display);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.3;
font-size: 8.4745762712vw;
}
.category-row .category-input:focus {
outline: none;
}
.category-row .edit-button,
.category-row .save-button {
color: var(--grey-100);
cursor: pointer;
padding-right: 0.5em;
padding-left: 0.5em;
margin-top: 1.5em;
}
.category-row .edit-button {
opacity: 0;
pointer-events: none;
}
.category-row.editable:hover .edit-button {
opacity: 1;
pointer-events: auto;
}
.category-row::after {
content: "----------------------------------------";
position: absolute;
bottom: 0;
left: 0;
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 3.3898305085vw;
}
.category-row.router-link-exact-active {
.category-row.router-link-exact-active, .category-row.editable {
cursor: default;
}
.category-row:hover:not(.router-link-exact-active) {
.category-row:hover:not(.router-link-exact-active):not(.editable) {
color: var(--theme-accent);
}/* Breakpoints */
@keyframes flip-r {
@@ -687,10 +941,10 @@ pre {
opacity: 0;
}
}
.directory {
main.directory {
padding-top: 5.0847457627vw;
}
.directory .label {
main.directory .label {
text-transform: uppercase;
margin: 4.802259887vw 0 6.7796610169vw;
font-family: var(--font-mono);
@@ -698,7 +952,7 @@ pre {
line-height: 1;
font-size: 3.3898305085vw;
}
.directory .notes {
main.directory .notes {
display: flex;
flex-direction: column;
gap: 3.9548022599vw;
@@ -743,27 +997,27 @@ pre {
opacity: 0;
}
}
.editor {
main.editor {
padding-top: 2.2598870056vw;
padding-bottom: 5.6497175141vw;
}
.editor h1 {
main.editor h1 {
font-weight: 700 !important;
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 3.3898305085vw;
}
.editor h1:first-child {
main.editor h1:first-child {
font-family: var(--font-mono);
font-size: 3.3898305085vw;
font-weight: 400 !important;
}
.editor h1:first-child:first-child::first-letter {
main.editor h1:first-child:first-child::first-letter {
font-family: var(--font-display);
font-size: 11.8644067797vw;
}
.editor h1.is-editor-empty:first-child::before {
main.editor h1.is-editor-empty:first-child::before {
color: var(--grey-100);
content: attr(data-placeholder);
pointer-events: none;
@@ -771,56 +1025,135 @@ pre {
font-size: 3.3898305085vw;
font-weight: 400 !important;
}
.editor h1.is-editor-empty:first-child::before:first-child::first-letter {
main.editor h1.is-editor-empty:first-child::before:first-child::first-letter {
font-family: var(--font-display);
font-size: 11.8644067797vw;
}
.editor p strong {
main.editor p strong {
font-weight: 700;
}
.editor p em {
main.editor p em {
font-style: italic;
}
.editor hr::before {
main.editor hr::before {
content: "----------------------------------------";
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 3.3898305085vw;
}
.editor ul {
main.editor ul {
list-style-type: disc;
}
.editor ul li {
main.editor ul li {
display: list-item;
margin-left: 1em;
}
.editor ol {
main.editor ol {
list-style-type: decimal;
}
.editor ol li {
main.editor ol li {
display: list-item;
margin-left: 1.5em;
}
.editor ol li::marker {
main.editor ol li::marker {
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 3.3898305085vw;
}
.editor a {
main.editor a {
color: var(--theme-link);
cursor: pointer;
}
.editor .editor-wrap > div {
main.editor code {
border: 1px solid var(--grey-100);
color: var(--theme-accent);
padding: 0.2em;
border-radius: 0.2em;
}
main.editor pre code {
display: block;
color: inherit;
padding: 1em;
/* Code styling */
}
main.editor pre code .hljs-comment,
main.editor pre code .hljs-quote {
color: #616161;
}
main.editor pre code .hljs-variable,
main.editor pre code .hljs-template-variable,
main.editor pre code .hljs-attribute,
main.editor pre code .hljs-tag,
main.editor pre code .hljs-name,
main.editor pre code .hljs-regexp,
main.editor pre code .hljs-link,
main.editor pre code .hljs-name,
main.editor pre code .hljs-selector-id,
main.editor pre code .hljs-selector-class {
color: #f98181;
}
main.editor pre code .hljs-number,
main.editor pre code .hljs-meta,
main.editor pre code .hljs-built_in,
main.editor pre code .hljs-builtin-name,
main.editor pre code .hljs-literal,
main.editor pre code .hljs-type,
main.editor pre code .hljs-params {
color: #fbbc88;
}
main.editor pre code .hljs-string,
main.editor pre code .hljs-symbol,
main.editor pre code .hljs-bullet {
color: #b9f18d;
}
main.editor pre code .hljs-title,
main.editor pre code .hljs-section {
color: #faf594;
}
main.editor pre code .hljs-keyword,
main.editor pre code .hljs-selector-tag {
color: #70cff8;
}
main.editor pre code .hljs-emphasis {
font-style: italic;
}
main.editor pre code .hljs-strong {
font-weight: 700;
}
main.editor blockquote {
border-left: 4px solid var(--grey-100);
padding-left: 0.5em;
}
main.editor s {
position: relative;
}
main.editor s::after {
content: " ";
display: block;
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 0.2824858757vw;
background: currentColor;
}
main.editor mark {
background: var(--theme-accent);
color: var(--theme-bg);
padding: 0.2em;
border-radius: 0.2em;
}
main.editor .editor-wrap > div {
display: flex;
flex-direction: column;
gap: 5.6497175141vw;
}
.editor .editor-wrap > div:focus {
main.editor .editor-wrap > div:focus {
outline: none;
}
.editor .bubble-menu {
main.editor .bubble-menu {
display: flex;
gap: 1.4124293785vw;
border: 1px solid var(--grey-100);
@@ -828,16 +1161,16 @@ pre {
border-radius: 0.2em;
background: var(--theme-bg);
}
.editor .bubble-menu button {
main.editor .bubble-menu button {
cursor: pointer;
padding: 0.2em;
border-radius: 0.2em;
}
.editor .bubble-menu button:hover {
main.editor .bubble-menu button:hover {
background: var(--grey-100);
color: var(--theme-bg);
}
.editor .bubble-menu button.active {
main.editor .bubble-menu button.active {
background: var(--theme-fg);
color: var(--theme-bg);
}/* Breakpoints */
@@ -881,17 +1214,67 @@ pre {
opacity: 0;
}
}
.category .back {
main.category .back {
display: block;
opacity: 0.25;
margin-top: 2.5423728814vw;
}
.category .category-row {
main.category .category-row {
margin-top: 1.1299435028vw;
}
.category .notes {
main.category .notes {
display: flex;
flex-direction: column;
gap: 3.9548022599vw;
margin-top: 2.5423728814vw;
}
main.category .new-note {
display: block;
margin: 14.1242937853vw auto 0;
}/* Breakpoints */
@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;
}
}
main.instructions .back-link {
opacity: 0.25;
display: block;
margin-top: 2.5423728814vw;
margin-bottom: 3.9548022599vw;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
http-equiv="Content-Security-Policy"
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>
<link rel="stylesheet" crossorigin href="./assets/index-CZWw79gc.css">
<script type="module" crossorigin src="./assets/index-GKstRZpo.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-D9ZUihqb.css">
</head>
<body>

313
package-lock.json generated
View File

@@ -1,23 +1,23 @@
{
"name": "app.takerofnotes.com",
"version": "1.0.0",
"name": "takerofnotes-app",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "app.takerofnotes.com",
"version": "1.0.0",
"name": "takerofnotes-app",
"version": "0.1.0",
"hasInstallScript": true,
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@fuzzco/font-loader": "^1.0.2",
"@takerofnotes/plugin-filesystem": "^0.1.1",
"@takerofnotes/plugin-filesystem": "^0.2.0",
"@takerofnotes/plugin-supabase": "^0.1.0",
"@tiptap/extension-code-block-lowlight": "^3.20.0",
"@tiptap/extension-document": "^3.19.0",
"@tiptap/extension-image": "^3.19.0",
"@tiptap/extension-table": "^3.19.0",
"@tiptap/markdown": "^3.19.0",
"@tiptap/extension-highlight": "^3.20.0",
"@tiptap/extension-list": "^3.20.0",
"@tiptap/starter-kit": "^3.19.0",
"@tiptap/vue-3": "^3.19.0",
"@vueuse/core": "^14.2.1",
@@ -27,7 +27,9 @@
"flexsearch": "^0.8.212",
"gsap": "^3.14.2",
"lenis": "^1.3.17",
"libsodium-wrappers": "^0.8.2",
"lodash": "^4.17.23",
"lowlight": "^3.3.0",
"sass": "^1.97.3",
"sass-embedded": "^1.97.3",
"tempus": "^1.0.0-dev.17",
@@ -2343,19 +2345,18 @@
}
},
"node_modules/@takerofnotes/plugin-filesystem": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-filesystem/-/plugin-filesystem-0.1.1.tgz",
"integrity": "sha512-u3L6HLxN/+t7PTtzzRA2uzgaVh/O1vasngU/tQeC6JsTnlIrypWFVbAlTS5zT9Km4y+8w6C16eEq7tcN8+5lPg==",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-filesystem/-/plugin-filesystem-0.2.0.tgz",
"integrity": "sha512-BP7HBN0SKAqBiv5pDtXpyVmkW9UrOnPXpKeThTwQTIShHyN5aPaD8ZEyv8+vfTSs3qnXWGplNdEPVjbQmc27+Q==",
"license": "MIT",
"dependencies": {
"@takerofnotes/plugin-sdk": "^0.1.0",
"gray-matter": "^4.0.3"
"@takerofnotes/plugin-sdk": "^0.3.1"
}
},
"node_modules/@takerofnotes/plugin-sdk": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-sdk/-/plugin-sdk-0.1.0.tgz",
"integrity": "sha512-ofhwwiQ59kNMEg2vvYoNq5JdXHB9/6TkDsbyroM5nsP/VPUvJSQ5g0UWCYBhteSzJ36iFUB+LtUwVt6gOXDClw==",
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@takerofnotes/plugin-sdk/-/plugin-sdk-0.3.1.tgz",
"integrity": "sha512-9GfPKyu1n52N00zYlLK32wdmGdc2uSd0jTj6UEixauW0TXn/7hD6SLpLRGSXcfZOJGGoi3iQk4MfjNsthe2ucw==",
"license": "MIT",
"dependencies": {
"zod": "^4.3.6"
@@ -2464,17 +2465,34 @@
}
},
"node_modules/@tiptap/extension-code-block": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.19.0.tgz",
"integrity": "sha512-b/2qR+tMn8MQb+eaFYgVk4qXnLNkkRYmwELQ8LEtEDQPxa5Vl7J3eu8+4OyoIFhZrNDZvvoEp80kHMCP8sI6rg==",
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz",
"integrity": "sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.19.0",
"@tiptap/pm": "^3.19.0"
"@tiptap/core": "^3.20.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": {
@@ -2558,6 +2576,19 @@
"@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": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.19.0.tgz",
@@ -2572,19 +2603,6 @@
"@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": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.19.0.tgz",
@@ -2694,20 +2712,6 @@
"@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": {
"version": "3.19.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.19.0.tgz",
@@ -2748,23 +2752,6 @@
"@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": {
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz",
@@ -2890,6 +2877,15 @@
"@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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
@@ -2970,6 +2966,12 @@
"@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": {
"version": "1.10.11",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
@@ -4422,6 +4424,15 @@
"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": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -4439,6 +4450,19 @@
"license": "MIT",
"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": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
@@ -5105,19 +5129,6 @@
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
@@ -5137,18 +5148,6 @@
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
@@ -5625,49 +5624,6 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"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": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz",
@@ -5738,6 +5694,15 @@
"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": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
@@ -5930,15 +5895,6 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -6155,15 +6111,6 @@
"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": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
@@ -6196,6 +6143,21 @@
}
}
},
"node_modules/libsodium": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz",
"integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==",
"license": "ISC"
},
"node_modules/libsodium-wrappers": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz",
"integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==",
"license": "ISC",
"dependencies": {
"libsodium": "^0.8.0"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
@@ -6273,6 +6235,21 @@
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -6359,18 +6336,6 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@@ -8121,19 +8086,6 @@
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
"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": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -8416,15 +8368,6 @@
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "app.takerofnotes.com",
"version": "1.0.0",
"name": "takerofnotes-app",
"version": "0.1.0",
"description": "An Electron application with Vue",
"main": "./out/main/index.js",
"author": "example.com",
@@ -28,12 +28,12 @@
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@fuzzco/font-loader": "^1.0.2",
"@takerofnotes/plugin-filesystem": "^0.1.1",
"@takerofnotes/plugin-filesystem": "^0.2.0",
"@takerofnotes/plugin-supabase": "^0.1.0",
"@tiptap/extension-code-block-lowlight": "^3.20.0",
"@tiptap/extension-document": "^3.19.0",
"@tiptap/extension-image": "^3.19.0",
"@tiptap/extension-table": "^3.19.0",
"@tiptap/markdown": "^3.19.0",
"@tiptap/extension-highlight": "^3.20.0",
"@tiptap/extension-list": "^3.20.0",
"@tiptap/starter-kit": "^3.19.0",
"@tiptap/vue-3": "^3.19.0",
"@vueuse/core": "^14.2.1",
@@ -43,7 +43,9 @@
"flexsearch": "^0.8.212",
"gsap": "^3.14.2",
"lenis": "^1.3.17",
"libsodium-wrappers": "^0.8.2",
"lodash": "^4.17.23",
"lowlight": "^3.3.0",
"sass": "^1.97.3",
"sass-embedded": "^1.97.3",
"tempus": "^1.0.0-dev.17",

View File

@@ -1,14 +1,17 @@
import { Index } from 'flexsearch'
import crypto from 'crypto'
import sodium from 'libsodium-wrappers'
export default class NotesAPI {
constructor(adapter) {
constructor(adapter, encryptionKey = null) {
if (!adapter) {
throw new Error('NotesAPI requires a storage adapter')
}
this.adapter = adapter
this.notesCache = new Map()
this.encryptionKey = encryptionKey || process.env.NOTES_ENCRYPTION_KEY
this._sodiumReady = false
this.index = new Index({
tokenize: 'tolerant',
@@ -16,14 +19,109 @@ export default class NotesAPI {
})
}
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 = Buffer.from(this.encryptionKey, 'hex')
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(
Buffer.from(message),
nonce,
key,
)
const combined = Buffer.concat([nonce, ciphertext])
return combined.toString('base64')
}
_decrypt(encryptedData) {
if (!this.encryptionKey) {
throw new Error('Encryption key not set')
}
const key = Buffer.from(this.encryptionKey, 'hex')
if (key.length !== 32) {
throw new Error(
'Encryption key must be 64 hex characters (32 bytes)',
)
}
let combined
try {
combined = Buffer.from(encryptedData, 'base64')
} 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 = Buffer.from(decrypted).toString('utf8')
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 notes = await this.adapter.getAll()
const encryptedNotes = 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)
for (const encryptedNote of encryptedNotes) {
try {
const note = this._decrypt(encryptedNote.data || encryptedNote)
this.notesCache.set(note.id, note)
this.index.add(note.id, note.title + '\n' + note.content)
} catch (error) {
console.error('Failed to decrypt note:', error)
}
}
}
@@ -65,10 +163,15 @@ export default class NotesAPI {
content,
}
const encryptedNote = {
id: note.id,
data: this._encrypt(note),
}
this.notesCache.set(id, note)
this.index.add(id, note.title + '\n' + content)
await this.adapter.create(note)
await this.adapter.create(encryptedNote)
return note
}
@@ -87,9 +190,14 @@ export default class NotesAPI {
note.content = content
note.updatedAt = new Date().toISOString()
const encryptedNote = {
id: note.id,
data: this._encrypt(note),
}
this.index.update(id, note.title + '\n' + content)
await this.adapter.update(note)
await this.adapter.update(encryptedNote)
return note
}
@@ -115,9 +223,14 @@ export default class NotesAPI {
note.updatedAt = new Date().toISOString()
const encryptedNote = {
id: note.id,
data: this._encrypt(note),
}
this.index.update(id, note.title + '\n' + note.content)
await this.adapter.update(note)
await this.adapter.update(encryptedNote)
return note
}

View File

@@ -82,24 +82,48 @@ app.whenReady().then(async () => {
const config = await new PluginConfig(filesystemPlugin).load()
// Create instance of active adapter
// const plugin = registry.get(config.activeAdapter)
const plugin = registry.get(supabasePlugin.id)
// const adapter = plugin.createAdapter(config.adapterConfig)
const adapter = plugin.createAdapter({
supabaseKey: process.env.SUPABASE_KEY,
supabaseUrl: process.env.SUPABASE_URL,
})
const plugin = registry.get(config.activeAdapter)
// const plugin = registry.get(supabasePlugin.id)
const adapter = plugin.createAdapter(config.adapterConfig)
// const adapter = plugin.createAdapter({
// supabaseKey: process.env.SUPABASE_KEY,
// supabaseUrl: process.env.SUPABASE_URL,
// })
// Init Notes API
const notesAPI = new NotesAPI(adapter)
const notesAPI = new NotesAPI(
adapter,
'729a0d21d783654c68f1a0123e2a0e986350de536b5324f1f35876ea12ffeaf5',
)
await notesAPI.init()
// Broadcast note changes to all windows
const broadcastNoteChange = (event, data) => {
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send(event, data)
})
}
// Handle Notes API
ipcMain.handle('notesAPI:call', (_, method, args) => {
ipcMain.handle('notesAPI:call', async (_, method, args) => {
if (!notesAPI[method]) {
throw new Error('Invalid method')
}
return notesAPI[method](...args)
const result = await notesAPI[method](...args)
// Broadcast changes to all windows
if (method === 'createNote') {
broadcastNoteChange('note-created', result)
} else if (method === 'updateNote') {
broadcastNoteChange('note-updated', result)
} else if (method === 'updateNoteMetadata') {
broadcastNoteChange('note-updated', result)
} else if (method === 'deleteNote') {
broadcastNoteChange('note-deleted', { id: args[0] })
}
return result
})
electronApp.setAppUserModelId('com.electron')

View File

@@ -5,6 +5,15 @@ const api = {
openNoteWindow: (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))
},
}
// Implement notes API

View File

@@ -1,17 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -1,18 +1,84 @@
<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="title h1">{{ category }}</span>
</router-link>
<form v-if="isEditing" @submit.prevent="onSave">
<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>
<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>
<style lang="scss">
.category-row {
display: grid;
grid-template-columns: size-vw(26px) 1fr;
grid-template-columns: size-vw(26px) 1fr auto;
align-items: flex-start;
width: 100%;
position: relative;
padding: size-vw(5px) 0 size-vw(15px);
@@ -20,22 +86,52 @@ const props = defineProps({ index: Number, category: String })
.index {
margin-top: size-vw(19px);
@include p;
}
.title {
display: block;
width: 100%;
@include line-clamp(2);
}
.category-input {
display: block;
width: 100%;
@include h1;
&:focus {
outline: none;
}
}
.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;
}
}
&::after {
content: '----------------------------------------';
position: absolute;
bottom: 0;
left: 0;
@include p;
}
&.router-link-exact-active {
&.router-link-exact-active,
&.editable {
cursor: default;
}
&:hover:not(.router-link-exact-active) {
&:hover:not(.router-link-exact-active):not(.editable) {
color: var(--theme-accent);
}
}

View File

@@ -5,7 +5,9 @@
<div class="menu-wrap layout-block-inner">
<new-note class="menu-item" @noteOpened="closeMenu" />
<button class="menu-item">+ New Capitulum</button>
<router-link class="menu-item" to="/category"
>+ New Capitulum</router-link
>
<button class="menu-item">Change Theme</button>
<router-link class="menu-item" to="/instructions"
>Instructio</router-link

View File

@@ -6,7 +6,7 @@ export default () => {
const route = useRoute()
const router = useRouter()
const menuOpen = computed(() => route.query.menuOpen === 'true')
const menuOpen = computed(() => route.query?.menuOpen === 'true')
const closeMenu = () => {
router.push({

View File

@@ -2,8 +2,30 @@ import { ref } from 'vue'
const categories = ref([])
const searchResults = ref([])
const notesChangeCount = ref(0)
let initialized = false
function setupListeners() {
if (initialized || typeof window === 'undefined') return
initialized = true
window.api.onNoteCreated(() => {
notesChangeCount.value++
})
window.api.onNoteUpdated(() => {
notesChangeCount.value++
})
window.api.onNoteDeleted(() => {
notesChangeCount.value++
})
}
export default () => {
setupListeners()
/* -------------------------
Initialization
--------------------------*/
@@ -46,6 +68,16 @@ export default () => {
return note
}
async function updateCategory(category, update) {
const notes = await loadCategoryNotes(category)
notes.forEach(async (note) => {
await updateNoteMetadata(note.id, { category: update })
})
await loadCategories()
}
/* -------------------------
Search
--------------------------*/
@@ -61,6 +93,7 @@ export default () => {
return {
categories,
searchResults,
notesChangeCount,
loadCategories,
loadCategoryNotes,
@@ -69,6 +102,7 @@ export default () => {
createNote,
updateNoteContent,
updateNoteMetadata,
updateCategory,
search,
}

View File

@@ -2,12 +2,14 @@ import { createRouter, createWebHistory } from 'vue-router'
import Directory from '@/views/Directory.vue'
import Editor from '@/views/Editor.vue'
import CreateCategory from '@/views/CreateCategory.vue'
import Category from '@/views/Category.vue'
import Instructions from '@/views/Instructions.vue'
const routes = [
{ path: '/', name: 'directory', component: Directory },
{ path: '/note/:id', name: 'note', component: Editor },
{ path: '/category', name: 'create-category', component: CreateCategory },
{ path: '/category/:id', name: 'category', component: Category },
{ path: '/instructions', name: 'instructions', component: Instructions },
]

View File

@@ -9,8 +9,8 @@
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
*/
*: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;
display: revert;
}

View File

@@ -2,7 +2,12 @@
<main class="category layout-block">
<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">
<note-row v-for="note in notes" :note="note" :key="note.id" />
@@ -13,26 +18,44 @@
</template>
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import useNotes from '@/composables/useNotes'
import NoteRow from '@/components/NoteRow.vue'
import CategoryRow from '@/components/CategoryRow.vue'
import NewNote from '@/components/NewNote.vue'
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()
async function refreshNotes() {
if (id) {
notes.value = await loadCategoryNotes(id)
}
}
onMounted(async () => {
notes.value = await loadCategoryNotes(id)
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(() => {
return categories.value?.findIndex((category) => category === id) || 0
return categories.value?.findIndex((category) => category === id) || 1
})
</script>

View File

@@ -0,0 +1,16 @@
<template>
<main class="create-category layout-block-inner">
<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>

View File

@@ -17,17 +17,26 @@
<script setup>
import useNotes from '@/composables/useNotes'
import { onMounted, ref } from 'vue'
import { onMounted, ref, watch } from 'vue'
import CategoryRow from '@/components/CategoryRow.vue'
import NoteRow from '@/components/NoteRow.vue'
const { categories, loadCategories, loadCategoryNotes } = useNotes()
const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
useNotes()
const notes = ref()
onMounted(async () => {
async function refreshNotes() {
await loadCategories()
notes.value = await loadCategoryNotes()
}
onMounted(async () => {
await refreshNotes()
})
watch(notesChangeCount, async () => {
await refreshNotes()
})
</script>

View File

@@ -14,6 +14,12 @@
>
Italic
</button>
<button
@click="editor.chain().focus().toggleHighlight().run()"
:class="{ active: editor.isActive('highlight') }"
>
Highlight
</button>
</div>
</bubble-menu>
@@ -24,12 +30,14 @@
<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 { TaskList, TaskItem } from '@tiptap/extension-list'
import { Highlight } from '@tiptap/extension-highlight'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import { BubbleMenu } from '@tiptap/vue-3/menus'
import { all, createLowlight } from 'lowlight'
import useNotes from '@/composables/useNotes'
import { useRoute } from 'vue-router'
import _debounce from 'lodash/debounce'
@@ -46,9 +54,9 @@ const CustomDocument = Document.extend({
const editor = shallowRef()
const updateNote = _debounce(async ({ editor }) => {
const markdown = editor.getMarkdown()
const json = editor.getJSON()
await updateNoteContent(id, markdown)
await updateNoteContent(id, json)
updateTitle(editor)
}, 300)
@@ -73,6 +81,9 @@ onMounted(async () => {
const note = await loadNote(id)
lastTitle = note.title
// Lowlight setup
const lowlight = createLowlight(all)
editor.value = new Editor({
extensions: [
CustomDocument,
@@ -90,11 +101,15 @@ onMounted(async () => {
}
},
}),
Markdown.configure({}),
Image,
TaskList,
TaskItem,
Highlight,
CodeBlockLowlight.configure({
lowlight,
enableTabIndentation: true,
}),
],
content: note.content,
contentType: 'markdown',
content: note.content || [],
onUpdate: updateNote,
})
})
@@ -156,6 +171,94 @@ main.editor {
color: var(--theme-link);
cursor: pointer;
}
code {
border: 1px solid var(--grey-100);
color: var(--theme-accent);
padding: 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: size-vw(1px);
background: currentColor;
}
}
mark {
background: var(--theme-accent);
color: var(--theme-bg);
padding: 0.2em;
border-radius: 0.2em;
}
.editor-wrap {
> div {