"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();
else if (e.key === "e" && images[current_idx]?.source) window.open(images[current_idx].source, "_blank");
});
// ── 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();