Files
client/www/note.html
T

1099 lines
35 KiB
HTML

<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>Note</title>
<link rel="stylesheet" href="./css/client.css" />
<!-- Initialize theme BEFORE any components load -->
<script>
(function () {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark-mode');
if (document.body) {
document.body.classList.add('dark-mode');
}
}
})();
</script>
<link rel="shortcut icon" type="image/x-icon" href="./favicon/favicon-dots2.ico" />
<!-- SVG.js library (required by HamburgerMorphing) -->
<script src="./js/vendor/svg.min.js"></script>
<style media="screen">
#divBody {
flex-direction: column !important;
flex-wrap: nowrap !important;
/* ADD THIS - prevents wrapping */
align-items: stretch !important;
justify-content: flex-start !important;
align-content: flex-start !important;
}
.divNoteTitle {
margin: 5px;
padding: 5px;
font-family: var(--font-mono);
}
.divNoteTitle:hover {
background-color: var(--button-hover-color);
cursor: pointer;
}
#NewNoteTitle {
border: 1px solid black;
margin: 5px;
padding: 5px;
font-family: var(--font-mono);
}
#NewNoteTitle:hover {
border: 2px solid black;
cursor: text;
}
.MainBox {
margin: 20px;
padding: 15px;
}
#taNote {
box-sizing: border-box;
font-family: var(--font-mono);
resize: none;
font-size: 150%;
height: 70vh;
width: calc(100% - 40px);
margin-left: 20px;
overflow: visible;
background-color: var(--background-color);
color: var(--primary-color);
border: 1px solid var(--secondary-color);
}
#divHTMLOutBox {
margin: 20px;
border: 2px solid black;
overflow-y: scroll;
min-height: 100%;
width: calc(100% - 40px);
}
#iframeHTMLOut {
box-sizing: border-box;
padding: 20px;
width: calc(100% - 40px);
/* Match taNote */
margin-left: 20px;
/* Match taNote */
height: 70vh;
border: none;
background-color: white;
display: block;
overflow-y: auto;
}
#iframeHTMLOut.clsVisible {
display: block;
}
#iframeHTMLOut.clsHidden {
display: none;
}
#divHTMLCode {
font-family: var(--font-mono);
}
.clsVisible {
display: block;
}
.clsHidden {
display: none;
}
#tblTags {
width: 100%;
padding: 20px;
padding-top: 60px;
color: var(--primary-color);
}
.tdEditable {
border: 0.1px solid var(--secondary-color);
padding: 5px;
background-color: var(--background-color);
color: var(--primary-color);
}
.tdTitle {
width: 1%;
padding-left: 5px;
padding-right: 15px;
color: var(--primary-color);
font-family: var(--font-mono);
font-weight: bold;
}
#imgMain {
width: 60%;
padding: 20px;
display: block;
/* Add this */
margin: 0 auto;
/* Add this to center the image */
}
#divTitle {
text-align: center;
margin-top: 40px;
padding: 20px;
font-size: 150%;
color: var(--primary-color);
min-height: 1.5em;
display: block;
/* Add this */
clear: both;
/* Add this to ensure it's on its own line */
}
/* SIDENAV NOTES TABLE */
#divSideNavBody {
display: flex;
flex-direction: column;
overflow-y: auto;
}
.divNewNote {
border: 1px solid var(--secondary-color);
margin: 3px;
height: 1.7em;
margin-left: 30px;
margin-right: 20px;
padding-left: 15px;
padding: 2px;
cursor: pointer;
}
.divNewNote:hover {
background-color: var(--button-hover-color);
}
#tblNotes {
font-family: var(--font-mono);
font-size: 60%;
width: 100%;
color: var(--primary-color);
}
#tblNotes tr:hover {
background: var(--button-hover-color);
cursor: pointer;
}
.col0 {
font-weight: 900;
padding-left: 20px;
font-family: var(--font-mono);
font-size: 140%;
}
.col1 {
padding-left: 20px;
font-family: var(--font-mono);
}
.col2 {
padding-left: 20px;
text-align: right;
font-family: var(--font-mono);
}
.col3 {
padding-left: 20px;
font-family: var(--font-mono);
}
.divTableButtons {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 20px;
z-index: 3;
}
/* Sidenav structure for notes + relay section */
#divNotesSection {
flex: 1;
overflow-y: auto;
padding: 10px;
}
/* Header buttons */
#divSvgCheck {
cursor: pointer;
}
#divSvgEdit {
cursor: pointer;
}
#divFullScreen {
cursor: pointer;
}
#divHeaderText {
outline: none;
}
</style>
<script src="./js/marked.min.js"></script>
</head>
<body>
<!-- ================================================================
HAMBURGER BUTTON (Fixed, separate from header)
================================================================ -->
<div id="divSvgHam" class="divHeaderButtons">
<!-- HamburgerMorphing will be injected here -->
</div>
<!-- ================================================================
HEADER
================================================================ -->
<div id="divHeader">
<div id="divHeaderFlexLeft">
<!-- Check button for save -->
<div id="divSvgCheck" class="divHeaderButtons" title="Save Note">
<svg id="svgCheck" class="svgHeaderButtons" viewBox="0 0 10 10">
<path id="svgCheckPath" d="M2,6 L4,8 L8,2" stroke="currentColor" stroke-width="1" fill="none" />
</svg>
</div>
</div>
<div id="divHeaderFlexCenter">
<div id="divHeaderText" class="divHeaderText" contenteditable="true">TITLE</div>
</div>
<div id="divHeaderFlexRight">
<!-- Fullscreen button -->
<div id="divFullScreen" class="divHeaderButtons" title="Toggle Fullscreen">
<svg class="svgHeaderButtons" viewBox="0 0 10 10">
<line x1="5" y1="2" x2="5" y2="8" stroke="currentColor" stroke-width="1" />
<line x1="5" y1="2" x2="3" y2="4" stroke="currentColor" stroke-width="1" />
<line x1="5" y1="2" x2="7" y2="4" stroke="currentColor" stroke-width="1" />
</svg>
</div>
<!-- Edit/Preview toggle -->
<div id="divSvgEdit" class="divHeaderButtons" title="Toggle Edit/Preview">
<svg id="svgEdit" class="svgHeaderButtons" viewBox="0 0 10 10">
<path id="svgEditP1" d="M8,2 L2,5 L8,8" stroke="currentColor" stroke-width="1" fill="none" />
<path id="svgEditP2" d="M6,3 L7,5 L6,7" stroke="currentColor" stroke-width="1" fill="none" />
</svg>
</div>
</div>
</div>
<!-- ================================================================
BODY - Note Editor
================================================================ -->
<div id="divBody">
<table id="tblTags">
<tr>
<td class="tdTitle">Note ID</td>
<td id="txtID"></td>
</tr>
<tr>
<td class="tdTitle">Title</td>
<td id="txtTitle" class="tdEditable" contenteditable="true"></td>
</tr>
<tr>
<td class="tdTitle">Image</td>
<td id="txtImg" class="tdEditable" contenteditable="true"></td>
</tr>
<tr>
<td class="tdTitle">Summary</td>
<td id="txtSummary" class="tdEditable" contenteditable="true"></td>
</tr>
<tr>
<td class="tdTitle">Tags</td>
<td id="txtTags" class="tdEditable" contenteditable="true"></td>
</tr>
<tr>
<td class="tdTitle">Autosave</td>
<td id="txtAutosave" class="tdEditable" contenteditable="true">10</td>
</tr>
<tr>
<td class="tdTitle"></td>
<td>
<button id="btnSaveNote" class="btn">Save Note</button>
</td>
</tr>
</table>
<div id="divTitle"></div>
<img id="imgMain" src="" class="clsHidden" />
<textarea id="taNote" class="MainBox clsVisible clsThreeThird"></textarea>
<iframe id="iframeHTMLOut" scrolling="auto" class="clsHidden"></iframe>
</div>
<!-- ================================================================
FOOTER
================================================================ -->
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox"></div>
<div id="divFooterRight" class="divFooterBox"></div>
<div id="divFooterBalance" class="divFooterBox">0 sats</div>
</div>
<!-- ================================================================
SIDENAV - Notes list + Relay section at bottom
================================================================ -->
<div id="divSideNav">
<div id="divSideNavHeader">
<!-- No close button - use main hamburger to close -->
</div>
<!-- Notes section - scrollable -->
<div id="divNotesSection">
<div class="divNewNote" id="divNewNote">+ New Note</div>
<div id="divFiles"></div>
</div>
<!-- Relay section - at bottom -->
<div id="divAiSection" class="sidenavSection">
<div id="divAiSectionTitle" class="sidenavSectionTitle">AI</div>
<div id="divAiList" class="sidenavSectionList">
<div id="divAiProvidersList">No saved providers yet.</div>
</div>
</div>
<div id="divRelaySection">
<div id="divRelaySectionTitle">
リレー
</div>
<div id="divRelayList">
Loading relays...
</div>
</div>
<div id="divBlossomSection">
<div id="divBlossomSectionTitle">ブロッサム</div>
<div id="divBlossomList">Loading blossom servers...</div>
</div>
<div id="divVersionBar">
<span id="versionDisplay">v0.0.1</span>
<div id="divVersionBarButtons">
<button id="themeToggleButton" title="Toggle Dark/Light Mode">
<div id="themeToggleHamburgerContainer"></div>
</button>
<button id="logoutButton" title="Logout">
<div id="logoutHamburgerContainer"></div>
</button>
</div>
</div>
</div>
<!-- ================================================================
REQUIRED SCRIPTS
================================================================ -->
<script src="./nostr.bundle.js"></script>
<script src="./nostr-lite.js"></script>
<script type="module">
/* ================================================================
IMPORTS
================================================================ */
import { initNDKPage, getPubkey, injectHeaderAvatar, subscribe, publishEvent, disconnect, getVersion, updateVersionDisplay } from './js/init-ndk.mjs';
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
const versionInfo = await getVersion();
const VERSION = versionInfo.VERSION;
console.log(`[note.html ${VERSION}] Loading...`);
/* ================================================================
GLOBAL VARIABLES
================================================================ */
let updateIntervalId = null;
let currentPubkey = null;
// Hamburger menu
let hamburgerInstance = null;
let isNavOpen = false;
// Version bar buttons
let logoutHamburger = null;
let themeToggleHamburger = null;
let isDarkMode = false;
// Note-specific variables
let OBJ_NOTES = {};
let CURRENT_NOTE = "";
let ENCRYPTED_TAG_TYPES = ["title", "image", "summary", "t"];
let EDIT_MODE = false;
let LastText = "";
let NUM_LAST_EDIT_TIME = Math.floor(Date.now() / 1000);
let numAutoSaveSec = 30;
/* ================================================================
DOM VARIABLES
================================================================ */
const divBody = document.getElementById("divBody");
const divSideNav = document.getElementById("divSideNav");
const divSideNavBody = document.getElementById("divSideNavBody");
const divFooterCenter = document.getElementById("divFooterCenter");
const divFooterRight = document.getElementById("divFooterRight");
const divFooterLeft = document.getElementById("divFooterLeft");
// Note-specific DOM elements
const taNote = document.getElementById('taNote');
const iframeHTMLOut = document.getElementById('iframeHTMLOut');
const imgMain = document.getElementById('imgMain');
const txtID = document.getElementById('txtID');
const txtTitle = document.getElementById('txtTitle');
const divTitle = document.getElementById('divTitle');
const txtImg = document.getElementById('txtImg');
const txtSummary = document.getElementById('txtSummary');
const txtTags = document.getElementById('txtTags');
const txtAutosave = document.getElementById('txtAutosave');
const divHeaderText = document.getElementById('divHeaderText');
const tblTags = document.getElementById('tblTags');
const divFiles = document.getElementById('divFiles');
/* ================================================================
HAMBURGER MENU
================================================================ */
function initHamburgerMenu() {
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
hamburgerInstance.animateTo('burger');
}
/* ================================================================
SIDENAV FUNCTIONS
================================================================ */
function openNav() {
divSideNav.style.zIndex = 3;
divSideNav.style.width = "clamp(400px, 50vw, 600px)";
isNavOpen = true;
if (hamburgerInstance) {
hamburgerInstance.animateTo('arrow_left');
}
LoadSidenav();
// Initialize version bar buttons when sidenav opens (lazy load)
if (!logoutHamburger) {
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
logoutHamburger.animateTo('x');
}
if (!themeToggleHamburger) {
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
const savedTheme = localStorage.getItem('theme');
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
const initialShape = isDarkMode ? 'moon' : 'circle';
themeToggleHamburger.animateTo(initialShape);
}
}
function closeNav() {
divSideNav.style.width = "0vw";
divSideNav.style.zIndex = -1;
isNavOpen = false;
if (hamburgerInstance) {
hamburgerInstance.animateTo('burger');
}
}
function toggleNav() {
if (isNavOpen) {
closeNav();
} else {
openNav();
}
}
/* ================================================================
NOTE FUNCTIONS
================================================================ */
const LoadSidenav = async () => {
// Subscribe to long-form notes (kind 30023 and 30024)
const notesSub = subscribe(
{ kinds: [30023, 30024], authors: [currentPubkey] },
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);
// Wait a moment for events to come in
await new Promise(resolve => setTimeout(resolve, 500));
// Get notes from cache via events
let htmlOut = `<table id="tblNotes">`;
htmlOut += `<tr><th colspan="8"> NOTES </th></tr>`;
htmlOut += `<tr><th colspan="8"> &nbsp </th></tr>`;
htmlOut += `<tr>
<th>Evt</th>
<th>Del</th>
<th>Pub</th>
<th>Title</th>
<th>d</th>
<th>Created</th>
<th>Edited</th>
<th>Type</th>
<th>Encrypted</th>
</tr>`;
// Build table from OBJ_NOTES
for (let Each of Object.keys(OBJ_NOTES).sort((a, b) => b - a)) {
let d = new Date(Number(Each) * 1000);
let dCreated = d.toLocaleString();
d = new Date(Number(OBJ_NOTES[Each].created_at) * 1000);
let dEdited = d.toLocaleString();
const dTagValue = OBJ_NOTES[Each].objTags?.d ?? `${OBJ_NOTES[Each].created_at} (from created_at)`;
let Status = "Published";
let svgPub = `<div class="divTableButtons"></div>`;
if (OBJ_NOTES[Each].kind == 30024) {
Status = "Encrypted";
svgPub = `<div id="divSVGPublish-${Each}" class="divTableButtons">➜</div>`;
}
htmlOut += `<tr>
<td class="col5"> <div id="divSVG-${Each}" class="divTableButtons">📋</div> </td>
<td class="col5"> <div id="divSVGDelete-${Each}" class="divTableButtons">✕</div> </td>
<td class="col5"> ${svgPub} </td>
<td class="col0" id="divFileID-${Each}">${OBJ_NOTES[Each].objTags?.title || 'Untitled'}</td>
<td class="col1">${dTagValue}</td>
<td class="col1">${dCreated}</td>
<td class="col1">${dEdited}</td>
<td class="col3">${OBJ_NOTES[Each].kind} </td>
<td class="col3">${OBJ_NOTES[Each].encrypted ? 'Yes' : 'No'} </td>
</tr>`;
}
divFiles.innerHTML = htmlOut;
// SET EVENT LISTENERS
for (let Each of Object.keys(OBJ_NOTES)) {
document.getElementById(`divSVGDelete-${Each}`)?.addEventListener("click", function () {
DeleteNote(`${Each}`);
});
document.getElementById(`divSVGDelete-${Each}`)?.addEventListener("click", closeNav);
document.getElementById(`divSVG-${Each}`)?.addEventListener("click", closeNav);
document.getElementById(`divFileID-${Each}`)?.addEventListener("click", function () {
LoadNote(Each);
});
const pubBtn = document.getElementById(`divSVGPublish-${Each}`);
if (pubBtn) {
pubBtn.addEventListener("click", function () {
Publish30023Note(Each);
});
}
}
};
const SetEditMode = async (EditMode) => {
EDIT_MODE = EditMode;
console.log("Edit Mode ", EDIT_MODE);
if (EditMode) {
taNote.className = "MainBox clsVisible";
iframeHTMLOut.className = "clsHidden";
tblTags.className = "clsVisible";
imgMain.className = "clsHidden";
document.getElementById("svgEditP1").setAttribute("d", "M8,2 L2,5 L8,8");
document.getElementById("svgEditP2").setAttribute("d", "M6,3 L7,5 L6,7");
imgMain.src = "";
} else {
taNote.className = "MainBox clsHidden";
iframeHTMLOut.className = "clsVisible";
tblTags.className = "clsHidden";
document.getElementById("svgEditP1").setAttribute("d", "M2,8 L2,6 L6,2 L8,4 L4,8 L2,8");
document.getElementById("svgEditP2").setAttribute("d", "");
if (txtImg.textContent.trim() === "") {
imgMain.className = "clsHidden";
} else {
imgMain.className = "clsVisible";
imgMain.src = txtImg.textContent.trim();
}
// Save when switching from edit to view mode if there are unsaved changes
if (taNote.value.trim() !== LastText) {
console.log('[autosave] Switching to view mode with unsaved changes - triggering save');
await Publish30024Note();
LastText = taNote.value.trim();
}
}
};
const LoadNote = async (id) => {
console.log("Load Note", id);
closeNav();
if (!OBJ_NOTES[id]) {
console.error('Note not found:', id);
return;
}
taNote.value = OBJ_NOTES[id].content || '';
LastText = OBJ_NOTES[id].content || '';
txtTitle.textContent = OBJ_NOTES[id].objTags?.title || '';
divTitle.textContent = OBJ_NOTES[id].objTags?.title || '';
txtSummary.textContent = OBJ_NOTES[id].objTags?.summary || '';
txtTags.textContent = OBJ_NOTES[id].objTags?.t?.join(' ') || '';
CURRENT_NOTE = id;
txtID.textContent = CURRENT_NOTE;
// Parse markdown for preview
console.log('[note.html] LoadNote - window.marked exists:', !!window.marked);
console.log('[note.html] LoadNote - taNote.value length:', taNote.value?.length || 0);
console.log('[note.html] LoadNote - content preview:', taNote.value?.substring(0, 50));
if (window.marked && taNote.value && taNote.value.trim()) {
const parsed = marked.parse(taNote.value.trim());
iframeHTMLOut.srcdoc = parsed;
console.log('[note.html] Loaded note into iframe, parsed length:', parsed.length);
} else {
const reason = !window.marked ? 'marked library not loaded' :
(!taNote.value ? 'taNote.value is null/undefined' :
(taNote.value.trim() === '' ? 'content is empty after trim' : 'unknown'));
iframeHTMLOut.srcdoc = '<p><em>No content</em></p>';
console.log('[note.html] No content to display - reason:', reason);
}
if (OBJ_NOTES[id].objTags?.image === "" || !OBJ_NOTES[id].objTags?.image) {
txtImg.textContent = "";
imgMain.className = "clsHidden";
} else {
txtImg.textContent = OBJ_NOTES[id].objTags.image;
imgMain.className = "clsVisible";
}
SetEditMode(false);
};
const DeleteNote = async (NoteD) => {
console.log("Delete Note", NoteD);
// Create a deletion event (kind 5)
const deleteEvent = {
kind: 5,
content: "",
tags: [["e", OBJ_NOTES[NoteD].id]],
created_at: Math.floor(Date.now() / 1000)
};
try {
await publishEvent(deleteEvent);
console.log("Delete event published");
delete OBJ_NOTES[NoteD];
closeNav();
} catch (error) {
console.error("Failed to delete note:", error);
}
};
const Publish30023Note = async (NoteD) => {
console.log("Publish Note", NoteD);
if (window.confirm("Are you sure you want to publish this note? You can't take it back.")) {
console.log("Publish");
} else {
return;
}
let Now = Math.floor(Date.now() / 1000);
// Clone and modify for publishing
let objOut = JSON.parse(JSON.stringify(OBJ_NOTES[NoteD]));
objOut.kind = 30023;
objOut.created_at = Now;
for (let Tag of objOut.tags) {
if (Tag[0] == "d" || Tag[0] == "published_at") {
Tag[1] = `${Now}`;
}
}
try {
await publishEvent(objOut);
console.log("Published new note", Now);
CURRENT_NOTE = Now;
DeleteNote(NoteD);
} catch (error) {
console.error("Failed to publish:", error);
}
};
const Publish30024Note = async () => {
console.log("CurrentNote", CURRENT_NOTE);
let Now = Math.floor(Date.now() / 1000);
let kind = 30024;
// For a new note
if (CURRENT_NOTE == "") {
CURRENT_NOTE = Now;
txtID.textContent = CURRENT_NOTE;
}
let arrTags = [];
arrTags.push(["d", `${CURRENT_NOTE}`]);
arrTags.push(["published_at", `${CURRENT_NOTE}`]);
arrTags.push(["title", txtTitle.textContent.trim()]);
arrTags.push(["image", txtImg.textContent.trim()]);
arrTags.push(["summary", txtSummary.textContent.trim()]);
// Process hashtags
let tagText = txtTags.textContent.replaceAll(",", " ").replaceAll("#", "");
let arrHashtags = tagText.split(" ").filter(t => t.trim());
for (let Each of arrHashtags) {
arrTags.push(["t", Each]);
}
let objEvent = {
created_at: Now,
kind: kind,
tags: arrTags,
content: taNote.value.trim()
};
try {
await publishEvent(objEvent);
console.log("Note published");
LastText = taNote.value.trim();
} catch (error) {
console.error("Failed to publish note:", error);
}
};
const UpdateFooter = async () => {
try {
await updateFooterRelayStatus();
await updateSidenavRelaySection();
await updateBlossomSection();
// Note-specific footer info
if (taNote.value.trim() != LastText) {
divFooterCenter.textContent = "UNSAVED";
} else {
divFooterCenter.textContent = "Saved";
}
let letters = taNote.value.length;
let words = taNote.value.split(/\s+/).filter(w => w).length;
let paragraphs = taNote.value.split("\n").filter(p => p.trim()).length;
let row = taNote.value.substr(0, taNote.selectionStart).split("\n").length;
divFooterRight.textContent = `C:${letters} W:${words} P:${paragraphs} R:${row}`;
// divFooterLeft is populated by updateFooterRelayStatus() with relay visualization
} catch (error) {
console.error('[note.html] Error updating footer:', error);
}
};
/* ================================================================
LOGOUT
================================================================ */
const Logout = async () => {
console.log("[note.html] Starting logout process...");
if (updateIntervalId) {
clearInterval(updateIntervalId);
updateIntervalId = null;
}
disconnect();
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
await window.NOSTR_LOGIN_LITE.logout();
}
localStorage.clear();
sessionStorage.clear();
if (window.indexedDB) {
const databases = await window.indexedDB.databases();
for (const db of databases) {
if (db.name) {
window.indexedDB.deleteDatabase(db.name);
}
}
}
console.log("[note.html] Logged out, reloading page");
location.reload(true);
};
/* ================================================================
INITIALIZATION
================================================================ */
(async function main() {
console.log("[note.html] Starting initialization...");
try {
// Initialize hamburger menu first
initHamburgerMenu();
// Add click handler to hamburger
const divSvgHam = document.getElementById('divSvgHam');
if (divSvgHam) {
divSvgHam.addEventListener('click', toggleNav);
}
// Initialize version bar buttons
const themeToggleButton = document.getElementById('themeToggleButton');
const logoutButton = document.getElementById('logoutButton');
if (themeToggleButton) {
themeToggleButton.addEventListener('click', () => {
isDarkMode = !isDarkMode;
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
document.documentElement.classList.toggle('dark-mode', isDarkMode);
document.body.classList.toggle('dark-mode', isDarkMode);
if (themeToggleHamburger) {
themeToggleHamburger.animateTo(isDarkMode ? 'moon' : 'circle');
}
});
}
if (logoutButton) {
logoutButton.addEventListener('click', async () => {
try {
await Logout();
} catch (error) {
console.error('Logout failed:', error);
}
});
}
// Initialize NDK
await initNDKPage();
// Get authenticated pubkey
currentPubkey = await getPubkey();
await injectHeaderAvatar(currentPubkey);
console.log("[note.html] Authenticated as:", currentPubkey);
// Initialize relay UI components
initFooterRelayStatus();
initSidenavRelaySection();
await initBlossomSection();
initAiSectionWithLocalConfig();
await UpdateFooter(); // Initial update before interval starts
// Listen for relay activity
window.addEventListener('ndkRelayActivity', (event) => {
const { relayUrl, activity, stats } = event.detail;
setRelayActivityState(relayUrl, activity);
});
// Subscribe to notes
const notesSub = subscribe(
{ kinds: [30023, 30024], authors: [currentPubkey] },
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);
// Listen for incoming note events
window.addEventListener('ndkEvent', (event) => {
const evt = event.detail;
if (evt.kind === 30023 || evt.kind === 30024) {
console.log("[note.html] Received note:", evt);
// Parse tags into objTags
let objTags = {};
for (let T of evt.tags) {
if (T[0] == "t") {
if (objTags["t"]) {
objTags["t"].push(T[1]);
} else {
objTags[T[0]] = [T[1]];
}
} else {
objTags[T[0]] = T[1];
}
}
const dTag = objTags.d || evt.created_at;
OBJ_NOTES[dTag] = {
...evt,
objTags: objTags,
encrypted: evt.kind === 30024
};
}
});
// Event listeners for note editing
document.getElementById('divSvgCheck').addEventListener('click', Publish30024Note);
document.getElementById('divSvgEdit').addEventListener('click', () => SetEditMode(!EDIT_MODE));
document.getElementById('divFullScreen').addEventListener('click', () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
});
document.getElementById('divNewNote').addEventListener('click', () => {
CURRENT_NOTE = "";
txtID.textContent = "";
taNote.value = "";
LastText = "";
txtTitle.textContent = "";
divTitle.textContent = "";
txtImg.textContent = "";
txtSummary.textContent = "";
txtTags.textContent = "";
divHeaderText.textContent = "TITLE";
SetEditMode(true);
closeNav();
});
txtImg.addEventListener("input", function () {
imgMain.src = txtImg.textContent.trim();
});
// Save button
document.getElementById('btnSaveNote').addEventListener('click', async () => {
console.log("[note.html] Save button clicked");
await Publish30024Note();
LastText = taNote.value.trim();
// Update markdown preview
if (window.marked && taNote.value.trim()) {
iframeHTMLOut.srcdoc = marked.parse(taNote.value.trim());
}
});
// Track edits on all editable fields for autosave timing
const editableFields = [taNote, txtTitle, txtImg, txtSummary, txtTags];
editableFields.forEach(field => {
if (field) {
field.addEventListener("input", function () {
NUM_LAST_EDIT_TIME = Math.floor(Date.now() / 1000);
console.log('[autosave] Edit detected, timer reset');
});
}
});
// Start update loop
updateIntervalId = setInterval(UpdateFooter, 1000);
// Update version display
await updateVersionDisplay();
// Autosave interval
setInterval(async () => {
iframeHTMLOut.height = iframeHTMLOut.contentWindow?.document?.body?.scrollHeight + 100 || 500;
// Get current autosave setting
const autoSaveSetting = parseInt(txtAutosave?.textContent?.trim() || "10", 10);
// console.log('[autosave] Setting:', autoSaveSetting, 'seconds');
// Skip if autosave disabled (0 or negative)
if (autoSaveSetting <= 0) {
// console.log('[autosave] Skipping: autosave disabled (<=0)');
return;
}
// Skip if no changes
const currentText = taNote.value.trim();
const hasChanges = currentText !== LastText;
// console.log('[autosave] Has changes:', hasChanges, '| current length:', currentText.length, '| last length:', LastText.length);
if (!hasChanges) {
console.log('[autosave] Skipping: no changes');
return;
}
// Skip if not enough time passed since last edit
const secondsSinceEdit = Math.floor(Date.now() / 1000) - NUM_LAST_EDIT_TIME;
// console.log('[autosave] Seconds since edit:', secondsSinceEdit, '| required:', autoSaveSetting);
if (secondsSinceEdit < autoSaveSetting) {
// console.log('[autosave] Skipping: not enough time passed');
return;
}
// Skip if default title still set
const titleText = txtTitle.textContent.trim();
// console.log('[autosave] Title text:', titleText);
if (titleText === "" || titleText === "TITLE") {
// console.log('[autosave] Skipping: no title set');
return;
}
// Skip if title is being edited
const isEditingTitle = document.activeElement === divHeaderText;
// console.log('[autosave] Is editing title:', isEditingTitle);
if (isEditingTitle) {
// console.log('[autosave] Skipping: title is being edited');
return;
}
// console.log("[autosave] ✓ All checks passed - Autosaving after", secondsSinceEdit, "seconds");
// Parse markdown
if (window.marked && taNote.value.trim()) {
iframeHTMLOut.srcdoc = marked.parse(taNote.value.trim());
}
await Publish30024Note();
LastText = taNote.value.trim();
// console.log('[autosave] ✓ Autosave complete, LastText updated to length:', LastText.length);
}, 1000);
// Initial state
SetEditMode(true);
console.log('[note.html] Initialization complete');
} catch (error) {
console.error('[note.html] Initialization failed:', error);
divBody.innerHTML = `<div style="text-align: center; padding: 50px;">
<div style="font-size: 24px; margin-bottom: 20px; color: red;">❌ Authentication Error</div>
<div style="font-size: 16px; color: #666;">${error.message}</div>
<div style="margin-top: 20px;">
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px;">Retry</button>
</div>
</div>`;
}
})();
</script>
</body>
</html>