<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<title>Tiles Grid + Pinch Zoom + PHP Gallery</title>
<style>
:root{ --bg:#121212; --bg2:#1e1e1e; --card:#2c2c2c; --ink:#eee; --ink2:#bbb; --accent:#4fc3f7; }
*{ box-sizing:border-box }
body{ margin:0; font-family:system-ui,ui-sans-serif; background:var(--bg); color:var(--ink); height:100vh; display:flex; flex-direction:column; }
.topbar{ display:flex; gap:0; background:var(--bg2); box-shadow:0 2px 8px rgba(0,0,0,.5); z-index:10; }
.topbar button{ flex:1; background:none; border:none; color:var(--ink); padding:.9rem 0; font-size:1.4rem; cursor:pointer; }
.topbar button:hover{ color:var(--accent); background:#262626; }
.panel{ max-height:0; overflow:hidden; background:#1b1b1b; transition:max-height .35s ease; border-bottom:1px solid #242424; }
.panel.open{ max-height:360px; }
/* Settings (steppers) */
.settings-track{ display:flex; gap:1rem; overflow-x:auto; padding:.6rem 1rem; }
.stepper{ flex:0 0 auto; background:var(--card); border:1px solid #2a2a2a; border-radius:.6rem; padding:.5rem; min-width:122px; text-align:center; }
.stepper label{ display:block; font-size:.8rem; color:var(--ink2); margin-bottom:.35rem; }
.stepper-controls{ display:flex; align-items:center; background:var(--bg2); border-radius:.5rem; overflow:hidden; }
.stepper button{ background:#333; border:none; color:var(--ink); font-size:1.1rem; padding:.35rem 0; width:34%; cursor:pointer; }
.stepper button:hover{ background:var(--accent); color:#000; }
.stepper input{ flex:1; background:var(--bg2); border:none; color:var(--ink); text-align:center; padding:.25rem 0; font-size:1rem; width:56px; }
/* Gallery from PHP */
.folder-bar{ display:flex; align-items:center; gap:.5rem; padding:.6rem 1rem; border-bottom:1px solid #242424; }
.crumbs{ display:flex; flex-wrap:wrap; gap:.4rem; font-size:.9rem; }
.crumbs button{ background:none; border:none; color:var(--accent); cursor:pointer; padding:.2rem .3rem; }
.crumbs .sep{ color:#666; }
.folders-track, .gallery-track{ display:flex; gap:.8rem; overflow-x:auto; padding:.8rem 1rem; }
.folder-card{ flex:0 0 auto; display:flex; align-items:center; gap:.6rem; background:var(--card); border:1px solid #2a2a2a; border-radius:.7rem; padding:.55rem .75rem; cursor:pointer; }
.folder-card:hover{ border-color:var(--accent); background:#2f2f2f; }
.gallery-track img{ width:220px; height:150px; object-fit:cover; border-radius:.8rem; background:#333; flex-shrink:0; border:1px solid #2a2a2a; cursor:pointer; }
.gallery-track img:hover{ border-color:var(--accent); }
.empty-hint{ color:#aaa; font-size:.9rem; padding:0 1rem .8rem 1rem; }
/* Stage */
.content{ flex:1; display:flex; flex-direction:column; gap:.6rem; padding:1rem; }
.meta{ font-size:.9rem; color:#cfcfcf; opacity:.9; }
.stage{
position:relative; flex:1; background:#0f0f0f; border:1px solid #2a2a2a; border-radius:.8rem; overflow:hidden;
touch-action:none;
}
.viewport{ position:absolute; left:0; top:0; transform-origin:0 0; will-change: transform; }
.img-layer{ position:absolute; left:0; top:0; }
.img-layer img{ display:block; image-rendering:pixelated; }
.grid{ position:absolute; left:0; top:0; pointer-events:none; }
.tile{ position:absolute; border:1px solid red; box-sizing:border-box; }
</style>
</head>
<body>
<div class="topbar">
<button id="settingsBtn" title="Settings">โ๏ธ</button>
<button id="imagesBtn" title="Images">๐ผ๏ธ</button>
<button id="resetBtn" title="Reset view">๐</button>
</div>
<!-- Settings panel -->
<div class="panel" id="settingsPanel">
<div class="settings-track">
<div class="stepper" data-key="tileWidth"><label>tileWidth</label>
<div class="stepper-controls"><button>-</button><input type="number" value="16"><button>+</button></div>
</div>
<div class="stepper" data-key="tileHeight"><label>tileHeight</label>
<div class="stepper-controls"><button>-</button><input type="number" value="16"><button>+</button></div>
</div>
<div class="stepper" data-key="xOffset"><label>xOffset</label>
<div class="stepper-controls"><button>-</button><input type="number" value="0"><button>+</button></div>
</div>
<div class="stepper" data-key="yOffset"><label>yOffset</label>
<div class="stepper-controls"><button>-</button><input type="number" value="0"><button>+</button></div>
</div>
<div class="stepper" data-key="Hspacing"><label>Hspacing</label>
<div class="stepper-controls"><button>-</button><input type="number" value="0"><button>+</button></div>
</div>
<div class="stepper" data-key="Vspacing"><label>Vspacing</label>
<div class="stepper-controls"><button>-</button><input type="number" value="0"><button>+</button></div>
</div>
</div>
</div>
<!-- Images panel (PHP-driven) -->
<div class="panel" id="imagesPanel">
<div class="folder-bar">
<strong style="margin-right:.5rem">๐</strong>
<div class="crumbs" id="crumbs"></div>
<div style="flex:1"></div>
<button id="goUpBtn" title="Up one level" style="background:none;border:1px solid #2a2a2a;border-radius:.4rem;padding:.35rem .6rem;color:var(--ink)">โฌ๏ธ Up</button>
<button id="refreshBtn" title="Refresh" style="margin-left:.4rem;background:none;border:1px solid #2a2a2a;border-radius:.4rem;padding:.35rem .6rem;color:var(--ink)">๐</button>
</div>
<div class="folders-track" id="foldersTrack"></div>
<div class="empty-hint" id="emptyFolders" hidden>No folders here.</div>
<div class="gallery-track" id="galleryTrack"></div>
<div class="empty-hint" id="emptyImages" hidden>No images in this folder.</div>
</div>
<div class="content">
<div class="meta" id="meta">No image loaded</div>
<div class="stage" id="stage">
<div class="viewport" id="viewport">
<div class="img-layer" id="imgLayer"></div>
<div class="grid" id="grid"></div>
</div>
</div>
</div>
<script>
/* ---------- Panels ---------- */
const settingsBtn = document.getElementById('settingsBtn');
const imagesBtn = document.getElementById('imagesBtn');
const resetBtn = document.getElementById('resetBtn');
const settingsPanel = document.getElementById('settingsPanel');
const imagesPanel = document.getElementById('imagesPanel');
function closeAll(){ settingsPanel.classList.remove('open'); imagesPanel.classList.remove('open'); }
function toggle(p){ const isOpen = p.classList.contains('open'); closeAll(); if(!isOpen) p.classList.add('open'); }
settingsBtn.addEventListener('click', ()=> toggle(settingsPanel));
imagesBtn .addEventListener('click', ()=> { toggle(imagesPanel); if (!imagesPanel.dataset.inited){ imagesPanel.dataset.inited='1'; openSub(''); } });
/* ---------- Settings / steppers ---------- */
const defaults = { tileWidth:16, tileHeight:16, xOffset:0, yOffset:0, Hspacing:0, Vspacing:0 };
let settings = {...defaults};
document.querySelectorAll('.stepper').forEach(st=>{
const key = st.dataset.key;
const input = st.querySelector('input');
const [minus, plus] = st.querySelectorAll('button');
input.value = settings[key];
const stepSize = (key === 'tileWidth' || key === 'tileHeight') ? 8 : 1;
const commit = ()=>{
let v = parseInt(input.value)||0;
if (key === 'tileWidth' || key === 'tileHeight') v = Math.max(1, Math.round(v/8)*8);
else v = Math.max(0, v);
settings[key] = v; input.value = v; drawGrid();
};
minus.addEventListener('click', ()=>{ input.value = (parseInt(input.value)||0) - stepSize; commit(); });
plus .addEventListener('click', ()=>{ input.value = (parseInt(input.value)||0) + stepSize; commit(); });
input.addEventListener('change', commit);
});
/* ---------- Image & Grid ---------- */
const imgLayer = document.getElementById('imgLayer');
const gridEl = document.getElementById('grid');
const meta = document.getElementById('meta');
let imgEl = null, imgW = 0, imgH = 0;
function setImage(src){
const img = new Image();
img.decoding = 'async';
img.onload = ()=>{
imgW = img.naturalWidth; imgH = img.naturalHeight;
imgLayer.innerHTML = '';
img.style.position='absolute'; img.style.left='0px'; img.style.top='0px';
img.style.width = imgW+'px'; img.style.height = imgH+'px';
imgLayer.appendChild(img);
gridEl.style.width = imgW+'px'; gridEl.style.height = imgH+'px';
meta.textContent = `Image: ${imgW}ร${imgH} ยท pinch to zoom, drag to pan, double-tap reset`;
drawGrid(); centerView();
};
img.src = src;
imgEl = img;
}
function drawGrid(){
if (!imgW || !imgH) { gridEl.innerHTML=''; return; }
const tw = settings.tileWidth|0, th = settings.tileHeight|0;
const ox = settings.xOffset|0, oy = settings.yOffset|0;
const hs = settings.Hspacing|0, vs = settings.Vspacing|0;
gridEl.innerHTML = '';
for (let y = oy; y + th <= imgH; y += th + vs){
for (let x = ox; x + tw <= imgW; x += tw + hs){
const d = document.createElement('div');
d.className = 'tile';
d.style.left = x + 'px';
d.style.top = y + 'px';
d.style.width = tw + 'px';
d.style.height = th + 'px';
gridEl.appendChild(d);
}
}
}
/* ---------- Pinch-zoom + pan ---------- */
const stage = document.getElementById('stage');
const viewport = document.getElementById('viewport');
let scale = 1, tx = 0, ty = 0;
const MIN_SCALE = 0.25, MAX_SCALE = 16;
const pts = new Map();
function applyTransform(){ viewport.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`; }
function centerView(){
if(!imgW || !imgH) return;
const r = stage.getBoundingClientRect();
scale = Math.min( Math.max(1, Math.min(r.width/imgW, r.height/imgH)), 2 );
tx = (r.width - imgW*scale)/2;
ty = (r.height - imgH*scale)/2;
applyTransform();
}
resetBtn.addEventListener('click', centerView);
function dist(a,b){ const dx=a.x-b.x, dy=a.y-b.y; return Math.hypot(dx,dy); }
function screenToWorld(x,y){
const r = stage.getBoundingClientRect();
const sx = x - r.left, sy = y - r.top;
return { wx:(sx - tx)/scale, wy:(sy - ty)/scale };
}
let lastPan=null, pinchRef=null;
stage.addEventListener('pointerdown', e=>{
stage.setPointerCapture(e.pointerId); pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
if (pts.size===1) lastPan={x:e.clientX,y:e.clientY};
else if (pts.size===2){ const [a,b]=Array.from(pts.values()); pinchRef={ d0:dist(a,b), m0:{x:(a.x+b.x)/2,y:(a.y+b.y)/2}, s0:scale }; }
});
stage.addEventListener('pointermove', e=>{
if(!pts.has(e.pointerId)) return; pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
if (pts.size===1 && lastPan){ const p=pts.get(e.pointerId); tx += p.x-lastPan.x; ty += p.y-lastPan.y; lastPan={x:p.x,y:p.y}; applyTransform(); }
else if (pts.size===2 && pinchRef){ const [a,b]=Array.from(pts.values()); const d1=dist(a,b); if(pinchRef.d0>0){
const world = screenToWorld(pinchRef.m0.x, pinchRef.m0.y);
let newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, pinchRef.s0 * (d1/pinchRef.d0)));
const r = stage.getBoundingClientRect(); const sx=pinchRef.m0.x - r.left; const sy=pinchRef.m0.y - r.top;
tx = sx - world.wx * newScale; ty = sy - world.wy * newScale; scale = newScale; applyTransform();
}}
});
function endPointer(e){ try{stage.releasePointerCapture(e.pointerId);}catch{} pts.delete(e.pointerId); if(pts.size<2) pinchRef=null; if(pts.size===0) lastPan=null; }
stage.addEventListener('pointerup', endPointer);
stage.addEventListener('pointercancel', endPointer);
stage.addEventListener('pointerleave', e=>{ if(pts.has(e.pointerId)) endPointer(e); });
let lastTap=0; stage.addEventListener('pointerdown', e=>{ const now=performance.now(); if(now-lastTap<300) centerView(); lastTap=now; }, {capture:true});
stage.addEventListener('wheel', e=>{
if(!imgW||!imgH) return; e.preventDefault();
const k = Math.exp(-e.deltaY * 0.0015);
const r = stage.getBoundingClientRect(); const sx=e.clientX - r.left; const sy=e.clientY - r.top;
const world = screenToWorld(e.clientX, e.clientY);
let newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale * k));
tx = sx - world.wx * newScale; ty = sy - world.wy * newScale; scale = newScale; applyTransform();
},{passive:false});
/* ---------- PHP gallery wiring ---------- */
const crumbsEl = document.getElementById('crumbs');
const foldersTrack = document.getElementById('foldersTrack');
const emptyFolders = document.getElementById('emptyFolders');
const galleryTrack = document.getElementById('galleryTrack');
const emptyImages = document.getElementById('emptyImages');
const goUpBtn = document.getElementById('goUpBtn');
const refreshBtn = document.getElementById('refreshBtn');
let currentSub = '';
async function fetchDir(sub=''){
const res = await fetch(`media.php?sub=${encodeURIComponent(sub)}`, {cache:'no-store'});
if(!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json();
}
function renderBreadcrumb(bc){
crumbsEl.innerHTML=''; bc.forEach((c,i)=>{ const b=document.createElement('button'); b.textContent=c.label; b.onclick=()=>openSub(c.sub); crumbsEl.appendChild(b);
if(i<bc.length-1){ const s=document.createElement('span'); s.className='sep'; s.textContent='โบ'; crumbsEl.appendChild(s);} });
}
function renderFolders(items){
foldersTrack.innerHTML=''; emptyFolders.hidden = !!items.length;
for(const f of items){ const card=document.createElement('div'); card.className='folder-card';
card.innerHTML=`<span>๐</span><span>${f.name}</span>`; card.onclick=()=>openSub(f.sub); foldersTrack.appendChild(card); }
}
function renderImages(items){
galleryTrack.innerHTML=''; emptyImages.hidden = !!items.length;
for(const im of items){ const img=document.createElement('img'); img.alt=im.name; img.loading='lazy'; img.decoding='async'; img.src=im.url;
img.onclick=()=> setImage(im.url); galleryTrack.appendChild(img); }
}
async function openSub(sub){
try{ const data = await fetchDir(sub); currentSub = data.cwd||''; renderBreadcrumb(data.breadcrumb||[]); renderFolders(data.folders||[]); renderImages(data.images||[]); }
catch(err){ console.error(err); alert('Failed to load folder.'); }
}
function parentOf(sub){ if(!sub) return ''; const p=sub.split('/').filter(Boolean); p.pop(); return p.join('/'); }
goUpBtn .addEventListener('click', ()=> openSub(parentOf(currentSub)));
refreshBtn .addEventListener('click', ()=> openSub(currentSub));
/* ---------- Boot ---------- */
document.addEventListener('DOMContentLoaded', ()=>{
// Optional: auto-open PHP gallery root
openSub('');
});
</script>
</body>
</html>