Gallery
Autoβpulls from Tumblr (photos & videos). Tap any tile.
Load more
β
Close
(function(){ // ====== CONFIG (edit these) ====== const API_KEY = 'RBdwLlVYdFhdIinnwrYhmroUJf9ibkMc1yzl1eCmUsDCNmekEP'; const BLOG = 'artofmalus.tumblr.com'; // blog identifier, e.g. malusgallery.tumblr.com const TAG = ''; // optional, e.g. 'portfolio' const PAGE_SIZE = 20; // how many posts per fetch const PROXY_ORIGIN = ''; // e.g. 'https://wild-wood-4d9c.art-of-malus.workers.dev/' (leave blank to hit Tumblr directly; may be blocked by CORS) // ====== STATE ====== let offset = 0; let loading = false; let totalPosts = Infinity; const grid = document.getElementById('grid'); const lb = document.getElementById('lb'); const lbTitle = document.getElementById('lb-title'); const lbClose = document.getElementById('lb-close'); const moreBtn = document.getElementById('more'); // ====== HELPERS ====== function fmtTitle(post){ if (post.summary) return post.summary; if (post.slug) return post.slug.replace(/-/g,' ').replace(/\b\w/g,c=>c.toUpperCase()); return 'Untitled'; } function pickPhotoSizes(p){ // returns {thumb, full} const pic = p.photos?.[0]; if (!pic) return null; const sizes = pic.alt_sizes || []; const full = pic.original_size?.url || sizes[0]?.url; // pick a ~600px thumb if available const thumb = sizes.sort((a,b)=>b.width-a.width).reverse().find(s=>s.width>=400)?.url || sizes[sizes.length-1]?.url || full; return {thumb, full}; } // Extract first media from Neue Post Format (NPF) posts function extractFromNPF(post){ const blocks = Array.isArray(post.content) ? post.content : []; if (!blocks.length) return null; let mediaBlock = blocks.find(b => b.type === 'image') || blocks.find(b => b.type === 'video'); if (!mediaBlock) return null; if (mediaBlock.type === 'image'){ const media = mediaBlock.media || []; if (!media.length) return null; const sorted = media.slice().sort((a,b)=> (b.width||0)-(a.width||0)); const best = sorted[0]; const thumbCandidate = sorted.find(m => (m.width||0) >= 400) || sorted[sorted.length-1]; return { kind:'photo', full: best?.url, thumb: thumbCandidate?.url }; } else if (mediaBlock.type === 'video'){ const media = mediaBlock.media || []; const best = media.slice().sort((a,b)=> (b.width||0)-(a.width||0))[0]; const poster = (mediaBlock.poster || [])[0]?.url || ''; return { kind:'video', video: best?.url || post.video_url || null, poster, embed: mediaBlock.embed_html || null }; } return null; } function cardForPost(post){ let type = post.type; let cover = ''; let badge = ''; // Handle NPF (text posts with image/video blocks) if ((type !== 'photo' && type !== 'video') && Array.isArray(post.content)){ const npf = extractFromNPF(post); if (npf){ if (npf.kind === 'photo'){ type = 'photo'; post.__npf_full = npf.full; cover = `
`; badge = 'Image'; } else if (npf.kind === 'video'){ type = 'video'; post.__npf_video = npf.video; post.__npf_embed = npf.embed; cover = npf.poster ? `
` : `
`; badge = 'Video'; } } } if (type === 'photo'){ const ph = pickPhotoSizes(post); if (!ph) return null; cover = `
`; badge = 'Image'; } else if (type === 'video'){ const thumb = post.thumbnail_url || ''; cover = thumb ? `
` : `
`; badge = 'Video'; } else { return null; // skip other types } const el = document.createElement('article'); el.className = 'card'; el.innerHTML = `
${cover}
${badge}
${fmtTitle(post)}
β
`; // open lightbox el.querySelector('.cover').addEventListener('click', (e)=>{ e.preventDefault(); openLB(post); }); return el; } function setLoading(x){ loading = x; moreBtn.disabled = x; moreBtn.textContent = x? 'Loadingβ¦' : 'Load more'; } async function fetchPosts(){ if (loading || offset>=totalPosts) return; setLoading(true); const BLOG_ID = BLOG.replace(/^https?:\/\/|\/$/g, ''); const API_HOST = PROXY_ORIGIN || 'https://api.tumblr.com'; const base = `${API_HOST}/v2/blog/${encodeURIComponent(BLOG_ID)}/posts`; const params = new URLSearchParams({ api_key: API_KEY, offset, limit: PAGE_SIZE }); if (TAG) params.set('tag', TAG); // Request Neue Post Format so we can read image/video blocks in modern posts params.set('npf', 'true'); try { const res = await fetch(`${base}?${params.toString()}`); const json = await res.json(); if (json.meta?.status !== 200) throw new Error(json.meta?.msg || 'Tumblr error'); const resp = json.response; totalPosts = resp.total_posts ?? totalPosts; // filter for photos/videos only const items = (resp.posts||[]).filter(p => p.type==='photo' || p.type==='video' || Array.isArray(p.content)); if (!items.length && offset===0){ const msg = document.createElement('div'); msg.style.padding='10px'; msg.style.color='#e7e0d6'; msg.textContent='No photo/video posts found yet.'; grid.appendChild(msg); } items.forEach(p=>{ const card = cardForPost(p); if (card) grid.appendChild(card); }); offset += resp.posts?.length || 0; if (offset>=totalPosts) { moreBtn.disabled = true; moreBtn.textContent = 'All caught up'; } } catch(err){ console.error(err); moreBtn.textContent = 'Error β try again'; const msg = document.createElement('div'); msg.style.padding='10px'; msg.style.color='#e7e0d6'; msg.textContent='Tumblr error: '+(err && err.message ? err.message : String(err)); grid.appendChild(msg); } finally { setLoading(false); } } function openLB(post){ const body = lb.querySelector('.lb-body'); body.innerHTML=''; lbTitle.textContent = fmtTitle(post); if (post.type==='photo'){ const ph = pickPhotoSizes(post); if (!ph && !post.__npf_full) return; const img = document.createElement('img'); img.src = (post.__npf_full || ph.full); img.alt = fmtTitle(post); img.className = 'zoomable'; enableZoomPan(img); body.appendChild(img); } else if (post.type==='video'){ if (post.__npf_video || post.video_url){ const vid = document.createElement('video'); vid.src = (post.__npf_video || post.video_url); vid.controls = true; vid.muted = true; vid.playsInline = true; vid.autoplay = true; vid.loop = true; body.appendChild(vid); } else if (post.__npf_embed){ body.innerHTML = post.__npf_embed; } else if (Array.isArray(post.player) && post.player.length){ // pick the largest embed body.innerHTML = post.player[post.player.length-1].embed_code; } } lb.classList.add('on'); } function closeLB(){ lb.classList.remove('on'); const body = lb.querySelector('.lb-body'); body.innerHTML=''; } lbClose.addEventListener('click', closeLB); lb.addEventListener('click', (e)=>{ if (e.target===lb) closeLB(); }); window.addEventListener('keydown',(e)=>{ if (e.key==='Escape') closeLB(); }); function enableZoomPan(img){ let zoomed=false, startX=0, startY=0, originX=0, originY=0; const body = img.closest('.lb-body'); img.addEventListener('click', ()=>{ zoomed=!zoomed; if (zoomed){ img.classList.remove('zoomable'); img.classList.add('zooming'); img.style.transform='scale(2)'; img.style.transformOrigin='center center'; } else { img.classList.add('zoomable'); img.classList.remove('zooming'); img.style.transform=''; } }); img.addEventListener('pointerdown', (e)=>{ if (!zoomed) return; startX=e.clientX; startY=e.clientY; const r=img.getBoundingClientRect(); originX=(e.clientX - r.left)/r.width*100; originY=(e.clientY - r.top)/r.height*100; img.style.transformOrigin=`${originX}% ${originY}%`; img.setPointerCapture(e.pointerId); }); img.addEventListener('pointermove', (e)=>{ if (!zoomed || e.pressure===0) return; const dx=e.clientX-startX, dy=e.clientY-startY; body.scrollLeft -= dx; body.scrollTop -= dy; startX=e.clientX; startY=e.clientY; }); img.addEventListener('wheel', (e)=>{ if (!zoomed) return; e.preventDefault(); body.scrollLeft += e.deltaY; }, {passive:false}); } // boot fetchPosts(); moreBtn.addEventListener('click', fetchPosts); })();