added AJAX loading for videos #1

Merged
schrumpel merged 3 commits from eins/f0bm:eins-patch-1 into f0bm 2026-01-23 13:33:59 +00:00
10 changed files with 1112 additions and 773 deletions

View File

@@ -1,23 +1,13 @@
(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') => {
@@ -50,7 +40,9 @@
a.href = `/tag/${tag.normalized}`;
a.style = "color: inherit !important";
a.innerHTML = tag.tag;
a.addEventListener("click", editTagEvent); // tmp
// 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");
@@ -61,7 +53,9 @@
const delbutton = document.createElement("a");
delbutton.innerHTML = " ×";
delbutton.href = "#";
delbutton.addEventListener("click", deleteEvent);
// Class for delegation
delbutton.classList.add("admin-deltag");
span.insertAdjacentElement("beforeend", a);
span.innerHTML += ' ';
span.insertAdjacentElement("beforeend", delbutton);
@@ -70,11 +64,38 @@
});
};
const addtagClick = (ae = false) => {
if(ae)
ae.preventDefault();
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");
@@ -96,8 +117,11 @@
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
});
@@ -105,41 +129,29 @@
alert(res.msg);
return false;
}
tags = res.tags.map(t => t.tag);
renderTags(res.tags);
addtagClick();
span.parentElement.removeChild(span);
testList.innerText = "";
addtagClick();
}
else if (e.key === "Escape") {
span.parentElement.removeChild(span);
testList.innerText = "";
}
else {
if(tt != null)
clearTimeout(tt);
if (tt != null) clearTimeout(tt);
tt = setTimeout(async () => {
tt = null;
const tmptag = input.value?.trim();
if(tmptag == lastInput || tmptag.length <= 1)
return false;
if (tmptag == lastInput || tmptag.length <= 1) return false;
testList.innerText = "";
lastInput = tmptag;
const res = await get('/api/v2/admin/tags/suggest', {
q: 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);
@@ -153,9 +165,11 @@
});
};
const toggleEvent = async (e = false) => {
if(e)
e.preventDefault();
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'
@@ -165,8 +179,11 @@
};
const deleteButtonEvent = async e => {
if(e)
e.preventDefault();
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", {
@@ -177,7 +194,11 @@
}
};
const toggleFavEvent = async e => {
const toggleFavEvent = async (e) => {
const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
const res = await post('/api/v2/admin/togglefav', {
postid: postid
});
@@ -185,38 +206,29 @@
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 += "&nbsp;";
});
}
else {
// lul
}
};
let tmptt = null;
const editTagEvent = async e => { // mousedown
const editTagEvent = async e => {
e.preventDefault();
if(e.detail === 2) {
if (e.detail === 2) { // Double click
clearTimeout(tmptt);
const old = e.target;
const parent = e.target.parentElement;
@@ -229,30 +241,25 @@
parent.insertAdjacentElement('afterbegin', textfield);
textfield.focus();
parent.removeChild(e.target);
parent.querySelector('a:last-child').style.display = 'none';
// 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);
// send
let res = await fetch('/api/v2/admin/tags/' + encodeURIComponent(oldtag), {
method: 'PUT',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
newtag: textfield.value
})
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 200:
case 201:
//parent.removeChild(textfield);
parent.insertAdjacentElement('afterbegin', old);
parent.querySelector('a:last-child').style.display = '';
if (delBtn) delBtn.style.display = '';
old.href = `/tag/${res.tag}`;
old.innerText = res.tag.trim();
break;
@@ -264,68 +271,72 @@
else if (e.key === 'Escape') {
parent.removeChild(textfield);
parent.insertAdjacentElement('afterbegin', old);
parent.querySelector('a:last-child').style.display = '';
if (delBtn) delBtn.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);
// 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 " &#215;" 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;
if(e.key === "p")
toggleEvent();
else if(e.key === "i")
addtagClick();
else if(e.key === "x")
deleteButtonEvent();
else if(e.key === "f")
toggleFavEvent();
});
}
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"
},
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ avatar })
});
const code = res.status;
res = await res.json();
switch(code) {
case 200:
if (code === 200) {
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
break;
default:
console.log(res);
break;
}
};
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); });
}
})();

