"use strict"; // ── state ────────────────────────────────────────────────────────────────── let images = []; let current_idx = 0; let current_query = ""; let current_page = 0; let is_loading = false; let is_fetching_more = false; // ── elements ─────────────────────────────────────────────────────────────── const search_screen = document.getElementById("search_screen"); const gallery_screen = document.getElementById("gallery_screen"); const search_input = document.getElementById("search_input"); const search_btn = document.getElementById("search_btn"); const recents_section= document.getElementById("recents_section"); const recents_list = document.getElementById("recents_list"); const img_track = document.getElementById("img_track"); const img_counter = document.getElementById("img_counter"); const back_btn = document.getElementById("back_btn"); const arrow_left = document.getElementById("arrow_left"); const arrow_right = document.getElementById("arrow_right"); const loading_bar = document.getElementById("loading_bar"); const toast_el = document.getElementById("toast"); // ── recents ──────────────────────────────────────────────────────────────── function load_recents() { return JSON.parse(localStorage.getItem("diashow_recents") || "[]"); } function save_recent(q) { let recents = load_recents().filter(r => r !== q); recents.unshift(q); recents = recents.slice(0, 8); localStorage.setItem("diashow_recents", JSON.stringify(recents)); } function render_recents() { const recents = load_recents(); if (!recents.length) { recents_section.style.display = "none"; return; } recents_section.style.display = "block"; recents_list.innerHTML = recents .map(r => ``) .join(""); } function escape_html(s) { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } // ── loading bar ──────────────────────────────────────────────────────────── let bar_timer = null; function show_loading() { loading_bar.style.width = "0%"; loading_bar.classList.add("active"); loading_bar.style.transition = "none"; requestAnimationFrame(() => { loading_bar.style.transition = "width 1.5s ease-out"; loading_bar.style.width = "75%"; }); } function hide_loading() { loading_bar.style.transition = "width 0.2s ease"; loading_bar.style.width = "100%"; clearTimeout(bar_timer); bar_timer = setTimeout(() => { loading_bar.classList.remove("active"); loading_bar.style.width = "0%"; }, 250); } // ── toast ────────────────────────────────────────────────────────────────── let toast_timer = null; function show_toast(msg) { toast_el.textContent = msg; toast_el.classList.add("show"); clearTimeout(toast_timer); toast_timer = setTimeout(() => toast_el.classList.remove("show"), 3000); } // ── API ──────────────────────────────────────────────────────────────────── async function fetch_images(query, page = 0) { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&page=${page}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (data.error) throw new Error(data.error); return data.images || []; } // ── search ───────────────────────────────────────────────────────────────── async function do_search(q) { if (!q.trim() || is_loading) return; q = q.trim(); is_loading = true; show_loading(); search_btn.disabled = true; try { const results = await fetch_images(q); if (!results.length) { show_toast("no images found :/"); return; } save_recent(q); current_query = q; current_page = 0; images = results; current_idx = 0; open_gallery(); } catch (e) { show_toast("fetch failed — " + e.message); } finally { is_loading = false; hide_loading(); search_btn.disabled = false; } } // ── gallery ──────────────────────────────────────────────────────────────── function make_slide(img_data, idx) { const slide = document.createElement("div"); slide.className = "img_slide"; slide.dataset.idx = idx; const el = document.createElement("img"); el.alt = ""; el.loading = "lazy"; el.src = img_data.url; el.onerror = () => { slide.innerHTML = `
image failed to load
`; }; slide.appendChild(el); return slide; } function open_gallery() { img_track.innerHTML = ""; images.forEach((img, i) => img_track.appendChild(make_slide(img, i))); search_screen.style.display = "none"; gallery_screen.classList.add("active"); go_to(0, false); document.body.style.overflow = "hidden"; } function close_gallery() { gallery_screen.classList.remove("active"); search_screen.style.display = ""; document.body.style.overflow = ""; render_recents(); } function update_counter() { img_counter.textContent = `${current_idx + 1} / ${images.length}`; } function go_to(idx, animate = true) { if (!animate) img_track.style.transition = "none"; else img_track.style.transition = ""; current_idx = Math.max(0, Math.min(idx, images.length - 1)); img_track.style.transform = `translateX(-${current_idx * 100}%)`; update_counter(); // reset transition after instant move if (!animate) requestAnimationFrame(() => img_track.style.transition = ""); // load more when 3 images from the end if (current_idx >= images.length - 3) load_more(); } async function load_more() { if (is_fetching_more) return; is_fetching_more = true; try { const next_page = current_page + 1; const more = await fetch_images(current_query, next_page); if (more.length) { current_page = next_page; more.forEach(img => { const slide = make_slide(img, images.length); img_track.appendChild(slide); images.push(img); }); update_counter(); } } catch (_) { // silently ignore load-more failures } finally { is_fetching_more = false; } } function next_image() { if (current_idx < images.length - 1) go_to(current_idx + 1); } function prev_image() { if (current_idx > 0) go_to(current_idx - 1); } // ── touch / swipe ────────────────────────────────────────────────────────── let touch_start_x = 0; let touch_start_y = 0; let touch_dx = 0; let is_swiping = false; gallery_screen.addEventListener("touchstart", e => { touch_start_x = e.touches[0].clientX; touch_start_y = e.touches[0].clientY; touch_dx = 0; is_swiping = false; }, { passive: true }); gallery_screen.addEventListener("touchmove", e => { const dx = e.touches[0].clientX - touch_start_x; const dy = e.touches[0].clientY - touch_start_y; if (!is_swiping) { // lock to horizontal if angle is mostly horizontal if (Math.abs(dx) < Math.abs(dy)) return; is_swiping = true; } e.preventDefault(); touch_dx = dx; // live drag feel const base = -current_idx * 100; const drag = (dx / window.innerWidth) * 100; img_track.style.transition = "none"; img_track.style.transform = `translateX(calc(${base}% + ${dx}px))`; }, { passive: false }); gallery_screen.addEventListener("touchend", () => { if (!is_swiping) return; const threshold = window.innerWidth * 0.2; if (touch_dx < -threshold) next_image(); else if (touch_dx > threshold) prev_image(); else go_to(current_idx); // snap back is_swiping = false; touch_dx = 0; }, { passive: true }); // ── keyboard ─────────────────────────────────────────────────────────────── document.addEventListener("keydown", e => { if (!gallery_screen.classList.contains("active")) return; if (e.key === "ArrowRight") next_image(); else if (e.key === "ArrowLeft") prev_image(); else if (e.key === "Escape") close_gallery(); }); // ── event listeners ──────────────────────────────────────────────────────── search_btn.addEventListener("click", () => do_search(search_input.value)); search_input.addEventListener("keydown", e => { if (e.key === "Enter") do_search(search_input.value); }); back_btn.addEventListener("click", close_gallery); arrow_left.addEventListener("click", prev_image); arrow_right.addEventListener("click", next_image); recents_list.addEventListener("click", e => { const chip = e.target.closest(".recent_chip"); if (!chip) return; const q = chip.dataset.q; search_input.value = q; do_search(q); }); // ── init ─────────────────────────────────────────────────────────────────── render_recents(); search_input.focus();