Merge pull request 'added AJAX loading for videos' (#1) from eins/f0bm:eins-patch-1 into f0bm
Reviewed-on: #1 Reviewed-by: Kibi Kelburton <schrumpel@noreply.DOMAIN>
This commit is contained in:
@@ -1,331 +1,342 @@
|
|||||||
(async () => {
|
(async () => {
|
||||||
if(_addtag = document.querySelector("a#a_addtag")) {
|
// Helper to get dynamic context
|
||||||
const postid = +document.querySelector("a.id-link").innerText;
|
const getContext = () => {
|
||||||
const poster = document.querySelector("a#a_username").innerText;
|
const idLink = document.querySelector("a.id-link");
|
||||||
let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2));
|
if (!idLink) return null;
|
||||||
|
return {
|
||||||
const deleteEvent = async e => {
|
postid: +idLink.innerText,
|
||||||
e.preventDefault();
|
poster: document.querySelector("a#a_username")?.innerText,
|
||||||
if(!confirm("Do you really want to delete this tag?"))
|
tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2))
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const queryapi = async (url, data, method = 'GET') => {
|
const queryapi = async (url, data, method = 'GET') => {
|
||||||
let req;
|
let req;
|
||||||
if(method == 'POST') {
|
if (method == 'POST') {
|
||||||
req = await fetch(url, {
|
req = await fetch(url, {
|
||||||
method: "POST",
|
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',
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json"
|
"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 })
|
body: JSON.stringify({ avatar })
|
||||||
});
|
});
|
||||||
const code = res.status;
|
const code = res.status;
|
||||||
res = await res.json();
|
res = await res.json();
|
||||||
|
if (code === 200) {
|
||||||
switch(code) {
|
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
|
||||||
case 200:
|
document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
|
||||||
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
|
|
||||||
document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log(res);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const sAvatar = document.querySelector('input#s_avatar');
|
||||||
document.querySelector('input#s_avatar').addEventListener('click', saveAvatar);
|
if (sAvatar) sAvatar.addEventListener('click', saveAvatar);
|
||||||
document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async e => {
|
const iAvatar = document.querySelector('input[name="i_avatar"]');
|
||||||
if(e.key === 'Enter')
|
if (iAvatar) iAvatar.addEventListener('keyup', async e => { if (e.key === 'Enter') await saveAvatar(e); });
|
||||||
await saveAvatar(e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,66 +1,259 @@
|
|||||||
|
|
||||||
window.requestAnimFrame = (function(){
|
window.requestAnimFrame = (function () {
|
||||||
return window.requestAnimationFrame
|
return window.requestAnimationFrame
|
||||||
|| window.webkitRequestAnimationFrame
|
|| window.webkitRequestAnimationFrame
|
||||||
|| window.mozRequestAnimationFrame
|
|| window.mozRequestAnimationFrame
|
||||||
|| function(callback) { window.setTimeout(callback, 1000 / 60);};
|
|| function (callback) { window.setTimeout(callback, 1000 / 60); };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
let video;
|
let video;
|
||||||
if(elem = document.querySelector("#my-video")) {
|
if (elem = document.querySelector("#my-video")) {
|
||||||
video = new v0ck(elem);
|
video = new v0ck(elem);
|
||||||
document.addEventListener("keydown", e => {
|
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']();
|
video[video.paused ? 'play' : 'pause']();
|
||||||
document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden');
|
document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('togglebg').addEventListener('click', function (e) {
|
const toggleBg = document.getElementById('togglebg');
|
||||||
e.preventDefault();
|
if (toggleBg) {
|
||||||
background = !background;
|
toggleBg.addEventListener('click', function (e) {
|
||||||
localStorage.setItem('background', background.toString());
|
e.preventDefault();
|
||||||
var canvas = document.getElementById('bg');
|
background = !background;
|
||||||
if (background) {
|
localStorage.setItem('background', background.toString());
|
||||||
|
var canvas = document.getElementById('bg');
|
||||||
|
if (background) {
|
||||||
canvas.classList.add('fader-in');
|
canvas.classList.add('fader-in');
|
||||||
canvas.classList.remove('fader-out');
|
canvas.classList.remove('fader-out');
|
||||||
} else {
|
} else {
|
||||||
canvas.classList.add('fader-out');
|
canvas.classList.add('fader-out');
|
||||||
canvas.classList.remove('fader-in');
|
canvas.classList.remove('fader-in');
|
||||||
}
|
}
|
||||||
animationLoop();
|
animationLoop();
|
||||||
});
|
});
|
||||||
|
|
||||||
if(elem !== null) {
|
|
||||||
if(localStorage.getItem('background') == undefined) {
|
|
||||||
localStorage.setItem('background', 'true');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (elem !== null) {
|
||||||
if(video.paused || video.ended || !background)
|
if (localStorage.getItem('background') == undefined) {
|
||||||
return;
|
localStorage.setItem('background', 'true');
|
||||||
context.drawImage(video, 0, 0, cw, ch);
|
}
|
||||||
window.requestAnimFrame(animationLoop);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
let tt = false;
|
||||||
const stimeout = 500;
|
const stimeout = 500;
|
||||||
const changePage = (e, pbwork = true) => {
|
|
||||||
pbwork && document.querySelector("nav.navbar").classList.add("pbwork");
|
const setupMedia = () => {
|
||||||
!tt && (tt = setTimeout(() => e.click(), stimeout));
|
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>
|
// <keybindings>
|
||||||
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
|
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
|
||||||
const keybindings = {
|
const keybindings = {
|
||||||
@@ -72,8 +265,8 @@ window.requestAnimFrame = (function(){
|
|||||||
" ": clickOnElementBinding("#f0ck-image")
|
" ": clickOnElementBinding("#f0ck-image")
|
||||||
};
|
};
|
||||||
document.addEventListener("keydown", e => {
|
document.addEventListener("keydown", e => {
|
||||||
if(e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
|
if (e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
|
||||||
if(e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
|
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
|
||||||
return;
|
return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
keybindings[e.key]();
|
keybindings[e.key]();
|
||||||
@@ -84,19 +277,21 @@ window.requestAnimFrame = (function(){
|
|||||||
// <image-responsive>
|
// <image-responsive>
|
||||||
const imgSize = e => new Promise((res, _) => {
|
const imgSize = e => new Promise((res, _) => {
|
||||||
const i = new Image();
|
const i = new Image();
|
||||||
i.addEventListener('load', function() {
|
i.addEventListener('load', function () {
|
||||||
res({ width: this.width, height: this.height });
|
res({ width: this.width, height: this.height });
|
||||||
});
|
});
|
||||||
i.src = e.src;
|
i.src = e.src;
|
||||||
});
|
});
|
||||||
|
|
||||||
// <wheeler>
|
// <wheeler>
|
||||||
const wheelEventListener = function(event) {
|
const wheelEventListener = function (event) {
|
||||||
if (event.target.closest('.media-object, .steuerung')) {
|
if (event.target.closest('.media-object, .steuerung')) {
|
||||||
if (event.deltaY < 0) {
|
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) {
|
} 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(){
|
|||||||
// </wheeler>
|
// </wheeler>
|
||||||
|
|
||||||
|
|
||||||
if(f0ckimage = document.querySelector("img#f0ck-image")) {
|
if (f0ckimage = document.querySelector("img#f0ck-image")) {
|
||||||
const f0ckimagescroll = document.querySelector("#image-scroll");
|
const f0ckimagescroll = document.querySelector("#image-scroll");
|
||||||
|
|
||||||
let isImageExpanded = false;
|
let isImageExpanded = false;
|
||||||
@@ -135,24 +330,32 @@ window.requestAnimFrame = (function(){
|
|||||||
// <scroller>
|
// <scroller>
|
||||||
let tts = 0;
|
let tts = 0;
|
||||||
const scroll_treshold = 1;
|
const scroll_treshold = 1;
|
||||||
if([...document.querySelectorAll("div.posts")].length === 1) {
|
if ([...document.querySelectorAll("div.posts")].length === 1) {
|
||||||
document.addEventListener("wheel", e => {
|
document.addEventListener("wheel", e => {
|
||||||
if(Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down
|
if (!document.querySelector('#main')) return;
|
||||||
if(elem = document.querySelector(".pagination > .next:not(.disabled)")) {
|
|
||||||
if(tts < scroll_treshold) {
|
if (Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down
|
||||||
document.querySelector("div#footbar").style.boxShadow = "inset 0px 4px 0px var(--footbar-color)";
|
if (elem = document.querySelector(".pagination > .next:not(.disabled)")) {
|
||||||
document.querySelector("div#footbar").style.color = "var(--footbar-color)";
|
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++;
|
tts++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
changePage(elem);
|
changePage(elem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(window.scrollY <= 0 && e.deltaY < 0) { // up
|
else if (window.scrollY <= 0 && e.deltaY < 0) { // up
|
||||||
if(elem = document.querySelector(".pagination > .prev:not(.disabled)")) {
|
if (elem = document.querySelector(".pagination > .prev:not(.disabled)")) {
|
||||||
if(tts < scroll_treshold) {
|
if (tts < scroll_treshold) {
|
||||||
document.querySelector("nav.navbar").style.boxShadow = "0px 2px 0px var(--loading-indicator-color)";
|
const nav = document.querySelector("nav.navbar");
|
||||||
document.querySelector("nav.navbar").style.transition = ".2s ease-in-out";
|
if (nav) {
|
||||||
|
nav.style.boxShadow = "0px 2px 0px var(--loading-indicator-color)";
|
||||||
|
nav.style.transition = ".2s ease-in-out";
|
||||||
|
}
|
||||||
tts++;
|
tts++;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -161,19 +364,23 @@ window.requestAnimFrame = (function(){
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
tts = 0;
|
tts = 0;
|
||||||
document.querySelector("div#footbar").style.boxShadow = "unset";
|
const foot = document.querySelector("div#footbar");
|
||||||
document.querySelector("div#footbar").style.color = "transparent";
|
if (foot) {
|
||||||
document.querySelector("nav.navbar").style.boxShadow = "unset";
|
foot.style.boxShadow = "unset";
|
||||||
|
foot.style.color = "transparent";
|
||||||
|
}
|
||||||
|
const nav = document.querySelector("nav.navbar");
|
||||||
|
if (nav) nav.style.boxShadow = "unset";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const rmatch = /\/p\/(\d+?)/;
|
const rmatch = /\/p\/(\d+?)/;
|
||||||
if(document.referrer.match(rmatch) && document.location.href.match(rmatch))
|
if (document.referrer.match(rmatch) && document.location.href.match(rmatch))
|
||||||
if(document.location.href.match(rmatch) < document.referrer.match(rmatch))
|
if (document.location.href.match(rmatch) < document.referrer.match(rmatch))
|
||||||
document.body.scrollTop = document.body.scrollHeight;
|
document.body.scrollTop = document.body.scrollHeight;
|
||||||
// </scroller>
|
// </scroller>
|
||||||
|
|
||||||
// <swipe>
|
// <swipe>
|
||||||
const swipeRT = {
|
const swipeRT = {
|
||||||
xDown: null,
|
xDown: null,
|
||||||
@@ -198,33 +405,33 @@ window.requestAnimFrame = (function(){
|
|||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
document.addEventListener('touchmove', e => {
|
document.addEventListener('touchmove', e => {
|
||||||
if(!swipeRT.xDown || !swipeRT.yDown)
|
if (!swipeRT.xDown || !swipeRT.yDown)
|
||||||
return;
|
return;
|
||||||
swipeRT.xDiff = swipeRT.xDown - e.touches[0].clientX;
|
swipeRT.xDiff = swipeRT.xDown - e.touches[0].clientX;
|
||||||
swipeRT.yDiff = swipeRT.yDown - e.touches[0].clientY;
|
swipeRT.yDiff = swipeRT.yDown - e.touches[0].clientY;
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
document.addEventListener('touchend', e => {
|
document.addEventListener('touchend', e => {
|
||||||
if(swipeRT.startEl !== e.target)
|
if (swipeRT.startEl !== e.target)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const timeDiff = Date.now() - swipeRT.timeDown;
|
const timeDiff = Date.now() - swipeRT.timeDown;
|
||||||
let elem;
|
let elem;
|
||||||
|
|
||||||
if(Math.abs(swipeRT.xDiff) > Math.abs(swipeRT.yDiff)) {
|
if (Math.abs(swipeRT.xDiff) > Math.abs(swipeRT.yDiff)) {
|
||||||
if(Math.abs(swipeRT.xDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) {
|
if (Math.abs(swipeRT.xDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) {
|
||||||
if(swipeRT.xDiff > 0) // left
|
if (swipeRT.xDiff > 0) // left
|
||||||
elem = document.querySelector(".pagination > .next:not(.disabled)");
|
elem = document.querySelector(".pagination > .next:not(.disabled)");
|
||||||
else // right
|
else // right
|
||||||
elem = document.querySelector(".pagination > .prev:not(.disabled)");
|
elem = document.querySelector(".pagination > .prev:not(.disabled)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if(Math.abs(swipeRT.yDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) {
|
if (Math.abs(swipeRT.yDiff) > swipeOpt.treshold && timeDiff < swipeOpt.timeout) {
|
||||||
if(navbar = document.querySelector("nav.navbar") && document.querySelector("div.posts")) {
|
if (navbar = document.querySelector("nav.navbar") && document.querySelector("div.posts")) {
|
||||||
if(swipeRT.yDiff > 0 && (window.innerHeight + window.scrollY) >= document.body.offsetHeight) // up
|
if (swipeRT.yDiff > 0 && (window.innerHeight + window.scrollY) >= document.body.offsetHeight) // up
|
||||||
elem = document.querySelector(".pagination > .next:not(.disabled)");
|
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)");
|
elem = document.querySelector(".pagination > .prev:not(.disabled)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,13 +441,13 @@ window.requestAnimFrame = (function(){
|
|||||||
swipeRT.yDown = null;
|
swipeRT.yDown = null;
|
||||||
swipeRT.timeDown = null;
|
swipeRT.timeDown = null;
|
||||||
|
|
||||||
if(elem)
|
if (elem)
|
||||||
changePage(elem);
|
changePage(elem);
|
||||||
}, false);
|
}, false);
|
||||||
// </swipe>
|
// </swipe>
|
||||||
|
|
||||||
// <visualizer>
|
// <visualizer>
|
||||||
if(audioElement = document.querySelector("audio")) {
|
if (audioElement = document.querySelector("audio")) {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
canvas.width = 1920;
|
canvas.width = 1920;
|
||||||
@@ -267,7 +474,7 @@ window.requestAnimFrame = (function(){
|
|||||||
draw(data);
|
draw(data);
|
||||||
}
|
}
|
||||||
function draw(data) {
|
function draw(data) {
|
||||||
data = [ ...data ];
|
data = [...data];
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent");
|
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent");
|
||||||
data.forEach((value, i) => {
|
data.forEach((value, i) => {
|
||||||
@@ -285,7 +492,7 @@ window.requestAnimFrame = (function(){
|
|||||||
// </visualizer>
|
// </visualizer>
|
||||||
|
|
||||||
// <mediakeys>
|
// <mediakeys>
|
||||||
if(elem = document.querySelector("#my-video") && "mediaSession" in navigator) {
|
if (elem = document.querySelector("#my-video") && "mediaSession" in navigator) {
|
||||||
const playpauseEvent = () => {
|
const playpauseEvent = () => {
|
||||||
video[video.paused ? 'play' : 'pause']();
|
video[video.paused ? 'play' : 'pause']();
|
||||||
document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden');
|
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('pause', playpauseEvent);
|
||||||
navigator.mediaSession.setActionHandler('stop', playpauseEvent);
|
navigator.mediaSession.setActionHandler('stop', playpauseEvent);
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||||
if(link = document.querySelector(".pagination > .prev:not(.disabled)"))
|
if (link = document.querySelector(".pagination > .prev:not(.disabled)"))
|
||||||
changePage(link);
|
changePage(link);
|
||||||
});
|
});
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||||
if(link = document.querySelector(".pagination > .next:not(.disabled)"))
|
if (link = document.querySelector(".pagination > .next:not(.disabled)"))
|
||||||
changePage(link);
|
changePage(link);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -326,18 +533,18 @@ function onWheel(e) {
|
|||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
const el = document.querySelector(targetSelector);
|
const el = document.querySelector(targetSelector);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.addEventListener('mouseenter', () => isMouseOver = true);
|
el.addEventListener('mouseenter', () => isMouseOver = true);
|
||||||
el.addEventListener('mouseleave', () => isMouseOver = false);
|
el.addEventListener('mouseleave', () => isMouseOver = false);
|
||||||
window.addEventListener('wheel', onWheel, { passive: false });
|
window.addEventListener('wheel', onWheel, { passive: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', init);
|
window.addEventListener('load', init);
|
||||||
|
|
||||||
document.getElementById('sbtForm').addEventListener('submit', (e) => {
|
document.getElementById('sbtForm').addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const input = document.getElementById('sbtInput').value.trim();
|
const input = document.getElementById('sbtInput').value.trim();
|
||||||
if (input) {
|
if (input) {
|
||||||
window.location.href = `/tag/${encodeURIComponent(input)}`;
|
window.location.href = `/tag/${encodeURIComponent(input)}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,200 +1,239 @@
|
|||||||
(async () => {
|
(async () => {
|
||||||
if(_addtag = document.querySelector("a#a_addtag")) {
|
// Helper to get dynamic context from the DOM
|
||||||
const postid = +document.querySelector("a.id-link").innerText;
|
const getContext = () => {
|
||||||
const poster = document.querySelector("a#a_username").innerText;
|
const idLink = document.querySelector("a.id-link");
|
||||||
let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2));
|
if (!idLink) return null;
|
||||||
|
return {
|
||||||
const queryapi = async (url, data, method = 'GET') => {
|
postid: +idLink.innerText,
|
||||||
let req;
|
poster: document.querySelector("a#a_username")?.innerText,
|
||||||
if(method == 'POST') {
|
tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2))
|
||||||
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 queryapi = async (url, data, method = 'GET') => {
|
||||||
const post = async (url, data) => queryapi(url, data, 'POST');
|
let req;
|
||||||
|
if (method == 'POST') {
|
||||||
const renderTags = _tags => {
|
req = await fetch(url, {
|
||||||
[...document.querySelectorAll("#tags > .badge")].forEach(tag => tag.parentElement.removeChild(tag));
|
method: "POST",
|
||||||
_tags.reverse().forEach(tag => {
|
headers: {
|
||||||
const a = document.createElement("a");
|
"Content-Type": "application/json"
|
||||||
a.href = `/tag/${tag.normalized}`;
|
},
|
||||||
a.style = "color: inherit !important";
|
body: JSON.stringify(data)
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
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) => {
|
const get = async (url, data) => queryapi(url, data, 'GET');
|
||||||
if(ae)
|
const post = async (url, data) => queryapi(url, data, 'POST');
|
||||||
ae.preventDefault();
|
|
||||||
|
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");
|
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");
|
tag.badge.split(" ").forEach(b => span.classList.add(b));
|
||||||
input.size = "10";
|
|
||||||
input.value = "";
|
|
||||||
input.setAttribute("list", "testlist");
|
|
||||||
input.setAttribute("autoComplete", "off");
|
|
||||||
|
|
||||||
span.insertAdjacentElement("afterbegin", input);
|
span.insertAdjacentElement("beforeend", a);
|
||||||
insert.insertAdjacentElement("beforebegin", span);
|
|
||||||
|
|
||||||
input.focus();
|
document.querySelector("#tags").insertAdjacentElement("afterbegin", span);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
let tt = null;
|
const addtagClick = (e) => {
|
||||||
let lastInput = '';
|
if (e) e.preventDefault();
|
||||||
const testList = document.querySelector('#testlist');
|
const ctx = getContext();
|
||||||
|
if (!ctx) return;
|
||||||
|
const { postid, tags } = ctx;
|
||||||
|
|
||||||
input.addEventListener("keyup", async e => {
|
const insert = document.querySelector("a#a_addtag");
|
||||||
if(e.key === "Enter") {
|
// Check if input already exists to prevent duplicates
|
||||||
const tmptag = input.value?.trim();
|
if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) {
|
||||||
if(tags.includes(tmptag))
|
insert.previousElementSibling.querySelector('input').focus();
|
||||||
return alert("tag already exists");
|
return;
|
||||||
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 () => {
|
const span = document.createElement("span");
|
||||||
tt = null;
|
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)
|
span.insertAdjacentElement("afterbegin", input);
|
||||||
return false;
|
insert.insertAdjacentElement("beforebegin", span);
|
||||||
|
|
||||||
testList.innerText = "";
|
input.focus();
|
||||||
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))
|
let tt = null;
|
||||||
option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`;
|
let lastInput = '';
|
||||||
|
const testList = document.querySelector('#testlist');
|
||||||
|
|
||||||
testList.insertAdjacentElement('beforeEnd', option);
|
input.addEventListener("keyup", async e => {
|
||||||
};
|
if (e.key === "Enter") {
|
||||||
}, 500);
|
const tmptag = input.value?.trim();
|
||||||
}
|
// Check fresh tags from DOM just in case? Or use captured tags?
|
||||||
return true;
|
// 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 => {
|
const res = await post("/api/v2/admin/" + postid + "/tags", {
|
||||||
if(input.value.length === 0)
|
tagname: tmptag
|
||||||
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 += " ";
|
|
||||||
});
|
});
|
||||||
|
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 {
|
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);
|
||||||
}
|
}
|
||||||
};
|
return true;
|
||||||
|
|
||||||
_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();
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
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 => {
|
const saveAvatar = async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -209,20 +248,23 @@
|
|||||||
const code = res.status;
|
const code = res.status;
|
||||||
res = await res.json();
|
res = await res.json();
|
||||||
|
|
||||||
switch(code) {
|
switch (code) {
|
||||||
case 200:
|
case 200:
|
||||||
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
|
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
|
||||||
document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
|
document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log(res);
|
console.log(res);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.querySelector('input#s_avatar').addEventListener('click', saveAvatar);
|
const sAvatar = document.querySelector('input#s_avatar');
|
||||||
document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async e => {
|
if (sAvatar) sAvatar.addEventListener('click', saveAvatar);
|
||||||
if(e.key === 'Enter')
|
|
||||||
|
const iAvatar = document.querySelector('input[name="i_avatar"]');
|
||||||
|
if (iAvatar) iAvatar.addEventListener('keyup', async e => {
|
||||||
|
if (e.key === 'Enter')
|
||||||
await saveAvatar(e);
|
await saveAvatar(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ const epochs = [
|
|||||||
["second", 1]
|
["second", 1]
|
||||||
];
|
];
|
||||||
const getDuration = timeAgoInSeconds => {
|
const getDuration = timeAgoInSeconds => {
|
||||||
for(let [name, seconds] of epochs) {
|
for (let [name, seconds] of epochs) {
|
||||||
const interval = ~~(timeAgoInSeconds / seconds);
|
const interval = ~~(timeAgoInSeconds / seconds);
|
||||||
if(interval >= 1) return {
|
if (interval >= 1) return {
|
||||||
interval: interval,
|
interval: interval,
|
||||||
epoch: name
|
epoch: name
|
||||||
};
|
};
|
||||||
@@ -32,7 +32,9 @@ export default new class {
|
|||||||
return (Math.round((b * 8 / s / 1e6) * 1e4) / 1e4);
|
return (Math.round((b * 8 / s / 1e6) * 1e4) / 1e4);
|
||||||
};
|
};
|
||||||
timeAgo(date) {
|
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`;
|
return `${interval} ${epoch}${interval === 1 ? "" : "s"} ago`;
|
||||||
};
|
};
|
||||||
md5(str) {
|
md5(str) {
|
||||||
@@ -40,19 +42,19 @@ export default new class {
|
|||||||
};
|
};
|
||||||
getMode(mode) {
|
getMode(mode) {
|
||||||
let tmp;
|
let tmp;
|
||||||
switch(mode) {
|
switch (mode) {
|
||||||
case 1: // nsfw
|
case 1: // nsfw
|
||||||
tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)";
|
tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)";
|
||||||
break;
|
break;
|
||||||
case 2: // untagged
|
case 2: // untagged
|
||||||
tmp = "items.id not in (select item_id from tags_assign group by item_id)";
|
tmp = "items.id not in (select item_id from tags_assign group by item_id)";
|
||||||
break;
|
break;
|
||||||
case 3: // all
|
case 3: // all
|
||||||
tmp = "1 = 1";
|
tmp = "1 = 1";
|
||||||
break;
|
break;
|
||||||
default: // sfw
|
default: // sfw
|
||||||
tmp = "items.id in (select item_id from tags_assign where tag_id = 1 group by item_id)";
|
tmp = "items.id in (select item_id from tags_assign where tag_id = 1 group by item_id)";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return tmp;
|
return tmp;
|
||||||
};
|
};
|
||||||
@@ -61,14 +63,14 @@ export default new class {
|
|||||||
};
|
};
|
||||||
genLink(env) {
|
genLink(env) {
|
||||||
const link = [];
|
const link = [];
|
||||||
if(env.tag) link.push("tag", env.tag);
|
if (env.tag) link.push("tag", env.tag);
|
||||||
if(env.user) link.push("user", env.user, env.type ?? 'f0cks');
|
if (env.user) link.push("user", env.user, env.type ?? 'f0cks');
|
||||||
if(env.mime?.length > 2) link.push(env.mime);
|
if (env.mime?.length > 2) link.push(env.mime);
|
||||||
|
|
||||||
let tmp = link.length === 0 ? '/' : link.join('/');
|
let tmp = link.length === 0 ? '/' : link.join('/');
|
||||||
if(!tmp.endsWith('/'))
|
if (!tmp.endsWith('/'))
|
||||||
tmp = tmp + '/';
|
tmp = tmp + '/';
|
||||||
if(!tmp.startsWith('/'))
|
if (!tmp.startsWith('/'))
|
||||||
tmp = '/' + tmp;
|
tmp = '/' + tmp;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -77,7 +79,7 @@ export default new class {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
parseTag(tag) {
|
parseTag(tag) {
|
||||||
if(!tag)
|
if (!tag)
|
||||||
return null;
|
return null;
|
||||||
return decodeURI(tag);
|
return decodeURI(tag);
|
||||||
}
|
}
|
||||||
@@ -129,7 +131,7 @@ export default new class {
|
|||||||
return "$f0ck$" + salt + ":" + derivedKey.toString("hex");
|
return "$f0ck$" + salt + ":" + derivedKey.toString("hex");
|
||||||
};
|
};
|
||||||
async verify(str, hash) {
|
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 keyBuffer = Buffer.from(key, "hex");
|
||||||
const derivedKey = await scrypt(str, salt, 64);
|
const derivedKey = await scrypt(str, salt, 64);
|
||||||
return crypto.timingSafeEqual(keyBuffer, derivedKey);
|
return crypto.timingSafeEqual(keyBuffer, derivedKey);
|
||||||
@@ -143,20 +145,20 @@ export default new class {
|
|||||||
where "tags_assign".item_id = ${+itemid}
|
where "tags_assign".item_id = ${+itemid}
|
||||||
order by "tags".id asc
|
order by "tags".id asc
|
||||||
`;
|
`;
|
||||||
for(let t = 0; t < tags.length; t++) {
|
for (let t = 0; t < tags.length; t++) {
|
||||||
if(tags[t].tag.startsWith(">"))
|
if (tags[t].tag.startsWith(">"))
|
||||||
tags[t].badge = "badge-greentext badge-light";
|
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";
|
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";
|
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";
|
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";
|
tags[t].badge = "badge-dutch badge-light";
|
||||||
else if(tags[t].normalized === "sfw")
|
else if (tags[t].normalized === "sfw")
|
||||||
tags[t].badge = "badge-success";
|
tags[t].badge = "badge-success";
|
||||||
else if(tags[t].normalized === "nsfw")
|
else if (tags[t].normalized === "nsfw")
|
||||||
tags[t].badge = "badge-danger";
|
tags[t].badge = "badge-danger";
|
||||||
else
|
else
|
||||||
tags[t].badge = "badge-light";
|
tags[t].badge = "badge-light";
|
||||||
@@ -183,11 +185,11 @@ export default new class {
|
|||||||
const tmp = Object.values(res)[0];
|
const tmp = Object.values(res)[0];
|
||||||
|
|
||||||
let nsfw = false;
|
let nsfw = false;
|
||||||
if(tmp.neutral >= .7)
|
if (tmp.neutral >= .7)
|
||||||
nsfw = false;
|
nsfw = false;
|
||||||
else if((tmp.sexy + tmp.porn + tmp.hentai) >= .7)
|
else if ((tmp.sexy + tmp.porn + tmp.hentai) >= .7)
|
||||||
nsfw = true;
|
nsfw = true;
|
||||||
else if(tmp.drawings >= .4)
|
else if (tmp.drawings >= .4)
|
||||||
nsfw = false;
|
nsfw = false;
|
||||||
else
|
else
|
||||||
nsfw = false;
|
nsfw = false;
|
||||||
@@ -197,7 +199,7 @@ export default new class {
|
|||||||
score: tmp.sexy + tmp.porn + tmp.hentai,
|
score: tmp.sexy + tmp.porn + tmp.hentai,
|
||||||
scores: tmp
|
scores: tmp
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
async getDefaultAvatar() {
|
async getDefaultAvatar() {
|
||||||
return (await db`
|
return (await db`
|
||||||
@@ -212,7 +214,7 @@ export default new class {
|
|||||||
|
|
||||||
// meddlware admin
|
// meddlware admin
|
||||||
async auth(req, res, next) {
|
async auth(req, res, next) {
|
||||||
if(!req.session || !req.session.admin) {
|
if (!req.session || !req.session.admin) {
|
||||||
return res.reply({
|
return res.reply({
|
||||||
code: 401,
|
code: 401,
|
||||||
body: "401 - Unauthorized"
|
body: "401 - Unauthorized"
|
||||||
@@ -223,7 +225,7 @@ export default new class {
|
|||||||
|
|
||||||
// meddlware user
|
// meddlware user
|
||||||
async userauth(req, res, next) {
|
async userauth(req, res, next) {
|
||||||
if(!req.session) {
|
if (!req.session) {
|
||||||
return res.reply({
|
return res.reply({
|
||||||
code: 401,
|
code: 401,
|
||||||
body: "401 - Unauthorized"
|
body: "401 - Unauthorized"
|
||||||
@@ -232,14 +234,14 @@ export default new class {
|
|||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
||||||
async loggedin(req, res, next) {
|
async loggedin(req, res, next) {
|
||||||
if(!req.session) {
|
if (!req.session) {
|
||||||
return res.reply({
|
return res.reply({
|
||||||
code: 401,
|
code: 401,
|
||||||
body: "401 - Unauthorized"
|
body: "401 - Unauthorized"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
68
src/inc/routes/ajax.mjs
Normal file
68
src/inc/routes/ajax.mjs
Normal 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
1
views/ajax-item.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@include(item-partial)
|
||||||
126
views/item-partial.html
Normal file
126
views/item-partial.html
Normal 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) <a class="removetag"
|
||||||
|
href="#">×</a>@endif
|
||||||
|
</span>
|
||||||
|
@endeach
|
||||||
|
@endif
|
||||||
|
@if(session)
|
||||||
|
<a href="#" id="a_addtag">add tag</a> - <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>
|
||||||
117
views/item.html
117
views/item.html
@@ -2,118 +2,9 @@
|
|||||||
<canvas class="hidden-xs" id="bg"></canvas>
|
<canvas class="hidden-xs" id="bg"></canvas>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div id="main">
|
<div id="main">
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="_204863">
|
@include(item-partial)
|
||||||
<div class="location">{{ (url.pathname) }}</div>
|
</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) <a class="removetag" href="#">×</a>@endif
|
|
||||||
</span>
|
|
||||||
@endeach
|
|
||||||
@endif
|
|
||||||
@if(session)
|
|
||||||
<a href="#" id="a_addtag">add tag</a> - <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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@include(snippets/footer)
|
||||||
@include(snippets/footer)
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
@if(session)
|
@if(session)
|
||||||
<!-- logged in -->
|
<!-- logged in -->
|
||||||
<nav class="navbar navbar-expand-lg">
|
<nav class="navbar navbar-expand-lg">
|
||||||
<a class="navbar-brand" href="/"><span class="f0ck" width="" height="">w0bm.com</span></a>
|
<a class="navbar-brand" href="/"><span class="f0ck" width="" height="">w0bm.com</span></a>
|
||||||
|
|
||||||
<div class="navigation-links-guest">
|
<div class="navigation-links-guest">
|
||||||
<ol>
|
<ol>
|
||||||
<a href="/tags">tags</a>
|
<a href="/tags">tags</a>
|
||||||
<a href="/about">about</a>
|
<a href="/about">about</a>
|
||||||
@if(!/^\/\d$/.test(url.pathname))
|
@if(!/^\/\d$/.test(url.pathname))
|
||||||
<a href="/random">rand</a>
|
<a href="/random">rand</a>
|
||||||
@endif
|
@endif
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<!-- show pagination only for tags and main page -->
|
<!-- show pagination only for tags and main page -->
|
||||||
@@ -17,21 +17,7 @@
|
|||||||
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
|
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
|
||||||
<div class="pagination-container-fluid">
|
<div class="pagination-container-fluid">
|
||||||
<div class="pagination-wrapper">
|
<div class="pagination-wrapper">
|
||||||
@if(typeof pagination !== "undefined")
|
@include(snippets/pagination)
|
||||||
<nav class="pagination">
|
|
||||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">«</a>
|
|
||||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}" class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">‹</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">›</a>
|
|
||||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}" class="page-item-4 btn start@if(!pagination.next) disabled@endif">»</a>
|
|
||||||
</nav>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,11 +30,11 @@
|
|||||||
|
|
||||||
<div class="navigation-links-guest">
|
<div class="navigation-links-guest">
|
||||||
<ol>
|
<ol>
|
||||||
<a href="/tags">tags</a>
|
<a href="/tags">tags</a>
|
||||||
<a href="/about">about</a>
|
<a href="/about">about</a>
|
||||||
@if(!/^\/\d$/.test(url.pathname))
|
@if(!/^\/\d$/.test(url.pathname))
|
||||||
<a href="/random">rand</a>
|
<a href="/random">rand</a>
|
||||||
@endif
|
@endif
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<!-- show pagination only for tags and main page -->
|
<!-- show pagination only for tags and main page -->
|
||||||
@@ -56,21 +42,7 @@
|
|||||||
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
|
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
|
||||||
<div class="pagination-container-fluid">
|
<div class="pagination-container-fluid">
|
||||||
<div class="pagination-wrapper">
|
<div class="pagination-wrapper">
|
||||||
@if(typeof pagination !== "undefined")
|
@include(snippets/pagination)
|
||||||
<nav class="pagination">
|
|
||||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">«</a>
|
|
||||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}" class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">‹</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">›</a>
|
|
||||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}" class="page-item-4 btn start@if(!pagination.next) disabled@endif">»</a>
|
|
||||||
</nav>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
19
views/snippets/pagination.html
Normal file
19
views/snippets/pagination.html
Normal 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">«</a>
|
||||||
|
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}"
|
||||||
|
class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">‹</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">›</a>
|
||||||
|
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}"
|
||||||
|
class="page-item-4 btn start@if(!pagination.next) disabled@endif">»</a>
|
||||||
|
</nav>
|
||||||
|
@endif
|
||||||
Reference in New Issue
Block a user