Files
slideshow/templates/slideshow.html

371 lines
16 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Booru Slideshow</title>
<style>
body{background:#111;color:#fff;text-align:center;margin:0}
h1{margin-top:2vh}
img{max-width:90vw;max-height:80vh;margin-top:2vh}
#fs-wrap:fullscreen, #fs-wrap:-webkit-full-screen {
width:100vw; height:100vh;
background:#000;
display:flex; align-items:center; justify-content:center;
position:relative;
}
#fs-wrap:fullscreen img, #fs-wrap:-webkit-full-screen img {
max-width:100vw; max-height:100vh;
object-fit:contain; margin:0; cursor:default;
}
#tap-prev, #tap-next {
display:none; position:absolute; top:0; height:100%; width:50%; z-index:10;
}
#tap-prev { left:0; }
#tap-next { right:0; }
#fs-wrap:fullscreen #tap-prev, #fs-wrap:fullscreen #tap-next,
#fs-wrap:-webkit-full-screen #tap-prev, #fs-wrap:-webkit-full-screen #tap-next {
display:block;
}
#controls{margin-top:2vh}
button{padding:.6rem 1.1rem;font-size:1rem;margin:.2rem;border:0;border-radius:.4rem;cursor:pointer}
#quality-toggle{background:#446;color:#ccf}
#quality-toggle.full{background:#464;color:#cfc}
#counter{margin-top:1vh;font-size:1.1rem}
#tag-form{margin-top:1.5vh}
#tag-form input[type=text]{padding:.5rem .8rem;font-size:1rem;border-radius:.4rem;border:0;width:20rem;background:#333;color:#fff}
#tag-form button{background:#444;color:#fff}
.tag-chip{display:inline-block;background:#335;border-radius:1rem;padding:.2rem .7rem;margin:.2rem;font-size:.85rem}
#no-results{font-size:1.4rem;margin-top:10vh;color:#888}
#site-select{padding:.5rem .6rem;font-size:1rem;border-radius:.4rem;border:0;background:#333;color:#fff;margin-left:.4rem}
#view{display:flex;justify-content:center;align-items:flex-start;margin-top:2vh;gap:1rem}
#tag-sidebar{width:180px;max-height:80vh;overflow-y:auto;background:#1a1a1a;border-radius:.5rem;padding:.5rem;flex-shrink:0;text-align:left}
.tag-row{display:flex;align-items:baseline;gap:.3rem}
.tag-row a{color:#aaa;font-size:.8rem;padding:.15rem .3rem;border-radius:.3rem;text-decoration:none;word-break:break-all}
.tag-row a:hover{background:#333;color:#fff}
.tag-row .tag-plus{flex-shrink:0;color:#555;font-size:.9rem;width:.9rem;text-align:center;padding:.15rem .1rem}
.tag-row .tag-plus:hover{color:#8b8;background:none}
.tag-count{font-size:.7rem;color:#555;flex-shrink:0}
#image-area{flex:1;min-width:0;text-align:center;position:relative}
#file-size{position:absolute;top:0;right:0;font-size:.72rem;color:#555;padding:.2rem .4rem;pointer-events:none}
#image-area img{max-width:90%;max-height:80vh}
#progress-box{margin-top:1vh;display:none}
#progress-bar-wrap{width:24rem;height:1rem;background:#333;border-radius:.5rem;display:inline-block;overflow:hidden;vertical-align:middle}
#progress-bar{height:100%;width:0%;background:#5a5;transition:width .2s}
#progress-label{margin-left:.6rem;font-size:.95rem;color:#8b8;vertical-align:middle}
</style>
</head>
<body>
<h1>Booru Slideshow</h1>
<form id="tag-form" method="get" action="">
<input type="text" name="tags" id="tag-input" placeholder="e.g. swimsuit blonde_hair" value="{{ tag_query }}" />
<button type="submit">Search</button>
<select name="_site" id="site-select">
<option value="e621">e621</option>
<option value="konachan">konachan</option>
<option value="yandere">yandere</option>
<option value="rule34">rule34.xxx</option>
</select>
<button type="button" id="get-btn">Get Images</button>
</form>
<div id="progress-box">
<div id="progress-bar-wrap"><div id="progress-bar"></div></div>
<span id="progress-label">Starting...</span>
</div>
{% if active_tags %}
<div>{% for tag in active_tags %}<span class="tag-chip">{{ tag }}</span>{% endfor %}</div>
{% endif %}
{% if images %}
<div id="view">
<div id="tag-sidebar"></div>
<div id="image-area">
<div id="file-size"></div>
<div id="fs-wrap">
<img id="slide" src="{{ preview_images[0] }}" />
<div id="tap-prev"></div>
<div id="tap-next"></div>
</div>
<div id="controls">
<button id="prev">◀︎ Prev</button>
<button id="toggle">Play</button>
<button id="next">Next ▶︎</button>
<button id="fullscreen">⛶ Fullscreen</button>
<button id="shuffle">Shuffle</button>
<button id="quality-toggle">Full Size</button>
<input id="delay" type="number" value="15" style="width:4rem" />
<label for="delay">sec</label>
</div>
<div id="counter">1 / {{ images|length }}</div>
</div>
</div>
{% else %}
<div id="no-results">
{% if tag_query %}No images found for "{{ tag_query }}".{% else %}No images yet — run the downloader first.{% endif %}
</div>
{% endif %}
<script>
document.getElementById('get-btn').onclick = () => {
const tags = document.getElementById('tag-input').value.trim();
const site = document.getElementById('site-select').value;
const form = document.createElement('form');
form.method = 'post';
form.action = 'download';
[['tags', tags], ['site', site]].forEach(([k, v]) => {
const inp = document.createElement('input');
inp.type = 'hidden'; inp.name = k; inp.value = v;
form.appendChild(inp);
});
document.body.appendChild(form);
form.submit();
};
const _site_param = new URLSearchParams(window.location.search).get('_site');
if (_site_param) {
const sel = document.getElementById('site-select');
if (sel) sel.value = _site_param;
}
document.getElementById('site-select').addEventListener('change', function() {
const p = new URLSearchParams(window.location.search);
p.set('_site', this.value);
history.replaceState(null, '', '?' + p.toString());
});
const job_id = new URLSearchParams(window.location.search).get('job_id');
if (job_id) {
const box = document.getElementById('progress-box');
const bar = document.getElementById('progress-bar');
const label = document.getElementById('progress-label');
box.style.display = 'block';
const es = new EventSource(`download/progress/${job_id}`);
let statusText = '';
es.onmessage = (e) => {
const d = JSON.parse(e.data);
if (d.error) { label.textContent = 'Error: ' + d.error; es.close(); return; }
if (d.status) statusText = d.status;
const pct = d.total > 0 ? Math.round(d.done / d.total * 100) : 0;
bar.style.width = pct + '%';
if (d.finished) {
label.innerHTML = `Done! ${d.done} images. <a href="?tags={{ tag_query }}" style="color:#8b8">Refresh</a>`;
es.close();
} else if (d.total > 0) {
label.textContent = (statusText ? statusText + ' — ' : '') + `${d.done} / ${d.total}`;
} else {
label.textContent = statusText || 'Scanning...';
}
};
}
</script>
{% if images %}
<script>
const images = {{ images|tojson }};
const previews = {{ preview_images|tojson }};
const post_urls = {{ post_urls|tojson }};
const tags_list = {{ tags_list|tojson }};
const file_sizes = {{ file_sizes|tojson }};
const preview_sizes = {{ preview_sizes|tojson }};
let use_full = false;
function cur_src(n) { return use_full ? images[n] : previews[n]; }
function cur_size(n) { return use_full ? file_sizes[n] : preview_sizes[n]; }
function fmt_size(b){ return b >= 1048576 ? (b/1048576).toFixed(1)+' MB' : Math.round(b/1024)+' KB'; }
const sidebar = document.getElementById('tag-sidebar');
const current_query = {{ tag_query|tojson }};
const tag_counts = {};
for (const tags of tags_list) {
for (const t of tags) {
tag_counts[t] = (tag_counts[t] || 0) + 1;
}
}
function render_tags(n) {
const tags = tags_list[n] || [];
sidebar.innerHTML = tags.map(t => {
const existing = current_query.trim().split(/\s+/).filter(Boolean);
const combined = [...new Set([...existing, t])].join(' ');
const add_url = '/?tags=' + encodeURIComponent(combined);
const count = tag_counts[t] || 1;
return `<div class="tag-row">` +
`<a class="tag-plus" href="${add_url}" title="Add to query">+</a>` +
`<a href="/?tags=${encodeURIComponent(t)}">${t}</a>` +
`<span class="tag-count">${count}</span>` +
`</div>`;
}).join('');
}
const img = document.getElementById('slide');
const fs_wrap = document.getElementById('fs-wrap');
const btnT = document.getElementById('toggle');
const btnF = document.getElementById('fullscreen');
const btnQ = document.getElementById('quality-toggle');
const counter = document.getElementById('counter');
const file_size_el = document.getElementById('file-size');
const _params = new URLSearchParams(window.location.search);
const _url_idx = parseInt(_params.get('idx') || '0', 10);
let i = (_url_idx > 0 && _url_idx < images.length) ? _url_idx : 0;
let playing=false, timer=null;
let shuffled = false;
let order = [...Array(images.length).keys()];
// zoom/pan state for fullscreen
let z_scale=1, z_tx=0, z_ty=0, z_drag=null;
function reset_zoom(){
z_scale=1; z_tx=0; z_ty=0;
img.style.transform='';
img.style.cursor='';
}
function apply_zoom(){
img.style.transform=`scale(${z_scale}) translate(${z_tx}px,${z_ty}px)`;
img.style.cursor = z_scale>1 ? 'grab' : 'default';
}
img.addEventListener('wheel', e=>{
if(!document.fullscreenElement) return;
e.preventDefault();
const factor = e.deltaY < 0 ? 1.15 : 1/1.15;
z_scale = Math.min(10, Math.max(1, z_scale*factor));
if(z_scale===1){ z_tx=0; z_ty=0; }
apply_zoom();
},{passive:false});
img.addEventListener('mousedown', e=>{
if(!document.fullscreenElement || z_scale<=1) return;
z_drag={x: e.clientX - z_tx*z_scale, y: e.clientY - z_ty*z_scale};
img.style.cursor='grabbing';
});
window.addEventListener('mousemove', e=>{
if(!z_drag) return;
z_tx=(e.clientX-z_drag.x)/z_scale;
z_ty=(e.clientY-z_drag.y)/z_scale;
apply_zoom();
});
window.addEventListener('mouseup', ()=>{
z_drag=null;
if(document.fullscreenElement && z_scale>1) img.style.cursor='grab';
});
let is_fullscreen = false;
let t_start_x = 0, t_start_y = 0;
fs_wrap.addEventListener('touchstart', e => {
t_start_x = e.touches[0].clientX;
t_start_y = e.touches[0].clientY;
}, { passive: true });
fs_wrap.addEventListener('touchend', e => {
if (!is_fullscreen) return;
const dx = e.changedTouches[0].clientX - t_start_x;
const dy = e.changedTouches[0].clientY - t_start_y;
if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 50) {
dx < 0 ? next() : prev();
}
}, { passive: true });
document.getElementById('tap-prev').addEventListener('click', prev);
document.getElementById('tap-next').addEventListener('click', next);
document.addEventListener('fullscreenchange', ()=>{
is_fullscreen = !!document.fullscreenElement;
if(!is_fullscreen) reset_zoom();
});
const PRELOAD_AHEAD = 3;
function preload_next() {
for (let k = 1; k <= PRELOAD_AHEAD; k++) {
const idx = shuffled
? order[(i + k) % order.length]
: (i + k) % images.length;
new Image().src = cur_src(idx);
}
}
function show(n){
if (shuffled) n = order[(n+order.length)%order.length];
else n = (n+images.length)%images.length;
i = n;
img.src = cur_src(n);
counter.textContent = `${i+1} / ${images.length}`;
file_size_el.textContent = fmt_size(cur_size(n));
render_tags(n);
reset_zoom();
preload_next();
const p = new URLSearchParams(window.location.search);
p.set('idx', i);
history.replaceState(null,'','?'+p.toString());
}
// init to url index
img.src = cur_src(i);
counter.textContent = `${i+1} / ${images.length}`;
file_size_el.textContent = fmt_size(cur_size(i));
render_tags(i);
preload_next();
btnQ.onclick = () => {
use_full = !use_full;
btnQ.textContent = use_full ? 'Preview' : 'Full Size';
btnQ.className = use_full ? 'full' : '';
img.src = cur_src(i);
file_size_el.textContent = fmt_size(cur_size(i));
};
function toggleShuffle(){
shuffled = !shuffled;
document.getElementById('shuffle').textContent = shuffled ? 'Unshuffle' : 'Shuffle';
if(shuffled){
order = [...Array(images.length).keys()];
for (let j = order.length - 1; j > 0; j--) {
const k = Math.floor(Math.random() * (j + 1));
[order[j], order[k]] = [order[k], order[j]];
}
}
}
function next(){show(i+1)}
function prev(){show(i-1)}
document.getElementById('next').onclick=next;
document.getElementById('prev').onclick=prev;
document.getElementById('shuffle').onclick = toggleShuffle;
btnT.onclick=()=>{
playing=!playing;
btnT.textContent=playing?'Pause':'Play';
if(playing){
const delaySec = parseFloat(document.getElementById('delay').value) || 15;
timer = setInterval(next, delaySec * 1000);
}
else{
clearInterval(timer);
}
};
btnF.onclick=()=>{
if(!is_fullscreen){
fs_wrap.requestFullscreen().catch(()=>{});
is_fullscreen = true;
}else{
document.exitFullscreen().catch(()=>{});
is_fullscreen = false;
}
};
document.addEventListener('keydown',e=>{
if(e.key==='ArrowRight')next();
if(e.key==='ArrowLeft') prev();
if(e.key==='e' && !window.getSelection().toString() && post_urls[i]){
window.open(post_urls[i], '_blank');
}
});
</script>
{% endif %}
</body>
</html>