~dricottone/noticable

2b896b8a90b76ebf5befb8d4f9dbff6b8e1f68f9 — Dominic Ricottone 3 years ago d9ad29e
Minimal working program
7 files changed, 336 insertions(+), 89 deletions(-)

M index.html
M main.js
M noticable.css
M package-lock.json
M package.json
M preload.js
M renderer.js
M index.html => index.html +10 -1
@@ 10,10 10,19 @@
  </head>
  <body>
    <div class="sidebar">
      <div id="settings">
        <p>Editor Mode:</p>
        <input id="ui-focus-editor" type="checkbox" checked onchange="uiToggleEditor()">
        <label for="ui-focus-editor"></label>
        <p>Enable Rendering:</p>
        <input id="ui-enable-render" type="checkbox" checked>
        <label for="ui-enable-render"></label>
      </div>
      <ul id="list-filenames"></ul>
    </div>
    <div class="container focused" id="container-editor"></div>
    <div class="container markdown-body" id="container-results"></div>
    <!--NOTE: .markdown-body is the target for Github Markdown CSS module; do not modify!-->
    <div class="container markdown-body" id="container-rendered"></div>
    <script src="node_modules/jquery/dist/jquery.min.js"></script>
    <script src="node_modules/monaco-editor/min/vs/loader.js"></script>
    <script src="renderer.js"></script>

M main.js => main.js +55 -12
@@ 1,8 1,31 @@
const { app, BrowserWindow } = require("electron");
// constants
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const shortcut = require("electron-localshortcut");
const path = require("path");

// initialize renderer
// configuration
const notesDir = "notes";

// messaging
function postFileName(window, event) {
  dialog.showSaveDialog(window, {
    title: "Create new note",
    defaultPath: notesDir,
    properties: ["showOverwriteConfirmation"],
    filters: [
      { name: "Markdown", extensions: ["md"] },
      { name: "Plain Text", extensions: ["txt"] },
    ]
  })
  .then(r => {
    if (!r.canceled) {
      console.log("[main] posting new filename...");
      event.sender.send("post-new-filename", r.filePath);
    }
  });
}

// utilities
let win;
function createWindow() {
  win = new BrowserWindow({


@@ 14,30 37,50 @@ function createWindow() {
      preload: path.join(__dirname, "preload.js")
    }
  });

  win.loadFile(path.join(__dirname, "index.html"));
  shortcut.register(win, "CmdOrCtrl+S", () => {
    console.log("[main] triggering 'request-focus-editor'...");
    win.webContents.send("request-focus-editor", "");
  });
  shortcut.register(win, "CmdOrCtrl+E", () => {
    console.log("[main] triggering 'request-render-markdown'...");
    win.webContents.send("request-render-markdown", "");
}
function initializeWindow() {
  createWindow();
  registerShortCuts(win);

  ipcMain.on("request-local-filename", (event) => {
    console.log("[main] caught request for local filename");
    console.log("[main] sending 'request-focus-editor'...");
    postFileName(win, event);
  });

  win.on("closed", () => {
    win = null;
  });
};
app.on("ready", createWindow);
function registerShortCuts(window) {
  shortcut.register(window, "CmdOrCtrl+E", () => {
    console.log("[main] focus editor");
    window.webContents.send("key-focus-editor", "");
  });
  shortcut.register(window, "CmdOrCtrl+S", () => {
    console.log("[main] save text and render markdown");
    window.webContents.send("key-render-markdown", "");
  });
  shortcut.register(window, "CmdOrCtrl+Shift+S", () => {
    console.log("[main] save text");
    window.webContents.send("key-save-text", "");
  });
}

// app event loop
app.on("ready", initializeWindow);

// macOS best practices
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  if (win === null) {
    createWindow();
    initializeWindow();
  }
});


