diff --git a/out/main/index.js b/out/main/index.js index 227a672..a9dd42b 100644 --- a/out/main/index.js +++ b/out/main/index.js @@ -1,7 +1,77 @@ "use strict"; -const electron = require("electron"); -const path = require("path"); const utils = require("@electron-toolkit/utils"); +const electron = require("electron"); +const fs = require("fs"); +const path = require("path"); +const BASE_DIR = path.join(electron.app.getPath("userData"), "notes-storage"); +const ensureBaseDir = () => { + if (!fs.existsSync(BASE_DIR)) { + fs.mkdirSync(BASE_DIR, { recursive: true }); + } +}; +const sanitizeRelativePath = (relativePath) => { + const resolved = path.join(BASE_DIR, relativePath); + if (!resolved.startsWith(BASE_DIR)) { + throw new Error("Invalid path"); + } + return resolved; +}; +const readAllNotesRecursive = (dir = BASE_DIR, base = BASE_DIR) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + let results = []; + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + results = results.concat(readAllNotesRecursive(fullPath, base)); + } + if (entry.isFile() && entry.name.endsWith(".md")) { + const content = fs.readFileSync(fullPath, "utf-8"); + results.push({ + name: entry.name, + path: path.relative(base, fullPath), + content + }); + } + } + return results; +}; +const createNote = (relativePath, content = "") => { + const fullPath = sanitizeRelativePath(relativePath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, content, "utf-8"); + return true; +}; +const createDirectory = (relativePath) => { + const fullPath = sanitizeRelativePath(relativePath); + fs.mkdirSync(fullPath, { recursive: true }); + return true; +}; +const readNote = (relativePath) => { + const fullPath = sanitizeRelativePath(relativePath); + if (!fs.existsSync(fullPath)) { + createNote(relativePath); + } + return fs.readFileSync(fullPath, "utf-8"); +}; +const updateNote = (relativePath, content) => { + const fullPath = sanitizeRelativePath(relativePath); + if (!fs.existsSync(fullPath)) { + throw new Error("Note does not exist"); + } + fs.writeFileSync(fullPath, content, "utf-8"); + return true; +}; +const notes = { + ensureBaseDir, + sanitizeRelativePath, + readAllNotesRecursive, + createNote, + createDirectory, + readNote, + updateNote +}; +const preloadPath = path.join(__dirname, "../preload/index.js"); +const rendererPath = path.join(__dirname, "../renderer/index.html"); function createWindow() { const mainWindow2 = new electron.BrowserWindow({ width: 354, @@ -9,7 +79,7 @@ function createWindow() { show: false, autoHideMenuBar: true, webPreferences: { - preload: path.join(__dirname, "../preload/index.js"), + preload: preloadPath, sandbox: false } }); @@ -23,7 +93,7 @@ function createWindow() { if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) { mainWindow2.loadURL(process.env["ELECTRON_RENDERER_URL"]); } else { - mainWindow2.loadFile(path.join(__dirname, "../renderer/index.html")); + mainWindow2.loadFile(rendererPath); } } function createNoteWindow(noteId) { @@ -32,7 +102,7 @@ function createNoteWindow(noteId) { height: 549, autoHideMenuBar: true, webPreferences: { - preload: path.join(__dirname, "preload.js"), + preload: preloadPath, contextIsolation: true, nodeIntegration: false } @@ -42,7 +112,7 @@ function createNoteWindow(noteId) { `${process.env["ELECTRON_RENDERER_URL"]}/note/${noteId}` ); } else { - mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"), { + mainWindow.loadFile(rendererPath, { path: `/notes/${noteId}` }); } @@ -52,14 +122,29 @@ electron.app.whenReady().then(() => { electron.app.on("browser-window-created", (_, window) => { utils.optimizer.watchWindowShortcuts(window); }); - console.log(electron.app.getPath("userData")); createWindow(); + notes.ensureBaseDir(); electron.app.on("activate", function() { if (electron.BrowserWindow.getAllWindows().length === 0) createWindow(); }); electron.ipcMain.on("open-note-window", (_, noteId) => { createNoteWindow(noteId); }); + electron.ipcMain.handle("notes:list", () => { + return notes.readAllNotesRecursive(); + }); + electron.ipcMain.handle("notes:create", (_, { path: path2, content }) => { + return notes.createNote(path2, content); + }); + electron.ipcMain.handle("notes:createDir", (_, path2) => { + return notes.createDirectory(path2); + }); + electron.ipcMain.handle("notes:read", (_, path2) => { + return notes.readNote(path2); + }); + electron.ipcMain.handle("notes:update", (_, { path: path2, content }) => { + return notes.updateNote(path2, content); + }); }); electron.app.on("window-all-closed", () => { if (process.platform !== "darwin") { diff --git a/out/preload/index.js b/out/preload/index.js index cf9718e..fc9eb36 100644 --- a/out/preload/index.js +++ b/out/preload/index.js @@ -1,19 +1,21 @@ "use strict"; const electron = require("electron"); -const preload = require("@electron-toolkit/preload"); const api = { openNoteWindow: (noteId) => { electron.ipcRenderer.send("open-note-window", noteId); - } + }, + listNotes: () => electron.ipcRenderer.invoke("notes:list"), + createNote: (path, content) => electron.ipcRenderer.invoke("notes:create", { path, content }), + createNoteDir: (path) => electron.ipcRenderer.invoke("notes:createDir", path), + readNote: (path) => electron.ipcRenderer.invoke("notes:read", path), + updateNote: (path, content) => electron.ipcRenderer.invoke("notes:update", { path, content }) }; if (process.contextIsolated) { try { - electron.contextBridge.exposeInMainWorld("electron", preload.electronAPI); electron.contextBridge.exposeInMainWorld("api", api); } catch (error) { console.error(error); } } else { - window.electron = preload.electronAPI; window.api = api; } diff --git a/package-lock.json b/package-lock.json index 1cc4770..6abec00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "fecha": "^4.2.3", "gsap": "^3.14.2", "lenis": "^1.3.17", + "lodash": "^4.17.23", "sass": "^1.97.3", "sass-embedded": "^1.97.3", "tempus": "^1.0.0-dev.17", @@ -5945,7 +5946,6 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true, "license": "MIT" }, "node_modules/lodash.escaperegexp": { diff --git a/package.json b/package.json index 0f7ca86..c20fc48 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "fecha": "^4.2.3", "gsap": "^3.14.2", "lenis": "^1.3.17", + "lodash": "^4.17.23", "sass": "^1.97.3", "sass-embedded": "^1.97.3", "tempus": "^1.0.0-dev.17", diff --git a/src/main/index.js b/src/main/index.js index 0a704b5..655d0c5 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,6 +1,10 @@ -import { app, shell, BrowserWindow, ipcMain } from 'electron' -import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' +import { app, shell, BrowserWindow, ipcMain } from 'electron' +import notes from './notesStorage' +import { join } from 'path' + +const preloadPath = join(__dirname, '../preload/index.js') +const rendererPath = join(__dirname, '../renderer/index.html') function createWindow() { // Create the browser window. @@ -10,7 +14,7 @@ function createWindow() { show: false, autoHideMenuBar: true, webPreferences: { - preload: join(__dirname, '../preload/index.js'), + preload: preloadPath, sandbox: false, }, }) @@ -29,7 +33,7 @@ function createWindow() { if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + mainWindow.loadFile(rendererPath) } } @@ -39,7 +43,7 @@ function createNoteWindow(noteId) { height: 549, autoHideMenuBar: true, webPreferences: { - preload: join(__dirname, 'preload.js'), + preload: preloadPath, contextIsolation: true, nodeIntegration: false, }, @@ -50,7 +54,7 @@ function createNoteWindow(noteId) { `${process.env['ELECTRON_RENDERER_URL']}/note/${noteId}`, ) } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html'), { + mainWindow.loadFile(rendererPath, { path: `/notes/${noteId}`, }) } @@ -70,10 +74,12 @@ app.whenReady().then(() => { optimizer.watchWindowShortcuts(window) }) - console.log(app.getPath('userData')) - + // Create main window createWindow() + // Ensure data directory is present + notes.ensureBaseDir() + app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. @@ -84,6 +90,23 @@ app.whenReady().then(() => { ipcMain.on('open-note-window', (_, noteId) => { createNoteWindow(noteId) }) + + // File access + ipcMain.handle('notes:list', () => { + return notes.readAllNotesRecursive() + }) + ipcMain.handle('notes:create', (_, { path, content }) => { + return notes.createNote(path, content) + }) + ipcMain.handle('notes:createDir', (_, path) => { + return notes.createDirectory(path) + }) + ipcMain.handle('notes:read', (_, path) => { + return notes.readNote(path) + }) + ipcMain.handle('notes:update', (_, { path, content }) => { + return notes.updateNote(path, content) + }) }) // Quit when all windows are closed, except on macOS. There, it's common diff --git a/src/main/notesStorage.js b/src/main/notesStorage.js new file mode 100644 index 0000000..395e265 --- /dev/null +++ b/src/main/notesStorage.js @@ -0,0 +1,91 @@ +import { app } from 'electron' +import fs from 'fs' +import { join, relative, dirname } from 'path' + +const BASE_DIR = join(app.getPath('userData'), 'notes-storage') + +export const ensureBaseDir = () => { + if (!fs.existsSync(BASE_DIR)) { + fs.mkdirSync(BASE_DIR, { recursive: true }) + } +} + +export const sanitizeRelativePath = (relativePath) => { + const resolved = join(BASE_DIR, relativePath) + if (!resolved.startsWith(BASE_DIR)) { + throw new Error('Invalid path') + } + return resolved +} + +export const readAllNotesRecursive = (dir = BASE_DIR, base = BASE_DIR) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + let results = [] + + for (const entry of entries) { + const fullPath = join(dir, entry.name) + + if (entry.isDirectory()) { + results = results.concat(readAllNotesRecursive(fullPath, base)) + } + + if (entry.isFile() && entry.name.endsWith('.md')) { + const content = fs.readFileSync(fullPath, 'utf-8') + + results.push({ + name: entry.name, + path: relative(base, fullPath), + content, + }) + } + } + + return results +} + +export const createNote = (relativePath, content = '') => { + const fullPath = sanitizeRelativePath(relativePath) + + fs.mkdirSync(dirname(fullPath), { recursive: true }) + fs.writeFileSync(fullPath, content, 'utf-8') + + return true +} + +export const createDirectory = (relativePath) => { + const fullPath = sanitizeRelativePath(relativePath) + fs.mkdirSync(fullPath, { recursive: true }) + return true +} + +export const readNote = (relativePath) => { + const fullPath = sanitizeRelativePath(relativePath) + + if (!fs.existsSync(fullPath)) { + createNote(relativePath) + } + + return fs.readFileSync(fullPath, 'utf-8') +} + +export const updateNote = (relativePath, content) => { + const fullPath = sanitizeRelativePath(relativePath) + + if (!fs.existsSync(fullPath)) { + throw new Error('Note does not exist') + } + + fs.writeFileSync(fullPath, content, 'utf-8') + + return true +} + +export default { + ensureBaseDir, + sanitizeRelativePath, + readAllNotesRecursive, + createNote, + createDirectory, + readNote, + updateNote, +} diff --git a/src/preload/index.js b/src/preload/index.js index 3b8fa3e..12d7114 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -1,24 +1,25 @@ import { contextBridge, ipcRenderer } from 'electron' -import { electronAPI } from '@electron-toolkit/preload' // Custom APIs for renderer const api = { openNoteWindow: (noteId) => { ipcRenderer.send('open-note-window', noteId) }, + listNotes: () => ipcRenderer.invoke('notes:list'), + createNote: (path, content) => + ipcRenderer.invoke('notes:create', { path, content }), + createNoteDir: (path) => ipcRenderer.invoke('notes:createDir', path), + readNote: (path) => ipcRenderer.invoke('notes:read', path), + updateNote: (path, content) => + ipcRenderer.invoke('notes:update', { path, content }), } -// Use `contextBridge` APIs to expose Electron APIs to -// renderer only if context isolation is enabled, otherwise -// just add to the DOM global. if (process.contextIsolated) { try { - contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('api', api) } catch (error) { console.error(error) } } else { - window.electron = electronAPI window.api = api } diff --git a/src/renderer/src/composables/useNotes.js b/src/renderer/src/composables/useNotes.js new file mode 100644 index 0000000..678c927 --- /dev/null +++ b/src/renderer/src/composables/useNotes.js @@ -0,0 +1,48 @@ +import { ref } from 'vue' + +export default () => { + const notes = ref([]) + const loading = ref(false) + const error = ref(null) + + const fetchNotes = async () => { + try { + loading.value = true + notes.value = await window.api.listNotes() + } catch (err) { + error.value = err.message + } finally { + loading.value = false + } + } + + const createNote = async (path, content = '') => { + await window.api.createNote(path, content) + await fetchNotes() + } + + const createDirectory = async (path) => { + await window.api.createNoteDir(path) + await fetchNotes() + } + + const readNote = async (path) => { + console.log(path) + return await window.api.readNote(path) + } + + const updateNote = async (path, content) => { + return await window.api.updateNote(path, content) + } + + return { + notes, + loading, + error, + fetchNotes, + createNote, + createDirectory, + readNote, + updateNote, + } +} diff --git a/src/renderer/src/views/Directory.vue b/src/renderer/src/views/Directory.vue index 47759aa..c329176 100644 --- a/src/renderer/src/views/Directory.vue +++ b/src/renderer/src/views/Directory.vue @@ -1,10 +1,6 @@