289 lines
10 KiB
JavaScript
289 lines
10 KiB
JavaScript
"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 fs_btn = document.getElementById("fs_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 => `<button class="recent_chip" data-q="${escape_html(r)}">${escape_html(r)}</button>`)
|
|
.join("");
|
|
}
|
|
|
|
function escape_html(s) {
|
|
return s.replace(/&/g, "&").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 = `<div class="img_err">image failed to load</div>`;
|
|
};
|
|
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);
|
|
|
|
fs_btn.addEventListener("click", toggle_fullscreen);
|
|
|
|
function toggle_fullscreen() {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen().catch(() => {});
|
|
} else {
|
|
document.exitFullscreen().catch(() => {});
|
|
}
|
|
}
|
|
|
|
document.addEventListener("fullscreenchange", () => {
|
|
fs_btn.innerHTML = document.fullscreenElement ? "✕" : "⛶";
|
|
});
|
|
|
|
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();
|