M noticable.css => noticable.css +40 -1
@@ 14,11 14,50 @@ body {
.sidebar li {
  color: #fff;
}
.sidebar p {
  color: white;
  display: inline-block;
}
.sidebar a {
  color: #fff;
  color: white;
  text-decoration: none;
  display: block;
}
#settings input {
  height: 0;
  width: 0;
  visibility: hidden;
}
#settings label {
  top: 0.25em;
  cursor: pointer;
  text-indent: -9999px;
  width: 40px;
  height: 20px;
  background: grey;
  display: inline-block;
  position: relative;
}
#settings label:after {
  content: "";
  position: absolute;
  top: 1px;
  left: 1px;
  width: 18px;
  height: 18px;
  background: white;
  transition: 0.5s;
}
#settings input:checked + label {
  background: seagreen;
}
#settings input:checked + label:after {
  left: calc(100% - 1px);
  transform: translateX(-100%);
}
#settings label:active:after {
  width: 130px;
}
.container {
  max-width: 1000px;
  height: 100%;

M package-lock.json => package-lock.json +21 -0
@@ 169,6 169,14 @@
      "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==",
      "optional": true
    },
    "doc-ready": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/doc-ready/-/doc-ready-1.0.4.tgz",
      "integrity": "sha1-N/U5GWnP+ZQwP9/vLl1QNX+BZNM=",
      "requires": {
        "eventie": "^1"
      }
    },
    "duplexer3": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",


@@ 200,6 208,14 @@
        "keyboardevents-areequal": "^0.2.1"
      }
    },
    "electron-prompt": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/electron-prompt/-/electron-prompt-1.6.0.tgz",
      "integrity": "sha512-CJaSo7o84JJygBGCHIY1zySn+j2ejsKZZFAmfIrOw8wgriufzl4RchTf9I2HNFvlOiekGj4WAznOaA6gFm5ddQ==",
      "requires": {
        "doc-ready": "^1.0.4"
      }
    },
    "encodeurl": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",