View File

@@ -17,7 +17,9 @@ window.requestAnimFrame = (function(){
}
});
document.getElementById('togglebg').addEventListener('click', function (e) {
const toggleBg = document.getElementById('togglebg');
if (toggleBg) {
toggleBg.addEventListener('click', function (e) {
e.preventDefault();
background = !background;
localStorage.setItem('background', background.toString());
@@ -31,6 +33,7 @@ window.requestAnimFrame = (function(){
}
animationLoop();
});
}
if (elem !== null) {
if (localStorage.getItem('background') == undefined) {
@@ -39,6 +42,7 @@ window.requestAnimFrame = (function(){
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;
@@ -53,14 +57,203 @@ window.requestAnimFrame = (function(){
elem.addEventListener('play', animationLoop);
}
}
}
let tt = false;
const stimeout = 500;
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];
// <context-preservation>
// 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]);
}
}
// </context-preservation>
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 = '<div class="container"></div>';
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);
});
// <keybindings>
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
const keybindings = {
@@ -94,9 +287,11 @@ window.requestAnimFrame = (function(){
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();
}
}
};
@@ -137,11 +332,16 @@ window.requestAnimFrame = (function(){
const scroll_treshold = 1;
if ([...document.querySelectorAll("div.posts")].length === 1) {
document.addEventListener("wheel", e => {
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) {
document.querySelector("div#footbar").style.boxShadow = "inset 0px 4px 0px var(--footbar-color)";
document.querySelector("div#footbar").style.color = "var(--footbar-color)";
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
@@ -151,8 +351,11 @@ window.requestAnimFrame = (function(){
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";
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,9 +364,13 @@ 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";
}
});
}

View File

@@ -1,8 +1,14 @@
(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));
// 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 queryapi = async (url, data, method = 'GET') => {
let req;
@@ -47,11 +53,19 @@
});
};
const addtagClick = (ae = false) => {
if(ae)
ae.preventDefault();
const addtagClick = (e) => {
if (e) e.preventDefault();
const ctx = getContext();
if (!ctx) return;
const { postid, tags } = ctx;
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;
}
const span = document.createElement("span");
span.classList.add("badge", "badge-light", "mr-2");
@@ -73,8 +87,11 @@
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");
const res = await post("/api/v2/admin/" + postid + "/tags", {
tagname: tmptag
});
@@ -82,10 +99,15 @@
alert(res.msg);
return false;
}
tags = res.tags.map(t => t.tag);
// No need to update 'tags' local var, renderTags updates DOM, and next click reads DOM.
renderTags(res.tags);
addtagClick();
// 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);
@@ -125,14 +147,18 @@
});
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 = false) => {
if(e)
e.preventDefault();
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'
@@ -141,7 +167,12 @@
renderTags(res.tags);
};
const toggleFavEvent = async e => {
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
});
@@ -176,24 +207,32 @@
}
};
_addtag.addEventListener("click", addtagClick);
document.querySelector("a#a_toggle").addEventListener("click", toggleEvent);
document.querySelector("svg#a_favo").addEventListener("click", toggleFavEvent);
// 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")
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 === "x")
deleteButtonEvent();
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();
@@ -220,8 +259,11 @@
}
};
document.querySelector('input#s_avatar').addEventListener('click', saveAvatar);
document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async 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);
});

View File

@@ -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) {

68
src/inc/routes/ajax.mjs Normal file
View File

@@ -0,0 +1,68 @@
import f0cklib from "../routeinc/f0cklib.mjs";
import url from "url";
export default (router, tpl) => {
router.get(/\/ajax\/item\/(?<itemid>\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: "<h1>404 - Not f0cked</h1>"
});
}
// 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;
};

1
views/ajax-item.html Normal file
View File

