Files
client/www/slide-show.html

1157 lines
34 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>SLIDES</title>
<link rel="stylesheet" href="./css/client.css" />
<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" />
<script src="./js/vendor/svg.min.js"></script>
<style>
#divBody {
min-height: calc(100vh - max(var(--header-height), var(--header-min-height)) - max(var(--footer-height), var(--footer-min-height)));
height: auto !important;
display: block;
overflow-y: auto;
padding: 12px;
box-sizing: border-box;
}
#divSlideStatus {
margin: 0 0 12px 0;
color: var(--primary-color);
opacity: 0.9;
font-size: 0.95rem;
}
#divSlideGrid {
display: flex;
gap: 12px;
align-items: flex-start;
}
.slideColumn {
flex: 1 1 0;
min-width: 0;
display: block;
}
.slideTile {
display: block;
width: 100%;
margin: 0 0 12px 0;
padding: 0;
appearance: none;
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--secondary-color);
cursor: pointer;
filter: grayscale(var(--image-grayscale));
transition: filter 0.18s ease, transform 0.18s ease;
overflow: hidden;
}
.slideTile:hover {
filter: grayscale(var(--image-grayscale-hover));
transform: translateY(-2px);
}
.slideTile img {
display: block;
width: 100%;
height: auto;
object-fit: cover;
background: var(--secondary-color);
}
#divSlideEmpty {
display: none;
padding: 24px 12px;
text-align: center;
color: var(--primary-color);
opacity: 0.8;
}
#divLightbox {
position: fixed;
inset: 0;
z-index: 25;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.92);
}
#divLightbox.isOpen {
display: flex;
}
#imgLightboxMain {
max-width: 92vw;
max-height: 86vh;
width: auto;
height: auto;
border-radius: 8px;
box-shadow: 0 8px 34px rgba(0, 0, 0, 0.35);
background: #111;
}
.lightboxBtn {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 46px;
height: 46px;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 50%;
background: rgba(0, 0, 0, 0.35);
color: #fff;
font-size: 26px;
line-height: 40px;
text-align: center;
cursor: pointer;
user-select: none;
}
#btnLightboxPrev {
left: 18px;
}
#btnLightboxNext {
right: 18px;
}
#btnLightboxClose {
position: absolute;
top: 16px;
right: 16px;
width: 40px;
height: 40px;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 50%;
background: rgba(0, 0, 0, 0.35);
color: #fff;
font-size: 22px;
line-height: 34px;
text-align: center;
cursor: pointer;
user-select: none;
}
#divLightboxMeta {
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 14px;
color: #fff;
opacity: 0.85;
font-size: 0.9rem;
letter-spacing: 0.01em;
}
#divSlideControlsFooter {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
}
.slideControlWord {
color: var(--primary-color);
cursor: pointer;
user-select: none;
font-size: 80%;
line-height: 1;
transition: color 0.2s, opacity 0.2s;
}
.slideControlWord:hover {
color: var(--button-hover-color);
}
.slideControlWord.is-active {
color: var(--button-hover-color);
}
#divSlideMoreWrap {
display: flex;
justify-content: center;
padding: 8px 0 14px;
}
#btnSlideMore {
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
background: var(--secondary-color);
color: var(--primary-color);
padding: 6px 16px;
font-size: 0.9rem;
cursor: pointer;
filter: grayscale(var(--image-grayscale));
}
#btnSlideMore:hover {
color: var(--button-hover-color);
filter: grayscale(var(--image-grayscale-hover));
}
@media (max-width: 640px) {
#divSlideGrid {
gap: 8px;
}
.slideTile {
margin-bottom: 8px;
}
.lightboxBtn {
width: 40px;
height: 40px;
font-size: 22px;
}
}
</style>
</head>
<body>
<div id="divSvgHam" class="divHeaderButtons"></div>
<div id="divHeader">
<div id="divHeaderFlexLeft"></div>
<div id="divHeaderFlexCenter">
</div>
<div id="divHeaderFlexRight"></div>
</div>
<div id="divBody">
<div id="divSlideGrid"></div>
<div id="divSlideEmpty">No kind 1 posts with direct image URLs were found.</div>
<div id="divSlideMoreWrap">
<button id="btnSlideMore" type="button">more</button>
</div>
</div>
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox">
<div id="divSlideControlsFooter">
<span id="slideCtlSlideshow" class="slideControlWord" data-action="slideshow">slideshow</span>
<span id="slideCtlSlideshowFull" class="slideControlWord" data-action="slideshow-full">slideshow-full</span>
<span id="slideCtlFull" class="slideControlWord" data-action="full">full</span>
</div>
</div>
<div id="divFooterRight" class="divFooterBox"></div>
<div id="divFooterBalance" class="divFooterBox">0 sats</div>
</div>
<div id="divSideNav">
<div id="divSideNavHeader"></div>
<div id="divSideNavBody">
<table>
<tbody>
<tr>
<td><label for="inputSlideSeconds">slideshow seconds</label></td>
<td><input id="inputSlideSeconds" type="number" min="1" max="30" step="1" value="2" /></td>
</tr>
<tr>
<td><label for="selectSlideMode">mode</label></td>
<td>
<select id="selectSlideMode">
<option value="in-order">In order</option>
<option value="random">Random</option>
<option value="latest">Last published</option>
</select>
</td>
</tr>
</tbody>
</table>
<div id="divFiles"></div>
</div>
<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>
<div id="divLightbox" aria-hidden="true">
<button id="btnLightboxClose" aria-label="Close">×</button>
<button id="btnLightboxPrev" class="lightboxBtn" aria-label="Previous"></button>
<img id="imgLightboxMain" alt="Slide image" />
<button id="btnLightboxNext" class="lightboxBtn" aria-label="Next"></button>
<div id="divLightboxMeta"></div>
</div>
<script src="./nostr.bundle.js"></script>
<script src="/nostr-login-lite/nostr-lite.js"></script>
<script type="module">
import {
initNDKPage,
getPubkey,
injectHeaderAvatar,
subscribe,
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(`[slide-show.html ${VERSION}] Loading...`);
const DIRECT_IMAGE_URL_REGEX = /\bhttps?:\/\/[^\s<>"')]+\.(?:png|jpe?g|gif|webp|avif|bmp|svg)(?:\?[^\s<>"')]*)?/ig;
let updateIntervalId = null;
let currentPubkey = null;
let targetPubkey = null;
// Auth behavior modes for this page:
// - required: no target in URL => authenticate immediately.
// - none: target in URL (?npub / ?pubkey) => do not prompt auth on load.
let isAuthenticated = false;
let authMode = 'required';
let hamburgerInstance = null;
let isNavOpen = false;
let logoutHamburger = null;
let themeToggleHamburger = null;
let isDarkMode = false;
let notesSubscription = null;
let initialLoadSettled = false;
let initialLoadTimer = null;
const noteMap = new Map();
let imagePosts = [];
let currentLightboxIndex = -1;
let publicPool = null;
let publicSub = null;
let slideshowIntervalId = null;
let slideshowIntervalMs = 2200;
const SLIDESHOW_SECONDS_STORAGE_KEY = 'slideShowSeconds';
const SLIDESHOW_MODE_STORAGE_KEY = 'slideShowMode';
const INITIAL_VISIBLE_IMAGE_COUNT = 200;
const MORE_IMAGES_CHUNK = 200;
let visibleImageCount = INITIAL_VISIBLE_IMAGE_COUNT;
const PUBLIC_READ_RELAYS = [
'wss://relay.damus.io',
'wss://relay.primal.net',
'wss://nos.lol',
'wss://relay.nostr.band',
'wss://purplepag.es'
];
const divBody = document.getElementById('divBody');
const divSideNav = document.getElementById('divSideNav');
const divFooterCenter = document.getElementById('divFooterCenter');
const divFooterRight = document.getElementById('divFooterRight');
const divSlideGrid = document.getElementById('divSlideGrid');
const divSlideEmpty = document.getElementById('divSlideEmpty');
const divLightbox = document.getElementById('divLightbox');
const imgLightboxMain = document.getElementById('imgLightboxMain');
const divLightboxMeta = document.getElementById('divLightboxMeta');
const btnLightboxClose = document.getElementById('btnLightboxClose');
const btnLightboxPrev = document.getElementById('btnLightboxPrev');
const btnLightboxNext = document.getElementById('btnLightboxNext');
const divSlideControlsFooter = document.getElementById('divSlideControlsFooter');
const slideCtlSlideshow = document.getElementById('slideCtlSlideshow');
const slideCtlSlideshowFull = document.getElementById('slideCtlSlideshowFull');
const slideCtlFull = document.getElementById('slideCtlFull');
const btnSlideMore = document.getElementById('btnSlideMore');
const inputSlideSeconds = document.getElementById('inputSlideSeconds');
const selectSlideMode = document.getElementById('selectSlideMode');
let slideshowMode = 'in-order';
function initHamburgerMenu() {
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
hamburgerInstance.animateTo('burger');
}
function openNav() {
divSideNav.style.zIndex = 3;
divSideNav.style.width = 'clamp(400px, 50vw, 600px)';
isNavOpen = true;
if (hamburgerInstance) hamburgerInstance.animateTo('arrow_left');
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');
themeToggleHamburger.animateTo(isDarkMode ? 'moon' : 'circle');
}
}
function closeNav() {
divSideNav.style.width = '0vw';
divSideNav.style.zIndex = -1;
isNavOpen = false;
if (hamburgerInstance) hamburgerInstance.animateTo('burger');
}
function toggleNav() {
isNavOpen ? closeNav() : openNav();
}
const UpdateFooter = async () => {
try {
if (isAuthenticated) {
await updateFooterRelayStatus();
await updateSidenavRelaySection();
await updateBlossomSection();
}
divFooterRight.textContent = `${imagePosts.length} photos`;
} catch (error) {
console.error('[slide-show.html] Error updating footer:', error);
}
};
const Logout = async () => {
if (updateIntervalId) {
clearInterval(updateIntervalId);
updateIntervalId = null;
}
disconnect();
if (window.NOSTR_LOGIN_LITE?.logout) {
await window.NOSTR_LOGIN_LITE.logout();
}
localStorage.clear();
sessionStorage.clear();
location.reload(true);
};
function isHexPubkey(value) {
return /^[a-fA-F0-9]{64}$/.test(String(value || '').trim());
}
function parseTargetPubkeyFromUrl(fallbackPubkey) {
const params = new URLSearchParams(window.location.search || '');
const npubParam = String(params.get('npub') || '').trim();
const hexParam = String(params.get('pubkey') || '').trim();
if (isHexPubkey(hexParam)) {
return hexParam.toLowerCase();
}
if (npubParam) {
if (isHexPubkey(npubParam)) {
return npubParam.toLowerCase();
}
try {
const decoded = window?.NostrTools?.nip19?.decode(npubParam);
if (decoded?.type === 'npub' && isHexPubkey(decoded?.data)) {
return String(decoded.data).toLowerCase();
}
} catch (_error) {
// fall through to fallback
}
}
return fallbackPubkey || null;
}
function resolveAuthModeFromUrl() {
const params = new URLSearchParams(window.location.search || '');
const hasTarget = Boolean(String(params.get('npub') || '').trim() || String(params.get('pubkey') || '').trim());
return hasTarget ? 'none' : 'required';
}
function encodeNpub(pubkeyHex) {
try {
if (!isHexPubkey(pubkeyHex)) return null;
return window?.NostrTools?.nip19?.npubEncode
? window.NostrTools.nip19.npubEncode(pubkeyHex)
: null;
} catch (_error) {
return null;
}
}
function populateOwnNpubInUrlIfNeeded() {
if (!isHexPubkey(currentPubkey) || targetPubkey !== currentPubkey) return;
const params = new URLSearchParams(window.location.search || '');
const hasTargetParam = Boolean(String(params.get('npub') || '').trim() || String(params.get('pubkey') || '').trim());
if (hasTargetParam) return;
const npub = encodeNpub(currentPubkey);
if (!npub) return;
params.set('npub', npub);
const query = params.toString();
const nextUrl = `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash || ''}`;
window.history.replaceState(null, '', nextUrl);
}
async function initializeAuthentication(mode) {
if (mode === 'required') {
await initNDKPage();
currentPubkey = await getPubkey();
isAuthenticated = true;
return;
}
// mode === 'none': do not request login on page load.
isAuthenticated = false;
currentPubkey = null;
}
async function promptLoginIfNeeded() {
if (isAuthenticated) return true;
await initNDKPage();
currentPubkey = await getPubkey();
isAuthenticated = true;
return true;
}
function extractFirstDirectImageUrl(content) {
const text = String(content || '');
if (!text) return null;
DIRECT_IMAGE_URL_REGEX.lastIndex = 0;
const match = DIRECT_IMAGE_URL_REGEX.exec(text);
return match ? match[0] : null;
}
function rebuildImagePosts() {
imagePosts = Array.from(noteMap.values())
.filter((evt) => evt && evt.imageUrl)
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
}
function setLoadingState(_isLoading) {}
function getVisibleImagePosts() {
const visibleCount = Math.max(0, Math.min(visibleImageCount, imagePosts.length));
return imagePosts.slice(0, visibleCount);
}
function updateMoreButton() {
if (!btnSlideMore) return;
const visibleCount = Math.min(visibleImageCount, imagePosts.length);
const remaining = Math.max(0, imagePosts.length - visibleCount);
if (remaining <= 0) {
btnSlideMore.style.display = 'none';
return;
}
btnSlideMore.style.display = 'inline-block';
btnSlideMore.textContent = `more (${remaining})`;
}
function sanitizeSlideMode(value) {
const mode = String(value || '').trim().toLowerCase();
if (mode === 'random' || mode === 'latest' || mode === 'in-order') return mode;
return 'in-order';
}
function applySlideMode(mode) {
slideshowMode = sanitizeSlideMode(mode);
if (selectSlideMode) selectSlideMode.value = slideshowMode;
localStorage.setItem(SLIDESHOW_MODE_STORAGE_KEY, slideshowMode);
if (slideshowMode === 'latest') {
stopSlideshowAutoplay();
}
}
function initializeSlideModeSetting() {
const storedMode = localStorage.getItem(SLIDESHOW_MODE_STORAGE_KEY);
applySlideMode(storedMode || 'in-order');
}
function sanitizeSlideshowSeconds(value) {
const parsed = Number.parseInt(String(value ?? ''), 10);
if (!Number.isFinite(parsed)) return 2;
return Math.max(1, Math.min(30, parsed));
}
function applySlideshowSeconds(seconds) {
const safeSeconds = sanitizeSlideshowSeconds(seconds);
slideshowIntervalMs = safeSeconds * 1000;
if (inputSlideSeconds) inputSlideSeconds.value = String(safeSeconds);
localStorage.setItem(SLIDESHOW_SECONDS_STORAGE_KEY, String(safeSeconds));
if (slideshowIntervalId) startSlideshowAutoplay();
}
function initializeSlideshowSecondsSetting() {
const stored = localStorage.getItem(SLIDESHOW_SECONDS_STORAGE_KEY);
const initialSeconds = sanitizeSlideshowSeconds(stored ?? 2);
applySlideshowSeconds(initialSeconds);
}
function getSlideColumnCount() {
const width = divSlideGrid?.clientWidth || window.innerWidth || 1024;
const minTileWidth = window.innerWidth <= 640 ? 160 : 240;
return Math.max(1, Math.floor(width / minTileWidth));
}
function renderGrid() {
divSlideGrid.innerHTML = '';
if (imagePosts.length === 0) {
divSlideEmpty.style.display = 'block';
updateMoreButton();
return;
}
divSlideEmpty.style.display = 'none';
const visiblePosts = getVisibleImagePosts();
const visibleTotal = visiblePosts.length;
const columnCount = getSlideColumnCount();
const columns = [];
for (let i = 0; i < columnCount; i += 1) {
const col = document.createElement('div');
col.className = 'slideColumn';
columns.push(col);
divSlideGrid.appendChild(col);
}
// Row-major visual ordering:
// 0->col0, 1->col1, ... n->col(n%columnCount)
visiblePosts.forEach((post, index) => {
const tile = document.createElement('button');
tile.className = 'slideTile';
tile.type = 'button';
tile.dataset.index = String(index);
tile.title = `Open image ${index + 1} of ${visibleTotal}`;
const img = document.createElement('img');
img.src = post.imageUrl;
img.alt = 'Post image';
img.loading = 'lazy';
tile.appendChild(img);
tile.addEventListener('click', () => openLightbox(index));
const targetColumn = columns[index % columnCount];
targetColumn.appendChild(tile);
});
updateMoreButton();
}
function updateLightboxMeta() {
const visibleTotal = getVisibleImagePosts().length;
if (currentLightboxIndex < 0 || currentLightboxIndex >= visibleTotal) {
divLightboxMeta.textContent = '';
return;
}
divLightboxMeta.textContent = `${currentLightboxIndex + 1} / ${visibleTotal}`;
}
function renderLightboxImage() {
const visiblePosts = getVisibleImagePosts();
if (currentLightboxIndex < 0 || currentLightboxIndex >= visiblePosts.length) return;
const post = visiblePosts[currentLightboxIndex];
imgLightboxMain.src = post.imageUrl;
updateLightboxMeta();
}
function openLightbox(index) {
const visibleTotal = getVisibleImagePosts().length;
if (visibleTotal === 0) return;
currentLightboxIndex = Math.max(0, Math.min(index, visibleTotal - 1));
renderLightboxImage();
divLightbox.classList.add('isOpen');
divLightbox.setAttribute('aria-hidden', 'false');
}
function closeLightbox() {
stopSlideshowAutoplay();
currentLightboxIndex = -1;
divLightbox.classList.remove('isOpen');
divLightbox.setAttribute('aria-hidden', 'true');
}
function showPrevLightbox() {
const visibleTotal = getVisibleImagePosts().length;
if (visibleTotal === 0) return;
if (currentLightboxIndex < 0) return;
currentLightboxIndex = (currentLightboxIndex - 1 + visibleTotal) % visibleTotal;
renderLightboxImage();
}
function showNextLightbox() {
const visibleTotal = getVisibleImagePosts().length;
if (visibleTotal === 0) return;
if (currentLightboxIndex < 0) return;
if (slideshowMode === 'random') {
currentLightboxIndex = getRandomNextIndex(currentLightboxIndex, visibleTotal);
} else {
currentLightboxIndex = (currentLightboxIndex + 1) % visibleTotal;
}
renderLightboxImage();
}
function syncSlideControlsState() {
if (slideCtlSlideshow) {
slideCtlSlideshow.classList.toggle('is-active', Boolean(slideshowIntervalId));
}
if (slideCtlSlideshowFull) {
slideCtlSlideshowFull.classList.toggle('is-active', Boolean(slideshowIntervalId));
}
}
function stopSlideshowAutoplay() {
if (slideshowIntervalId) {
clearInterval(slideshowIntervalId);
slideshowIntervalId = null;
}
syncSlideControlsState();
}
function getRandomNextIndex(currentIndex, total) {
if (total <= 1) return 0;
let next = currentIndex;
while (next === currentIndex) {
next = Math.floor(Math.random() * total);
}
return next;
}
function showLatestPublishedImageIfEnabled() {
if (slideshowMode !== 'latest') return;
const visibleTotal = getVisibleImagePosts().length;
if (visibleTotal === 0) return;
openLightbox(0);
}
function startSlideshowAutoplay() {
if (slideshowMode === 'latest') return;
if (getVisibleImagePosts().length === 0) return;
if (!divLightbox.classList.contains('isOpen')) {
openLightbox(Math.max(0, currentLightboxIndex));
}
stopSlideshowAutoplay();
slideshowIntervalId = setInterval(() => {
showNextLightbox();
}, slideshowIntervalMs);
syncSlideControlsState();
}
function toggleSlideshowAutoplay() {
if (slideshowIntervalId) {
stopSlideshowAutoplay();
} else {
startSlideshowAutoplay();
}
}
async function ensurePageFullscreen() {
try {
if (!document.fullscreenElement) {
await divBody.requestFullscreen();
}
} catch (error) {
console.warn('[slide-show.html] Fullscreen enable failed:', error?.message || error);
}
}
async function ensureSlideshowFullscreen() {
try {
if (document.fullscreenElement === divLightbox) return;
if (document.fullscreenElement && document.fullscreenElement !== divLightbox) {
await document.exitFullscreen();
}
if (!document.fullscreenElement) {
await divLightbox.requestFullscreen();
}
} catch (error) {
console.warn('[slide-show.html] Slideshow fullscreen enable failed:', error?.message || error);
}
}
async function startSlideshowFullscreen() {
if (!divLightbox.classList.contains('isOpen')) {
openLightbox(Math.max(0, currentLightboxIndex));
}
await ensureSlideshowFullscreen();
startSlideshowAutoplay();
}
async function togglePageFullscreen() {
try {
if (document.fullscreenElement === divBody) {
await document.exitFullscreen();
} else if (!document.fullscreenElement) {
await divBody.requestFullscreen();
}
} catch (error) {
console.warn('[slide-show.html] Fullscreen toggle failed:', error?.message || error);
}
}
function onWindowKeydown(event) {
if (divLightbox.classList.contains('isOpen')) {
if (event.key === 'Escape') {
closeLightbox();
} else if (event.key === 'ArrowLeft') {
showPrevLightbox();
} else if (event.key === 'ArrowRight') {
showNextLightbox();
}
}
}
function upsertNoteIfImage(evt) {
if (!evt?.id || evt.kind !== 1 || evt.pubkey !== targetPubkey) return false;
const imageUrl = extractFirstDirectImageUrl(evt.content);
if (!imageUrl) return false;
const prev = noteMap.get(evt.id);
if (prev && (prev.created_at || 0) >= (evt.created_at || 0)) return false;
noteMap.set(evt.id, {
id: evt.id,
pubkey: evt.pubkey,
created_at: evt.created_at || 0,
content: evt.content || '',
imageUrl
});
return true;
}
function settleInitialLoad() {
if (initialLoadSettled) return;
initialLoadSettled = true;
if (initialLoadTimer) {
clearTimeout(initialLoadTimer);
initialLoadTimer = null;
}
setLoadingState(false);
renderGrid();
}
function handleNdkEvent(event) {
const evt = event?.detail;
if (!evt) return;
if (evt.kind === 1) {
const changed = upsertNoteIfImage(evt);
if (changed) {
rebuildImagePosts();
renderGrid();
setLoadingState(false);
if (initialLoadSettled) showLatestPublishedImageIfEnabled();
}
}
}
function startKind1Subscription() {
notesSubscription = subscribe(
{
kinds: [1],
authors: [targetPubkey],
limit: 2000
},
{
closeOnEose: false,
cacheUsage: 'CACHE_FIRST'
}
);
initialLoadTimer = setTimeout(() => {
settleInitialLoad();
}, 1800);
}
function startPublicKind1Subscription() {
if (!targetPubkey) return;
if (!window?.NostrTools?.SimplePool) {
console.warn('[slide-show.html] SimplePool unavailable for public mode');
settleInitialLoad();
return;
}
publicPool = new window.NostrTools.SimplePool();
publicSub = publicPool.subscribeMany(
PUBLIC_READ_RELAYS,
[{ kinds: [1], authors: [targetPubkey], limit: 2000 }],
{
onevent: (evt) => {
const changed = upsertNoteIfImage(evt);
if (changed) {
rebuildImagePosts();
renderGrid();
if (initialLoadSettled) showLatestPublishedImageIfEnabled();
}
},
oneose: () => {
settleInitialLoad();
}
}
);
initialLoadTimer = setTimeout(() => {
settleInitialLoad();
}, 2200);
}
function cleanup() {
stopSlideshowAutoplay();
if (notesSubscription?.unsubscribe) {
notesSubscription.unsubscribe();
notesSubscription = null;
}
if (publicSub?.close) {
publicSub.close();
publicSub = null;
}
if (publicPool?.close) {
publicPool.close(PUBLIC_READ_RELAYS);
publicPool = null;
}
window.removeEventListener('keydown', onWindowKeydown);
window.removeEventListener('ndkEvent', handleNdkEvent);
window.removeEventListener('resize', renderGrid);
if (updateIntervalId) {
clearInterval(updateIntervalId);
updateIntervalId = null;
}
if (initialLoadTimer) {
clearTimeout(initialLoadTimer);
initialLoadTimer = null;
}
}
(async function main() {
try {
initHamburgerMenu();
const divSvgHam = document.getElementById('divSvgHam');
if (divSvgHam) divSvgHam.addEventListener('click', toggleNav);
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 () => {
if (!isAuthenticated) {
await promptLoginIfNeeded();
window.location.reload();
return;
}
await Logout();
});
}
btnLightboxClose.addEventListener('click', closeLightbox);
btnLightboxPrev.addEventListener('click', showPrevLightbox);
btnLightboxNext.addEventListener('click', showNextLightbox);
divLightbox.addEventListener('click', (event) => {
if (event.target === divLightbox) closeLightbox();
});
window.addEventListener('keydown', onWindowKeydown);
window.addEventListener('ndkEvent', handleNdkEvent);
window.addEventListener('resize', renderGrid);
window.addEventListener('beforeunload', cleanup, { once: true });
if (divSlideControlsFooter) {
divSlideControlsFooter.addEventListener('click', async (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const action = target.getAttribute('data-action');
if (!action) return;
if (action === 'slideshow') {
toggleSlideshowAutoplay();
return;
}
if (action === 'slideshow-full') {
await startSlideshowFullscreen();
return;
}
if (action === 'full') {
await togglePageFullscreen();
}
});
syncSlideControlsState();
}
if (btnSlideMore) {
btnSlideMore.addEventListener('click', () => {
visibleImageCount += MORE_IMAGES_CHUNK;
renderGrid();
});
}
if (inputSlideSeconds) {
inputSlideSeconds.addEventListener('change', (event) => {
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
applySlideshowSeconds(target.value);
});
}
if (selectSlideMode) {
selectSlideMode.addEventListener('change', (event) => {
const target = event.target;
if (!(target instanceof HTMLSelectElement)) return;
applySlideMode(target.value);
});
}
initializeSlideModeSetting();
initializeSlideshowSecondsSetting();
authMode = resolveAuthModeFromUrl();
await initializeAuthentication(authMode);
targetPubkey = parseTargetPubkeyFromUrl(currentPubkey);
if (!targetPubkey) {
throw new Error('Missing target user. Provide ?npub=<npub1...> or ?pubkey=<hex>.');
}
if (isAuthenticated) {
populateOwnNpubInUrlIfNeeded();
await injectHeaderAvatar(targetPubkey);
initFooterRelayStatus();
initSidenavRelaySection();
await initBlossomSection();
initAiSectionWithLocalConfig();
startKind1Subscription();
window.addEventListener('ndkRelayActivity', (event) => {
const { relayUrl, activity } = event.detail || {};
if (relayUrl && activity) setRelayActivityState(relayUrl, activity);
});
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'relayActivity') {
const { relayUrl, activity } = event.data;
setRelayActivityState(relayUrl, activity);
}
});
} else {
// Public mode: no login prompt when URL already specifies target npub/pubkey.
startPublicKind1Subscription();
}
await UpdateFooter();
updateIntervalId = setInterval(UpdateFooter, 1000);
await updateVersionDisplay();
document.title = 'SLIDES';
const headerText = document.querySelector('.divHeaderText');
if (headerText) headerText.textContent = '';
} catch (error) {
console.error('[slide-show.html] Initialization failed:', error);
divBody.innerHTML = `<div style="text-align:center;padding:50px;">
<div style="font-size:24px;margin-bottom:20px;color:red;">❌ Error</div>
<div style="font-size:16px;">${error.message}</div>
</div>`;
}
})();
</script>
</body>
</html>