From a4f9c48e132f8d19ef98cd82ad42c7a5af6491e5 Mon Sep 17 00:00:00 2001 From: eins Date: Fri, 23 Jan 2026 08:44:46 +0000 Subject: [PATCH 1/3] various fixes to get it working for myself --- debug/check_last_item.mjs | 15 ++++ debug/trigger.mjs | 14 +-- init.sh | 3 + src/inc/trigger/parser.mjs | 178 ++++++++++++++++++++----------------- 4 files changed, 122 insertions(+), 88 deletions(-) create mode 100644 debug/check_last_item.mjs create mode 100755 init.sh diff --git a/debug/check_last_item.mjs b/debug/check_last_item.mjs new file mode 100644 index 0000000..40e9c59 --- /dev/null +++ b/debug/check_last_item.mjs @@ -0,0 +1,15 @@ +import db from "../src/inc/sql.mjs"; + +(async () => { + try { + const item = (await db`select * from items order by id desc limit 1`)?.[0]; + console.log("Last Item:", item); + if (item) { + const tags = await db`select * from tags_assign where item_id = ${item.id}`; + console.log("Tags:", tags); + } + } catch (err) { + console.error(err); + } + process.exit(0); +})(); diff --git a/debug/trigger.mjs b/debug/trigger.mjs index 63e899c..3adf86d 100644 --- a/debug/trigger.mjs +++ b/debug/trigger.mjs @@ -6,13 +6,14 @@ import { promises as fs } from "fs"; network: "console", message: _args.join(" "), args: _args.slice(1), - channel: "console", + channel: "#w0bm", user: { prefix: "console!console@console", nick: "console", username: "console", account: "console" }, + raw: {}, reply: (...args) => console.log(args), replyAction: (...args) => console.log(args), replyNotice: (...args) => console.log(args) @@ -24,11 +25,14 @@ import { promises as fs } from "fs"; )).filter(t => t[0].call.test(_e.message)).map(t => ({ name: t[0].name, f: t[0].f })); try { - if(trigger.length === 0) + if (trigger.length === 0) return console.error("no matches"); - console.log(`triggered > ${trigger[0].name} (${_e.message})`); - await trigger[0].f(_e); - } catch(err) { + for (const t of trigger) { + console.log(`triggered > ${t.name} (${_e.message})`); + await t.f(_e); + } + } catch (err) { console.error(err); } + process.exit(0); })(); diff --git a/init.sh b/init.sh new file mode 100755 index 0000000..ce6ee5d --- /dev/null +++ b/init.sh @@ -0,0 +1,3 @@ +#!/bin/bash +npm i +npm start diff --git a/src/inc/trigger/parser.mjs b/src/inc/trigger/parser.mjs index ddb3c14..357d2ff 100644 --- a/src/inc/trigger/parser.mjs +++ b/src/inc/trigger/parser.mjs @@ -9,8 +9,8 @@ import fs from "fs"; import path from "path"; const regex = { - all: /https?:\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/gi, - yt: /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/gi, + all: /https?:\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?/gi, + yt: /(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\/?\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11})/gi, imgur: /(?:https?:)?\/\/(\w+\.)?imgur\.com\/(\S*)(\..{3,4})/i, fourchan: /https?:\/\/i\.4cdn\.org\/(\w+)\/(\d+)\.(\w{3,4})/i, instagram: /(?:https?:\/\/www\.)?instagram\.com\S*?\/(?:p|reel)\/(\w{11})\/?/im @@ -23,35 +23,35 @@ export default async bot => { name: "parser", call: regex.all, active: true, - f: e => { + f: async e => { const links = e.message.match(regex.all)?.filter(link => !link.includes(cfg.main.url.domain)) || []; let repost; - if(e.media) + if (e.media) links.push(e.media); - - if(links.length === 0) + + if (links.length === 0) return false; - if(e.message.match(/\!i(gnore)?\b/)) + if (e.message.match(/\!i(gnore)?\b/)) return false; - if(!e.channel.includes("w0bm") && (!e.message.match(/\!w(0bm)?\b/i) && (typeof e.raw.forward_from == 'undefined'))) + if (!e.channel.includes("w0bm") && (!e.message.match(/\!w(0bm)?\b/i) && (typeof e.raw.forward_from == 'undefined'))) return false; - if(e.type === 'tg' && // proto: tg + if (e.type === 'tg' && // proto: tg !e.message.match(/\!w(0bm)?\b/i) && // !w / !w0bm !e.raw.forward_date && // is forwarded? !mediagroupids.has(e.raw.media_group_id) // prepared mediagroup? ) { return false; } - else if(e.raw.media_group_id && e.message.match(/\!w(0bm)?\b/i)) { + else if (e.raw.media_group_id && e.message.match(/\!w(0bm)?\b/i)) { mediagroupids.add(e.raw.media_group_id); } console.log(`parsing ${links.length} link${links.length > 1 ? "s" : ""}...`); - links.forEach(async link => { + for (const link of links) { //if(regex.imgur.test(link)) // return await e.reply(`fuck imgur... seriously`); @@ -63,7 +63,7 @@ export default async bot => { // check repost (link) repost = await queue.checkrepostlink(link); - if(repost) + if (repost) return await e.reply(`repost motherf0cker (link): ${cfg.main.url.full}/${repost}`); // generate uuid @@ -73,39 +73,41 @@ export default async bot => { // read metadata let ext; - if(link.match(regex.instagram)) { + const proxyArgs = cfg.main.socks ? `--proxy ${cfg.main.socks}` : ''; + if (link.match(regex.instagram)) { // is instagram try { // @flummi -> is there a variable for the actual work directory so it doesn't have to be hardcoded? - const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout); + const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout); ext = meta.ext; - } catch(err) { + } catch (err) { const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"]; // this can be undefined for unsupported mime types, but will be caught in the general mime check below ext = cfg.mimes[tmphead]; } } - else if(link.match(regex.imgur)) { + else if (link.match(regex.imgur)) { // imghure ext = link.split('.').pop(); } - else if(link.match(regex.yt)) { + else if (link.match(regex.yt)) { //yt - fuck anti bot protection try { - const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' -I 1 --skip-download --dump-json "${link}"`)).stdout); + const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' -I 1 --skip-download --dump-json "${link}"`)).stdout); ext = meta.ext; - } catch(err) { + } catch (err) { + console.error("YT-DLP Error:", err); const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"]; // this can be undefined for unsupported mime types, but will be caught in the general mime check below ext = cfg.mimes[tmphead]; } } - else if(link.match(regex.fourchan)) { + else if (link.match(regex.fourchan)) { //4chan - fuck cloudflare :) try { - const meta = JSON.parse((await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout); + const meta = JSON.parse((await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout); ext = meta.ext; - } catch(err) { + } catch (err) { const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"]; // this can be undefined for unsupported mime types, but will be caught in the general mime check below ext = cfg.mimes[tmphead]; @@ -117,14 +119,15 @@ export default async bot => { try { const meta = JSON.parse((await queue.exec(`yt-dlp -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' --skip-download --dump-json "${link}"`)).stdout); ext = meta.ext; - } catch(err) { - const tmphead = (await fetch(link, { method: "HEAD" })).headers["content-type"]; - // this can be undefined for unsupported mime types, but will be caught in the general mime check below - ext = cfg.mimes[tmphead]; + } catch (err) { + console.error('err:', err); + if (e.type == 'tg') + return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err); + return await e.reply('something went wrong lol / check maxfilesize?'); } } - if(!Object.values(cfg.mimes).includes(ext?.toLowerCase())) { + if (!Object.values(cfg.mimes).includes(ext?.toLowerCase())) { return console.log('mime schmime ' + ext); } @@ -136,44 +139,44 @@ export default async bot => { const start = new Date(); let source; - if(link.match(regex.instagram)) { + if (link.match(regex.instagram)) { try { // add --cookies on local instance if you want to avoid getting rate limited or optionally use a socks proxy from a network that is not being detected as a public network - source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); - } catch(err) { - if(e.type == 'tg') + source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=1080]+ba/b[height<=1080] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); + } catch (err) { + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "instagram dl error"); return await e.reply("instagram dl error", err); } } - else if(link.match(regex.imgur)) { + else if (link.match(regex.imgur)) { // imghure via torsocks // needs torsocks setup so this is optional try { await queue.exec(`torsocks wget ${link} -O ./tmp/${uuid}.${ext}`); source = `./tmp/${uuid}.${ext}`; - } catch(err) { + } catch (err) { console.error('err:', err); - if(e.type == 'tg') + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err); return await e.reply('something went wrong lol'); } } - else if(link.match(regex.yt)) { + else if (link.match(regex.yt)) { try { // add --cookies on local instance if you want to avoid getting rate limited or optionally use a socks proxy from a network that is not being detected as a public network - source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" -I 1 --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); - } catch(err) { - if(e.type == 'tg') + source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" -I 1 --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); + } catch (err) { + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "yt dl error"); return await e.reply("yt dl error", err); } } - else if(link.match(regex.fourchan)) { + else if (link.match(regex.fourchan)) { // 4chan via proxy - fuck cloudflare try { - source = (await queue.exec(`yt-dlp --proxy ${cfg.main.socks} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); - } catch(err) { - if(e.type == 'tg') + source = (await queue.exec(`yt-dlp ${proxyArgs} -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); + } catch (err) { + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "yt dl error"); return await e.reply("yt dl error", err); } @@ -182,23 +185,23 @@ export default async bot => { // everything except the exceptions try { source = (await queue.exec(`yt-dlp -f 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w' "${link}" --max-filesize ${maxfilesize / 1024}k --postprocessor-args "ffmpeg:-bitexact" -o "./tmp/${uuid}.%(ext)s" --print after_move:filepath --merge-output-format "mp4"`)).stdout.trim(); - } catch(err) { + } catch (err) { console.error('err:', err); - if(e.type == 'tg') + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, err); return await e.reply('something went wrong lol / check maxfilesize?'); } } // - if(!source) { - if(e.type == 'tg') + if (!source) { + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol"); return await e.reply("something went wrong lol"); } - if(source.match(/larger than/)) { - if(e.type == 'tg') + if (source.match(/larger than/)) { + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "too large lol"); return await e.reply("too large lol"); } @@ -206,38 +209,38 @@ export default async bot => { // filesize check const size = fs.statSync(source).size; - if(size > maxfilesize) { - await fs.promises.unlink(source).catch(_=>{}); - if(e.type == 'tg') + if (size > maxfilesize) { + await fs.promises.unlink(source).catch(_ => { }); + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`); return await e.reply(`too large lol. (${lib.formatSize(size)} / ${lib.formatSize(maxfilesize)})`); } - + // mime check let mime = (await queue.exec(`file --mime-type -b ${source}`)).stdout.trim(); try { - if(mime == 'video/x-matroska') { // mkv failsafe + if (mime == 'video/x-matroska') { // mkv failsafe await queue.exec(`ffmpeg -i ./tmp/${uuid}.mkv -codec copy ./tmp/${uuid}.mp4`); - await fs.promises.unlink(source).catch(_=>{}); + await fs.promises.unlink(source).catch(_ => { }); source = source.replace(/\.mkv$/, '.mp4'); mime = 'video/mp4'; } - if(source.match(/\.opus$/)) { // opus failsafe + if (source.match(/\.opus$/)) { // opus failsafe await queue.exec(`ffmpeg -i ./tmp/${uuid}.opus -codec copy ./tmp/${uuid}.ogg`); await fs.promises.unlink(source); source = source.replace(/\.opus$/, '.ogg'); mime = 'audio/ogg'; } - } catch(err) { - await fs.promises.unlink(source).catch(_=>{}); - if(e.type == 'tg') + } catch (err) { + await fs.promises.unlink(source).catch(_ => { }); + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, "something went wrong lol"); return await e.reply("something went wrong lol"); } - if(!Object.keys(cfg.mimes).includes(mime)) { - await fs.promises.unlink(source).catch(_=>{}); - if(e.type == 'tg') + if (!Object.keys(cfg.mimes).includes(mime)) { + await fs.promises.unlink(source).catch(_ => { }); + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `lol, go f0ck yourself (${mime})`); return await e.reply(`lol, go f0ck yourself (${mime})`); } @@ -247,9 +250,9 @@ export default async bot => { // check repost (checksum) repost = await queue.checkrepostsum(checksum); - if(repost) { - await fs.promises.unlink(source).catch(_=>{}); - if(e.type == 'tg') + if (repost) { + await fs.promises.unlink(source).catch(_ => { }); + if (e.type == 'tg') return await e.editMessageText(msg.result.chat.id, msg.result.message_id, `repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`); return await e.reply(`repost motherf0cker (checksum): ${cfg.main.url.full}/${repost}`); } @@ -257,7 +260,7 @@ export default async bot => { const filename = path.basename(source); await fs.promises.copyFile(source, `./public/b/${filename}`); - await fs.promises.unlink(source).catch(_=>{}); + await fs.promises.unlink(source).catch(_ => { }); // user alias let username = e.user.nick || e.user.username; @@ -268,33 +271,42 @@ export default async bot => { where lower(user_alias.alias) ilike ${username} limit 1 `)?.[0]?.user; - if(alias) { + if (alias) { username = alias; } await db` - insert into items ${ - db({ - src: e.media ? "" : link, - dest: filename, - mime: mime, - size: size, - checksum: checksum, - username: username, - userchannel: e.channel, - usernetwork: e.network, - stamp: ~~(new Date() / 1000), - active: true - }, 'src', 'dest', 'mime', 'size', 'checksum', 'username', 'userchannel', 'usernetwork', 'stamp', 'active') + insert into items ${db({ + src: e.media ? "" : link, + dest: filename, + mime: mime, + size: size, + checksum: checksum, + username: username, + userchannel: e.channel, + usernetwork: e.network, + stamp: ~~(new Date() / 1000), + active: true + }, 'src', 'dest', 'mime', 'size', 'checksum', 'username', 'userchannel', 'usernetwork', 'stamp', 'active') } `; const itemid = await queue.getItemID(filename); + // auto-tag sfw + await db` + insert into tags_assign ${db({ + item_id: itemid, + tag_id: 1, // sfw + user_id: 2 // f0ck + }, 'item_id', 'tag_id', 'user_id') + } + `; + // generate thumbnail try { await queue.genThumbnail(filename, mime, itemid, link); - } catch(err) { + } catch (err) { await queue.exec(`magick ./mugge.png ./public/t/${itemid}.webp`); } @@ -315,13 +327,13 @@ export default async bot => { let outputmsgirc = `[f0cked] link: ${cfg.main.url.full}/${itemid} | size: ${lib.formatSize(size)} | speed: ${speed}`; let outputmsgtg = `[f0cked] size: ${lib.formatSize(size)} | speed: ${speed}`; - if(tags?.length > 0) { + if (tags?.length > 0) { const tagstr = tags.join(', '); outputmsgirc += ` | tags: ${tagstr}`; outputmsgtg += ` | tags: ${tagstr}`; } - if(e.type == 'tg') { + if (e.type == 'tg') { await e.deleteMessage(msg.result.chat.id, msg.result.message_id); await e.reply(outputmsgtg, { reply_markup: JSON.stringify({ @@ -339,7 +351,7 @@ export default async bot => { else { await e.reply(outputmsgirc); } - }); + } } }]; }; From b72fcaa42698af5e69cdacc9c5f68d5f19b6cbfe Mon Sep 17 00:00:00 2001 From: eins Date: Fri, 23 Jan 2026 13:18:55 +0000 Subject: [PATCH 2/3] added AJAX loading for videos --- public/s/js/admin.js | 645 +++++++++++++++++---------------- public/s/js/f0ck.js | 381 ++++++++++++++----- public/s/js/user.js | 398 +++++++++++--------- src/inc/lib.mjs | 76 ++-- src/inc/routes/ajax.mjs | 68 ++++ views/ajax-item.html | 1 + views/item-partial.html | 126 +++++++ views/item.html | 117 +----- views/snippets/navbar.html | 54 +-- views/snippets/pagination.html | 19 + 10 files changed, 1112 insertions(+), 773 deletions(-) create mode 100644 src/inc/routes/ajax.mjs create mode 100644 views/ajax-item.html create mode 100644 views/item-partial.html create mode 100644 views/snippets/pagination.html diff --git a/public/s/js/admin.js b/public/s/js/admin.js index c5c0d0c..ed2d0f2 100644 --- a/public/s/js/admin.js +++ b/public/s/js/admin.js @@ -1,331 +1,342 @@ (async () => { - if(_addtag = document.querySelector("a#a_addtag")) { - const postid = +document.querySelector("a.id-link").innerText; - const poster = document.querySelector("a#a_username").innerText; - let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2)); - - const deleteEvent = async e => { - e.preventDefault(); - if(!confirm("Do you really want to delete this tag?")) - return; - const tagname = e.target.parentElement.querySelector('a:first-child').innerText; - - const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), { - method: 'DELETE' - })).json(); - - if(!res.success) - return alert("uff"); - tags = res.tags.map(t => t.tag); - renderTags(res.tags); + // Helper to get dynamic context + const getContext = () => { + const idLink = document.querySelector("a.id-link"); + if (!idLink) return null; + return { + postid: +idLink.innerText, + poster: document.querySelector("a#a_username")?.innerText, + tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2)) }; + }; - const queryapi = async (url, data, method = 'GET') => { - let req; - if(method == 'POST') { - req = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(data) - }); - } - else { - let s = []; - for(const [ key, val ] of Object.entries(data)) - s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); - req = await fetch(url + '?' + s.join('&')); - } - return await req.json(); - }; - - const get = async (url, data) => queryapi(url, data, 'GET'); - const post = async (url, data) => queryapi(url, data, 'POST'); - - const renderTags = _tags => { - [...document.querySelectorAll("#tags > .badge")].forEach(tag => tag.parentElement.removeChild(tag)); - _tags.reverse().forEach(tag => { - const a = document.createElement("a"); - a.href = `/tag/${tag.normalized}`; - a.style = "color: inherit !important"; - a.innerHTML = tag.tag; - a.addEventListener("click", editTagEvent); // tmp - - const span = document.createElement("span"); - span.classList.add("badge", "mr-2"); - span.setAttribute('tooltip', tag.user); - - tag.badge.split(" ").forEach(b => span.classList.add(b)); - - const delbutton = document.createElement("a"); - delbutton.innerHTML = " ×"; - delbutton.href = "#"; - delbutton.addEventListener("click", deleteEvent); - span.insertAdjacentElement("beforeend", a); - span.innerHTML += ' '; - span.insertAdjacentElement("beforeend", delbutton); - - document.querySelector("#tags").insertAdjacentElement("afterbegin", span); - }); - }; - - const addtagClick = (ae = false) => { - if(ae) - ae.preventDefault(); - - const insert = document.querySelector("a#a_addtag"); - const span = document.createElement("span"); - span.classList.add("badge", "badge-light", "mr-2"); - - const input = document.createElement("input"); - input.size = "10"; - input.value = ""; - input.setAttribute("list", "testlist"); - input.setAttribute("autoComplete", "off"); - - span.insertAdjacentElement("afterbegin", input); - insert.insertAdjacentElement("beforebegin", span); - - input.focus(); - - let tt = null; - let lastInput = ''; - const testList = document.querySelector('#testlist'); - - input.addEventListener("keyup", async e => { - if(e.key === "Enter") { - const tmptag = input.value?.trim(); - if(tags.includes(tmptag)) - return alert("tag already exists"); - const res = await post("/api/v2/admin/" + postid + "/tags", { - tagname: tmptag - }); - if(!res.success) { - alert(res.msg); - return false; - } - tags = res.tags.map(t => t.tag); - renderTags(res.tags); - addtagClick(); - testList.innerText = ""; - } - else if(e.key === "Escape") { - span.parentElement.removeChild(span); - testList.innerText = ""; - } - else { - if(tt != null) - clearTimeout(tt); - - tt = setTimeout(async () => { - tt = null; - - const tmptag = input.value?.trim(); - - if(tmptag == lastInput || tmptag.length <= 1) - return false; - - testList.innerText = ""; - lastInput = tmptag; - - const res = await get('/api/v2/admin/tags/suggest', { - q: tmptag - }); - - for(const entry of res.suggestions) { - const option = document.createElement('option'); - option.value = entry.tag; - - if(!/fox/.test(navigator.userAgent)) - option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`; - - testList.insertAdjacentElement('beforeEnd', option); - }; - }, 500); - } - return true; - }); - - input.addEventListener("focusout", ie => { - if(input.value.length === 0) - input.parentElement.parentElement.removeChild(input.parentElement); - }); - }; - - const toggleEvent = async (e = false) => { - if(e) - e.preventDefault(); - - const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', { - method: 'PUT' - })).json(); - - renderTags(res.tags); - }; - - const deleteButtonEvent = async e => { - if(e) - e.preventDefault(); - if(!confirm(`Reason for deleting f0ckpost ${postid} by ${poster} (Weihnachten™)`)) - return; - const res = await post("/api/v2/admin/deletepost", { - postid: postid - }); - if(!res.success) { - alert(res.msg); - } - }; - - const toggleFavEvent = async e => { - const res = await post('/api/v2/admin/togglefav', { - postid: postid - }); - if(res.success) { - const fav = document.querySelector("svg#a_favo > use").href; - fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid"); - - // span#favs - const favcontainer = document.querySelector('span#favs'); - favcontainer.innerHTML = ""; - - favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0); - - res.favs.forEach(f => { - const a = document.createElement('a'); - a.href = `/user/${f.user}/favs`; - a.setAttribute('tooltip', f.user); - a.setAttribute('flow', 'up'); - - const img = document.createElement('img'); - img.src = `/t/${f.avatar}.webp`; - img.style.height = "32px"; - img.style.width = "32px"; - - a.insertAdjacentElement('beforeend', img); - favcontainer.insertAdjacentElement('beforeend', a); - favcontainer.innerHTML += " "; - }); - } - else { - // lul - } - }; - - let tmptt = null; - const editTagEvent = async e => { // mousedown - e.preventDefault(); - - if(e.detail === 2) { - clearTimeout(tmptt); - const old = e.target; - const parent = e.target.parentElement; - const oldtag = e.target.innerText; - - const textfield = document.createElement('input'); - textfield.value = e.target.innerText; - textfield.size = 10; - - parent.insertAdjacentElement('afterbegin', textfield); - textfield.focus(); - parent.removeChild(e.target); - parent.querySelector('a:last-child').style.display = 'none'; - - textfield.addEventListener("keyup", async e => { - if(e.key === 'Enter') { - parent.removeChild(textfield); - // send - let res = await fetch('/api/v2/admin/tags/' + encodeURIComponent(oldtag), { - method: 'PUT', - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - newtag: textfield.value - }) - }); - const status = res.status; - res = await res.json(); - - switch(status) { - case 200: // success, change - case 201: - //parent.removeChild(textfield); - parent.insertAdjacentElement('afterbegin', old); - parent.querySelector('a:last-child').style.display = ''; - old.href = `/tag/${res.tag}`; - old.innerText = res.tag.trim(); - break; - default: - console.log(res); - break; - } - } - else if(e.key === 'Escape') { - parent.removeChild(textfield); - parent.insertAdjacentElement('afterbegin', old); - parent.querySelector('a:last-child').style.display = ''; - } - }); - } - else - tmptt = setTimeout(() => location.href = e.target.href, 250); - - return false; - }; - - _addtag.addEventListener("click", addtagClick); - document.querySelector("a#a_toggle").addEventListener("click", toggleEvent); - [...document.querySelectorAll("#tags > .badge > a:first-child")].map(t => t.addEventListener("click", editTagEvent)); - [...document.querySelectorAll("#tags > .badge > a:last-child")].map(t => t.addEventListener("click", deleteEvent)); - if(document.querySelector("svg#a_delete")) - document.querySelector("svg#a_delete").addEventListener("click", deleteButtonEvent); - document.querySelector("svg#a_favo").addEventListener("click", toggleFavEvent); - - document.addEventListener("keyup", e => { - if(e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") - return; - if(e.key === "p") - toggleEvent(); - else if(e.key === "i") - addtagClick(); - else if(e.key === "x") - deleteButtonEvent(); - else if(e.key === "f") - toggleFavEvent(); - }); - } - - if(document.location.pathname === '/settings') { - const saveAvatar = async e => { - e.preventDefault(); - - const avatar = +document.querySelector('input[name="i_avatar"]').value; - let res = await fetch('/api/v2/settings/setAvatar', { - method: 'PUT', + const queryapi = async (url, data, method = 'GET') => { + let req; + if (method == 'POST') { + req = await fetch(url, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data) + }); + } + else { + let s = []; + for (const [key, val] of Object.entries(data)) + s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); + req = await fetch(url + '?' + s.join('&')); + } + return await req.json(); + }; + + const get = async (url, data) => queryapi(url, data, 'GET'); + const post = async (url, data) => queryapi(url, data, 'POST'); + + const renderTags = _tags => { + [...document.querySelectorAll("#tags > .badge")].forEach(tag => tag.parentElement.removeChild(tag)); + _tags.reverse().forEach(tag => { + const a = document.createElement("a"); + a.href = `/tag/${tag.normalized}`; + a.style = "color: inherit !important"; + a.innerHTML = tag.tag; + // Admin specific: edit event + // Note: delegation handles this now if we set it up, OR we can attach here since elements are new. + // But delegation is cleaner if possible. However, editTagEvent relies on 'e.target'. + + const span = document.createElement("span"); + span.classList.add("badge", "mr-2"); + span.setAttribute('tooltip', tag.user); + + tag.badge.split(" ").forEach(b => span.classList.add(b)); + + const delbutton = document.createElement("a"); + delbutton.innerHTML = " ×"; + delbutton.href = "#"; + // Class for delegation + delbutton.classList.add("admin-deltag"); + + span.insertAdjacentElement("beforeend", a); + span.innerHTML += ' '; + span.insertAdjacentElement("beforeend", delbutton); + + document.querySelector("#tags").insertAdjacentElement("afterbegin", span); + }); + }; + + const deleteEvent = async e => { + e.preventDefault(); + const ctx = getContext(); + if (!ctx) return; + const { postid } = ctx; + + if (!confirm("Do you really want to delete this tag?")) + return; + const tagname = e.target.parentElement.querySelector('a:first-child').innerText; + + const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), { + method: 'DELETE' + })).json(); + + if (!res.success) + return alert("uff"); + + renderTags(res.tags); + }; + + const addtagClick = (e) => { + if (e) e.preventDefault(); + const ctx = getContext(); + if (!ctx) return; + const { postid, tags } = ctx; + + const insert = document.querySelector("a#a_addtag"); + if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) { + insert.previousElementSibling.querySelector('input').focus(); + return; + } + + const span = document.createElement("span"); + span.classList.add("badge", "badge-light", "mr-2"); + + const input = document.createElement("input"); + input.size = "10"; + input.value = ""; + input.setAttribute("list", "testlist"); + input.setAttribute("autoComplete", "off"); + + span.insertAdjacentElement("afterbegin", input); + insert.insertAdjacentElement("beforebegin", span); + + input.focus(); + + let tt = null; + let lastInput = ''; + const testList = document.querySelector('#testlist'); + + input.addEventListener("keyup", async e => { + if (e.key === "Enter") { + const tmptag = input.value?.trim(); + // We should re-check tags from DOM? Or trust captured tags? + // Captured 'tags' is safe for immediate check. + if (tags.includes(tmptag)) + return alert("tag already exists"); + + const res = await post("/api/v2/admin/" + postid + "/tags", { + tagname: tmptag + }); + if (!res.success) { + alert(res.msg); + return false; + } + renderTags(res.tags); + span.parentElement.removeChild(span); + testList.innerText = ""; + addtagClick(); + } + else if (e.key === "Escape") { + span.parentElement.removeChild(span); + testList.innerText = ""; + } + else { + if (tt != null) clearTimeout(tt); + tt = setTimeout(async () => { + tt = null; + const tmptag = input.value?.trim(); + if (tmptag == lastInput || tmptag.length <= 1) return false; + testList.innerText = ""; + lastInput = tmptag; + const res = await get('/api/v2/admin/tags/suggest', { q: tmptag }); + for (const entry of res.suggestions) { + const option = document.createElement('option'); + option.value = entry.tag; + if (!/fox/.test(navigator.userAgent)) + option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`; + testList.insertAdjacentElement('beforeEnd', option); + }; + }, 500); + } + return true; + }); + + input.addEventListener("focusout", ie => { + if (input.value.length === 0) + input.parentElement.parentElement.removeChild(input.parentElement); + }); + }; + + const toggleEvent = async (e) => { + if (e) e.preventDefault(); + const ctx = getContext(); + if (!ctx) return; + const { postid } = ctx; + + const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', { + method: 'PUT' + })).json(); + + renderTags(res.tags); + }; + + const deleteButtonEvent = async e => { + if (e) e.preventDefault(); + const ctx = getContext(); + if (!ctx) return; + const { postid, poster } = ctx; + + if (!confirm(`Reason for deleting f0ckpost ${postid} by ${poster} (Weihnachten™)`)) + return; + const res = await post("/api/v2/admin/deletepost", { + postid: postid + }); + if (!res.success) { + alert(res.msg); + } + }; + + const toggleFavEvent = async (e) => { + const ctx = getContext(); + if (!ctx) return; + const { postid } = ctx; + + const res = await post('/api/v2/admin/togglefav', { + postid: postid + }); + if (res.success) { + const fav = document.querySelector("svg#a_favo > use").href; + fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid"); + + const favcontainer = document.querySelector('span#favs'); + favcontainer.innerHTML = ""; + favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0); + res.favs.forEach(f => { + const a = document.createElement('a'); + a.href = `/user/${f.user}/favs`; + a.setAttribute('tooltip', f.user); + a.setAttribute('flow', 'up'); + const img = document.createElement('img'); + img.src = `/t/${f.avatar}.webp`; + img.style.height = "32px"; + img.style.width = "32px"; + a.insertAdjacentElement('beforeend', img); + favcontainer.insertAdjacentElement('beforeend', a); + favcontainer.innerHTML += " "; + }); + } + }; + + let tmptt = null; + const editTagEvent = async e => { + e.preventDefault(); + if (e.detail === 2) { // Double click + clearTimeout(tmptt); + const old = e.target; + const parent = e.target.parentElement; + const oldtag = e.target.innerText; + + const textfield = document.createElement('input'); + textfield.value = e.target.innerText; + textfield.size = 10; + + parent.insertAdjacentElement('afterbegin', textfield); + textfield.focus(); + parent.removeChild(e.target); + // Hide delete button while editing + const delBtn = parent.querySelector('a:last-child'); + if (delBtn) delBtn.style.display = 'none'; + + textfield.addEventListener("keyup", async e => { + if (e.key === 'Enter') { + parent.removeChild(textfield); + let res = await fetch('/api/v2/admin/tags/' + encodeURIComponent(oldtag), { + method: 'PUT', + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ newtag: textfield.value }) + }); + const status = res.status; + res = await res.json(); + switch (status) { + case 200: + case 201: + parent.insertAdjacentElement('afterbegin', old); + if (delBtn) delBtn.style.display = ''; + old.href = `/tag/${res.tag}`; + old.innerText = res.tag.trim(); + break; + default: + console.log(res); + break; + } + } + else if (e.key === 'Escape') { + parent.removeChild(textfield); + parent.insertAdjacentElement('afterbegin', old); + if (delBtn) delBtn.style.display = ''; + } + }); + } + else + tmptt = setTimeout(() => location.href = e.target.href, 250); + return false; + }; + + // Event Delegation + document.addEventListener("click", e => { + if (e.target.matches("a#a_addtag")) { + addtagClick(e); + } else if (e.target.matches("a#a_toggle")) { + toggleEvent(e); + } else if (e.target.closest("svg#a_favo")) { + toggleFavEvent(e); + } else if (e.target.closest("svg#a_delete")) { + deleteButtonEvent(e); + } else if (e.target.matches("#tags > .badge > a:first-child")) { + editTagEvent(e); + } else if (e.target.innerText === " \u00d7" && e.target.closest(".badge")) { // check text " x" or similar for delete? + // Original was " ×" which is × (\u00d7). + // Logic in deleteEvent expects match. + // Let's rely on class or structure. + // In renderTags I added class 'admin-deltag'. + // Existing tags in HTML might NOT have this class unless rendered by JS? + // But existing tags are just HTML. We should match structure. + // selector: "#tags > .badge > a:last-child" + if (e.target.matches("#tags > .badge > a:last-child")) { + deleteEvent(e); + } + } + }); + + document.addEventListener("keyup", e => { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return; + const ctx = getContext(); + if (!ctx) return; + + if (e.key === "p") toggleEvent(); + else if (e.key === "i") addtagClick(); + else if (e.key === "x") deleteButtonEvent(); + else if (e.key === "f") toggleFavEvent(); + }); + + // Settings page + if (document.location.pathname === '/settings') { + const saveAvatar = async e => { + e.preventDefault(); + const avatar = +document.querySelector('input[name="i_avatar"]').value; + let res = await fetch('/api/v2/settings/setAvatar', { + method: 'PUT', + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ avatar }) }); const code = res.status; res = await res.json(); - - switch(code) { - case 200: - document.querySelector('#img_avatar').src = `/t/${avatar}.webp`; - document.querySelector('img.avatar').src = `/t/${avatar}.webp`; - break; - default: - console.log(res); - break; + if (code === 200) { + document.querySelector('#img_avatar').src = `/t/${avatar}.webp`; + document.querySelector('img.avatar').src = `/t/${avatar}.webp`; } }; - - document.querySelector('input#s_avatar').addEventListener('click', saveAvatar); - document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async e => { - if(e.key === 'Enter') - await saveAvatar(e); - }); + const sAvatar = document.querySelector('input#s_avatar'); + if (sAvatar) sAvatar.addEventListener('click', saveAvatar); + const iAvatar = document.querySelector('input[name="i_avatar"]'); + if (iAvatar) iAvatar.addEventListener('keyup', async e => { if (e.key === 'Enter') await saveAvatar(e); }); } })(); diff --git a/public/s/js/f0ck.js b/public/s/js/f0ck.js index d67fa98..b0dbf38 100644 --- a/public/s/js/f0ck.js +++ b/public/s/js/f0ck.js @@ -1,66 +1,259 @@ -window.requestAnimFrame = (function(){ +window.requestAnimFrame = (function () { return window.requestAnimationFrame - || window.webkitRequestAnimationFrame - || window.mozRequestAnimationFrame - || function(callback) { window.setTimeout(callback, 1000 / 60);}; + || window.webkitRequestAnimationFrame + || window.mozRequestAnimationFrame + || function (callback) { window.setTimeout(callback, 1000 / 60); }; })(); (() => { let video; - if(elem = document.querySelector("#my-video")) { + if (elem = document.querySelector("#my-video")) { video = new v0ck(elem); document.addEventListener("keydown", e => { - if(e.key === " " && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { + if (e.key === " " && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { video[video.paused ? 'play' : 'pause'](); document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden'); } }); - document.getElementById('togglebg').addEventListener('click', function (e) { - e.preventDefault(); - background = !background; - localStorage.setItem('background', background.toString()); - var canvas = document.getElementById('bg'); - if (background) { + const toggleBg = document.getElementById('togglebg'); + if (toggleBg) { + toggleBg.addEventListener('click', function (e) { + e.preventDefault(); + background = !background; + localStorage.setItem('background', background.toString()); + var canvas = document.getElementById('bg'); + if (background) { canvas.classList.add('fader-in'); canvas.classList.remove('fader-out'); - } else { + } else { canvas.classList.add('fader-out'); canvas.classList.remove('fader-in'); - } - animationLoop(); - }); - - if(elem !== null) { - if(localStorage.getItem('background') == undefined) { - localStorage.setItem('background', 'true'); + } + animationLoop(); + }); } - - var background = localStorage.getItem('background') === 'true'; - var canvas = document.getElementById('bg'); - var context = canvas.getContext('2d'); - var cw = canvas.width = canvas.clientWidth | 0; - var ch = canvas.height = canvas.clientHeight | 0; - function animationLoop() { - if(video.paused || video.ended || !background) - return; - context.drawImage(video, 0, 0, cw, ch); - window.requestAnimFrame(animationLoop); - } + if (elem !== null) { + if (localStorage.getItem('background') == undefined) { + localStorage.setItem('background', 'true'); + } - elem.addEventListener('play', animationLoop); - } + var background = localStorage.getItem('background') === 'true'; + var canvas = document.getElementById('bg'); + if (canvas) { + var context = canvas.getContext('2d'); + var cw = canvas.width = canvas.clientWidth | 0; + var ch = canvas.height = canvas.clientHeight | 0; + + function animationLoop() { + if (video.paused || video.ended || !background) + return; + context.drawImage(video, 0, 0, cw, ch); + window.requestAnimFrame(animationLoop); + } + + elem.addEventListener('play', animationLoop); + } + } } let tt = false; const stimeout = 500; - const changePage = (e, pbwork = true) => { - pbwork && document.querySelector("nav.navbar").classList.add("pbwork"); - !tt && (tt = setTimeout(() => e.click(), stimeout)); + + const setupMedia = () => { + if (elem = document.querySelector("#my-video")) { + video = new v0ck(elem); + } }; + const loadItemAjax = async (url, inheritContext = true) => { + console.log("loadItemAjax called with:", url, "inheritContext:", inheritContext); + // Show loading indicator + const navbar = document.querySelector("nav.navbar"); + if (navbar) navbar.classList.add("pbwork"); + + // Extract item ID from URL. Regex now handles query params, hashes, and trailing slashes. + const match = url.match(/\/(\d+)(?:\/|#|\?|$)/); + + if (!match) { + console.warn("loadItemAjax: No ID match found in URL", url); + // fallback for weird/external links + window.location.href = url; + return; + } + const itemid = match[1]; + + // + // Extract context from Target URL first + let tag = null, user = null; + const tagMatch = url.match(/\/tag\/([^/]+)/); + if (tagMatch) tag = decodeURIComponent(tagMatch[1]); + + const userMatch = url.match(/\/user\/([^/]+)/); + if (userMatch) user = decodeURIComponent(userMatch[1]); // Note: "user" variable shadowed? No, block scope or different name? let user defined above. + + // If missing and inheritContext is true, check Window Location + if (inheritContext) { + if (!tag) { + const wTagMatch = window.location.href.match(/\/tag\/([^/]+)/); + if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]); + } + if (!user) { + const wUserMatch = window.location.href.match(/\/user\/([^/]+)/); + if (wUserMatch) user = decodeURIComponent(wUserMatch[1]); + } + } + // + + try { + // Construct AJAX URL + let ajaxUrl = `/ajax/item/${itemid}`; + + const params = new URLSearchParams(); + if (tag) params.append('tag', tag); + if (user) params.append('user', user); + + if ([...params].length > 0) { + ajaxUrl += '?' + params.toString(); + } + + console.log("Fetching:", ajaxUrl); + const response = await fetch(ajaxUrl); + if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`); + + const rawText = await response.text(); + let html, paginationHtml; + + try { + // Optimistically try to parse as JSON first + const data = JSON.parse(rawText); + if (data && typeof data.html === 'string') { + html = data.html; + paginationHtml = data.pagination; + } else { + html = rawText; + } + } catch (e) { + // If JSON parse fails, assume it's HTML text + html = rawText; + } + + let container = document.querySelector('.container'); + + if (!container && document.querySelector('.index-container')) { + // Transition from Index to Item View + const main = document.getElementById('main'); + main.innerHTML = '
'; + container = main.querySelector('.container'); + } else if (container) { + // Already in Item View, clear usage + const oldContent = container.querySelector('.content'); + const oldMetadata = container.querySelector('.metadata'); + const oldHeader = container.querySelector('._204863'); + if (oldHeader) oldHeader.remove(); + if (oldContent) oldContent.remove(); + if (oldMetadata) oldMetadata.remove(); + } + + container.insertAdjacentHTML('beforeend', html); + + // Update pagination if present + if (paginationHtml) { + const pagWrappers = document.querySelectorAll('.pagination-wrapper'); + pagWrappers.forEach(el => el.innerHTML = paginationHtml); + } + + // Construct proper History URL (Context Aware) + // If we inherited context, we should reflect it in the URL + let pushUrl = `/${itemid}`; + // Logic from ajax.mjs context reconstruction: + if (user) pushUrl = `/user/${user}/${itemid}`; // User takes precedence usually? Or strictly mutually exclusive in UI + else if (tag) pushUrl = `/tag/${tag}/${itemid}`; + + // We overwrite proper URL even if the link clicked was "naked" + history.pushState({}, '', pushUrl); + + setupMedia(); + // Try to extract ID from response if possible or just use itemid + document.title = `f0bm - ${itemid}`; + if (navbar) navbar.classList.remove("pbwork"); + console.log("AJAX load complete"); + + } catch (err) { + console.error("AJAX load failed:", err); + } + }; + + const changePage = (e, pbwork = true) => { + if (e.tagName === 'A') { + e.preventDefault(); + loadItemAjax(e.href); + } else { + pbwork && document.querySelector("nav.navbar").classList.add("pbwork"); + !tt && (tt = setTimeout(() => e.click(), stimeout)); + } + }; + + // Intercept clicks + document.addEventListener('click', (e) => { + + // Check for thumbnail links on index page + const thumbnail = e.target.closest('.posts > a'); + if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { + e.preventDefault(); + // Thumbnails inherit context (e.g. from Tag Index) + loadItemAjax(thumbnail.href, true); + return; + } + + const link = e.target.closest('#next, #prev, #random, .id-link, .nav-next, .nav-prev'); + if (link && link.href && link.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { + // Special check for random + if (link.id === 'random') { + e.preventDefault(); + const nav = document.querySelector("nav.navbar"); + if (nav) nav.classList.add("pbwork"); + + // Extract current context from window location + let randomUrl = '/api/v2/random'; + const params = new URLSearchParams(); + + const wTagMatch = window.location.href.match(/\/tag\/([^/]+)/); + if (wTagMatch) params.append('tag', decodeURIComponent(wTagMatch[1])); + + const wUserMatch = window.location.href.match(/\/user\/([^/]+)/); + if (wUserMatch) params.append('user', decodeURIComponent(wUserMatch[1])); + + if ([...params].length > 0) { + randomUrl += '?' + params.toString(); + } + + fetch(randomUrl) + .then(r => r.json()) + .then(data => { + if (data.success && data.items && data.items.id) { + // Inherit context so URL matches current filter + loadItemAjax(`/${data.items.id}`, true); + } else { + window.location.href = link.href; + } + }) + .catch(() => window.location.href = link.href); + return; + } + + // Standard item links + e.preventDefault(); + loadItemAjax(link.href, true); + } + }); + + window.addEventListener('popstate', (e) => { + loadItemAjax(window.location.href, true); + }); + // const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null; const keybindings = { @@ -72,8 +265,8 @@ window.requestAnimFrame = (function(){ " ": clickOnElementBinding("#f0ck-image") }; document.addEventListener("keydown", e => { - if(e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { - if(e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) + if (e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { + if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return; e.preventDefault(); keybindings[e.key](); @@ -84,19 +277,21 @@ window.requestAnimFrame = (function(){ // const imgSize = e => new Promise((res, _) => { const i = new Image(); - i.addEventListener('load', function() { + i.addEventListener('load', function () { res({ width: this.width, height: this.height }); }); i.src = e.src; }); // - const wheelEventListener = function(event) { + const wheelEventListener = function (event) { if (event.target.closest('.media-object, .steuerung')) { if (event.deltaY < 0) { - document.getElementById('next').click(); + const el = document.getElementById('next'); + if (el && el.href && !el.href.endsWith('#')) el.click(); } else if (event.deltaY > 0) { - document.getElementById('prev').click(); + const el = document.getElementById('prev'); + if (el && el.href && !el.href.endsWith('#')) el.click(); } } }; @@ -105,7 +300,7 @@ window.requestAnimFrame = (function(){ // - if(f0ckimage = document.querySelector("img#f0ck-image")) { + if (f0ckimage = document.querySelector("img#f0ck-image")) { const f0ckimagescroll = document.querySelector("#image-scroll"); let isImageExpanded = false; @@ -135,24 +330,32 @@ window.requestAnimFrame = (function(){ // let tts = 0; const scroll_treshold = 1; - if([...document.querySelectorAll("div.posts")].length === 1) { + if ([...document.querySelectorAll("div.posts")].length === 1) { document.addEventListener("wheel", e => { - if(Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down - if(elem = document.querySelector(".pagination > .next:not(.disabled)")) { - if(tts < scroll_treshold) { - document.querySelector("div#footbar").style.boxShadow = "inset 0px 4px 0px var(--footbar-color)"; - document.querySelector("div#footbar").style.color = "var(--footbar-color)"; + if (!document.querySelector('#main')) return; + + if (Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down + if (elem = document.querySelector(".pagination > .next:not(.disabled)")) { + if (tts < scroll_treshold) { + const foot = document.querySelector("div#footbar"); + if (foot) { + foot.style.boxShadow = "inset 0px 4px 0px var(--footbar-color)"; + foot.style.color = "var(--footbar-color)"; + } tts++; } else changePage(elem); } } - else if(window.scrollY <= 0 && e.deltaY < 0) { // up - if(elem = document.querySelector(".pagination > .prev:not(.disabled)")) { - if(tts < scroll_treshold) { - document.querySelector("nav.navbar").style.boxShadow = "0px 2px 0px var(--loading-indicator-color)"; - document.querySelector("nav.navbar").style.transition = ".2s ease-in-out"; + else if (window.scrollY <= 0 && e.deltaY < 0) { // up + if (elem = document.querySelector(".pagination > .prev:not(.disabled)")) { + if (tts < scroll_treshold) { + const nav = document.querySelector("nav.navbar"); + if (nav) { + nav.style.boxShadow = "0px 2px 0px var(--loading-indicator-color)"; + nav.style.transition = ".2s ease-in-out"; + } tts++; } else @@ -161,19 +364,23 @@ window.requestAnimFrame = (function(){ } else { tts = 0; - document.querySelector("div#footbar").style.boxShadow = "unset"; - document.querySelector("div#footbar").style.color = "transparent"; - document.querySelector("nav.navbar").style.boxShadow = "unset"; + const foot = document.querySelector("div#footbar"); + if (foot) { + foot.style.boxShadow = "unset"; + foot.style.color = "transparent"; + } + const nav = document.querySelector("nav.navbar"); + if (nav) nav.style.boxShadow = "unset"; } }); } const rmatch = /\/p\/(\d+?)/; - if(document.referrer.match(rmatch) && document.location.href.match(rmatch)) - if(document.location.href.match(rmatch) < document.referrer.match(rmatch)) + if (document.referrer.match(rmatch) && document.location.href.match(rmatch)) + if (document.location.href.match(rmatch) < document.referrer.match(rmatch)) document.body.scrollTop = document.body.scrollHeight; // - + // const swipeRT = { xDown: null, @@ -198,33 +405,33 @@ window.requestAnimFrame = (function(){ }, false); document.addEventListener('touchmove', e => { - if(!swipeRT.xDown || !swipeRT.yDown) + if (!swipeRT.xDown || !swipeRT.yDown) return; swipeRT.xDiff = swipeRT.xDown - e.touches[0].clientX; swipeRT.yDiff = swipeRT.yDown - e.touches[0].clientY; }, false); document.addEventListener('touchend', e => { - if(swipeRT.startEl !== e.target) + if (swipeRT.startEl !== e.target) return; const timeDiff = Date.now() - swipeRT.timeDown; let elem; - if(Math.abs(swipeRT.xDiff) > Math.abs(swipeRT.yDiff)) { - if(Math.abs(swipeRT.xDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) { - if(swipeRT.xDiff > 0) // left + if (Math.abs(swipeRT.xDiff) > Math.abs(swipeRT.yDiff)) { + if (Math.abs(swipeRT.xDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) { + if (swipeRT.xDiff > 0) // left elem = document.querySelector(".pagination > .next:not(.disabled)"); else // right elem = document.querySelector(".pagination > .prev:not(.disabled)"); } } else { - if(Math.abs(swipeRT.yDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) { - if(navbar = document.querySelector("nav.navbar") && document.querySelector("div.posts")) { - if(swipeRT.yDiff > 0 && (window.innerHeight + window.scrollY) >= document.body.offsetHeight) // up + if (Math.abs(swipeRT.yDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) { + if (navbar = document.querySelector("nav.navbar") && document.querySelector("div.posts")) { + if (swipeRT.yDiff > 0 && (window.innerHeight + window.scrollY) >= document.body.offsetHeight) // up elem = document.querySelector(".pagination > .next:not(.disabled)"); - else if(swipeRT.yDiff <= 0 && window.scrollY <= 0 && document.querySelector("div.posts")) // down + else if (swipeRT.yDiff <= 0 && window.scrollY <= 0 && document.querySelector("div.posts")) // down elem = document.querySelector(".pagination > .prev:not(.disabled)"); } } @@ -234,13 +441,13 @@ window.requestAnimFrame = (function(){ swipeRT.yDown = null; swipeRT.timeDown = null; - if(elem) + if (elem) changePage(elem); }, false); // // - if(audioElement = document.querySelector("audio")) { + if (audioElement = document.querySelector("audio")) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); canvas.width = 1920; @@ -267,7 +474,7 @@ window.requestAnimFrame = (function(){ draw(data); } function draw(data) { - data = [ ...data ]; + data = [...data]; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent"); data.forEach((value, i) => { @@ -285,7 +492,7 @@ window.requestAnimFrame = (function(){ // // - if(elem = document.querySelector("#my-video") && "mediaSession" in navigator) { + if (elem = document.querySelector("#my-video") && "mediaSession" in navigator) { const playpauseEvent = () => { video[video.paused ? 'play' : 'pause'](); document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden'); @@ -294,11 +501,11 @@ window.requestAnimFrame = (function(){ navigator.mediaSession.setActionHandler('pause', playpauseEvent); navigator.mediaSession.setActionHandler('stop', playpauseEvent); navigator.mediaSession.setActionHandler('previoustrack', () => { - if(link = document.querySelector(".pagination > .prev:not(.disabled)")) + if (link = document.querySelector(".pagination > .prev:not(.disabled)")) changePage(link); }); navigator.mediaSession.setActionHandler('nexttrack', () => { - if(link = document.querySelector(".pagination > .next:not(.disabled)")) + if (link = document.querySelector(".pagination > .next:not(.disabled)")) changePage(link); }); } @@ -326,18 +533,18 @@ function onWheel(e) { function init() { const el = document.querySelector(targetSelector); - if (!el) return; - el.addEventListener('mouseenter', () => isMouseOver = true); - el.addEventListener('mouseleave', () => isMouseOver = false); - window.addEventListener('wheel', onWheel, { passive: false }); + if (!el) return; + el.addEventListener('mouseenter', () => isMouseOver = true); + el.addEventListener('mouseleave', () => isMouseOver = false); + window.addEventListener('wheel', onWheel, { passive: false }); } window.addEventListener('load', init); - document.getElementById('sbtForm').addEventListener('submit', (e) => { - e.preventDefault(); - const input = document.getElementById('sbtInput').value.trim(); - if (input) { - window.location.href = `/tag/${encodeURIComponent(input)}`; - } - }); +document.getElementById('sbtForm').addEventListener('submit', (e) => { + e.preventDefault(); + const input = document.getElementById('sbtInput').value.trim(); + if (input) { + window.location.href = `/tag/${encodeURIComponent(input)}`; + } +}); diff --git a/public/s/js/user.js b/public/s/js/user.js index 0c5e963..9ebd742 100644 --- a/public/s/js/user.js +++ b/public/s/js/user.js @@ -1,200 +1,239 @@ (async () => { - if(_addtag = document.querySelector("a#a_addtag")) { - const postid = +document.querySelector("a.id-link").innerText; - const poster = document.querySelector("a#a_username").innerText; - let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2)); - - const queryapi = async (url, data, method = 'GET') => { - let req; - if(method == 'POST') { - req = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify(data) - }); - } - else { - let s = []; - for(const [ key, val ] of Object.entries(data)) - s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); - req = await fetch(url + '?' + s.join('&')); - } - return await req.json(); + // Helper to get dynamic context from the DOM + const getContext = () => { + const idLink = document.querySelector("a.id-link"); + if (!idLink) return null; + return { + postid: +idLink.innerText, + poster: document.querySelector("a#a_username")?.innerText, + tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2)) }; + }; - const get = async (url, data) => queryapi(url, data, 'GET'); - const post = async (url, data) => queryapi(url, data, 'POST'); - - const renderTags = _tags => { - [...document.querySelectorAll("#tags > .badge")].forEach(tag => tag.parentElement.removeChild(tag)); - _tags.reverse().forEach(tag => { - const a = document.createElement("a"); - a.href = `/tag/${tag.normalized}`; - a.style = "color: inherit !important"; - a.innerHTML = tag.tag; - - const span = document.createElement("span"); - span.classList.add("badge", "mr-2"); - span.setAttribute('tooltip', tag.user); - - tag.badge.split(" ").forEach(b => span.classList.add(b)); - - span.insertAdjacentElement("beforeend", a); - - document.querySelector("#tags").insertAdjacentElement("afterbegin", span); + const queryapi = async (url, data, method = 'GET') => { + let req; + if (method == 'POST') { + req = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(data) }); - }; + } + else { + let s = []; + for (const [key, val] of Object.entries(data)) + s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); + req = await fetch(url + '?' + s.join('&')); + } + return await req.json(); + }; - const addtagClick = (ae = false) => { - if(ae) - ae.preventDefault(); + const get = async (url, data) => queryapi(url, data, 'GET'); + const post = async (url, data) => queryapi(url, data, 'POST'); + + const renderTags = _tags => { + [...document.querySelectorAll("#tags > .badge")].forEach(tag => tag.parentElement.removeChild(tag)); + _tags.reverse().forEach(tag => { + const a = document.createElement("a"); + a.href = `/tag/${tag.normalized}`; + a.style = "color: inherit !important"; + a.innerHTML = tag.tag; - const insert = document.querySelector("a#a_addtag"); const span = document.createElement("span"); - span.classList.add("badge", "badge-light", "mr-2"); + span.classList.add("badge", "mr-2"); + span.setAttribute('tooltip', tag.user); - const input = document.createElement("input"); - input.size = "10"; - input.value = ""; - input.setAttribute("list", "testlist"); - input.setAttribute("autoComplete", "off"); + tag.badge.split(" ").forEach(b => span.classList.add(b)); - span.insertAdjacentElement("afterbegin", input); - insert.insertAdjacentElement("beforebegin", span); + span.insertAdjacentElement("beforeend", a); - input.focus(); + document.querySelector("#tags").insertAdjacentElement("afterbegin", span); + }); + }; - let tt = null; - let lastInput = ''; - const testList = document.querySelector('#testlist'); + const addtagClick = (e) => { + if (e) e.preventDefault(); + const ctx = getContext(); + if (!ctx) return; + const { postid, tags } = ctx; - input.addEventListener("keyup", async e => { - if(e.key === "Enter") { - const tmptag = input.value?.trim(); - if(tags.includes(tmptag)) - return alert("tag already exists"); - const res = await post("/api/v2/admin/" + postid + "/tags", { - tagname: tmptag - }); - if(!res.success) { - alert(res.msg); - return false; - } - tags = res.tags.map(t => t.tag); - renderTags(res.tags); - addtagClick(); - testList.innerText = ""; - } - else if(e.key === "Escape") { - span.parentElement.removeChild(span); - testList.innerText = ""; - } - else { - if(tt != null) - clearTimeout(tt); + const insert = document.querySelector("a#a_addtag"); + // Check if input already exists to prevent duplicates + if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) { + insert.previousElementSibling.querySelector('input').focus(); + return; + } - tt = setTimeout(async () => { - tt = null; + const span = document.createElement("span"); + span.classList.add("badge", "badge-light", "mr-2"); - const tmptag = input.value?.trim(); + const input = document.createElement("input"); + input.size = "10"; + input.value = ""; + input.setAttribute("list", "testlist"); + input.setAttribute("autoComplete", "off"); - if(tmptag == lastInput || tmptag.length <= 1) - return false; + span.insertAdjacentElement("afterbegin", input); + insert.insertAdjacentElement("beforebegin", span); - testList.innerText = ""; - lastInput = tmptag; - - const res = await get('/api/v2/admin/tags/suggest', { - q: tmptag - }); - - for(const entry of res.suggestions) { - const option = document.createElement('option'); - option.value = entry.tag; + input.focus(); - if(!/fox/.test(navigator.userAgent)) - option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`; + let tt = null; + let lastInput = ''; + const testList = document.querySelector('#testlist'); - testList.insertAdjacentElement('beforeEnd', option); - }; - }, 500); - } - return true; - }); + input.addEventListener("keyup", async e => { + if (e.key === "Enter") { + const tmptag = input.value?.trim(); + // Check fresh tags from DOM just in case? Or use captured tags? + // Using captured 'tags' from when clicked is safe enough for immediate check. + if (tags.includes(tmptag)) + return alert("tag already exists"); - input.addEventListener("focusout", ie => { - if(input.value.length === 0) - input.parentElement.parentElement.removeChild(input.parentElement); - }); - }; - - const toggleEvent = async (e = false) => { - if(e) - e.preventDefault(); - - const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', { - method: 'PUT' - })).json(); - - renderTags(res.tags); - }; - - const toggleFavEvent = async e => { - const res = await post('/api/v2/admin/togglefav', { - postid: postid - }); - if(res.success) { - const fav = document.querySelector("svg#a_favo > use").href; - fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid"); - - // span#favs - const favcontainer = document.querySelector('span#favs'); - favcontainer.innerHTML = ""; - - favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0); - - res.favs.forEach(f => { - const a = document.createElement('a'); - a.href = `/user/${f.user}/favs`; - a.setAttribute('tooltip', f.user); - a.setAttribute('flow', 'up'); - - const img = document.createElement('img'); - img.src = `/t/${f.avatar}.webp`; - img.style.height = "32px"; - img.style.width = "32px"; - - a.insertAdjacentElement('beforeend', img); - favcontainer.insertAdjacentElement('beforeend', a); - favcontainer.innerHTML += " "; + const res = await post("/api/v2/admin/" + postid + "/tags", { + tagname: tmptag }); + if (!res.success) { + alert(res.msg); + return false; + } + // No need to update 'tags' local var, renderTags updates DOM, and next click reads DOM. + renderTags(res.tags); + + // Remove input and reset + span.parentElement.removeChild(span); + testList.innerText = ""; + + // Re-open input? Original code called addtagClick() again. + addtagClick(); + } + else if (e.key === "Escape") { + span.parentElement.removeChild(span); + testList.innerText = ""; } else { - // lul + if (tt != null) + clearTimeout(tt); + + tt = setTimeout(async () => { + tt = null; + + const tmptag = input.value?.trim(); + + if (tmptag == lastInput || tmptag.length <= 1) + return false; + + testList.innerText = ""; + lastInput = tmptag; + + const res = await get('/api/v2/admin/tags/suggest', { + q: tmptag + }); + + for (const entry of res.suggestions) { + const option = document.createElement('option'); + option.value = entry.tag; + + if (!/fox/.test(navigator.userAgent)) + option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`; + + testList.insertAdjacentElement('beforeEnd', option); + }; + }, 500); } - }; - - _addtag.addEventListener("click", addtagClick); - document.querySelector("a#a_toggle").addEventListener("click", toggleEvent); - document.querySelector("svg#a_favo").addEventListener("click", toggleFavEvent); - - document.addEventListener("keyup", e => { - if(e.target.tagName === "INPUT") - return; - if(e.key === "p") - toggleEvent(); - else if(e.key === "i") - addtagClick(); - else if(e.key === "x") - deleteButtonEvent(); - else if(e.key === "f") - toggleFavEvent(); + return true; }); - } - if(document.location.pathname === '/settings') { + input.addEventListener("focusout", ie => { + // Small delay to allow click events on suggestions or other checks? + // Original code: + if (input.value.length === 0) + input.parentElement.parentElement.removeChild(input.parentElement); + }); + }; + + const toggleEvent = async (e) => { + if (e) e.preventDefault(); + const ctx = getContext(); + if (!ctx) return; + const { postid } = ctx; + + const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', { + method: 'PUT' + })).json(); + + renderTags(res.tags); + }; + + const toggleFavEvent = async (e) => { + // e is the click event or undefined + const ctx = getContext(); + if (!ctx) return; + const { postid } = ctx; + + const res = await post('/api/v2/admin/togglefav', { + postid: postid + }); + if (res.success) { + const fav = document.querySelector("svg#a_favo > use").href; + fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid"); + + // span#favs + const favcontainer = document.querySelector('span#favs'); + favcontainer.innerHTML = ""; + + favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0); + + res.favs.forEach(f => { + const a = document.createElement('a'); + a.href = `/user/${f.user}/favs`; + a.setAttribute('tooltip', f.user); + a.setAttribute('flow', 'up'); + + const img = document.createElement('img'); + img.src = `/t/${f.avatar}.webp`; + img.style.height = "32px"; + img.style.width = "32px"; + + a.insertAdjacentElement('beforeend', img); + favcontainer.insertAdjacentElement('beforeend', a); + favcontainer.innerHTML += " "; + }); + } + else { + // lul + } + }; + + // Event Delegation + document.addEventListener("click", e => { + if (e.target.matches("a#a_addtag")) { + addtagClick(e); + } else if (e.target.matches("a#a_toggle")) { + toggleEvent(e); + } else if (e.target.closest("svg#a_favo")) { + toggleFavEvent(e); + } + }); + + document.addEventListener("keyup", e => { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") + return; + const ctx = getContext(); + if (!ctx) return; // Only trigger if on an item page + + if (e.key === "p") + toggleEvent(); + else if (e.key === "i") + addtagClick(); + else if (e.key === "f") + toggleFavEvent(); + }); + + // Settings page logic (unchanged essentially, but kept inside IIFE scope) + if (document.location.pathname === '/settings') { const saveAvatar = async e => { e.preventDefault(); @@ -209,20 +248,23 @@ const code = res.status; res = await res.json(); - switch(code) { + switch (code) { case 200: document.querySelector('#img_avatar').src = `/t/${avatar}.webp`; document.querySelector('img.avatar').src = `/t/${avatar}.webp`; - break; + break; default: console.log(res); - break; + break; } }; - document.querySelector('input#s_avatar').addEventListener('click', saveAvatar); - document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async e => { - if(e.key === 'Enter') + const sAvatar = document.querySelector('input#s_avatar'); + if (sAvatar) sAvatar.addEventListener('click', saveAvatar); + + const iAvatar = document.querySelector('input[name="i_avatar"]'); + if (iAvatar) iAvatar.addEventListener('keyup', async e => { + if (e.key === 'Enter') await saveAvatar(e); }); } diff --git a/src/inc/lib.mjs b/src/inc/lib.mjs index aa5628c..009c713 100644 --- a/src/inc/lib.mjs +++ b/src/inc/lib.mjs @@ -15,9 +15,9 @@ const epochs = [ ["second", 1] ]; const getDuration = timeAgoInSeconds => { - for(let [name, seconds] of epochs) { + for (let [name, seconds] of epochs) { const interval = ~~(timeAgoInSeconds / seconds); - if(interval >= 1) return { + if (interval >= 1) return { interval: interval, epoch: name }; @@ -32,7 +32,9 @@ export default new class { return (Math.round((b * 8 / s / 1e6) * 1e4) / 1e4); }; timeAgo(date) { - const { interval, epoch } = getDuration(~~((new Date() - new Date(date)) / 1e3)); + const duration = getDuration(~~((new Date() - new Date(date)) / 1e3)); + if (!duration) return "just now"; + const { interval, epoch } = duration; return `${interval} ${epoch}${interval === 1 ? "" : "s"} ago`; }; md5(str) { @@ -40,19 +42,19 @@ export default new class { }; getMode(mode) { let tmp; - switch(mode) { + switch (mode) { case 1: // nsfw tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)"; - break; + break; case 2: // untagged tmp = "items.id not in (select item_id from tags_assign group by item_id)"; - break; + break; case 3: // all tmp = "1 = 1"; - break; + break; default: // sfw tmp = "items.id in (select item_id from tags_assign where tag_id = 1 group by item_id)"; - break; + break; } return tmp; }; @@ -61,14 +63,14 @@ export default new class { }; genLink(env) { const link = []; - if(env.tag) link.push("tag", env.tag); - if(env.user) link.push("user", env.user, env.type ?? 'f0cks'); - if(env.mime?.length > 2) link.push(env.mime); + if (env.tag) link.push("tag", env.tag); + if (env.user) link.push("user", env.user, env.type ?? 'f0cks'); + if (env.mime?.length > 2) link.push(env.mime); let tmp = link.length === 0 ? '/' : link.join('/'); - if(!tmp.endsWith('/')) + if (!tmp.endsWith('/')) tmp = tmp + '/'; - if(!tmp.startsWith('/')) + if (!tmp.startsWith('/')) tmp = '/' + tmp; return { @@ -77,7 +79,7 @@ export default new class { }; }; parseTag(tag) { - if(!tag) + if (!tag) return null; return decodeURI(tag); } @@ -129,7 +131,7 @@ export default new class { return "$f0ck$" + salt + ":" + derivedKey.toString("hex"); }; async verify(str, hash) { - const [ salt, key ] = hash.substring(6).split(":"); + const [salt, key] = hash.substring(6).split(":"); const keyBuffer = Buffer.from(key, "hex"); const derivedKey = await scrypt(str, salt, 64); return crypto.timingSafeEqual(keyBuffer, derivedKey); @@ -143,20 +145,20 @@ export default new class { where "tags_assign".item_id = ${+itemid} order by "tags".id asc `; - for(let t = 0; t < tags.length; t++) { - if(tags[t].tag.startsWith(">")) + for (let t = 0; t < tags.length; t++) { + if (tags[t].tag.startsWith(">")) tags[t].badge = "badge-greentext badge-light"; - else if(tags[t].normalized === "ukraine") + else if (tags[t].normalized === "ukraine") tags[t].badge = "badge-ukraine badge-light"; - else if(/[а-яё]/.test(tags[t].normalized) || tags[t].normalized === "russia") + else if (/[а-яё]/.test(tags[t].normalized) || tags[t].normalized === "russia") tags[t].badge = "badge-russia badge-light"; - else if(tags[t].normalized === "german") + else if (tags[t].normalized === "german") tags[t].badge = "badge-german badge-light"; - else if(tags[t].normalized === "dutch") + else if (tags[t].normalized === "dutch") tags[t].badge = "badge-dutch badge-light"; - else if(tags[t].normalized === "sfw") + else if (tags[t].normalized === "sfw") tags[t].badge = "badge-success"; - else if(tags[t].normalized === "nsfw") + else if (tags[t].normalized === "nsfw") tags[t].badge = "badge-danger"; else tags[t].badge = "badge-light"; @@ -183,11 +185,11 @@ export default new class { const tmp = Object.values(res)[0]; let nsfw = false; - if(tmp.neutral >= .7) + if (tmp.neutral >= .7) nsfw = false; - else if((tmp.sexy + tmp.porn + tmp.hentai) >= .7) + else if ((tmp.sexy + tmp.porn + tmp.hentai) >= .7) nsfw = true; - else if(tmp.drawings >= .4) + else if (tmp.drawings >= .4) nsfw = false; else nsfw = false; @@ -197,7 +199,7 @@ export default new class { score: tmp.sexy + tmp.porn + tmp.hentai, scores: tmp }; - + }; async getDefaultAvatar() { return (await db` @@ -212,7 +214,7 @@ export default new class { // meddlware admin async auth(req, res, next) { - if(!req.session || !req.session.admin) { + if (!req.session || !req.session.admin) { return res.reply({ code: 401, body: "401 - Unauthorized" @@ -223,7 +225,7 @@ export default new class { // meddlware user async userauth(req, res, next) { - if(!req.session) { + if (!req.session) { return res.reply({ code: 401, body: "401 - Unauthorized" @@ -232,14 +234,14 @@ export default new class { return next(); }; - async loggedin(req, res, next) { - if(!req.session) { - return res.reply({ - code: 401, - body: "401 - Unauthorized" - }); - } - return next(); + async loggedin(req, res, next) { + if (!req.session) { + return res.reply({ + code: 401, + body: "401 - Unauthorized" + }); + } + return next(); }; }; diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs new file mode 100644 index 0000000..a933050 --- /dev/null +++ b/src/inc/routes/ajax.mjs @@ -0,0 +1,68 @@ +import f0cklib from "../routeinc/f0cklib.mjs"; +import url from "url"; + +export default (router, tpl) => { + router.get(/\/ajax\/item\/(?\d+)/, async (req, res) => { + let query = {}; + if (typeof req.url === 'string') { + const parsedUrl = url.parse(req.url, true); + query = parsedUrl.query; + } else { + // flummpress uses req.url.qs for query string parameters + query = req.url.qs || {}; + } + + let contextUrl = `/${req.params.itemid}`; + if (query.tag) contextUrl = `/tag/${query.tag}/${req.params.itemid}`; + if (query.user) contextUrl = `/user/${query.user}/${req.params.itemid}`; // User filter takes precedence if both? usually mutually exclusive + + const data = await f0cklib.getf0ck({ + itemid: req.params.itemid, + mode: req.session.mode, + session: !!req.session, + url: contextUrl, + user: query.user, + tag: query.tag, + mime: query.mime + }); + + if (!data.success) { + return res.reply({ + code: 404, + body: "

