initial diashow webapp
This commit is contained in:
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
CMD ["python", "-u", "app.py"]
|
||||
91
app.py
Normal file
91
app.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import re
|
||||
import random
|
||||
import requests
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
|
||||
app = Flask(__name__, static_folder="static")
|
||||
|
||||
USER_AGENTS = [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
]
|
||||
|
||||
def get_headers():
|
||||
return {
|
||||
"User-Agent": random.choice(USER_AGENTS),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"DNT": "1",
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
}
|
||||
|
||||
def scrape_images(query, page=0):
|
||||
url = "https://www.google.com/search"
|
||||
params = {
|
||||
"q": query,
|
||||
"tbm": "isch",
|
||||
"ijn": str(page),
|
||||
"start": str(page * 20),
|
||||
"asearch": "ichunk",
|
||||
"async": "_id:rg_s,_pms:s,_fmt:pc",
|
||||
}
|
||||
try:
|
||||
resp = requests.get(url, params=params, headers=get_headers(), timeout=10)
|
||||
resp.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
return [], str(e)
|
||||
|
||||
# Extract original image URLs: Google embeds them as "ou":"https://..."
|
||||
ou_matches = re.findall(r'"ou"\s*:\s*"(https?://[^"]+)"', resp.text)
|
||||
|
||||
# Fallback: look for image URLs in JSON arrays ["https://...", width, height]
|
||||
if not ou_matches:
|
||||
ou_matches = re.findall(
|
||||
r'(?:^|[^"])(https?://(?!encrypted-tbn)[^"\'\\]+\.(?:jpg|jpeg|png|gif|webp)(?:\?[^"\'\\]*)?)',
|
||||
resp.text,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
seen = set()
|
||||
images = []
|
||||
for img_url in ou_matches:
|
||||
if img_url in seen:
|
||||
continue
|
||||
seen.add(img_url)
|
||||
# skip google's own thumbnails / tracking pixels
|
||||
if "google.com" in img_url or "gstatic.com" in img_url:
|
||||
continue
|
||||
images.append({"url": img_url})
|
||||
if len(images) >= 20:
|
||||
break
|
||||
|
||||
return images, None
|
||||
|
||||
|
||||
@app.route("/api/search")
|
||||
def search():
|
||||
q = request.args.get("q", "").strip()
|
||||
page = max(0, int(request.args.get("page", 0)))
|
||||
if not q:
|
||||
return jsonify({"error": "missing query"}), 400
|
||||
images, err = scrape_images(q, page)
|
||||
if err:
|
||||
return jsonify({"error": err}), 502
|
||||
return jsonify({"images": images, "page": page, "query": q})
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return send_from_directory("static", "index.html")
|
||||
|
||||
|
||||
@app.route("/<path:path>")
|
||||
def static_files(path):
|
||||
return send_from_directory("static", path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
diashow:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- caddy
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
flask==3.0.3
|
||||
requests==2.32.3
|
||||
272
static/app.js
Normal file
272
static/app.js
Normal file
@@ -0,0 +1,272 @@
|
||||
"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 => `<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();
|
||||
});
|
||||
|
||||
// ── 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();
|
||||
56
static/index.html
Normal file
56
static/index.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<title>diashow</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="loading_bar"></div>
|
||||
<div id="toast"></div>
|
||||
|
||||
<!-- SEARCH SCREEN -->
|
||||
<div id="search_screen">
|
||||
<div class="logo">dia<span>show</span></div>
|
||||
|
||||
<div class="search_row">
|
||||
<input
|
||||
id="search_input"
|
||||
type="search"
|
||||
placeholder="search images..."
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
enterkeyhint="search"
|
||||
/>
|
||||
<button id="search_btn">go</button>
|
||||
</div>
|
||||
|
||||
<div class="recents" id="recents_section" style="display:none">
|
||||
<div class="recents_label">recent</div>
|
||||
<div class="recents_list" id="recents_list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GALLERY SCREEN -->
|
||||
<div id="gallery_screen">
|
||||
<div id="img_track"></div>
|
||||
|
||||
<div id="gallery_overlay">
|
||||
<div id="gallery_top">
|
||||
<button id="back_btn">←</button>
|
||||
<span id="img_counter"></span>
|
||||
</div>
|
||||
<button id="arrow_left">‹</button>
|
||||
<button id="arrow_right">›</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
297
static/style.css
Normal file
297
static/style.css
Normal file
@@ -0,0 +1,297 @@
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #0a0a0a;
|
||||
--surface: #161616;
|
||||
--surface2: #222;
|
||||
--accent: #e8e8e8;
|
||||
--muted: #666;
|
||||
--radius: 12px;
|
||||
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--accent);
|
||||
font-family: var(--font);
|
||||
overflow: hidden;
|
||||
touch-action: pan-y;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ── SEARCH SCREEN ── */
|
||||
#search_screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
color: var(--muted);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.search_row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#search_input {
|
||||
flex: 1;
|
||||
background: var(--surface);
|
||||
border: 1px solid #333;
|
||||
border-radius: var(--radius);
|
||||
color: var(--accent);
|
||||
font-size: 1rem;
|
||||
padding: 14px 16px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
#search_input:focus {
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
#search_input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#search_btn {
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
#search_btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.recents {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.recents_label {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.recents_list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.recent_chip {
|
||||
background: var(--surface2);
|
||||
border: 1px solid #333;
|
||||
border-radius: 999px;
|
||||
color: var(--accent);
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
|
||||
.recent_chip:active {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
/* ── GALLERY SCREEN ── */
|
||||
#gallery_screen {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#gallery_screen.active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* image container with slide transition */
|
||||
#img_track {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.img_slide {
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.img_slide img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.img_slide .img_err {
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* overlay UI */
|
||||
#gallery_overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#gallery_top {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#back_btn {
|
||||
background: rgba(0,0,0,0.5);
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
font-size: 1.2rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
#back_btn:active {
|
||||
background: rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
#img_counter {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255,255,255,0.8);
|
||||
background: rgba(0,0,0,0.4);
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* desktop nav arrows */
|
||||
#arrow_left, #arrow_right {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0,0,0,0.45);
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
color: #fff;
|
||||
font-size: 1.4rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: all;
|
||||
transition: background 0.15s, opacity 0.15s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#gallery_screen:hover #arrow_left,
|
||||
#gallery_screen:hover #arrow_right {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#arrow_left { left: 16px; }
|
||||
#arrow_right { right: 16px; }
|
||||
|
||||
#arrow_left:active, #arrow_right:active {
|
||||
background: rgba(0,0,0,0.75);
|
||||
}
|
||||
|
||||
/* loading indicator */
|
||||
#loading_bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
background: #fff;
|
||||
width: 0%;
|
||||
transition: width 0.3s;
|
||||
z-index: 100;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#loading_bar.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* error toast */
|
||||
#toast {
|
||||
position: fixed;
|
||||
bottom: 32px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(60px);
|
||||
background: #c0392b;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
z-index: 200;
|
||||
transition: transform 0.25s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#toast.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
/* mobile: hide arrows */
|
||||
@media (hover: none) {
|
||||
#arrow_left, #arrow_right { display: none; }
|
||||
}
|
||||
Reference in New Issue
Block a user