1350 lines
43 KiB
HTML
1350 lines
43 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;
|
|
}
|
|
|
|
#txtDecryptTrace {
|
|
font-family: var(--font-mono);
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
#divDiagnosticBadge {
|
|
display: inline-block;
|
|
margin-left: 10px;
|
|
padding: 4px 8px;
|
|
border: 1px solid #b45309;
|
|
border-radius: 4px;
|
|
color: #b45309;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
</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">Decrypt Trace</td>
|
|
<td id="txtDecryptTrace">None</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="tdTitle"></td>
|
|
<td>
|
|
<button id="btnSaveNote" class="btn">Save Note</button>
|
|
<span id="divDiagnosticBadge" class="clsHidden">DIAGNOSTIC VIEW — do not save</span>
|
|
</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-login-lite/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"];
|
|
const MAX_DECRYPT_PASSES = 8;
|
|
let EDIT_MODE = false;
|
|
let LastText = "";
|
|
let NUM_LAST_EDIT_TIME = Math.floor(Date.now() / 1000);
|
|
let numAutoSaveSec = 30;
|
|
let CURRENT_DECRYPT_TRACE = null;
|
|
let DIAGNOSTIC_ACTIVE = false;
|
|
|
|
/* ================================================================
|
|
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');
|
|
const txtDecryptTrace = document.getElementById('txtDecryptTrace');
|
|
const divDiagnosticBadge = document.getElementById('divDiagnosticBadge');
|
|
|
|
/* ================================================================
|
|
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
|
|
================================================================ */
|
|
async function nip44DecryptFromSelf(ciphertext) {
|
|
const selfPubkey = String(currentPubkey || await getPubkey() || '').trim();
|
|
if (!selfPubkey) throw new Error('Missing pubkey for NIP-44 decryption');
|
|
if (!window.nostr?.nip44?.decrypt) throw new Error('NIP-44 decrypt unavailable');
|
|
return await window.nostr.nip44.decrypt(selfPubkey, String(ciphertext || ''));
|
|
}
|
|
|
|
async function nip04DecryptFromSelf(ciphertext) {
|
|
const selfPubkey = String(currentPubkey || await getPubkey() || '').trim();
|
|
if (!selfPubkey) throw new Error('Missing pubkey for NIP-04 decryption');
|
|
if (!window.nostr?.nip04?.decrypt) throw new Error('NIP-04 decrypt unavailable');
|
|
return await window.nostr.nip04.decrypt(selfPubkey, String(ciphertext || ''));
|
|
}
|
|
|
|
function looksLikeNip04Ciphertext(input) {
|
|
const text = String(input || '').trim();
|
|
return /^[A-Za-z0-9+/_-]+={0,2}\?iv=[A-Za-z0-9+/_-]+={0,2}$/.test(text);
|
|
}
|
|
|
|
function looksLikeBase64(input) {
|
|
const text = String(input || '').trim();
|
|
if (!text || text.length < 24 || text.length % 4 !== 0) return false;
|
|
return /^[A-Za-z0-9+/]+={0,2}$/.test(text);
|
|
}
|
|
|
|
function looksLikeNip44Ciphertext(input) {
|
|
const text = String(input || '').trim();
|
|
if (!looksLikeBase64(text)) return false;
|
|
try {
|
|
const decoded = atob(text);
|
|
if (!decoded || decoded.length < 2) return false;
|
|
const versionByte = decoded.charCodeAt(0);
|
|
return versionByte === 1 || versionByte === 2;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function classifyCiphertext(input) {
|
|
const text = String(input || '').trim();
|
|
if (!text) return 'plaintext';
|
|
if (looksLikeNip04Ciphertext(text)) return 'nip04';
|
|
if (looksLikeNip44Ciphertext(text)) return 'nip44';
|
|
|
|
if (/[\n\r\t]/.test(text) || /[#*_`\[\]{}()]/.test(text)) {
|
|
return 'plaintext';
|
|
}
|
|
|
|
if (looksLikeBase64(text)) return 'base64-unknown';
|
|
return 'plaintext';
|
|
}
|
|
|
|
async function decryptOnePass(input) {
|
|
const text = String(input || '').trim();
|
|
const kind = classifyCiphertext(text);
|
|
const methods = kind === 'nip04'
|
|
? ['nip04', 'nip44']
|
|
: ['nip44', 'nip04'];
|
|
|
|
for (const method of methods) {
|
|
try {
|
|
const output = method === 'nip44'
|
|
? await nip44DecryptFromSelf(text)
|
|
: await nip04DecryptFromSelf(text);
|
|
return {
|
|
ok: true,
|
|
method,
|
|
text: String(output || '')
|
|
};
|
|
} catch (_error) {
|
|
// try next method
|
|
}
|
|
}
|
|
|
|
return {
|
|
ok: false,
|
|
method: null,
|
|
text
|
|
};
|
|
}
|
|
|
|
async function peelDecrypt(input, maxPasses = MAX_DECRYPT_PASSES) {
|
|
let current = String(input || '').trim();
|
|
const trace = [];
|
|
let stoppedReason = 'max-passes';
|
|
|
|
for (let pass = 1; pass <= maxPasses; pass++) {
|
|
const beforeClass = classifyCiphertext(current);
|
|
if (beforeClass === 'plaintext') {
|
|
stoppedReason = 'plaintext';
|
|
break;
|
|
}
|
|
|
|
const one = await decryptOnePass(current);
|
|
if (!one.ok) {
|
|
stoppedReason = 'decrypt-failed';
|
|
break;
|
|
}
|
|
|
|
trace.push({
|
|
pass,
|
|
method: one.method,
|
|
inLength: current.length,
|
|
outLength: one.text.length
|
|
});
|
|
|
|
current = String(one.text || '');
|
|
}
|
|
|
|
return {
|
|
finalText: current,
|
|
trace,
|
|
stoppedReason,
|
|
finalClass: classifyCiphertext(current)
|
|
};
|
|
}
|
|
|
|
function formatDecryptTrace(diag) {
|
|
if (!diag) return 'None';
|
|
const passes = Number(diag.passes || 0);
|
|
const methods = Array.isArray(diag.methods) && diag.methods.length > 0
|
|
? diag.methods.join(' → ')
|
|
: 'none';
|
|
const reason = String(diag.stoppedReason || 'unknown');
|
|
const finalClass = String(diag.finalClass || 'unknown');
|
|
const finalLength = Number(diag.finalLength || 0);
|
|
return `passes=${passes} | methods=${methods} | stop=${reason} | final=${finalClass} | len=${finalLength}`;
|
|
}
|
|
|
|
async function peelTagsMaybeEncrypted(objTags) {
|
|
const safe = objTags && typeof objTags === 'object' ? { ...objTags } : {};
|
|
|
|
for (const key of ['title', 'image', 'summary']) {
|
|
const value = String(safe[key] || '').trim();
|
|
if (!value) continue;
|
|
const result = await peelDecrypt(value, 3);
|
|
safe[key] = result.trace.length > 0 ? result.finalText : value;
|
|
}
|
|
|
|
if (Array.isArray(safe.t)) {
|
|
const decryptedTags = [];
|
|
for (const tagValue of safe.t) {
|
|
const value = String(tagValue || '').trim();
|
|
if (!value) continue;
|
|
const result = await peelDecrypt(value, 3);
|
|
decryptedTags.push(result.trace.length > 0 ? result.finalText : value);
|
|
}
|
|
safe.t = decryptedTags;
|
|
}
|
|
|
|
return safe;
|
|
}
|
|
|
|
async function ensureNoteDiagnostics(note) {
|
|
if (!note || Number(note.kind) !== 30024) return note;
|
|
if (note.decryptDiagnostics) return note;
|
|
|
|
const peel = await peelDecrypt(String(note.content || ''));
|
|
const decryptedTags = await peelTagsMaybeEncrypted(note.objTags || {});
|
|
|
|
note.decryptedContent = peel.finalText;
|
|
note.decryptedObjTags = decryptedTags;
|
|
note.decryptDiagnostics = {
|
|
passes: peel.trace.length,
|
|
methods: peel.trace.map((step) => String(step.method || '').toUpperCase()),
|
|
stoppedReason: peel.stoppedReason,
|
|
finalClass: peel.finalClass,
|
|
finalLength: String(peel.finalText || '').length
|
|
};
|
|
|
|
return note;
|
|
}
|
|
|
|
function setDiagnosticUI(note) {
|
|
CURRENT_DECRYPT_TRACE = note?.decryptDiagnostics || null;
|
|
txtDecryptTrace.textContent = formatDecryptTrace(CURRENT_DECRYPT_TRACE);
|
|
|
|
DIAGNOSTIC_ACTIVE = Boolean(
|
|
Number(note?.kind) === 30024 &&
|
|
(Number(CURRENT_DECRYPT_TRACE?.passes || 0) > 0 || CURRENT_DECRYPT_TRACE?.stoppedReason === 'decrypt-failed')
|
|
);
|
|
|
|
if (DIAGNOSTIC_ACTIVE) {
|
|
divDiagnosticBadge.className = '';
|
|
} else {
|
|
divDiagnosticBadge.className = 'clsHidden';
|
|
}
|
|
}
|
|
|
|
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="10"> NOTES </th></tr>`;
|
|
htmlOut += `<tr><th colspan="10">   </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>
|
|
<th>Layers</th>
|
|
</tr>`;
|
|
|
|
// Build table from OBJ_NOTES
|
|
for (let Each of Object.keys(OBJ_NOTES).sort((a, b) => b - a)) {
|
|
const note = OBJ_NOTES[Each];
|
|
if (Number(note?.kind) === 30024) {
|
|
await ensureNoteDiagnostics(note);
|
|
}
|
|
|
|
let d = new Date(Number(Each) * 1000);
|
|
let dCreated = d.toLocaleString();
|
|
d = new Date(Number(note?.created_at) * 1000);
|
|
let dEdited = d.toLocaleString();
|
|
|
|
const dTagValue = note?.objTags?.d ?? `${note?.created_at} (from created_at)`;
|
|
const rowTitle = note?.decryptedObjTags?.title || note?.objTags?.title || 'Untitled';
|
|
|
|
let svgPub = `<div class="divTableButtons"></div>`;
|
|
if (note?.kind == 30024) {
|
|
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}">${rowTitle}</td>
|
|
<td class="col1">${dTagValue}</td>
|
|
<td class="col1">${dCreated}</td>
|
|
<td class="col1">${dEdited}</td>
|
|
<td class="col3">${note?.kind} </td>
|
|
<td class="col3">${note?.encrypted ? 'Yes' : 'No'} </td>
|
|
<td class="col3">${note?.kind == 30024 ? Number(note?.decryptDiagnostics?.passes || 0) : '-'}</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) {
|
|
if (DIAGNOSTIC_ACTIVE) {
|
|
console.log('[autosave] Diagnostic view active - skipping implicit save');
|
|
LastText = taNote.value.trim();
|
|
} else {
|
|
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;
|
|
}
|
|
|
|
const note = OBJ_NOTES[id];
|
|
await ensureNoteDiagnostics(note);
|
|
|
|
const displayTags = note.decryptedObjTags || note.objTags || {};
|
|
const displayContent = Number(note.kind) === 30024
|
|
? String(note.decryptedContent || note.content || '')
|
|
: String(note.content || '');
|
|
|
|
taNote.value = displayContent;
|
|
LastText = taNote.value.trim();
|
|
txtTitle.textContent = displayTags?.title || '';
|
|
divTitle.textContent = displayTags?.title || '';
|
|
txtSummary.textContent = displayTags?.summary || '';
|
|
txtTags.textContent = displayTags?.t?.join(' ') || '';
|
|
|
|
CURRENT_NOTE = id;
|
|
txtID.textContent = CURRENT_NOTE;
|
|
setDiagnosticUI(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 (displayTags?.image === "" || !displayTags?.image) {
|
|
txtImg.textContent = "";
|
|
imgMain.className = "clsHidden";
|
|
} else {
|
|
txtImg.textContent = displayTags.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', async (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
|
|
};
|
|
|
|
if (evt.kind === 30024) {
|
|
await ensureNoteDiagnostics(OBJ_NOTES[dTag]);
|
|
}
|
|
|
|
if (isNavOpen) {
|
|
LoadSidenav();
|
|
}
|
|
}
|
|
});
|
|
|
|
// 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";
|
|
setDiagnosticUI(null);
|
|
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;
|
|
}
|
|
|
|
if (DIAGNOSTIC_ACTIVE) {
|
|
console.log('[autosave] Skipping: diagnostic mode active');
|
|
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> |