@@ 236,6 252,11 @@
      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
      "optional": true
    },
    "eventie": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/eventie/-/eventie-1.0.6.tgz",
      "integrity": "sha1-1P/IsMK15JPCqhsiy+kY067nRDc="
    },
    "extract-zip": {
      "version": "1.7.0",
      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",

M package.json => package.json +1 -0
@@ 15,6 15,7 @@
    "cash-dom": "^8.1.0",
    "electron": "^11.1.0",
    "electron-localshortcut": "^3.2.1",
    "electron-prompt": "^1.6.0",
    "github-markdown-css": "^4.0.0",
    "jquery": "^3.5.1",
    "markdown-it": "^12.0.3",

M preload.js => preload.js +128 -44
@@ 1,78 1,162 @@
// constants
const { ipcRenderer } = require("electron");
const fs = require("fs")
const path = require("path");
const md = require("markdown-it")();
let currentFilename = "";
let currentFileContent = "";

// configuration
const newFileButton = "+ New Note";
const notesDir = "notes";

// messaging
function postFile(text) {
  console.log("[preload] sending 'post-file'...");
  window.postMessage({ type: "post-file", text: text }, "*");
function requestLocalFilename() {
  console.log("[preload -> main] requesting local filename...");
  ipcRenderer.send("request-local-filename", "");
}
function postFileText(text) {
  console.log("[preload -> renderer] posting file text...");
  window.postMessage({ type: "post-file-text", text: text }, "*");
}
function postDisplayFilename(filename) {
  console.log("[preload -> renderer] posting display file name...");
  window.postMessage({ type: "post-display-filename", text: filename }, "*");
}
function requestInsertFilename(filename) {
  console.log("[preload] sending 'request-insert-filename'...");
  window.postMessage({ type: "request-insert-filename", text: filename }, "*");
function postHTML(html) {
  console.log("[preload -> renderer] posting HTML...");
  window.postMessage({ type: "post-html", text: html }, "*");
}
function requestInsertHTML(html) {
  console.log("[preload] sending 'request-insert-html'...");
  window.postMessage({ type: "request-insert-html", text: html }, "*");
function requestEditorText() {
  console.log("[preload -> renderer] requesting editor text...");
  window.postMessage({ type: "request-editor-text" }, "*");
}
function requestPostMarkdown() {
  console.log("[preload] sending 'request-post-markdown'...");
  window.postMessage({ type: "request-post-markdown" }, "*");
function requestFocusEditor() {
  console.log("[preload -> renderer] requesting focus editor...");
  window.postMessage({ type: "request-focus-editor" }, "*");
}
function requestUnfocusEditor() {
  console.log("[preload -> renderer] requesting unfocus editor...");
  window.postMessage({ type: "request-unfocus-editor" }, "*");
}
function requestToggleEditor() {
  console.log("[preload] sending 'request-toggle-editor'...");
  console.log("[preload -> renderer] requesting toggle editor focus...");
  window.postMessage({ type: "request-toggle-editor" }, "*");
}
function requestAlertInvalidFilename() {
  console.log("[preload -> renderer] requesting alert about invalid filename...");
  window.postMessage({ type: "request-alert-invalid-filename" }, "*");
}

// utilities
const newFileButton = "+ New Note";
const notesDir = "notes"
function initNotesList() {
  // initialize the new file button
  postDisplayFilename(newFileButton);
  // initialize the notes files
  fs.readdir(notesDir, (err, files) => {
    if (err) throw err;
    files.forEach(file => {
      postDisplayFilename(getPrettyFilename(file));
    });
  });
}
function getActualFilename(filename) {
  unprettyFilename = path.basename(filename, ".md").split(" ").join("_") + ".md";
  return path.join(notesDir, unprettyFilename);
}
function getPrettyFilename(filename) {
  return path.basename(filename, ".md").split("_").join(" ");
}
function readFile(filename) {
  if (filename == newFileButton) {
    postFile("");
    updateState("","");
    requestLocalFilename();
  } else {
    fs.readFile(path.join(notesDir, filename + ".md"), "utf8", (err, data) => {
      if (err) throw err;
      postFile(data);
    let actualFilename = getActualFilename(filename);
    fs.readFile(actualFilename, "utf8", (err, content) => {
      if (err) requestAlertInvalidFilename();
      updateState(actualFilename,content);
    });
  };
  }
}
function renderMarkdown(markdown) {
  requestInsertHTML(md.render(markdown));
function renderMarkdown(content) {
  postHTML(md.render(content));
}
function initNotesList() {
  requestInsertFilename(newFileButton);

  fs.readdir(notesDir, (err, files) => {
    if (err) throw err;
    files.forEach(file => {
      requestInsertFilename(file.slice(0,-3))
function updateState(filename, content) {
  currentFilename = filename;
  currentFileContent = content;
  postFileText(content);
}
function saveFile(content) {
  if (currentFilename != "") {
    currentFileContent = content;
    console.log("[preload] writing file '" + currentFilename + "'");
    fs.writeFile(currentFilename, currentFileContent, (err) => {
      if (err) throw err;
    });
  });
  } else {
    console.log("[preload] no filename; cannot write file");
  }
}
function saveFileConditional(content) {
  if (currentFilename != "") {
    if (content != currentFileContent) {
      currentFileContent = content;
      console.log("[preload] writing updated file '" + currentFilename + "'");
      fs.writeFile(currentFilename, currentFileContent, (err) => {
        if (err) throw err;
      });
      return true;
    } else {
      console.log("[preload] no updates; not writing file");
      return false;
    }
  } else {
    console.log("[preload] no filename; cannot write file");
    return false;
  }
}

// listen to main process
ipcRenderer.on("request-render-markdown", () => {
  console.log("[preload] caught 'request-render-markdown'!");
  requestPostMarkdown();
ipcRenderer.on("post-new-filename", (event, filename) => {
  console.log("[preload] caught file name");
  currentFilename = getActualFilename(filename);
  postDisplayFilename(getPrettyFilename(filename));
});
ipcRenderer.on("key-save-text", () => {
  console.log("[preload] caught keybind for save text");
  requestEditorText();
});
ipcRenderer.on("key-render-markdown", () => {
  console.log("[preload] caught keybind for render markdown");
  requestEditorText();
  requestUnfocusEditor();
});
ipcRenderer.on("key-focus-editor", () => {
  console.log("[preload] caught keybind for focus editor");
  requestFocusEditor();
});
ipcRenderer.on("request-focus-editor", () => {
  console.log("[preload] caught 'request-focus-editor'!");
  requestToggleEditor();
ipcRenderer.on("key-unfocus-editor", () => {
  console.log("[preload] caught keybind for unfocus editor");
  requestUnfocusEditor();
});

// listen to renderer
window.addEventListener("message", (event) => {
  if (event.source != window) return;
  if (event.data.type && (event.data.type == "post-markdown")) {
    console.log("[preload] caught 'post-markdown'!");
    renderMarkdown(event.data.text);
  };
  if (event.data.type && (event.data.type == "request-post-file")) {
    console.log("[preload] caught 'request-post-file'!");
    readFile(event.data.text);
  };
  if (event.data.type) {
    switch(event.data.type) {
      case "post-editor-text":
        console.log("[preload] caught editor text");
        saveFileConditional(event.data.text);
        renderMarkdown(event.data.text);
        break;
      case "request-file-text":
        console.log("[preload] caught request for file text");
        readFile(event.data.text);
        break;
    }
  }
}, false);

// initialize renderer

M renderer.js => renderer.js +81 -31
@@ 11,50 11,100 @@ require(['vs/editor/editor.main'], function () {
});

// messaging
function postMarkdown(markdown) {
  console.log("[renderer] sending 'post-markdown'...");
  window.postMessage({ type: "post-markdown", text: markdown }, "*");
function postEditorText() {
  console.log("[renderer -> preload] posting editor text...");
  window.postMessage({ type: "post-editor-text", text: window.editor.getValue() }, "*");
}
function requestPostFile(filename) {
  console.log("[renderer] triggering 'request-post-file' on '" + filename + "'...");
  window.postMessage({ type: "request-post-file", text: filename }, "*");
function requestFileText(filename) {
  console.log("[renderer -> preload] requesting file text...");
  window.postMessage({ type: "request-file-text", text: filename }, "*");
}

// utilities
function buildFileName(filename) {
function buildFilename(filename) {
  var el = document.createElement("li");
  el.innerHTML = filename;
  el.addEventListener("click", () => { requestPostFile(filename); });
  el.addEventListener("click", () => { requestFileText(filename); });
  return el
}
function sortFileNames(a, b) {
function sortFilenames(a, b) {
  return ($(b).text()) < ($(a).text());
}
function focusEditor() {
  $("#container-editor").addClass("focused");
  $("#container-rendered").removeClass("focused");
  $("#ui-focus-editor").prop("checked", true);
}
function unfocusEditor() {
  if ($("#ui-enable-render").prop("checked")) {
    $("#container-editor").removeClass("focused");
    $("#container-rendered").addClass("focused");
    $("#ui-focus-editor").prop("checked", false);
  } else {
    console.log("[renderer] rendering disabled");
    focusEditor();
  }
}
function toggleEditor() {
  if ($("#ui-focus-editor").prop("checked")) {
    unfocusEditor();
    postEditorText();
  } else {
    focusEditor();
  }
}
function uiToggleEditor() {
  //NOTE: Remember that the status accessed here is the 'new' status, i.e. after it had been toggled
  if ($("#ui-focus-editor").prop("checked")) {
    console.log("[renderer] UI focus editor");
    focusEditor();
  } else {
    console.log("[renderer] UI unfocus editor");
    unfocusEditor();
    postEditorText();
  }
}

// listen to preload
window.addEventListener("message", (event) => {
  if (event.source != window) return;
  if (event.data.type && (event.data.type == "request-post-markdown")) {
    console.log("[renderer] caught 'request-post-markdown'!");
    postMarkdown(window.editor.getValue());
  };
  if (event.data.type && (event.data.type == "request-insert-filename")) {
    console.log("[renderer] caught 'request-insert-filename'!");
    $("#list-filenames").append(buildFileName(event.data.text));
    $("#list-filenames li").sort(sortFileNames).appendTo("#list-filenames");
  };
  if (event.data.type && (event.data.type == "request-insert-html")) {
    console.log("[renderer] caught 'request-insert-html'!");
    $("#container-results").html(event.data.text);
    $(".container").toggleClass("focused");
  };
  if (event.data.type && (event.data.type == "request-toggle-editor")) {
    console.log("[renderer] caught 'request-toggle-editor'!");
    $(".container").toggleClass("focused");
  };
  if (event.data.type && (event.data.type == "post-file")) {
    console.log("[renderer] caught 'post-file'!");
    window.editor.setValue(event.data.text);
  };
  if (event.data.type) {
    switch(event.data.type) {
      case "post-file-text":
        console.log("[renderer] caught file text");
        window.editor.setValue(event.data.text);
        focusEditor();
        break;
      case "post-display-filename":
        console.log("[renderer] caught display filename");
        $("#list-filenames").append(buildFilename(event.data.text));
        $("#list-filenames li").sort(sortFilenames).appendTo("#list-filenames");
        break;
      case "post-html":
        console.log("[renderer] caught HTML");
        $("#container-rendered").html(event.data.text);
        break;
      case "request-editor-text":
        console.log("[renderer] caught request for editor text");
        postEditorText();
        break;
      case "request-focus-editor":
        console.log("[renderer] caught request to focus editor");
        focusEditor();
        break;
      case "request-unfocus-editor":
        console.log("[renderer] caught request to unfocus editor");
        unfocusEditor();
        break;
      case "request-toggle-editor":
        console.log("[renderer] caught request to toggle editor");
        toggleEditor();
        break;
      case "request-alert-invalid-filename":
        console.log("[renderer] caught request to alert about invalid filename");
        alert("Error: File could not be read");
        break;
    }
  }
}, false);