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(pagination.prev)
+
+ @else
+
+ @endif
+
+
+
+ @if(pagination.next)
+
+ @else
+
+ @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(pagination.prev)
-
- @else
-
- @endif
-
-
-
- @if(pagination.next)
-
- @else
-
- @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)
-
+
w0bm.com
- tags
- about
- @if(!/^\/\d$/.test(url.pathname))
- rand
- @endif
+ tags
+ about
+ @if(!/^\/\d$/.test(url.pathname))
+ rand
+ @endif
@@ -17,21 +17,7 @@
@@ -44,11 +30,11 @@
- tags
- about
- @if(!/^\/\d$/.test(url.pathname))
- rand
- @endif
+ tags
+ about
+ @if(!/^\/\d$/.test(url.pathname))
+ rand
+ @endif
@@ -56,21 +42,7 @@
diff --git a/views/snippets/pagination.html b/views/snippets/pagination.html
new file mode 100644
index 0000000..6dad328
--- /dev/null
+++ b/views/snippets/pagination.html
@@ -0,0 +1,19 @@
+@if(typeof pagination !== "undefined")
+
+@endif
\ No newline at end of file