Cybrkyd's Git Repositories

notes - commit: 90e9642

commit 90e9642da07518260fc408cc520489b7f45836649d6029a28a60c894ffd7bff1
author Cybrkyd <git@cybrkyd.com> 2026-06-09 10:27:15 +0100
committer Cybrkyd <git@cybrkyd.com> 2026-06-09 10:27:15 +0100

Commit Message

notes.py

📊 Diffstat

notes.py 686
1 files changed, 686 insertions(+), 0 deletions(-)

Diff

diff --git a/notes.py b/notes.py
new file mode 100644
index 0000000..4caf2f5
--- /dev/null
+++ b/notes.py
@@ -0,0 +1,686 @@
+ #!/usr/bin/env python3
+
+ import http.server
+ import json
+ import sqlite3
+ import os
+ import re
+ from datetime import datetime
+
+ PORT = 5001
+ DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "notes.sqlite")
+
+ HTML = r"""<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Notes</title>
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+ body {
+ font-family: Arial, sans-serif;
+ background-color: #fff1e5;
+ min-height: 100vh;
+ }
+ .app-wrapper {
+ display: flex;
+ min-height: 100vh;
+ }
+ h1 {
+ margin-bottom: 20px;
+ font-size: 1.5rem;
+ color: #807973;
+ text-align: center;
+ }
+ #docStatus {
+ font-size: 13px;
+ color: #555;
+ margin-bottom: 8px;
+ min-height: 18px;
+ font-style: italic;
+ text-align: center;
+ }
+ textarea {
+ width: 100%;
+ height: 600px;
+ padding: 12px;
+ font-size: 16px;
+ resize: vertical;
+ box-sizing: border-box;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ font-family: Arial, sans-serif;
+ background-color: #fff1e5;
+ }
+ textarea:focus {
+ outline: none;
+ border: 1px solid #aaa;
+ box-shadow: 0 0 4px rgba(0,0,0,0.05);
+ }
+ .link-btn {
+ background: none;
+ border: none;
+ padding: 0;
+ margin-top: 10px;
+ margin-right: 14px;
+ font-size: 14px;
+ color: #0f8e99;
+ text-decoration: underline;
+ cursor: pointer;
+ font-family: inherit;
+ }
+ .link-btn:hover {
+ color: #0a6b75;
+ }
+ #countContainer {
+ margin-top: 10px;
+ font-size: 0.8rem;
+ color: #555;
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ }
+ #linkBtnContainer {
+ text-align: center;
+ }
+ .saved-column {
+ background: #fde8d0;
+ width: 260px;
+ flex-shrink: 0;
+ box-shadow: 2px 0 8px rgba(0,0,0,0.07);
+ transition: width 0.3s ease;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ }
+ .saved-column.collapsed {
+ width: 44px;
+ }
+ .saved-column.collapsed .docs-content {
+ display: none;
+ }
+ .column-header {
+ padding: 14px 12px;
+ border-bottom: 1px solid #e0c8b0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: #fde8d0;
+ }
+ .saved-column.collapsed .column-header {
+ justify-content: center;
+ padding: 14px 5px;
+ }
+ .column-header h2 {
+ font-size: 0.95rem;
+ color: #807973;
+ font-weight: 600;
+ margin: 0;
+ }
+ .saved-column.collapsed .column-header h2 {
+ display: none;
+ }
+ .toggle-btn {
+ background: none;
+ color: #0f8e99;
+ border: 1px solid #0f8e99;
+ width: 24px;
+ height: 24px;
+ border-radius: 3px;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s, color 0.2s;
+ flex-shrink: 0;
+ line-height: 1;
+ }
+ .toggle-btn:hover {
+ background: #0f8e99;
+ color: #fff1e5;
+ }
+ .docs-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 12px;
+ }
+ .doc-single-line {
+ margin-bottom: 10px;
+ padding: 8px;
+ background: #fff1e5;
+ border-radius: 4px;
+ border: 1px solid #e0c8b0;
+ }
+ .doc-single-line.active-doc {
+ background: #e8f0fe;
+ border-color: #007bff;
+ }
+ .doc-line1 {
+ font-size: 0.72rem;
+ color: #444;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-bottom: 5px;
+ }
+ .doc-line2 {
+ display: flex;
+ gap: 10px;
+ }
+ .small-link-btn {
+ background: none;
+ border: none;
+ padding: 0;
+ font-size: 0.72rem;
+ color: #0f8e99;
+ text-decoration: underline;
+ cursor: pointer;
+ font-family: inherit;
+ }
+ .small-link-btn:hover {
+ color: #0a6b75;
+ }
+ .content-column {
+ flex: 1;
+ padding: 40px;
+ max-width: 900px;
+ margin: 0 auto;
+ }
+ </style>
+ </head>
+ <body>
+
+ <div class="app-wrapper">
+ <div class="saved-column" id="savedColumn">
+ <div class="column-header">
+ <h2>Saved Notes</h2>
+ <button class="toggle-btn" id="toggleBtn">−</button>
+ </div>
+ <div class="docs-content">
+ <div id="docList"></div>
+ </div>
+ </div>
+
+ <div class="content-column">
+ <h1>Notes</h1>
+ <div id="docStatus"></div>
+ <textarea id="editor" placeholder="Write something..."></textarea>
+ <br>
+ <div id="countContainer">
+ <div id="wordCount">0 words</div> |
+ <div id="charCount">0 characters</div>
+ </div>
+ <div id="linkBtnContainer">
+ <button class="link-btn" id="saveBtn">Save Note</button>
+ <button class="link-btn" id="newBtn">New Note</button>
+ <button class="link-btn" id="exportBtn">Export as TXT</button>
+ <button class="link-btn" id="exportDbBtn">Export NotesDB</button>
+ <button class="link-btn" id="importDbBtn">Import NotesDB</button>
+ </div>
+ <input type="file" id="importFile" accept=".json" style="display:none;">
+ </div>
+ </div>
+
+
+ <script>
+ let currentDocId = null;
+
+ async function apiGet(path) {
+ const r = await fetch(path);
+ if (!r.ok) throw new Error(await r.text());
+ return r.json();
+ }
+
+ async function apiPost(path, body) {
+ const r = await fetch(path, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body)
+ });
+ if (!r.ok) throw new Error(await r.text());
+ return r.json();
+ }
+
+ async function apiPut(path, body) {
+ const r = await fetch(path, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body)
+ });
+ if (!r.ok) throw new Error(await r.text());
+ return r.json();
+ }
+
+ async function apiDelete(path) {
+ const r = await fetch(path, { method: "DELETE" });
+ if (!r.ok) throw new Error(await r.text());
+ return r.json();
+ }
+
+ function updateStatus() {
+ const status = document.getElementById("docStatus");
+ if (currentDocId !== null) {
+ status.textContent = `Editing: Note #${currentDocId} — saving will overwrite the original`;
+ } else {
+ status.textContent = "New Note — saving will create a new entry";
+ }
+ }
+
+
+ async function loadDocuments() {
+ let docs;
+ try {
+ docs = await apiGet("/api/notes");
+ } catch (e) {
+ alert("Could not load notes: " + e.message);
+ return;
+ }
+
+ const list = document.getElementById("docList");
+ list.innerHTML = "";
+
+ docs.reverse();
+
+ docs.forEach(doc => {
+ const div = document.createElement("div");
+ div.className = "doc-single-line" + (doc.id === currentDocId ? " active-doc" : "");
+
+ const line1 = document.createElement("div");
+ line1.className = "doc-line1";
+ line1.textContent = doc.name ? doc.name : `Note #${doc.id}`;
+
+ const line2 = document.createElement("div");
+ line2.className = "doc-line1";
+ line2.textContent = doc.created;
+
+ const line3 = document.createElement("div");
+ line3.className = "doc-line2";
+
+ const loadBtn = document.createElement("button");
+ loadBtn.textContent = "Load";
+ loadBtn.className = "small-link-btn";
+
+ const deleteBtn = document.createElement("button");
+ deleteBtn.textContent = "Delete";
+ deleteBtn.className = "small-link-btn";
+
+ const renameBtn = document.createElement("button");
+ renameBtn.textContent = "Rename";
+ renameBtn.className = "small-link-btn";
+
+ loadBtn.onclick = async function() {
+ try {
+ const note = await apiGet(`/api/notes/${doc.id}`);
+ document.getElementById("editor").value = note.content;
+ updateCounts(note.content);
+ currentDocId = doc.id;
+ loadDocuments();
+ updateStatus();
+ } catch (e) {
+ alert("Could not load note: " + e.message);
+ }
+ };
+
+ deleteBtn.onclick = function() { deleteDocument(doc.id); };
+ renameBtn.onclick = function() { renameDocument(doc.id, doc.name ? doc.name : `Note #${doc.id}`); };
+
+ line3.appendChild(loadBtn);
+ line3.appendChild(deleteBtn);
+ line3.appendChild(renameBtn);
+
+ div.appendChild(line1);
+ div.appendChild(line2);
+ div.appendChild(line3);
+ list.appendChild(div);
+ });
+ }
+
+
+ async function saveDocument(text) {
+ try {
+ if (currentDocId !== null) {
+ await apiPut(`/api/notes/${currentDocId}`, { content: text });
+ } else {
+ const result = await apiPost("/api/notes", { content: text });
+ currentDocId = result.id;
+ }
+ loadDocuments();
+ updateStatus();
+ } catch (e) {
+ alert("Save failed: " + e.message);
+ }
+ }
+
+ async function renameDocument(id, currentName) {
+ const newName = prompt("Enter a new name for this note:", currentName);
+ if (newName === null) return;
+ const trimmed = newName.trim();
+ if (!trimmed) { alert("Name cannot be empty"); return; }
+
+ try {
+ await apiPut(`/api/notes/${id}`, { name: trimmed });
+ loadDocuments();
+ } catch (e) {
+ alert("Rename failed: " + e.message);
+ }
+ }
+
+
+ async function deleteDocument(id) {
+ if (!confirm("Delete this document?")) return;
+ try {
+ await apiDelete(`/api/notes/${id}`);
+ if (currentDocId === id) {
+ currentDocId = null;
+ document.getElementById("editor").value = "";
+ updateCounts("");
+ updateStatus();
+ }
+ loadDocuments();
+ } catch (e) {
+ alert("Delete failed: " + e.message);
+ }
+ }
+
+
+
+ function exportDocument() {
+ const text = document.getElementById("editor").value;
+ if (!text.trim()) { alert("Nothing to export"); return; }
+ const blob = new Blob([text], { type: "text/plain" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `note-${new Date().toISOString().replace(/[:.]/g, "-")}.txt`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }
+
+
+ async function exportDatabase() {
+ try {
+ const docs = await apiGet("/api/notes");
+ if (!docs.length) { alert("No notes to export"); return; }
+ const blob = new Blob([JSON.stringify(docs, null, 2)], { type: "application/json" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `NotesDB-${new Date().toISOString().replace(/[:.]/g, "-")}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ } catch (e) {
+ alert("Export failed: " + e.message);
+ }
+ }
+
+
+ async function importDatabase(file) {
+ const reader = new FileReader();
+ reader.onload = async function(event) {
+ try {
+ const data = JSON.parse(event.target.result);
+ if (!Array.isArray(data)) { alert("Invalid backup file"); return; }
+ await apiPost("/api/import", { notes: data });
+ currentDocId = null;
+ document.getElementById("editor").value = "";
+ updateCounts("");
+ loadDocuments();
+ updateStatus();
+ alert("Import complete");
+ } catch (e) {
+ alert("Import failed: " + e.message);
+ }
+ };
+ reader.readAsText(file);
+ }
+
+
+ function updateCounts(text) {
+ const characters = text.length;
+ const words = text.trim().split(/\s+/).filter(Boolean).length;
+ document.getElementById("charCount").textContent =
+ `${characters} character${characters !== 1 ? "s" : ""}`;
+ document.getElementById("wordCount").textContent =
+ `${words} word${words !== 1 ? "s" : ""}`;
+ }
+
+
+ document.getElementById("saveBtn").onclick = function() {
+ const text = document.getElementById("editor").value.trim();
+ if (!text) { alert("Enter some text first"); return; }
+ saveDocument(text);
+ };
+
+ document.getElementById("newBtn").onclick = function() {
+ document.getElementById("editor").value = "";
+ updateCounts("");
+ currentDocId = null;
+ loadDocuments();
+ updateStatus();
+ };
+
+ document.getElementById("exportBtn").onclick = exportDocument;
+ document.getElementById("exportDbBtn").onclick = exportDatabase;
+ document.getElementById("importDbBtn").onclick = function() {
+ document.getElementById("importFile").click();
+ };
+ document.getElementById("importFile").onchange = function(event) {
+ const file = event.target.files[0];
+ if (file) importDatabase(file);
+ };
+
+
+ const savedColumn = document.getElementById("savedColumn");
+ const toggleBtn = document.getElementById("toggleBtn");
+ toggleBtn.addEventListener("click", function() {
+ savedColumn.classList.toggle("collapsed");
+ toggleBtn.textContent = savedColumn.classList.contains("collapsed") ? "+" : "−";
+ });
+
+
+ document.getElementById("editor").addEventListener("input", function() {
+ updateCounts(this.value);
+ });
Diff truncated. 193 more lines not shown.