505 lines
19 KiB
JavaScript
505 lines
19 KiB
JavaScript
|
|
/* ================================================================
|
|
IMPORTS
|
|
================================================================ */
|
|
import { initNDKPage, getPubkey, injectHeaderAvatar, 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';
|
|
// Version will be loaded asynchronously
|
|
const versionInfo = await getVersion();
|
|
const VERSION = versionInfo.VERSION;
|
|
console.log(`[index.html ${VERSION}] Loading...`);
|
|
|
|
/* ================================================================
|
|
GLOBAL VARIABLES
|
|
================================================================ */
|
|
let updateIntervalId = null;
|
|
let currentPubkey = null;
|
|
let startupStatusMessage = '';
|
|
let startupStatusUntil = 0;
|
|
let walletFooterStatusMessage = '';
|
|
let walletFooterStatusUntil = 0;
|
|
|
|
// Hamburger menu
|
|
let hamburgerInstance = null;
|
|
let isNavOpen = false;
|
|
|
|
// Version bar buttons
|
|
let logoutHamburger = null;
|
|
let themeToggleHamburger = null;
|
|
let isDarkMode = 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");
|
|
|
|
/* ================================================================
|
|
APP DEFINITIONS
|
|
================================================================ */
|
|
const arrApps = [
|
|
{
|
|
id: `relays`,
|
|
svg: `<circle r="1" cx="7" cy="3" /> <circle r="1" cx="3" cy="3" /> <circle r="1" cx="3" cy="7" /> <circle r="1" cx="7" cy="7" />`,
|
|
name: `RELAYS`,
|
|
},
|
|
{
|
|
id: `db`,
|
|
svg: `<line x1="2" y1="2" x2="8" y2="2" /> <line x1="2" y1="5" x2="8" y2="5" /> <line x1="2" y1="8" x2="8" y2="8" />`,
|
|
name: `DB`
|
|
},
|
|
{
|
|
id: `tools`,
|
|
svg: `<path d="M5,8 L5,3 L8,4 L8,2 L5,3 L2,3 "/>`,
|
|
name: `TOOLS`,
|
|
},
|
|
|
|
// {
|
|
// id: `files`,
|
|
// svg: `<path d="M2,2 L2,8 "/> <path d="M4,2 L4,8 "/><path d="M6,2 L6,8"/><path d="M8,2 L8,8"/> `,
|
|
// name: `FILES`,
|
|
// },
|
|
|
|
{
|
|
id: `links`,
|
|
svg: `<path d="M2,2 L2,2 M2,5 L2,5 M2,8 L2,8 M5,2 L5,2 M5,5 L5,5 M5,8 L5,8 M8,2 L8,2 M8,5 L8,5 M8,8 L8,8 "/>`,
|
|
name: `LINKS`,
|
|
},
|
|
{
|
|
id: `profile`,
|
|
svg: `<circle cx=5 cy=5 r=3 fill="none" />`,
|
|
name: `P-FILE`,
|
|
},
|
|
{
|
|
id: `ws`,
|
|
svg: `<circle r="2.8284271247461903" cx="5" cy="5" />
|
|
<line x1="6" y1="6" x2="6" y2="6" />
|
|
<line x1="4" y1="4" x2="4" y2="4" /> `,
|
|
name: `WS`,
|
|
},
|
|
{
|
|
id: `post`,
|
|
svg: `<path d="M2,8 L8,2 L5,2 L8,2 L8,5 " /> `,
|
|
name: `POST`,
|
|
},
|
|
{
|
|
id: `feed`,
|
|
svg: `<path d="M2,5 L2,8 L5,8 L2,8 L8,2 " />`,
|
|
name: `FEED`,
|
|
},
|
|
{
|
|
id: `view`,
|
|
svg: `<path d="M2,5 L2,8 L5,8 L2,8 L8,2 " />`,
|
|
name: `VIEW`,
|
|
},
|
|
{
|
|
id: `love`,
|
|
svg: `<circle cx="3.5" cy="5" r="1.5" /><circle cx="7" cy="5" r="1.5" />`,
|
|
name: `LOVE`,
|
|
},
|
|
{
|
|
id: `msg`,
|
|
svg: `<path d="M5,2 L8,2 L8,5 L8,2 L2,8 L2,5 L2,8 L5,8 " /> `,
|
|
name: `MSG`,
|
|
},
|
|
{
|
|
id: `event`,
|
|
svg: `<circle cx=5 cy=5 r=3 fill="none" />`,
|
|
name: `EVENT`,
|
|
},
|
|
{
|
|
id: `note`,
|
|
svg: `<polygon fill="none" points="6,2 6,4 8,4 8,8 2,8 2,2 " />`,
|
|
name: `NOTE`,
|
|
},
|
|
{ id: `todo`, svg: `<path d=" M2,6 L4,8 L8,2"/>`, name: `TODO` },
|
|
{ id: `map`, svg: `<path d="M2,8 L2,2 L8,2 L8,8 L4,8 L4,4 L6,4 L6,6 "/>`, name: `MAP` },
|
|
{
|
|
id: `people`,
|
|
svg: `<path d="M2,2 L6,2 L8,6 L7,6 L7,8 L2,8 L2,2 " /> `,
|
|
name: `PEOPLE`,
|
|
},
|
|
{
|
|
id: `cal`,
|
|
svg: `<polygon points="2,3 3,6 3,8 7,8 7,6 8,3 6,8 5,8 " />
|
|
<line x1="6" y1="3" x2="6" y2="3" />
|
|
<line x1="4" y1="2" x2="4" y2="2" />
|
|
<line x1="5" y1="5" x2="5" y2="5" />
|
|
<rect x="3" y="6" width="4" height="2" />`,
|
|
name: `CAL`,
|
|
},
|
|
{
|
|
id: `block`,
|
|
svg: `<circle cx="5" cy="5" r="3" fill="none" /> <path d="M3,3 L7,7" />`,
|
|
name: `BLOCK`,
|
|
},
|
|
{
|
|
id: `conway`,
|
|
svg: `<circle cx="5" cy="2.45" r="0.85" fill="currentColor" stroke="none" />
|
|
<circle cx="7.55" cy="5" r="0.85" fill="currentColor" stroke="none" />
|
|
<circle cx="2.45" cy="7.55" r="0.85" fill="currentColor" stroke="none" />
|
|
<circle cx="5" cy="7.55" r="0.85" fill="currentColor" stroke="none" />
|
|
<circle cx="7.55" cy="7.55" r="0.85" fill="currentColor" stroke="none" />`,
|
|
name: `CONWAY`,
|
|
},
|
|
{
|
|
id: `slide-show`,
|
|
svg: `<circle cx="2" cy="2" r="1.3" fill="currentColor" stroke="none"/>
|
|
<circle cx="5" cy="2" r="1.3" fill="currentColor" stroke="none"/>
|
|
<circle cx="8" cy="2" r="1.3" fill="currentColor" stroke="none"/>
|
|
<circle cx="2" cy="5" r="1.3" fill="currentColor" stroke="none"/>
|
|
<circle cx="5" cy="5" r="1.3" fill="currentColor" stroke="none"/>
|
|
<circle cx="8" cy="5" r="1.3" fill="currentColor" stroke="none"/>
|
|
<circle cx="2" cy="8" r="1.3" fill="currentColor" stroke="none"/>
|
|
<circle cx="5" cy="8" r="1.3" fill="currentColor" stroke="none"/>
|
|
<circle cx="8" cy="8" r="1.3" fill="currentColor" stroke="none"/>`,
|
|
name: `SLIDES`,
|
|
},
|
|
{
|
|
id: `blobs`,
|
|
|
|
// svg: `<rect fill="none" x="2" y="2" width="6" height="6" />`,
|
|
svg: `<rect fill="none" x="4" y="4" width="2" height="2" />`,
|
|
name: `BLOBS`,
|
|
},
|
|
{ id: `ai`, svg: `<rect x="3" y="4" width="4" height="4" /><line x1="4" y1="6" x2="4" y2="6" /><line x1="6" y1="6" x2="6" y2="6" /><line x1="5" y1="4" x2="5" y2="2" /><line x1="5" y1="2" x2="5" y2="2" />`, name: `AI` },
|
|
{ id: `skills-tv`, svg: `<rect fill="none" x="2" y="3" width="6" height="4" /><line x1="3" y1="8" x2="7" y2="8" /><line x1="5" y1="3" x2="5" y2="2" />`, name: `SKILLS TV` },
|
|
{ id: `skills-edit`, svg: ``, name: `SKILLS EDIT` },
|
|
{ id: `cashu`, svg: `<circle cx="5" cy="4" r="2.5" /><circle cx="5" cy="6" r="2.5" />`, name: `CASHU` },
|
|
{ id: `didactyl`, svg: `<path stroke-width="3.98" d="M3,3 L3,7 L7,7 L7,3" />`, name: `AGENT` },
|
|
{ id: `strudel`, svg: `<circle cx="4" cy="7" r="1" /><line x1="5" y1="7" x2="5" y2="2" /><path d="M5,2 C6,2 7,3 7,4" />`, name: `STRUDEL` },
|
|
{ id: `music`, svg: `<path d="M2,6 C2,3 3,2 5,2 C7,2 8,3 8,6" /><rect x="2" y="6" width="1" height="2" /><rect x="7" y="6" width="1" height="2" />`, name: `MUSIC` },
|
|
{ id: `keep-alive`, svg: ``, name: `KEEP ALIVE` },
|
|
{ id: `vj`, svg: ``, name: `VJ` },
|
|
{ id: `ai-tv`, svg: ``, name: `AI TV` },
|
|
];
|
|
|
|
/* ================================================================
|
|
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');
|
|
}
|
|
divSideNavBody.innerHTML = "Settings and options coming soon...";
|
|
|
|
// 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)'
|
|
});
|
|
|
|
// Determine current theme
|
|
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();
|
|
}
|
|
}
|
|
|
|
/* ================================================================
|
|
LOAD APP SCREEN
|
|
================================================================ */
|
|
const LoadAppScreen = async (providedPubkey = null) => {
|
|
const xmlns = "http://www.w3.org/2000/svg";
|
|
|
|
divBody.innerHTML = "";
|
|
|
|
const pubkey = providedPubkey || currentPubkey || null;
|
|
|
|
let divTitle = document.createElement(`div`);
|
|
divTitle.setAttribute(`id`, `divNpubTitle`);
|
|
divTitle.innerHTML = "NPUB";
|
|
|
|
let div = document.createElement(`a`);
|
|
div.setAttribute(`id`, `divNpub`);
|
|
div.setAttribute(`class`, `divAppButtons`);
|
|
|
|
if (pubkey) {
|
|
const isDark = document.body.classList.contains('dark-mode');
|
|
const svgNpub = QRCode({ msg: pubkey, dim: 170, ecl: 'L', pal: isDark ? ['#d9d9d9', '#000000'] : ['#000000', '#ffffff'] });
|
|
svgNpub.style.background = isDark ? '#000000' : '#ffffff';
|
|
svgNpub.style.padding = '6px';
|
|
svgNpub.style.borderRadius = '8px';
|
|
div.setAttribute(`href`, `./npub.html?npub=${pubkey}`);
|
|
div.setAttribute(`target`, `_blank`);
|
|
div.setAttribute(`rel`, `noopener noreferrer`);
|
|
div.appendChild(svgNpub);
|
|
div.appendChild(divTitle);
|
|
}
|
|
divBody.appendChild(div);
|
|
|
|
let htmlOut = "";
|
|
for (let Each of arrApps) {
|
|
const url = Each.id === 'feed'
|
|
? './feed.html'
|
|
: Each.id === 'view'
|
|
? './post.html?follows=true&post=false&viewed=true'
|
|
: Each.id === 'love'
|
|
? './notifications.html'
|
|
: Each.id === 'event'
|
|
? './event-management.html'
|
|
: `./${Each.id}.html`;
|
|
htmlOut += `<a id="div${Each.id}" class="divAppButtons" href="${url}" target="_blank" rel="noopener noreferrer">
|
|
<svg id="${Each.id}" class="svgAppButtons" viewBox="0 0 10 10" >
|
|
${Each.svg}
|
|
</svg>
|
|
${Each.name}
|
|
</a>`;
|
|
}
|
|
|
|
divBody.innerHTML = divBody.innerHTML + htmlOut;
|
|
divBody.style.justifyContent = `space-around`;
|
|
divBody.style.alignContent = `space-around`;
|
|
divBody.style.alignItems = ``;
|
|
divBody.style.height = "";
|
|
};
|
|
|
|
/* ================================================================
|
|
UPDATE FOOTER
|
|
================================================================ */
|
|
const UpdateFooter = async () => {
|
|
try {
|
|
// Update relay status visuals in footer and sidenav
|
|
await updateFooterRelayStatus();
|
|
await updateSidenavRelaySection();
|
|
|
|
await updateBlossomSection();
|
|
// Keep startup/wallet status visible in footer center while active
|
|
const now = Date.now();
|
|
if (now < walletFooterStatusUntil && walletFooterStatusMessage) {
|
|
divFooterCenter.textContent = walletFooterStatusMessage;
|
|
} else if (now < startupStatusUntil && startupStatusMessage) {
|
|
divFooterCenter.textContent = startupStatusMessage;
|
|
} else {
|
|
divFooterCenter.innerHTML = '';
|
|
}
|
|
divFooterRight.innerHTML = '';
|
|
} catch (error) {
|
|
console.error('[index.html] Error updating footer:', error);
|
|
}
|
|
};
|
|
|
|
/* ================================================================
|
|
LOGOUT
|
|
================================================================ */
|
|
const Logout = async () => {
|
|
console.log("[index.html] Starting logout process...");
|
|
|
|
// Stop the update loop
|
|
if (updateIntervalId) {
|
|
clearInterval(updateIntervalId);
|
|
updateIntervalId = null;
|
|
}
|
|
|
|
// Disconnect from worker
|
|
disconnect();
|
|
|
|
// Logout from nostr-login-lite
|
|
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
|
|
await window.NOSTR_LOGIN_LITE.logout();
|
|
}
|
|
|
|
// Clear all storage
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
|
|
// Clear IndexedDB
|
|
if (window.indexedDB) {
|
|
const databases = await window.indexedDB.databases();
|
|
for (const db of databases) {
|
|
if (db.name) {
|
|
window.indexedDB.deleteDatabase(db.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log("[index.html] Logged out, reloading page");
|
|
location.reload(true);
|
|
};
|
|
|
|
/* ================================================================
|
|
EVENT LISTENERS
|
|
================================================================ */
|
|
|
|
/* ================================================================
|
|
INITIALIZATION
|
|
================================================================ */
|
|
(async function main() {
|
|
console.log("[index.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;
|
|
if (isDarkMode) {
|
|
localStorage.setItem('theme', 'dark');
|
|
} else {
|
|
localStorage.setItem('theme', 'light');
|
|
}
|
|
// Save sidenav state before reload
|
|
localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false');
|
|
window.location.reload();
|
|
});
|
|
}
|
|
|
|
if (logoutButton) {
|
|
logoutButton.addEventListener('click', async () => {
|
|
try {
|
|
await Logout();
|
|
} catch (error) {
|
|
console.error('Logout failed:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Render app shell immediately so worker startup does not block first paint
|
|
await LoadAppScreen();
|
|
|
|
// Initialize NDK (handles authentication automatically)
|
|
await initNDKPage();
|
|
|
|
// Get authenticated pubkey
|
|
currentPubkey = await getPubkey();
|
|
await injectHeaderAvatar(currentPubkey);
|
|
console.log("[index.html] Authenticated as:", currentPubkey);
|
|
|
|
// Initialize relay UI components
|
|
initFooterRelayStatus();
|
|
initSidenavRelaySection();
|
|
|
|
await initBlossomSection();
|
|
initAiSectionWithLocalConfig();
|
|
|
|
// Listen for relay activity broadcasts from worker
|
|
window.addEventListener('ndkRelayActivity', (event) => {
|
|
const { relayUrl, activity, stats } = event.detail;
|
|
console.log(`[index.html] Relay activity: ${relayUrl} - ${activity}`, stats);
|
|
setRelayActivityState(relayUrl, activity);
|
|
});
|
|
|
|
// Show startup progress messages from worker in center footer section
|
|
window.addEventListener('ndkStartupStatus', (event) => {
|
|
const msg = String(event?.detail?.message || '').trim();
|
|
if (!msg) return;
|
|
console.log(`[index.html] Startup status: ${msg}`, event?.detail || {});
|
|
startupStatusMessage = msg;
|
|
startupStatusUntil = Date.now() + 12000;
|
|
divFooterCenter.textContent = msg;
|
|
});
|
|
|
|
// Show wallet balance/status updates from worker in center footer section
|
|
window.addEventListener('ndkWalletBalance', (event) => {
|
|
const balance = Number(event?.detail?.balance || 0);
|
|
const msg = `Wallet balance: ${balance.toLocaleString()} sats`;
|
|
walletFooterStatusMessage = msg;
|
|
walletFooterStatusUntil = Date.now() + 12000;
|
|
console.log(`[index.html] ${msg}`, event?.detail || {});
|
|
divFooterCenter.textContent = msg;
|
|
});
|
|
|
|
window.addEventListener('ndkWalletStatus', (event) => {
|
|
const status = String(event?.detail?.status || '').trim();
|
|
if (!status) return;
|
|
const msg = `Wallet status: ${status}`;
|
|
walletFooterStatusMessage = msg;
|
|
walletFooterStatusUntil = Date.now() + 12000;
|
|
console.log(`[index.html] ${msg}`, event?.detail || {});
|
|
divFooterCenter.textContent = msg;
|
|
});
|
|
|
|
// Hydrate app screen with authenticated data (NPUB QR)
|
|
await LoadAppScreen(currentPubkey);
|
|
|
|
// Restore sidenav state if it was open before theme toggle
|
|
const sidenavWasOpen = localStorage.getItem('sidenavWasOpen');
|
|
if (sidenavWasOpen === 'true') {
|
|
localStorage.removeItem('sidenavWasOpen');
|
|
openNav();
|
|
}
|
|
|
|
// Start update loop (updates footer every second)
|
|
updateIntervalId = setInterval(UpdateFooter, 1000);
|
|
|
|
// Update version display
|
|
await updateVersionDisplay();
|
|
|
|
console.log('[index.html] Initialization complete');
|
|
} catch (error) {
|
|
console.error('[index.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>`;
|
|
}
|
|
})();
|
|
|