404 - Not f0cked

" + }); + } + + // Inject session into data for the template + // We clone session to avoid unintended side effects or collisions + if (req.session) { + data.session = { ...req.session }; + // data.user comes from f0cklib (uploader). req.session.user is logged-in user string. + // If template engine confuses them, removing session.user from this context might help. + // item-partial doesn't use session.user. + // Note: If anything fails, it prints literal code, so we ensure no collision. + if (data.session.user) delete data.session.user; + } else { + data.session = false; + } + + // Inject missing variables normally provided by req or middleware + data.url = { pathname: `/${req.params.itemid}` }; // Template expects url.pathname + data.fullscreen = req.cookies.fullscreen || 0; // Index.mjs uses req.cookies.fullscreen + + // Render both the item content and the pagination + const itemHtml = tpl.render('ajax-item', data); + const paginationHtml = tpl.render('snippets/pagination', data); + + // Return JSON response + return res.reply({ + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + html: itemHtml, + pagination: paginationHtml + }) + }); + }); + + return router; +}; diff --git a/views/ajax-item.html b/views/ajax-item.html new file mode 100644 index 0000000..8ae6234 --- /dev/null +++ b/views/ajax-item.html @@ -0,0 +1 @@ +@include(item-partial) \ No newline at end of file diff --git a/views/item-partial.html b/views/item-partial.html new file mode 100644 index 0000000..8f96e73 --- /dev/null +++ b/views/item-partial.html @@ -0,0 +1,126 @@ +
+
{{ (url.pathname) }}
+
+ @if(session) +
+ + + + + + + + @if(session.admin) + + @endif +
+ @endif +
+
+ +
+ @if(item.mime.startsWith("video")) +
+ +
+ @elseif(item.mime.startsWith("audio")) +
+ +
+ @elseif(item.mime.startsWith("image")) +
+
+ +
+
+ @else +

404 - Not f0cked

+ @endif +
+ +
+ \ No newline at end of file diff --git a/views/item.html b/views/item.html index a8fbdf0..7c182b3 100644 --- a/views/item.html +++ b/views/item.html @@ -2,118 +2,9 @@
- +
-
-
{{ (url.pathname) }}
-
- @if(session) -
- - - @if(session.admin)@endif -
- @endif -
-
- -
- @if(item.mime.startsWith("video")) -
- -
- @elseif(item.mime.startsWith("audio")) -
- -
- @elseif(item.mime.startsWith("image")) -
-
- -
-
- @else -

404 - Not f0cked

- @endif -
- -
- + @include(item-partial) +
-
-@include(snippets/footer) + @include(snippets/footer) \ No newline at end of file diff --git a/views/snippets/navbar.html b/views/snippets/navbar.html index 9e21ff4..cdbf1a9 100644 --- a/views/snippets/navbar.html +++ b/views/snippets/navbar.html @@ -1,15 +1,15 @@ @if(session) -