870 lines
26 KiB
JavaScript
870 lines
26 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
|
// Sticky Navigation
|
|
const stickyNav = document.getElementById('stickyNav');
|
|
const mainHeader = document.querySelector('.main-header');
|
|
const themeToggle = document.getElementById('themeToggle');
|
|
const themeToggleSmall = document.getElementById('themeToggleSmall');
|
|
let lastScrollY = window.scrollY;
|
|
let ticking = false;
|
|
|
|
// Function to update sticky nav visibility
|
|
function updateStickyNav() {
|
|
const scrollY = window.scrollY;
|
|
const headerHeight = mainHeader ? mainHeader.offsetHeight : 400;
|
|
|
|
if (scrollY > headerHeight - 100) {
|
|
stickyNav.classList.add('visible');
|
|
if (themeToggle) {
|
|
themeToggle.classList.add('hidden');
|
|
}
|
|
} else {
|
|
stickyNav.classList.remove('visible');
|
|
if (themeToggle) {
|
|
themeToggle.classList.remove('hidden');
|
|
}
|
|
}
|
|
lastScrollY = scrollY;
|
|
}
|
|
|
|
// Throttled scroll listener
|
|
window.addEventListener('scroll', function() {
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(function() {
|
|
updateStickyNav();
|
|
ticking = false;
|
|
});
|
|
ticking = true;
|
|
}
|
|
});
|
|
|
|
// Initial check
|
|
setTimeout(updateStickyNav, 100);
|
|
|
|
|
|
|
|
|
|
// Read More toggle for blog posts
|
|
const readMoreButtons = document.querySelectorAll('.read-more-toggle');
|
|
|
|
readMoreButtons.forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const content = this.nextElementSibling;
|
|
if (content.style.display === 'none') {
|
|
content.style.display = 'block';
|
|
this.innerHTML = '<span>Read Less</span>';
|
|
} else {
|
|
content.style.display = 'none';
|
|
this.innerHTML = '<span>Read More</span>';
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
// Smooth scrolling for navigation links (both main nav and sticky nav)
|
|
document.querySelectorAll('nav a').forEach(link => {
|
|
link.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
const targetId = this.getAttribute('href');
|
|
const targetElement = document.querySelector(targetId);
|
|
if (targetElement) {
|
|
// Calculate offset for sticky nav
|
|
const navHeight = stickyNav.offsetHeight;
|
|
const targetPosition = targetElement.getBoundingClientRect().top + window.pageYOffset - navHeight - 20;
|
|
|
|
window.scrollTo({
|
|
top: targetPosition,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// ============ CUSTOM LIGHTBOX ============
|
|
const lightbox = document.getElementById('customLightbox');
|
|
const lightboxImage = document.getElementById('lightboxImage');
|
|
const lightboxCaption = document.getElementById('lightboxCaption');
|
|
const lightboxMeta = document.getElementById('lightboxMeta');
|
|
const lightboxCounter = document.getElementById('lightboxCounter');
|
|
const lightboxClose = document.getElementById('lightboxClose');
|
|
const lightboxPrev = document.getElementById('lightboxPrev');
|
|
const lightboxNext = document.getElementById('lightboxNext');
|
|
|
|
let currentPhotos = [];
|
|
let currentIndex = 0;
|
|
|
|
// Get all photo items
|
|
const photoItems = document.querySelectorAll('.photo-item');
|
|
|
|
// Build photo array
|
|
photoItems.forEach(item => {
|
|
const image = item.querySelector('img');
|
|
const date = item.dataset.date || '';
|
|
const pubkey = item.dataset.pubkey || '';
|
|
const alt = item.dataset.alt || '';
|
|
|
|
if (image) {
|
|
currentPhotos.push({
|
|
url: image.src,
|
|
alt: alt,
|
|
date: date,
|
|
pubkey: pubkey
|
|
});
|
|
}
|
|
});
|
|
|
|
// Open lightbox
|
|
function openLightbox(index) {
|
|
if (index < 0) index = currentPhotos.length - 1;
|
|
if (index >= currentPhotos.length) index = 0;
|
|
currentIndex = index;
|
|
|
|
const photo = currentPhotos[currentIndex];
|
|
lightboxImage.src = photo.url;
|
|
lightboxImage.alt = photo.alt;
|
|
lightboxCaption.textContent = photo.alt;
|
|
lightboxMeta.textContent = `📅 ${photo.date} | 🖊️ ${photo.pubkey}`;
|
|
lightboxCounter.textContent = `${currentIndex + 1} / ${currentPhotos.length}`;
|
|
|
|
lightbox.classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
// Close lightbox
|
|
function closeLightbox() {
|
|
lightbox.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// Navigate
|
|
function prevPhoto() {
|
|
openLightbox(currentIndex - 1);
|
|
}
|
|
|
|
function nextPhoto() {
|
|
openLightbox(currentIndex + 1);
|
|
}
|
|
|
|
// Event listeners for photo clicks
|
|
photoItems.forEach((item, index) => {
|
|
const img = item.querySelector('img');
|
|
if (img) {
|
|
img.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
openLightbox(index);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Event listeners for lightbox controls
|
|
lightboxClose.addEventListener('click', closeLightbox);
|
|
lightboxPrev.addEventListener('click', prevPhoto);
|
|
lightboxNext.addEventListener('click', nextPhoto);
|
|
|
|
// Keyboard controls
|
|
document.addEventListener('keydown', function(e) {
|
|
if (!lightbox.classList.contains('active')) return;
|
|
|
|
if (e.key === 'Escape') {
|
|
closeLightbox();
|
|
} else if (e.key === 'ArrowLeft') {
|
|
prevPhoto();
|
|
} else if (e.key === 'ArrowRight') {
|
|
nextPhoto();
|
|
}
|
|
});
|
|
|
|
// Click outside image to close
|
|
lightbox.addEventListener('click', function(e) {
|
|
if (e.target === lightbox) {
|
|
closeLightbox();
|
|
}
|
|
});
|
|
});
|
|
|
|
// No external lightbox library needed anymore!
|
|
console.log('Custom lightbox initialized successfully!');
|
|
|
|
// Artist filter list
|
|
const allowedArtists = [
|
|
'徒 Setto セット',
|
|
'Settoshi Tonami',
|
|
'Sethy Bowoy'
|
|
];
|
|
|
|
// Function to check if a track is by an allowed artist
|
|
function isAllowedArtist(artist) {
|
|
if (!artist) return false;
|
|
// Normalize the artist name for comparison (remove extra spaces, lowercase)
|
|
const normalizedArtist = artist.trim().toLowerCase();
|
|
|
|
return allowedArtists.some(allowed => {
|
|
// Normalize the allowed artist name too
|
|
const normalizedAllowed = allowed.trim().toLowerCase();
|
|
// Check if the artist name contains the allowed artist name or vice versa
|
|
return normalizedArtist.includes(normalizedAllowed) ||
|
|
normalizedAllowed.includes(normalizedArtist);
|
|
});
|
|
}
|
|
|
|
// Render music tracks (randomized, 6 at a time, filtered by artist)
|
|
function renderMusicTracks(tracks, containerId) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
// Filter tracks by artist
|
|
const filteredTracks = tracks.filter(track => {
|
|
// Extract artist from tags
|
|
let artist = '';
|
|
if (track.tags) {
|
|
track.tags.forEach(tag => {
|
|
if (tag[0] === 'artist') {
|
|
artist = tag[1];
|
|
}
|
|
});
|
|
}
|
|
return isAllowedArtist(artist);
|
|
});
|
|
|
|
// Check if we have any filtered tracks
|
|
if (filteredTracks.length === 0) {
|
|
container.innerHTML = '<p style="text-align:center;padding:2rem;color:var(--text-secondary);font-family:\'Poppins\',sans-serif;">No tracks found for the selected artists.</p>';
|
|
return;
|
|
}
|
|
|
|
// Shuffle and get first 6
|
|
const shuffled = filteredTracks.sort(() => 0.5 - Math.random());
|
|
const selected = shuffled.slice(0, 6);
|
|
|
|
// Clear container
|
|
container.innerHTML = '';
|
|
|
|
// Render each track (same as before)
|
|
selected.forEach(track => {
|
|
// Extract data from tags
|
|
let title = '', url = '', image = '', artist = '', album = '',
|
|
trackNumber = '', duration = '', released = '', language = '',
|
|
tags = [], alt = '', content = '';
|
|
|
|
// Parse tags
|
|
if (track.tags) {
|
|
track.tags.forEach(tag => {
|
|
if (tag[0] === 'title') title = tag[1];
|
|
else if (tag[0] === 'url') url = tag[1];
|
|
else if (tag[0] === 'image') image = tag[1];
|
|
else if (tag[0] === 'artist') artist = tag[1];
|
|
else if (tag[0] === 'album') album = tag[1];
|
|
else if (tag[0] === 'track_number') trackNumber = tag[1];
|
|
else if (tag[0] === 'duration') duration = tag[1];
|
|
else if (tag[0] === 'released') released = tag[1];
|
|
else if (tag[0] === 'language') language = tag[1];
|
|
else if (tag[0] === 't') tags.push(tag[1]);
|
|
else if (tag[0] === 'alt') alt = tag[1];
|
|
});
|
|
}
|
|
|
|
content = track.content || '';
|
|
|
|
// Create music card HTML
|
|
const card = document.createElement('div');
|
|
card.className = 'music-card';
|
|
|
|
let cardHTML = `
|
|
${image ? `<img src="${image}" alt="${title}" class="music-image" loading="lazy">` : ''}
|
|
<div class="music-info-header">
|
|
<h4>${title || 'Untitled'}</h4>
|
|
${artist ? `<p class="music-artist">${artist}</p>` : ''}
|
|
${album ? `<p class="music-album">${album}</p>` : ''}
|
|
</div>
|
|
${url ? `<audio controls preload="none"><source src="${url}" type="audio/mpeg">Your browser does not support the audio element.</audio>` : ''}
|
|
<div class="music-details">
|
|
${trackNumber ? `<span class="track-number">Track ${trackNumber}</span>` : ''}
|
|
${duration ? `<span class="duration">${duration}s</span>` : ''}
|
|
${released ? `<span class="released">${released}</span>` : ''}
|
|
${language ? `<span class="language">${language}</span>` : ''}
|
|
</div>
|
|
<div class="music-tags">
|
|
${tags.map(t => `<span class="tag"><span>${t}</span></span>`).join('')}
|
|
</div>
|
|
${content ? `
|
|
<button class="lyrics-toggle"><span>Show Info</span></button>
|
|
<div class="lyrics" style="display: none;">${content.replace(/\n/g, '<br>')}</div>
|
|
` : ''}
|
|
`;
|
|
|
|
card.innerHTML = cardHTML;
|
|
container.appendChild(card);
|
|
});
|
|
|
|
// Re-initialize event listeners for new buttons
|
|
initMusicEventListeners();
|
|
}
|
|
|
|
// Initialize music event listeners
|
|
function initMusicEventListeners() {
|
|
// Lyrics toggle
|
|
document.querySelectorAll('.lyrics-toggle').forEach(button => {
|
|
button.removeEventListener('click', handleLyricsToggle);
|
|
button.addEventListener('click', handleLyricsToggle);
|
|
});
|
|
}
|
|
|
|
function handleLyricsToggle() {
|
|
const lyrics = this.nextElementSibling;
|
|
if (lyrics && lyrics.classList.contains('lyrics')) {
|
|
if (lyrics.style.display === 'none') {
|
|
lyrics.style.display = 'block';
|
|
this.innerHTML = '<span>Hide Info</span>';
|
|
} else {
|
|
lyrics.style.display = 'none';
|
|
this.innerHTML = '<span>Show Info</span>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Refresh music (randomize)
|
|
function refreshMusic() {
|
|
if (window.allMusicData) {
|
|
renderMusicTracks(window.allMusicData, 'musicGrid');
|
|
}
|
|
}
|
|
|
|
// Call this when page loads
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (window.allMusicData && window.allMusicData.length > 0) {
|
|
renderMusicTracks(window.allMusicData, 'musicGrid');
|
|
}
|
|
});
|
|
|
|
// ============ CUSTOM BLURHASH DECODER ============
|
|
// Based on the official blurhash implementation, but compact
|
|
|
|
const Blurhash = {
|
|
// Base83 character set
|
|
chars: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+,;<=>?@[]^_`{|}~',
|
|
|
|
// Decode base83 to integer
|
|
decode83: function(str) {
|
|
let value = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
const c = str[i];
|
|
const digit = this.chars.indexOf(c);
|
|
if (digit === -1) {
|
|
throw new Error('Invalid base83 character: ' + c);
|
|
}
|
|
value = value * 83 + digit;
|
|
}
|
|
return value;
|
|
},
|
|
|
|
// Decode blurhash to pixel data
|
|
decode: function(blurhash, width, height, punch = 1) {
|
|
// Parse metadata
|
|
const sizeFlag = this.decode83(blurhash[0]);
|
|
const numX = (sizeFlag % 9) + 1;
|
|
const numY = Math.floor(sizeFlag / 9) + 1;
|
|
|
|
// Calculate maximum AC component value
|
|
const maxAC = this.decode83(blurhash[1]) + 1;
|
|
|
|
// Decode DC component
|
|
const dcValue = this.decode83(blurhash.slice(2, 6));
|
|
const dc = this.decodeDC(dcValue);
|
|
|
|
// Decode AC components
|
|
const ac = [];
|
|
let acIndex = 6;
|
|
for (let y = 0; y < numY; y++) {
|
|
for (let x = 0; x < numX; x++) {
|
|
if (x === 0 && y === 0) continue;
|
|
const encoded = this.decode83(blurhash.slice(acIndex, acIndex + 2));
|
|
acIndex += 2;
|
|
const acValue = this.decodeAC(encoded, maxAC * punch);
|
|
ac.push({ x, y, value: acValue });
|
|
}
|
|
}
|
|
|
|
// Render pixels
|
|
const pixels = new Uint8ClampedArray(width * height * 4);
|
|
const size = width * height;
|
|
|
|
// Pre-calculate cos values for performance
|
|
const cosX = [];
|
|
const cosY = [];
|
|
for (let x = 0; x < width; x++) {
|
|
cosX[x] = Math.cos(Math.PI * x / width);
|
|
}
|
|
for (let y = 0; y < height; y++) {
|
|
cosY[y] = Math.cos(Math.PI * y / height);
|
|
}
|
|
|
|
for (let i = 0; i < size; i++) {
|
|
const x = i % width;
|
|
const y = Math.floor(i / width);
|
|
let r = dc[0];
|
|
let g = dc[1];
|
|
let b = dc[2];
|
|
|
|
for (const acVal of ac) {
|
|
const basis = cosX[x] * cosY[y];
|
|
const value = acVal.value;
|
|
r += value[0] * basis;
|
|
g += value[1] * basis;
|
|
b += value[2] * basis;
|
|
}
|
|
|
|
const index = i * 4;
|
|
pixels[index] = this.clamp(r + 128);
|
|
pixels[index + 1] = this.clamp(g + 128);
|
|
pixels[index + 2] = this.clamp(b + 128);
|
|
pixels[index + 3] = 255;
|
|
}
|
|
|
|
return pixels;
|
|
},
|
|
|
|
// Decode DC component
|
|
decodeDC: function(value) {
|
|
return [
|
|
this.signPow((value >> 16) & 0x3ff, 2),
|
|
this.signPow((value >> 8) & 0xff, 2),
|
|
this.signPow(value & 0xff, 2)
|
|
];
|
|
},
|
|
|
|
// Decode AC component
|
|
decodeAC: function(value, maxVal) {
|
|
const quantR = Math.floor(value / (19 * 19));
|
|
const quantG = Math.floor((value / 19) % 19);
|
|
const quantB = value % 19;
|
|
return [
|
|
this.signPow((quantR - 9) / 9, 2) * maxVal,
|
|
this.signPow((quantG - 9) / 9, 2) * maxVal,
|
|
this.signPow((quantB - 9) / 9, 2) * maxVal
|
|
];
|
|
},
|
|
|
|
// Sign power function
|
|
signPow: function(val, exp) {
|
|
return Math.sign(val) * Math.pow(Math.abs(val), exp);
|
|
},
|
|
|
|
// Clamp value to 0-255
|
|
clamp: function(value) {
|
|
return Math.min(255, Math.max(0, value));
|
|
},
|
|
|
|
// Render blurhash to canvas
|
|
renderToCanvas: function(blurhash, canvas, punch = 1) {
|
|
if (!blurhash) return false;
|
|
|
|
try {
|
|
const width = canvas.width;
|
|
const height = canvas.height;
|
|
const pixels = this.decode(blurhash, width, height, punch);
|
|
const ctx = canvas.getContext('2d');
|
|
const imageData = ctx.createImageData(width, height);
|
|
imageData.data.set(pixels);
|
|
ctx.putImageData(imageData, 0, 0);
|
|
return true;
|
|
} catch (e) {
|
|
console.warn('Failed to decode blurhash:', e);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// Get blurhash as data URL
|
|
toDataURL: function(blurhash, width = 64, height = 64, punch = 1) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const success = this.renderToCanvas(blurhash, canvas, punch);
|
|
return success ? canvas.toDataURL('image/png') : null;
|
|
}
|
|
};
|
|
|
|
// ============ RANDOMIZED GALLERIES ============
|
|
|
|
// Function to shuffle array (Fisher-Yates)
|
|
function shuffleArray(array) {
|
|
const shuffled = [...array];
|
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
}
|
|
return shuffled;
|
|
}
|
|
|
|
// Render random pictures (6 max)
|
|
|
|
function renderRandomPictures(picturesData, containerId) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
// Shuffle and get first 6
|
|
const shuffled = shuffleArray(picturesData);
|
|
const selected = shuffled.slice(0, 6);
|
|
|
|
// Clear container
|
|
container.innerHTML = '';
|
|
|
|
// Render each picture
|
|
selected.forEach(picture => {
|
|
// Parse imeta tag to get image URL
|
|
let imageUrl = '';
|
|
let alt = picture.content || '';
|
|
|
|
if (picture.tags) {
|
|
picture.tags.forEach(tag => {
|
|
if (tag[0] === 'imeta') {
|
|
const imetaStr = tag[1];
|
|
// Extract URL
|
|
const urlMatch = imetaStr.match(/url ([^\s]+)/);
|
|
if (urlMatch) {
|
|
imageUrl = urlMatch[1];
|
|
}
|
|
// Extract alt
|
|
const altMatch = imetaStr.match(/alt ([^\s]+)/);
|
|
if (altMatch) {
|
|
alt = altMatch[1];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Create picture item
|
|
const item = document.createElement('div');
|
|
item.className = 'photo-item';
|
|
item.dataset.image = imageUrl;
|
|
item.dataset.alt = alt;
|
|
|
|
const date = new Date(picture.created_at * 1000);
|
|
const dateStr = date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
|
|
item.innerHTML = `
|
|
${imageUrl ? `
|
|
<img src="${imageUrl}" alt="${alt}" loading="lazy">
|
|
` : ''}
|
|
<div class="photo-date">
|
|
<span>📅 ${dateStr}</span>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(item);
|
|
});
|
|
|
|
// Re-initialize the custom lightbox for new images
|
|
initCustomLightbox();
|
|
}
|
|
|
|
// Initialize custom lightbox (call this after any DOM updates)
|
|
function initCustomLightbox() {
|
|
const lightbox = document.getElementById('customLightbox');
|
|
if (!lightbox) return;
|
|
|
|
const lightboxImage = document.getElementById('lightboxImage');
|
|
const lightboxCaption = document.getElementById('lightboxCaption');
|
|
const lightboxMeta = document.getElementById('lightboxMeta');
|
|
const lightboxCounter = document.getElementById('lightboxCounter');
|
|
const lightboxClose = document.getElementById('lightboxClose');
|
|
const lightboxPrev = document.getElementById('lightboxPrev');
|
|
const lightboxNext = document.getElementById('lightboxNext');
|
|
|
|
// Get all photo items
|
|
const photoItems = document.querySelectorAll('.photo-item');
|
|
|
|
// Build photo array
|
|
let currentPhotos = [];
|
|
photoItems.forEach(item => {
|
|
const image = item.querySelector('img');
|
|
const date = item.dataset.date || '';
|
|
const pubkey = item.dataset.pubkey || '';
|
|
const alt = item.dataset.alt || '';
|
|
|
|
if (image) {
|
|
currentPhotos.push({
|
|
url: image.src,
|
|
alt: alt,
|
|
date: date,
|
|
pubkey: pubkey
|
|
});
|
|
}
|
|
});
|
|
|
|
let currentIndex = 0;
|
|
|
|
// Remove old event listeners by cloning and replacing
|
|
const newLightboxImage = lightboxImage.cloneNode(true);
|
|
const newLightboxCaption = lightboxCaption.cloneNode(true);
|
|
const newLightboxMeta = lightboxMeta.cloneNode(true);
|
|
const newLightboxCounter = lightboxCounter.cloneNode(true);
|
|
const newLightboxClose = lightboxClose.cloneNode(true);
|
|
const newLightboxPrev = lightboxPrev.cloneNode(true);
|
|
const newLightboxNext = lightboxNext.cloneNode(true);
|
|
|
|
lightboxImage.parentNode.replaceChild(newLightboxImage, lightboxImage);
|
|
lightboxCaption.parentNode.replaceChild(newLightboxCaption, lightboxCaption);
|
|
lightboxMeta.parentNode.replaceChild(newLightboxMeta, lightboxMeta);
|
|
lightboxCounter.parentNode.replaceChild(newLightboxCounter, lightboxCounter);
|
|
lightboxClose.parentNode.replaceChild(newLightboxClose, lightboxClose);
|
|
lightboxPrev.parentNode.replaceChild(newLightboxPrev, lightboxPrev);
|
|
lightboxNext.parentNode.replaceChild(newLightboxNext, lightboxNext);
|
|
|
|
// Update references
|
|
const newImage = document.getElementById('lightboxImage');
|
|
const newCaption = document.getElementById('lightboxCaption');
|
|
const newMeta = document.getElementById('lightboxMeta');
|
|
const newCounter = document.getElementById('lightboxCounter');
|
|
const newClose = document.getElementById('lightboxClose');
|
|
const newPrev = document.getElementById('lightboxPrev');
|
|
const newNext = document.getElementById('lightboxNext');
|
|
|
|
// Open lightbox
|
|
function openLightbox(index) {
|
|
if (index < 0) index = currentPhotos.length - 1;
|
|
if (index >= currentPhotos.length) index = 0;
|
|
currentIndex = index;
|
|
|
|
const photo = currentPhotos[currentIndex];
|
|
newImage.src = photo.url;
|
|
newImage.alt = photo.alt;
|
|
newCaption.textContent = photo.alt;
|
|
newMeta.textContent = `📅 ${photo.date} | 🖊️ ${photo.pubkey}`;
|
|
newCounter.textContent = `${currentIndex + 1} / ${currentPhotos.length}`;
|
|
|
|
lightbox.classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
// Close lightbox
|
|
function closeLightbox() {
|
|
lightbox.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// Navigate
|
|
function prevPhoto() {
|
|
openLightbox(currentIndex - 1);
|
|
}
|
|
|
|
function nextPhoto() {
|
|
openLightbox(currentIndex + 1);
|
|
}
|
|
|
|
// Event listeners for photo clicks
|
|
photoItems.forEach((item, index) => {
|
|
const img = item.querySelector('img');
|
|
if (img) {
|
|
// Remove old listener by cloning
|
|
const newImg = img.cloneNode(true);
|
|
img.parentNode.replaceChild(newImg, img);
|
|
newImg.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
openLightbox(index);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Event listeners for lightbox controls
|
|
newClose.addEventListener('click', closeLightbox);
|
|
newPrev.addEventListener('click', prevPhoto);
|
|
newNext.addEventListener('click', nextPhoto);
|
|
|
|
// Keyboard controls
|
|
document.removeEventListener('keydown', handleLightboxKeydown);
|
|
document.addEventListener('keydown', handleLightboxKeydown);
|
|
|
|
function handleLightboxKeydown(e) {
|
|
if (!lightbox.classList.contains('active')) return;
|
|
|
|
if (e.key === 'Escape') {
|
|
closeLightbox();
|
|
} else if (e.key === 'ArrowLeft') {
|
|
prevPhoto();
|
|
} else if (e.key === 'ArrowRight') {
|
|
nextPhoto();
|
|
}
|
|
}
|
|
|
|
// Click outside image to close
|
|
lightbox.removeEventListener('click', handleLightboxClick);
|
|
lightbox.addEventListener('click', handleLightboxClick);
|
|
|
|
function handleLightboxClick(e) {
|
|
if (e.target === lightbox) {
|
|
closeLightbox();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the DOMContentLoaded event listener
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// ... (your existing code for theme toggle, sticky nav, etc.) ...
|
|
|
|
// Initialize Music (if you already have this)
|
|
if (window.allMusicData && window.allMusicData.length > 0) {
|
|
renderMusicTracks(window.allMusicData, 'musicGrid');
|
|
}
|
|
|
|
// Initialize Pictures
|
|
if (window.allPicturesData && window.allPicturesData.length > 0) {
|
|
renderRandomPictures(window.allPicturesData, 'photoGrid');
|
|
} else {
|
|
// If no data, still initialize lightbox for static content
|
|
initCustomLightbox();
|
|
}
|
|
|
|
// Initialize Videos
|
|
if (window.allVideosData && window.allVideosData.length > 0) {
|
|
renderRandomVideos(window.allVideosData, 'videoGrid');
|
|
}
|
|
});
|
|
|
|
// Refresh functions
|
|
function refreshMusic() {
|
|
if (window.allMusicData) {
|
|
renderMusicTracks(window.allMusicData, 'musicGrid');
|
|
}
|
|
}
|
|
|
|
function refreshPictures() {
|
|
if (window.allPicturesData) {
|
|
renderRandomPictures(window.allPicturesData, 'photoGrid');
|
|
}
|
|
}
|
|
|
|
function refreshVideos() {
|
|
if (window.allVideosData) {
|
|
renderRandomVideos(window.allVideosData, 'videoGrid');
|
|
}
|
|
}
|
|
|
|
// Render random videos (2 max)
|
|
function renderRandomVideos(videosData, containerId) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) return;
|
|
|
|
// Shuffle and get first 2
|
|
const shuffled = shuffleArray(videosData);
|
|
const selected = shuffled.slice(0, 2);
|
|
|
|
// Clear container
|
|
container.innerHTML = '';
|
|
|
|
// Static poster image
|
|
const posterImage = '/images/videoposter.png';
|
|
|
|
|
|
let html = '';
|
|
|
|
selected.forEach(video => {
|
|
let videoUrl = '';
|
|
let mimeType = 'video/mp4';
|
|
let alt = video.content || '';
|
|
let dim = '';
|
|
let isVertical = false;
|
|
|
|
if (video.tags) {
|
|
video.tags.forEach(tag => {
|
|
if (tag[0] === 'imeta') {
|
|
const imetaStr = tag[1];
|
|
const urlMatch = imetaStr.match(/url ([^\s]+)/);
|
|
if (urlMatch) videoUrl = urlMatch[1];
|
|
|
|
const mimeMatch = imetaStr.match(/m ([^\s]+)/);
|
|
if (mimeMatch) mimeType = mimeMatch[1];
|
|
|
|
const altMatch = imetaStr.match(/alt ([^\s]+)/);
|
|
if (altMatch) alt = altMatch[1];
|
|
|
|
const dimMatch = imetaStr.match(/dim ([^\s]+)/);
|
|
if (dimMatch) {
|
|
dim = dimMatch[1];
|
|
const [width, height] = dim.split('x').map(Number);
|
|
if (height > width) isVertical = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const date = new Date(video.created_at * 1000);
|
|
const dateStr = date.toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
});
|
|
|
|
const wrapperStyle = isVertical
|
|
? 'style="position:relative;padding-bottom:177.78%;height:0;overflow:hidden;background:var(--accent-black);"'
|
|
: '';
|
|
|
|
const videoStyle = isVertical
|
|
? 'style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;"'
|
|
: 'style="width:100%;"';
|
|
|
|
html += `
|
|
<div class="video-item">
|
|
${videoUrl ? `
|
|
<div class="video-wrapper" ${wrapperStyle}>
|
|
<video controls preload="none" poster="${posterImage}" ${videoStyle}>
|
|
<source src="${videoUrl}" type="${mimeType}">
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
</div>
|
|
` : ''}
|
|
<div class="video-info">
|
|
<p class="video-caption">${alt}</p>
|
|
<div class="video-meta">
|
|
<span class="video-date">📅 ${dateStr}</span>
|
|
${dim ? `<span class="video-dim">📐 ${dim}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// ============ INITIALIZATION ============
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// ... (your existing code for theme toggle, sticky nav, etc.) ...
|
|
|
|
// Initialize Music (if you already have this)
|
|
if (window.allMusicData && window.allMusicData.length > 0) {
|
|
renderMusicTracks(window.allMusicData, 'musicGrid');
|
|
}
|
|
|
|
// Initialize Pictures
|
|
if (window.allPicturesData && window.allPicturesData.length > 0) {
|
|
renderRandomPictures(window.allPicturesData, 'photoGrid');
|
|
}
|
|
|
|
// Initialize Videos
|
|
if (window.allVideosData && window.allVideosData.length > 0) {
|
|
renderRandomVideos(window.allVideosData, 'videoGrid');
|
|
}
|
|
});
|
|
|
|
// Refresh functions for randomize buttons
|
|
function refreshMusic() {
|
|
if (window.allMusicData) {
|
|
renderMusicTracks(window.allMusicData, 'musicGrid');
|
|
}
|
|
}
|
|
|
|
function refreshPictures() {
|
|
if (window.allPicturesData) {
|
|
renderRandomPictures(window.allPicturesData, 'photoGrid');
|
|
}
|
|
}
|
|
|
|
function refreshVideos() {
|
|
if (window.allVideosData) {
|
|
renderRandomVideos(window.allVideosData, 'videoGrid');
|
|
}
|
|
}
|