@@ -0,0 +1 @@
@include(item-partial)

126
views/item-partial.html Normal file
View File

@@ -0,0 +1,126 @@
<div class="_204863">
<div class="location">{{ (url.pathname) }}</div>
<div class="gapLeft"></div>
@if(session)
<div class="gapRight">
<svg class="iconset" id="a_favo">
<use
href="/s/img/iconset.svg#heart_{{ Object.values(item.favorites).filter(u => u.user == session.user)[0] ? 'solid' : 'regular' }}">
</use>
</svg>
<svg class="iconset" id="a_tfull">
<use href="/s/img/iconset.svg#window-{{ fullscreen == 1 ? 'minimize' : 'maximize' }}"></use>
</svg>
@if(session.admin)<svg class="iconset" id="a_delete">
<use href="/s/img/iconset.svg#cross"></use>
</svg>@endif
</div>
@endif
</div>
<div class="content">
<div class="next-post">
@if(pagination.prev)
<div class="arrow-next">
<a id="next" href="{{ link.main }}{{ pagination.prev }}"></a>
</div>
@else
<div class="arrow-next">
<a id="next" href="#" style="color: #ccc !important;"></a>
</div>
@endif
</div>
<div class="media-object">
@if(item.mime.startsWith("video"))
<div class="embed-responsive embed-responsive-16by9">
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}"
preload="auto" autoplay controls loop playsinline></video>
</div>
@elseif(item.mime.startsWith("audio"))
<div class="embed-responsive embed-responsive-16by9"
style="background: url('@if(item.coverart)//w0bm.com{{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;">
<audio id="my-video" class="embed-responsive-item" autoplay controls loop src="{{ item.dest }}"
data-setup="{}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif"
type="{{ item.mime }}"></audio>
</div>
@elseif(item.mime.startsWith("image"))
<div class="embed-responsive embed-responsive-16by9">
<div class="embed-responsive-image" id="image-scroll">
<a href="{{ item.dest }}" id="elfe" target="_blank"><img id="f0ck-image" class="img-fluid"
src="{{ item.dest }}" loading="lazy" decoding="async" /></a>
</div>
</div>
@else
<h1>404 - Not f0cked</h1>
@endif
</div>
<div class="previous-post">
@if(pagination.next)
<div class="arrow-prev">
<a id="prev" href="{{ link.main }}{{ pagination.next }}"></a>
</div>
@else
<div class="arrow-prev">
<a id="prev" href="#" style="color: #ccc !important;"></a>
</div>
@endif
</div>
</div>
<div class="metadata">
<div class="kontrollelement">
<div class="einheit">
@if(typeof pagination !== "undefined")
<nav class="steuerung">
@if(pagination.prev)
<a class="nav-next" href="{{ link.main }}{{ pagination.prev }}">← next</a>
@else
<a class="nav-next" href="#" style="visibility: hidden">← next</a>
@endif
<span>|</span>
<a id="random" class="" href="/random">
<span>random</span>
</a>
<span>
|
</span>
@if(pagination.next)
<a class="nav-prev" href="{{ link.main }}{{ pagination.next }}">prev →</a>
@else
<a class="nav-prev" href="#" style="visibility: hidden">prev →</a>
@endif
</nav>
@endif
</div>
</div>
<div class="blahlol">
<span class="badge badge-dark">
<a href="/{{ item.id }}" class="id-link">{{ item.id }}</a>
@if(session)
(<a id="a_username" href="/user/{{ user.name.toLowerCase() }}/f0cks@if(tmp.mime)/{{ tmp.mime }}@endif">{{ user.name }}</a>)
@endif
</span>
<span class="badge badge-dark"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{ item.timestamp.timeago }}</time></span>
<span class="badge badge-dark" id="tags">
@if(typeof item.tags !== "undefined")
@each(item.tags as tag)
<span @if(session)tooltip="{{ tag.user }}" @endif class="badge {{ tag.badge }} mr-2">
<a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(session.admin)&nbsp;<a class="removetag"
href="#">&#215;</a>@endif
</span>
@endeach
@endif
@if(session)
<a href="#" id="a_addtag">add tag</a>&nbsp;-&nbsp;<a href="#" id="a_toggle">toggle</a>
<datalist id="testlist"></datalist>
@endif
</span>
<span class="badge" id="favs" @if(!item.favorites.length || !session) hidden@endif>
@if(item.favorites.length && session)
@each(item.favorites as fav)
<a href="/user/{{ fav.user.toLowerCase() }}/favs" tooltip="{{ fav.user }}" flow="up"><img
src="@if(fav.avatar)/t/{{ fav.avatar }}.webp@else/s/img/default.png@endif"
style="height: 32px; width: 32px" /></a>
@endeach
@endif
</span>
</div>
</div>

