big refactor go brrr *wags tail* uwu
patchie notesies >w< : new stuff arf arf!! - unified downloader!! one pawsome script for e621, konachan + yandere - sqlite db saves all imgs + tags so no more icky duplicates woof - api keys live in .env now so they stop being naked in the code omg - requirements.txt so setup is ez pz for good girls web ui glow up *zoomies*!! - tag search replaces boring folder picker yay!! - tag sidebar shows all tags on current pic w/ counts, click to search or + to add uwu - get images button right in the ui!! pick ur site n go!! - live download progress bar via SSE *bouncy bouncy* - press E to open the og post on the booru site hehe - fullscreen zoom + pan w/ scroll wheel + drag drag - reload remembers which pic u were on !! no more jumping to 1 >:( deleted stuff rip!! - Catgirl.py (rip girlie u served well) - get_pics_621/kona/yan.py (all grown up into downloader.py now)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Slideshow</title>
|
||||
<title>Booru Slideshow</title>
|
||||
<style>
|
||||
body{background:#111;color:#fff;text-align:center;margin:0}
|
||||
h1{margin-top:2vh}
|
||||
@@ -13,55 +13,219 @@
|
||||
height:100vh;
|
||||
object-fit:contain;
|
||||
margin:0;
|
||||
cursor:default;
|
||||
}
|
||||
|
||||
#controls{margin-top:2vh}
|
||||
button{padding:.6rem 1.1rem;font-size:1rem;margin:.2rem;border:0;border-radius:.4rem;cursor:pointer}
|
||||
|
||||
#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}
|
||||
#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>Slideshow</h1>
|
||||
<h1>Booru Slideshow</h1>
|
||||
|
||||
<img id="slide" src="{{ images[0] }}"/>
|
||||
|
||||
<div id="controls">
|
||||
<button id="prev">◀︎ Prev</button>
|
||||
<button id="toggle">Start</button>
|
||||
<button id="next">Next ▶︎</button>
|
||||
<button id="fullscreen">⛶ Fullscreen</button>
|
||||
<button id="shuffle">Shuffle</button>
|
||||
<select id="folder-select" onchange="changeFolder(this.value)">
|
||||
{% for folder in folders %}
|
||||
<option value="{{ folder }}" {% if folder == selected %}selected{% endif %}>{{ folder }}</option>
|
||||
{% endfor %}
|
||||
<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>
|
||||
</select>
|
||||
<input id="delay" type="number" min="1" value="15" style="width:4rem" />
|
||||
<label for="delay">sec</label>
|
||||
<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>
|
||||
|
||||
<div id="counter">1 / {{ images|length }}</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">
|
||||
<img id="slide" src="{{ images[0] }}" />
|
||||
<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>
|
||||
<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>
|
||||
const images = {{ images|tojson }};
|
||||
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}`);
|
||||
es.onmessage = (e) => {
|
||||
const d = JSON.parse(e.data);
|
||||
if (d.error) { label.textContent = 'Error: ' + d.error; es.close(); return; }
|
||||
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 {
|
||||
label.textContent = d.total > 0 ? `${d.done} / ${d.total}` : 'Fetching post list...';
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
{% if images %}
|
||||
<script>
|
||||
const images = {{ images|tojson }};
|
||||
const post_urls = {{ post_urls|tojson }};
|
||||
const tags_list = {{ tags_list|tojson }};
|
||||
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 btnT = document.getElementById('toggle');
|
||||
const btnF = document.getElementById('fullscreen');
|
||||
const counter= document.getElementById('counter');
|
||||
let i=0, playing=false, timer=null; //, timer=setInterval(next,15000);
|
||||
|
||||
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';
|
||||
});
|
||||
document.addEventListener('fullscreenchange', ()=>{
|
||||
if(!document.fullscreenElement) reset_zoom();
|
||||
});
|
||||
|
||||
function show(n){
|
||||
if (shuffled) n = order[(n+order.length)%order.length];
|
||||
else n = (n+images.length)%images.length;
|
||||
|
||||
i = images.indexOf(images[n]);
|
||||
i = n;
|
||||
img.src = images[n];
|
||||
counter.textContent = `${i+1} / ${images.length}`;
|
||||
render_tags(n);
|
||||
reset_zoom();
|
||||
const p = new URLSearchParams(window.location.search);
|
||||
p.set('idx', i);
|
||||
history.replaceState(null,'','?'+p.toString());
|
||||
}
|
||||
|
||||
// init to url index
|
||||
img.src = images[i];
|
||||
counter.textContent = `${i+1} / ${images.length}`;
|
||||
render_tags(i);
|
||||
|
||||
function toggleShuffle(){
|
||||
shuffled = !shuffled;
|
||||
document.getElementById('shuffle').textContent = shuffled ? 'Unshuffle' : 'Shuffle';
|
||||
@@ -84,18 +248,18 @@
|
||||
btnT.onclick=()=>{
|
||||
playing=!playing;
|
||||
btnT.textContent=playing?'Pause':'Play';
|
||||
if(playing){
|
||||
if(playing){
|
||||
const delaySec = parseInt(document.getElementById('delay').value, 10) || 15;
|
||||
timer = setInterval(next, delaySec * 1000);
|
||||
}
|
||||
else{
|
||||
else{
|
||||
clearInterval(timer);
|
||||
}
|
||||
};
|
||||
|
||||
btnF.onclick=()=>{
|
||||
if(!document.fullscreenElement){
|
||||
img.requestFullscreen(); // only the picture goes fullscreen
|
||||
img.requestFullscreen();
|
||||
}else{
|
||||
document.exitFullscreen();
|
||||
}
|
||||
@@ -104,11 +268,12 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
function changeFolder(folder){
|
||||
window.location.href = "/?folder=" + encodeURIComponent(folder);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user