359 lines
15 KiB
HTML
359 lines
15 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 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>
|