View File

@@ -4,116 +4,7 @@
<div id="main">
<div class="container">
<div class="_204863">
<div class="location">{{ (url.pathname) }}</div>
<div class="gapLeft"></div>
@if(session)
<div class="gapRight">
<svg class="iconset" id="a_favo"><use href="/s/img/iconset.svg#heart_{{ Object.values(item.favorites).filter(u => u.user == session.user)[0] ? 'solid' : 'regular' }}"></use></svg>
<svg class="iconset" id="a_tfull"><use href="/s/img/iconset.svg#window-{{ fullscreen == 1 ? 'minimize' : 'maximize' }}"></use></svg>
@if(session.admin)<svg class="iconset" id="a_delete"><use href="/s/img/iconset.svg#cross"></use></svg>@endif
</div>
@endif
</div>
<div class="content">
<div class="next-post">
@if(pagination.prev)
<div class="arrow-next">
<a id="next" href="{{ link.main }}{{ pagination.prev }}"></a>
</div>
@else
<div class="arrow-next">
<a id="next" href="#" style="color: #ccc !important;"></a>
</div>
@endif
</div>
<div class="media-object">
@if(item.mime.startsWith("video"))
<div class="embed-responsive embed-responsive-16by9">
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}" preload="auto" autoplay controls loop playsinline></video>
</div>
@elseif(item.mime.startsWith("audio"))
<div class="embed-responsive embed-responsive-16by9" style="background: url('@if(item.coverart)//w0bm.com{{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;">
<audio id="my-video" class="embed-responsive-item" autoplay controls loop src="{{ item.dest }}" data-setup="{}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" type="{{ item.mime }}"></audio>
</div>
@elseif(item.mime.startsWith("image"))
<div class="embed-responsive embed-responsive-16by9">
<div class="embed-responsive-image" id="image-scroll">
<a href="{{ item.dest }}" id="elfe" target="_blank"><img id="f0ck-image" class="img-fluid" src="{{ item.dest }}" loading="lazy" decoding="async"/></a>
</div>
</div>
@else
<h1>404 - Not f0cked</h1>
@endif
</div>
<div class="previous-post">
@if(pagination.next)
<div class="arrow-prev">
<a id="prev" href="{{ link.main }}{{ pagination.next }}"></a>
</div>
@else
<div class="arrow-prev">
<a id="prev" href="#" style="color: #ccc !important;"></a>
</div>
@endif
</div>
</div>
<div class="metadata">
<div class="kontrollelement">
<div class="einheit">
@if(typeof pagination !== "undefined")
<nav class="steuerung">
@if(pagination.prev)
<a id="" href="{{ link.main }}{{ pagination.prev }}">← next</a>
@else
<a id="" href="#" style="visibility: hidden">← next</a>
@endif
<span>|</span>
<a id="random" class="" href="/random">
<span>random</span>
</a>
<span>
|
</span>
@if(pagination.next)
<a id="" href="{{ link.main }}{{ pagination.next }}">prev →</a>
@else
<a id="" href="#" style="visibility: hidden">prev →</a>
@endif
</nav>
@endif
</div>
</div>
<div class="blahlol">
<span class="badge badge-dark">
<a href="/{{ item.id }}" class="id-link">{{ item.id }}</a>
@if(session)
(<a id="a_username" href="/user/{{ user.name.toLowerCase() }}/f0cks@if(tmp.mime)/{{ tmp.mime }}@endif">{{ user.name }}</a>)
@endif
</span>
<span class="badge badge-dark"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{ item.timestamp.timeago }}</time></span>
<span class="badge badge-dark" id="tags">
@if(typeof item.tags !== "undefined")
@each(item.tags as tag)
<span @if(session)tooltip="{{ tag.user }}"@endif class="badge {{ tag.badge }} mr-2">
<a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(session.admin)&nbsp;<a class="removetag" href="#">&#215;</a>@endif
</span>
@endeach
@endif
@if(session)
<a href="#" id="a_addtag">add tag</a>&nbsp;-&nbsp;<a href="#" id="a_toggle">toggle</a>
<datalist id="testlist"></datalist>
@endif
</span>
<span class="badge" id="favs"@if(!item.favorites.length || !session) hidden@endif>
@if(item.favorites.length && session)
@each(item.favorites as fav)
<a href="/user/{{ fav.user.toLowerCase() }}/favs" tooltip="{{ fav.user }}" flow="up"><img src="@if(fav.avatar)/t/{{ fav.avatar }}.webp@else/s/img/default.png@endif" style="height: 32px; width: 32px" /></a>
@endeach
@endif
</span>
</div>
</div>
@include(item-partial)
</div>
</div>
@include(snippets/footer)

View File

@@ -17,21 +17,7 @@
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
<div class="pagination-container-fluid">
<div class="pagination-wrapper">
@if(typeof pagination !== "undefined")
<nav class="pagination">
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">&laquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}" class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">&lsaquo;</a>
@each(pagination.cheat as i)
@if(i == pagination.page)
<span class="btn disabled">{{ i }}</span>
@else
<a href="{{ link.main }}{{ link.path }}{{ i }}" class="pagination-int-item btn">{{ i }}</a>
@endif
@endeach
<a href="{{ link.main }}{{ link.path }}{{ pagination.next }}" class="page-item-3 btn next@if(!pagination.next) disabled@endif">&rsaquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}" class="page-item-4 btn start@if(!pagination.next) disabled@endif">&raquo;</a>
</nav>
@endif
@include(snippets/pagination)
</div>
</div>
</div>
@@ -56,21 +42,7 @@
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
<div class="pagination-container-fluid">
<div class="pagination-wrapper">
@if(typeof pagination !== "undefined")
<nav class="pagination">
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">&laquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}" class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">&lsaquo;</a>
@each(pagination.cheat as i)
@if(i == pagination.page)
<span class="btn disabled">{{ i }}</span>
@else
<a href="{{ link.main }}{{ link.path }}{{ i }}" class="pagination-int-item btn">{{ i }}</a>
@endif
@endeach
<a href="{{ link.main }}{{ link.path }}{{ pagination.next }}" class="page-item-3 btn next@if(!pagination.next) disabled@endif">&rsaquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}" class="page-item-4 btn start@if(!pagination.next) disabled@endif">&raquo;</a>
</nav>
@endif
@include(snippets/pagination)
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
@if(typeof pagination !== "undefined")
<nav class="pagination">
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}"
class="page-item-1 btn start@if(!pagination.prev) disabled@endif">&laquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}"
class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">&lsaquo;</a>
@each(pagination.cheat as i)
@if(i == pagination.page)
<span class="btn disabled">{{ i }}</span>
@else
<a href="{{ link.main }}{{ link.path }}{{ i }}" class="pagination-int-item btn">{{ i }}</a>
@endif
@endeach
<a href="{{ link.main }}{{ link.path }}{{ pagination.next }}"
class="page-item-3 btn next@if(!pagination.next) disabled@endif">&rsaquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}"
class="page-item-4 btn start@if(!pagination.next) disabled@endif">&raquo;</a>
</nav>
@endif