Compare commits
13 Commits
c198566474
...
eins-f0bm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
224064d0ca | ||
|
|
3ee28fd0b7 | ||
|
|
964284d5c9 | ||
|
|
9a03d5f697 | ||
|
|
9b1041dda7 | ||
| 4bc8b8f436 | |||
|
|
45f9345e9c | ||
|
|
c74e5a7402 | ||
|
|
6799ec1567 | ||
| 007cf3189c | |||
| 4a2925b141 | |||
|
|
b72fcaa426 | ||
|
|
a4f9c48e13 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ deleted/b
|
||||
deleted/ca
|
||||
deleted/t
|
||||
tmp/*
|
||||
tools
|
||||
@@ -1197,7 +1197,9 @@ body {
|
||||
overscroll-behavior-y: contain;
|
||||
overflow: unset;
|
||||
font-size: 14px;
|
||||
height: /* 100%; */auto;
|
||||
height:
|
||||
/* 100%; */
|
||||
auto;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@@ -1318,6 +1320,13 @@ div.posts>a:hover::after {
|
||||
grid-template-columns: auto 1fr 0fr;
|
||||
justify-content: start;
|
||||
border-bottom: 1px solid var(--nav-border-color);
|
||||
background: var(--nav-bg);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.navbar.scrolled {
|
||||
background: #000 !important;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@@ -1576,14 +1585,10 @@ span.placeholder {
|
||||
}
|
||||
|
||||
@media (max-width: 1056px) {
|
||||
.navbar {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
grid-template-areas: 'f0ck f0ck f0ck';
|
||||
}
|
||||
|
||||
/* Navbar grid layout removed for modern-navbar compatibility */
|
||||
.navbar-brand {
|
||||
grid-area: f0ck;
|
||||
/* maintained for potential other uses or reset */
|
||||
}
|
||||
|
||||
.pagination-container-fluid {
|
||||
@@ -1784,7 +1789,8 @@ span.placeholder {
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
/* background-color: var(--navigation-links-bg);
|
||||
*/}
|
||||
*/
|
||||
}
|
||||
|
||||
@media (min-width: 361px) {
|
||||
.embed-responsive-image {
|
||||
@@ -2779,7 +2785,9 @@ ul.navbar-nav-guests li.nav-item {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.navbar-expand-lg .navbar-nav-guests .nav-link, .pagination > a, .pagination > span {
|
||||
.navbar-expand-lg .navbar-nav-guests .nav-link,
|
||||
.pagination>a,
|
||||
.pagination>span {
|
||||
padding-right: .5rem;
|
||||
padding-left: .5rem;
|
||||
}
|
||||
@@ -2812,12 +2820,16 @@ ul.navbar-nav-guests li.nav-item {
|
||||
|
||||
/* Pagination Responsiveness */
|
||||
@media (max-width: 799px) {
|
||||
.navbar-expand-lg .navbar-nav-guests .nav-link, .pagination > a, .pagination > span {
|
||||
|
||||
.navbar-expand-lg .navbar-nav-guests .nav-link,
|
||||
.pagination>a,
|
||||
.pagination>span {
|
||||
padding-right: 2px;
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.pagination > a, .pagination > span {
|
||||
.pagination>a,
|
||||
.pagination>span {
|
||||
margin-right: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
@@ -2832,12 +2844,16 @@ ul.navbar-nav-guests li.nav-item {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
img#f0ck-image, div.imageDoor, div.posts a, video {
|
||||
img#f0ck-image,
|
||||
div.imageDoor,
|
||||
div.posts a,
|
||||
video {
|
||||
animation: 1s ease-out 0s 1 fadeInFX;
|
||||
}
|
||||
|
||||
@@ -2892,11 +2908,13 @@ img#f0ck-image, div.imageDoor, div.posts a, video {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.f0cks h5, .favs h5 {
|
||||
.f0cks h5,
|
||||
.favs h5 {
|
||||
background: var(--dropdown-bg);
|
||||
}
|
||||
|
||||
.f0cks-header, .favs-header {
|
||||
.f0cks-header,
|
||||
.favs-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
background: var(--img-border-color);
|
||||
@@ -2925,7 +2943,7 @@ div.favs div.posts {
|
||||
filter: blur(100px);
|
||||
transform: translate3d(0, 0, 0);
|
||||
z-index: 0;
|
||||
transition: 2s ease;
|
||||
transition: opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
@@ -2940,6 +2958,7 @@ button#togglebg {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -2949,6 +2968,7 @@ button#togglebg {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -2959,6 +2979,7 @@ button#togglebg {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
@@ -2968,17 +2989,18 @@ button#togglebg {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.fader-in {
|
||||
animation: fadeIn .8s steps(100) forwards;
|
||||
opacity: 0.4 !important;
|
||||
}
|
||||
|
||||
.fader-out {
|
||||
animation: fadeOut .8s steps(100) forwards
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
.settings {
|
||||
@@ -3004,6 +3026,7 @@ input#s_avatar {
|
||||
#s_avatar:hover {
|
||||
background: #ffffff0f;
|
||||
}
|
||||
|
||||
.theforceofthree {
|
||||
display: grid;
|
||||
grid-template-columns: 0.4fr 1fr 0.4fr;
|
||||
@@ -3012,3 +3035,89 @@ input#s_avatar {
|
||||
.upload {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Infinite scroll loading indicator */
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
color: var(--footbar-color);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Modern Tags Layout */
|
||||
.tags-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.tag-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--badge-bg, #171717);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
text-decoration: none !important;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.1));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tag-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.4);
|
||||
background: var(--dropdown-bg, #232323);
|
||||
border-color: var(--accent, #9f0);
|
||||
}
|
||||
|
||||
.tag-card-image {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.tag-card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.5s;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tag-card:hover .tag-card-image img {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tag-card-content {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.tag-name {
|
||||
color: var(--white, #fff);
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
font-family: var(--font, monospace);
|
||||
}
|
||||
|
||||
.tag-count {
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
@@ -1,23 +1,13 @@
|
||||
(async () => {
|
||||
if(_addtag = document.querySelector("a#a_addtag")) {
|
||||
const postid = +document.querySelector("a.id-link").innerText;
|
||||
const poster = document.querySelector("a#a_username").innerText;
|
||||
let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2));
|
||||
|
||||
const deleteEvent = async e => {
|
||||
e.preventDefault();
|
||||
if(!confirm("Do you really want to delete this tag?"))
|
||||
return;
|
||||
const tagname = e.target.parentElement.querySelector('a:first-child').innerText;
|
||||
|
||||
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), {
|
||||
method: 'DELETE'
|
||||
})).json();
|
||||
|
||||
if(!res.success)
|
||||
return alert("uff");
|
||||
tags = res.tags.map(t => t.tag);
|
||||
renderTags(res.tags);
|
||||
// Helper to get dynamic context
|
||||
const getContext = () => {
|
||||
const idLink = document.querySelector("a.id-link");
|
||||
if (!idLink) return null;
|
||||
return {
|
||||
postid: +idLink.innerText,
|
||||
poster: document.querySelector("a#a_username")?.innerText,
|
||||
tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2))
|
||||
};
|
||||
};
|
||||
|
||||
const queryapi = async (url, data, method = 'GET') => {
|
||||
@@ -50,7 +40,9 @@
|
||||
a.href = `/tag/${tag.normalized}`;
|
||||
a.style = "color: inherit !important";
|
||||
a.innerHTML = tag.tag;
|
||||
a.addEventListener("click", editTagEvent); // tmp
|
||||
// Admin specific: edit event
|
||||
// Note: delegation handles this now if we set it up, OR we can attach here since elements are new.
|
||||
// But delegation is cleaner if possible. However, editTagEvent relies on 'e.target'.
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("badge", "mr-2");
|
||||
@@ -61,7 +53,9 @@
|
||||
const delbutton = document.createElement("a");
|
||||
delbutton.innerHTML = " ×";
|
||||
delbutton.href = "#";
|
||||
delbutton.addEventListener("click", deleteEvent);
|
||||
// Class for delegation
|
||||
delbutton.classList.add("admin-deltag");
|
||||
|
||||
span.insertAdjacentElement("beforeend", a);
|
||||
span.innerHTML += ' ';
|
||||
span.insertAdjacentElement("beforeend", delbutton);
|
||||
@@ -70,11 +64,38 @@
|
||||
});
|
||||
};
|
||||
|
||||
const addtagClick = (ae = false) => {
|
||||
if(ae)
|
||||
ae.preventDefault();
|
||||
const deleteEvent = async e => {
|
||||
e.preventDefault();
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid } = ctx;
|
||||
|
||||
if (!confirm("Do you really want to delete this tag?"))
|
||||
return;
|
||||
const tagname = e.target.parentElement.querySelector('a:first-child').innerText;
|
||||
|
||||
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), {
|
||||
method: 'DELETE'
|
||||
})).json();
|
||||
|
||||
if (!res.success)
|
||||
return alert("uff");
|
||||
|
||||
renderTags(res.tags);
|
||||
};
|
||||
|
||||
const addtagClick = (e) => {
|
||||
if (e) e.preventDefault();
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid, tags } = ctx;
|
||||
|
||||
const insert = document.querySelector("a#a_addtag");
|
||||
if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) {
|
||||
insert.previousElementSibling.querySelector('input').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("badge", "badge-light", "mr-2");
|
||||
|
||||
@@ -96,8 +117,11 @@
|
||||
input.addEventListener("keyup", async e => {
|
||||
if (e.key === "Enter") {
|
||||
const tmptag = input.value?.trim();
|
||||
// We should re-check tags from DOM? Or trust captured tags?
|
||||
// Captured 'tags' is safe for immediate check.
|
||||
if (tags.includes(tmptag))
|
||||
return alert("tag already exists");
|
||||
|
||||
const res = await post("/api/v2/admin/" + postid + "/tags", {
|
||||
tagname: tmptag
|
||||
});
|
||||
@@ -105,41 +129,29 @@
|
||||
alert(res.msg);
|
||||
return false;
|
||||
}
|
||||
tags = res.tags.map(t => t.tag);
|
||||
renderTags(res.tags);
|
||||
addtagClick();
|
||||
if (span.parentElement) span.parentElement.removeChild(span);
|
||||
testList.innerText = "";
|
||||
addtagClick();
|
||||
}
|
||||
else if (e.key === "Escape") {
|
||||
span.parentElement.removeChild(span);
|
||||
if (span.parentElement) span.parentElement.removeChild(span);
|
||||
testList.innerText = "";
|
||||
}
|
||||
else {
|
||||
if(tt != null)
|
||||
clearTimeout(tt);
|
||||
|
||||
if (tt != null) clearTimeout(tt);
|
||||
tt = setTimeout(async () => {
|
||||
tt = null;
|
||||
|
||||
const tmptag = input.value?.trim();
|
||||
|
||||
if(tmptag == lastInput || tmptag.length <= 1)
|
||||
return false;
|
||||
|
||||
if (tmptag == lastInput || tmptag.length <= 1) return false;
|
||||
testList.innerText = "";
|
||||
lastInput = tmptag;
|
||||
|
||||
const res = await get('/api/v2/admin/tags/suggest', {
|
||||
q: tmptag
|
||||
});
|
||||
|
||||
const res = await get('/api/v2/admin/tags/suggest', { q: tmptag });
|
||||
for (const entry of res.suggestions) {
|
||||
const option = document.createElement('option');
|
||||
option.value = entry.tag;
|
||||
|
||||
if (!/fox/.test(navigator.userAgent))
|
||||
option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`;
|
||||
|
||||
testList.insertAdjacentElement('beforeEnd', option);
|
||||
};
|
||||
}, 500);
|
||||
@@ -153,9 +165,11 @@
|
||||
});
|
||||
};
|
||||
|
||||
const toggleEvent = async (e = false) => {
|
||||
if(e)
|
||||
e.preventDefault();
|
||||
const toggleEvent = async (e) => {
|
||||
if (e) e.preventDefault();
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid } = ctx;
|
||||
|
||||
const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', {
|
||||
method: 'PUT'
|
||||
@@ -165,8 +179,11 @@
|
||||
};
|
||||
|
||||
const deleteButtonEvent = async e => {
|
||||
if(e)
|
||||
e.preventDefault();
|
||||
if (e) e.preventDefault();
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid, poster } = ctx;
|
||||
|
||||
if (!confirm(`Reason for deleting f0ckpost ${postid} by ${poster} (Weihnachten™)`))
|
||||
return;
|
||||
const res = await post("/api/v2/admin/deletepost", {
|
||||
@@ -177,7 +194,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFavEvent = async e => {
|
||||
const toggleFavEvent = async (e) => {
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid } = ctx;
|
||||
|
||||
const res = await post('/api/v2/admin/togglefav', {
|
||||
postid: postid
|
||||
});
|
||||
@@ -185,38 +206,29 @@
|
||||
const fav = document.querySelector("svg#a_favo > use").href;
|
||||
fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid");
|
||||
|
||||
// span#favs
|
||||
const favcontainer = document.querySelector('span#favs');
|
||||
favcontainer.innerHTML = "";
|
||||
|
||||
favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0);
|
||||
|
||||
res.favs.forEach(f => {
|
||||
const a = document.createElement('a');
|
||||
a.href = `/user/${f.user}/favs`;
|
||||
a.setAttribute('tooltip', f.user);
|
||||
a.setAttribute('flow', 'up');
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = `/t/${f.avatar}.webp`;
|
||||
img.style.height = "32px";
|
||||
img.style.width = "32px";
|
||||
|
||||
a.insertAdjacentElement('beforeend', img);
|
||||
favcontainer.insertAdjacentElement('beforeend', a);
|
||||
favcontainer.innerHTML += " ";
|
||||
});
|
||||
}
|
||||
else {
|
||||
// lul
|
||||
}
|
||||
};
|
||||
|
||||
let tmptt = null;
|
||||
const editTagEvent = async e => { // mousedown
|
||||
const editTagEvent = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
if(e.detail === 2) {
|
||||
if (e.detail === 2) { // Double click
|
||||
clearTimeout(tmptt);
|
||||
const old = e.target;
|
||||
const parent = e.target.parentElement;
|
||||
@@ -229,30 +241,25 @@
|
||||
parent.insertAdjacentElement('afterbegin', textfield);
|
||||
textfield.focus();
|
||||
parent.removeChild(e.target);
|
||||
parent.querySelector('a:last-child').style.display = 'none';
|
||||
// Hide delete button while editing
|
||||
const delBtn = parent.querySelector('a:last-child');
|
||||
if (delBtn) delBtn.style.display = 'none';
|
||||
|
||||
textfield.addEventListener("keyup", async e => {
|
||||
if (e.key === 'Enter') {
|
||||
parent.removeChild(textfield);
|
||||
// send
|
||||
let res = await fetch('/api/v2/admin/tags/' + encodeURIComponent(oldtag), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
newtag: textfield.value
|
||||
})
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ newtag: textfield.value })
|
||||
});
|
||||
const status = res.status;
|
||||
res = await res.json();
|
||||
|
||||
switch (status) {
|
||||
case 200: // success, change
|
||||
case 200:
|
||||
case 201:
|
||||
//parent.removeChild(textfield);
|
||||
parent.insertAdjacentElement('afterbegin', old);
|
||||
parent.querySelector('a:last-child').style.display = '';
|
||||
if (delBtn) delBtn.style.display = '';
|
||||
old.href = `/tag/${res.tag}`;
|
||||
old.innerText = res.tag.trim();
|
||||
break;
|
||||
@@ -264,68 +271,72 @@
|
||||
else if (e.key === 'Escape') {
|
||||
parent.removeChild(textfield);
|
||||
parent.insertAdjacentElement('afterbegin', old);
|
||||
parent.querySelector('a:last-child').style.display = '';
|
||||
if (delBtn) delBtn.style.display = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
tmptt = setTimeout(() => location.href = e.target.href, 250);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
_addtag.addEventListener("click", addtagClick);
|
||||
document.querySelector("a#a_toggle").addEventListener("click", toggleEvent);
|
||||
[...document.querySelectorAll("#tags > .badge > a:first-child")].map(t => t.addEventListener("click", editTagEvent));
|
||||
[...document.querySelectorAll("#tags > .badge > a:last-child")].map(t => t.addEventListener("click", deleteEvent));
|
||||
if(document.querySelector("svg#a_delete"))
|
||||
document.querySelector("svg#a_delete").addEventListener("click", deleteButtonEvent);
|
||||
document.querySelector("svg#a_favo").addEventListener("click", toggleFavEvent);
|
||||
// Event Delegation
|
||||
document.addEventListener("click", e => {
|
||||
if (e.target.matches("a#a_addtag")) {
|
||||
addtagClick(e);
|
||||
} else if (e.target.matches("a#a_toggle")) {
|
||||
toggleEvent(e);
|
||||
} else if (e.target.closest("svg#a_favo")) {
|
||||
toggleFavEvent(e);
|
||||
} else if (e.target.closest("svg#a_delete")) {
|
||||
deleteButtonEvent(e);
|
||||
} else if (e.target.matches("#tags > .badge > a:first-child")) {
|
||||
editTagEvent(e);
|
||||
} else if (e.target.innerText === " \u00d7" && e.target.closest(".badge")) { // check text " x" or similar for delete?
|
||||
// Original was " ×" which is × (\u00d7).
|
||||
// Logic in deleteEvent expects match.
|
||||
// Let's rely on class or structure.
|
||||
// In renderTags I added class 'admin-deltag'.
|
||||
// Existing tags in HTML might NOT have this class unless rendered by JS?
|
||||
// But existing tags are just HTML. We should match structure.
|
||||
// selector: "#tags > .badge > a:last-child"
|
||||
if (e.target.matches("#tags > .badge > a:last-child")) {
|
||||
deleteEvent(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", e => {
|
||||
if(e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
|
||||
return;
|
||||
if(e.key === "p")
|
||||
toggleEvent();
|
||||
else if(e.key === "i")
|
||||
addtagClick();
|
||||
else if(e.key === "x")
|
||||
deleteButtonEvent();
|
||||
else if(e.key === "f")
|
||||
toggleFavEvent();
|
||||
});
|
||||
}
|
||||
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
|
||||
if (e.key === "p") toggleEvent();
|
||||
else if (e.key === "i") addtagClick();
|
||||
else if (e.key === "x") deleteButtonEvent();
|
||||
else if (e.key === "f") toggleFavEvent();
|
||||
});
|
||||
|
||||
// Settings page
|
||||
if (document.location.pathname === '/settings') {
|
||||
const saveAvatar = async e => {
|
||||
e.preventDefault();
|
||||
|
||||
const avatar = +document.querySelector('input[name="i_avatar"]').value;
|
||||
let res = await fetch('/api/v2/settings/setAvatar', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ avatar })
|
||||
});
|
||||
const code = res.status;
|
||||
res = await res.json();
|
||||
|
||||
switch(code) {
|
||||
case 200:
|
||||
if (code === 200) {
|
||||
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
|
||||
document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
|
||||
break;
|
||||
default:
|
||||
console.log(res);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('input#s_avatar').addEventListener('click', saveAvatar);
|
||||
document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async e => {
|
||||
if(e.key === 'Enter')
|
||||
await saveAvatar(e);
|
||||
});
|
||||
const sAvatar = document.querySelector('input#s_avatar');
|
||||
if (sAvatar) sAvatar.addEventListener('click', saveAvatar);
|
||||
const iAvatar = document.querySelector('input[name="i_avatar"]');
|
||||
if (iAvatar) iAvatar.addEventListener('keyup', async e => { if (e.key === 'Enter') await saveAvatar(e); });
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -8,6 +8,25 @@ window.requestAnimFrame = (function(){
|
||||
|
||||
(() => {
|
||||
let video;
|
||||
|
||||
// Initialize background preference
|
||||
if (localStorage.getItem('background') == undefined) {
|
||||
localStorage.setItem('background', 'true');
|
||||
}
|
||||
var background = localStorage.getItem('background') === 'true';
|
||||
// Apply initial visual state
|
||||
var initialCanvas = document.getElementById('bg');
|
||||
if (initialCanvas) {
|
||||
if (background) {
|
||||
initialCanvas.classList.add('fader-in');
|
||||
initialCanvas.classList.remove('fader-out');
|
||||
} else {
|
||||
initialCanvas.classList.add('fader-out');
|
||||
initialCanvas.classList.remove('fader-in');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (elem = document.querySelector("#my-video")) {
|
||||
video = new v0ck(elem);
|
||||
document.addEventListener("keydown", e => {
|
||||
@@ -17,11 +36,27 @@ window.requestAnimFrame = (function(){
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('togglebg').addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
background = !background;
|
||||
localStorage.setItem('background', background.toString());
|
||||
var canvas = document.getElementById('bg');
|
||||
|
||||
|
||||
if (elem !== null) {
|
||||
// ... existing code ...
|
||||
}
|
||||
}
|
||||
|
||||
// Export init function for dynamic calls
|
||||
window.initBackground = () => {
|
||||
// Re-fetch elements as they might have been replaced
|
||||
const elem = document.querySelector("#my-video");
|
||||
const canvas = document.getElementById('bg');
|
||||
|
||||
if (elem) {
|
||||
// Initialize video wrapper if needed or just get instance
|
||||
// Assuming v0ck handles re-init or we just use raw element for events
|
||||
// But video variable is local.
|
||||
// We need to re-bind 'play' event if it's a new element.
|
||||
|
||||
if (canvas) {
|
||||
// Restore visual state on re-init
|
||||
if (background) {
|
||||
canvas.classList.add('fader-in');
|
||||
canvas.classList.remove('fader-out');
|
||||
@@ -29,38 +64,371 @@ window.requestAnimFrame = (function(){
|
||||
canvas.classList.add('fader-out');
|
||||
canvas.classList.remove('fader-in');
|
||||
}
|
||||
animationLoop();
|
||||
});
|
||||
|
||||
if(elem !== null) {
|
||||
if(localStorage.getItem('background') == undefined) {
|
||||
localStorage.setItem('background', 'true');
|
||||
}
|
||||
const context = canvas.getContext('2d');
|
||||
const cw = canvas.width = canvas.clientWidth | 0;
|
||||
const ch = canvas.height = canvas.clientHeight | 0;
|
||||
|
||||
var background = localStorage.getItem('background') === 'true';
|
||||
var canvas = document.getElementById('bg');
|
||||
var context = canvas.getContext('2d');
|
||||
var cw = canvas.width = canvas.clientWidth | 0;
|
||||
var ch = canvas.height = canvas.clientHeight | 0;
|
||||
|
||||
function animationLoop() {
|
||||
if(video.paused || video.ended || !background)
|
||||
const animationLoop = () => {
|
||||
if (elem.paused || elem.ended || !background)
|
||||
return;
|
||||
context.drawImage(video, 0, 0, cw, ch);
|
||||
context.drawImage(elem, 0, 0, cw, ch);
|
||||
window.requestAnimFrame(animationLoop);
|
||||
}
|
||||
|
||||
elem.addEventListener('play', animationLoop);
|
||||
|
||||
if (!elem.paused) {
|
||||
animationLoop();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initial call
|
||||
window.initBackground();
|
||||
|
||||
const loadPageAjax = async (url) => {
|
||||
// Show loading indicator
|
||||
const navbar = document.querySelector("nav.navbar");
|
||||
if (navbar) navbar.classList.add("pbwork");
|
||||
|
||||
try {
|
||||
// Extract page number, user, tag, etc.
|
||||
let page = 1;
|
||||
const pMatch = url.match(/\/p\/(\d+)/);
|
||||
if (pMatch) page = pMatch[1];
|
||||
|
||||
// Extract context
|
||||
let tag = null, user = null, mime = null;
|
||||
const tagMatch = url.match(/\/tag\/([^/]+)/);
|
||||
if (tagMatch) tag = decodeURIComponent(tagMatch[1]);
|
||||
|
||||
const userMatch = url.match(/\/user\/([^/]+)/);
|
||||
if (userMatch) user = decodeURIComponent(userMatch[1]);
|
||||
|
||||
const mimeMatch = url.match(/\/(image|audio|video)/);
|
||||
if (mimeMatch) mime = mimeMatch[1];
|
||||
|
||||
let ajaxUrl = `/ajax/items/?page=${page}`;
|
||||
if (tag) ajaxUrl += `&tag=${encodeURIComponent(tag)}`;
|
||||
if (user) ajaxUrl += `&user=${encodeURIComponent(user)}`;
|
||||
if (mime) ajaxUrl += `&mime=${encodeURIComponent(mime)}`;
|
||||
|
||||
console.log("Fetching Page:", ajaxUrl);
|
||||
const response = await fetch(ajaxUrl, { credentials: 'include' });
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Replace grid content
|
||||
// If "infinite scroll" we might append, but pagination implies jumping properly?
|
||||
// User said "resembled in pagination", which implies staying in sync.
|
||||
// If I click Next Page, I expect to SEE page 2.
|
||||
// But infinite scroll usually appends.
|
||||
// Let's implement REPLACE for explicit page navigation to be safe/standard.
|
||||
// Wait, the "infinite scroll" feature usually implies APPEND.
|
||||
// If the user wants infinite scroll, they shouldn't click pagination?
|
||||
// But if they scroll, `changePage` is called which clicks `.next`.
|
||||
// So if I replace content, it breaks infinite scroll flow (items disappear).
|
||||
// So I should APPEND if it's "next page" and we are already on the page?
|
||||
// But `changePage` is triggered by scroll.
|
||||
// Let's APPEND.
|
||||
|
||||
const posts = document.querySelector('.posts');
|
||||
if (posts) {
|
||||
// Check if we are appending (next page) or jumping
|
||||
// For simple "infinite scroll", we append.
|
||||
posts.insertAdjacentHTML('beforeend', data.html);
|
||||
}
|
||||
|
||||
// Update pagination
|
||||
if (data.pagination) {
|
||||
document.querySelectorAll('.pagination-wrapper').forEach(el => el.innerHTML = data.pagination);
|
||||
}
|
||||
|
||||
// Update History
|
||||
history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
window.location.href = url; // Fallback
|
||||
} finally {
|
||||
if (navbar) navbar.classList.remove("pbwork");
|
||||
// Restore pagination visibility for Grid View
|
||||
const navPag = document.querySelector('.pagination-container-fluid');
|
||||
if (navPag) navPag.style.display = '';
|
||||
}
|
||||
};
|
||||
|
||||
let tt = false;
|
||||
const stimeout = 500;
|
||||
const changePage = (e, pbwork = true) => {
|
||||
pbwork && document.querySelector("nav.navbar").classList.add("pbwork");
|
||||
!tt && (tt = setTimeout(() => e.click(), stimeout));
|
||||
|
||||
const setupMedia = () => {
|
||||
if (elem = document.querySelector("#my-video")) {
|
||||
video = new v0ck(elem);
|
||||
}
|
||||
};
|
||||
|
||||
// Navbar scroll effect - make background black when scrolling
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (navbar) {
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.scrollY > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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+)(?:\/|#|\?|$)/);
|
||||
|
||||
// Hide navbar pagination for Item View (matches SSR)
|
||||
const navPag = document.querySelector('.pagination-container-fluid');
|
||||
if (navPag) navPag.style.display = 'none';
|
||||
|
||||
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, isFavs = false;
|
||||
const tagMatch = url.match(/\/tag\/([^/]+)/);
|
||||
if (tagMatch) tag = decodeURIComponent(tagMatch[1]);
|
||||
|
||||
const userMatch = url.match(/\/user\/([^/]+)/);
|
||||
if (userMatch) {
|
||||
user = decodeURIComponent(userMatch[1]);
|
||||
if (url.includes(`/user/${userMatch[1]}/favs`)) isFavs = true;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
if (window.location.href.includes(`/user/${wUserMatch[1]}/favs`)) isFavs = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// </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 (isFavs) params.append('fav', 'true');
|
||||
|
||||
if ([...params].length > 0) {
|
||||
ajaxUrl += '?' + params.toString();
|
||||
}
|
||||
|
||||
console.log("Fetching:", ajaxUrl);
|
||||
const response = await fetch(ajaxUrl, { credentials: 'include' });
|
||||
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('#main .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) {
|
||||
// Check if we are on Tags Overview logic (which reuses .container)
|
||||
const tagsOverview = container.querySelector('.tags');
|
||||
if (tagsOverview) {
|
||||
container.innerHTML = '';
|
||||
} else {
|
||||
// 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}`;
|
||||
if (isFavs) pushUrl = `/user/${user}/favs/${itemid}`;
|
||||
}
|
||||
else if (tag) pushUrl = `/tag/${tag}/${itemid}`;
|
||||
|
||||
// We overwrite proper URL even if the link clicked was "naked"
|
||||
history.pushState({}, '', pushUrl);
|
||||
|
||||
setupMedia();
|
||||
if (window.initBackground) window.initBackground();
|
||||
// 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 (pbwork) {
|
||||
const nav = document.querySelector("nav.navbar");
|
||||
if (nav) nav.classList.add("pbwork");
|
||||
}
|
||||
// Trigger native click for navigation
|
||||
e.click();
|
||||
};
|
||||
|
||||
// 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, #nav-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' || link.id === 'nav-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((err) => {
|
||||
console.error("Random fetch failed:", err);
|
||||
window.location.href = link.href;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard item links
|
||||
e.preventDefault();
|
||||
if (link.href.match(/\/p\/\d+/) || link.href.match(/[?&]page=\d+/)) {
|
||||
loadPageAjax(link.href);
|
||||
} else {
|
||||
loadItemAjax(link.href, true);
|
||||
}
|
||||
} else if (e.target.closest('#togglebg')) {
|
||||
e.preventDefault();
|
||||
background = !background;
|
||||
localStorage.setItem('background', background.toString());
|
||||
var canvas = document.getElementById('bg');
|
||||
if (canvas) {
|
||||
if (background) {
|
||||
canvas.classList.remove('fader-out');
|
||||
canvas.classList.add('fader-in');
|
||||
// Re-trigger loop if started completely fresh or paused
|
||||
if (video && !video.paused) {
|
||||
// We need to access animationLoop from closure?
|
||||
// Accessing it via window.initBackground might be cleaner or just restart it.
|
||||
// But initBackground defines it locally.
|
||||
// We can just rely on initBackground being called or canvas update.
|
||||
// Actually, if we just change opacity, the loop doesn't need to stop/start technically,
|
||||
// but for performance we stopped it if !background.
|
||||
// So we should restart it.
|
||||
window.initBackground();
|
||||
}
|
||||
} else {
|
||||
canvas.classList.remove('fader-in');
|
||||
canvas.classList.add('fader-out');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', (e) => {
|
||||
if (window.location.href.match(/\/p\/\d+/) || window.location.href.match(/[?&]page=\d+/) || window.location.pathname === '/') {
|
||||
// Ideally we should reload page or call loadPageAjax(currentUrl) if it supports it
|
||||
// But if we are going BACK to index from item, we expect grid.
|
||||
// loadItemAjax fails on index.
|
||||
// loadPageAjax handles /p/N logic.
|
||||
// If just slash, loadPageAjax might default to page 1.
|
||||
loadPageAjax(window.location.href);
|
||||
} else {
|
||||
loadItemAjax(window.location.href, true);
|
||||
}
|
||||
});
|
||||
|
||||
// <keybindings>
|
||||
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
|
||||
const keybindings = {
|
||||
@@ -68,7 +436,7 @@ window.requestAnimFrame = (function(){
|
||||
"a": clickOnElementBinding("#next"),
|
||||
"ArrowRight": clickOnElementBinding("#prev"),
|
||||
"d": clickOnElementBinding("#prev"),
|
||||
"r": clickOnElementBinding("#random"),
|
||||
"r": clickOnElementBinding("#random, #nav-random"),
|
||||
" ": clickOnElementBinding("#f0ck-image")
|
||||
};
|
||||
document.addEventListener("keydown", e => {
|
||||
@@ -94,9 +462,11 @@ window.requestAnimFrame = (function(){
|
||||
const wheelEventListener = function (event) {
|
||||
if (event.target.closest('.media-object, .steuerung')) {
|
||||
if (event.deltaY < 0) {
|
||||
document.getElementById('next').click();
|
||||
const el = document.getElementById('next');
|
||||
if (el && el.href && !el.href.endsWith('#')) el.click();
|
||||
} else if (event.deltaY > 0) {
|
||||
document.getElementById('prev').click();
|
||||
const el = document.getElementById('prev');
|
||||
if (el && el.href && !el.href.endsWith('#')) el.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -132,47 +502,116 @@ window.requestAnimFrame = (function(){
|
||||
}
|
||||
// </image-responsive>
|
||||
|
||||
// <scroller>
|
||||
let tts = 0;
|
||||
const scroll_treshold = 1;
|
||||
if([...document.querySelectorAll("div.posts")].length === 1) {
|
||||
document.addEventListener("wheel", e => {
|
||||
if(Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down
|
||||
if(elem = document.querySelector(".pagination > .next:not(.disabled)")) {
|
||||
if(tts < scroll_treshold) {
|
||||
document.querySelector("div#footbar").style.boxShadow = "inset 0px 4px 0px var(--footbar-color)";
|
||||
document.querySelector("div#footbar").style.color = "var(--footbar-color)";
|
||||
tts++;
|
||||
// <infinite-scroll>
|
||||
const postsContainer = document.querySelector("div.posts");
|
||||
if (postsContainer) {
|
||||
// Infinite scroll state
|
||||
let infiniteState = {
|
||||
loading: false,
|
||||
hasMore: true,
|
||||
currentPage: 1
|
||||
};
|
||||
|
||||
// Extract current page from URL
|
||||
const pageMatch = window.location.pathname.match(/\/p\/(\d+)/);
|
||||
if (pageMatch) infiniteState.currentPage = parseInt(pageMatch[1]);
|
||||
|
||||
// Extract context (tag/user/mime) from URL
|
||||
const getContext = () => {
|
||||
const ctx = {};
|
||||
const tagMatch = window.location.pathname.match(/\/tag\/([^/]+)/);
|
||||
if (tagMatch) ctx.tag = decodeURIComponent(tagMatch[1]);
|
||||
const userMatch = window.location.pathname.match(/\/user\/([^/]+)/);
|
||||
if (userMatch) ctx.user = decodeURIComponent(userMatch[1]);
|
||||
const mimeMatch = window.location.pathname.match(/\/(image|audio|video)(?:\/|$)/);
|
||||
if (mimeMatch) ctx.mime = mimeMatch[1];
|
||||
return ctx;
|
||||
};
|
||||
|
||||
// Build URL path for history
|
||||
const buildUrl = (page) => {
|
||||
const ctx = getContext();
|
||||
let path = '/';
|
||||
if (ctx.tag) path += `tag/${ctx.tag}/`;
|
||||
if (ctx.user) path += `user/${ctx.user}/`;
|
||||
if (ctx.mime) path += `${ctx.mime}/`;
|
||||
if (page > 1) path += `p/${page}`;
|
||||
return path.replace(/\/$/, '') || '/';
|
||||
};
|
||||
|
||||
// Fetch and append more items
|
||||
const loadMoreItems = async () => {
|
||||
if (infiniteState.loading || !infiniteState.hasMore) return;
|
||||
|
||||
infiniteState.loading = true;
|
||||
const foot = document.querySelector("div#footbar");
|
||||
if (foot) {
|
||||
foot.innerHTML = '<span class="loading-spinner">Loading...</span>';
|
||||
foot.style.color = 'var(--footbar-color)';
|
||||
}
|
||||
else
|
||||
changePage(elem);
|
||||
|
||||
const nextPage = infiniteState.currentPage + 1;
|
||||
const ctx = getContext();
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', nextPage);
|
||||
if (ctx.tag) params.append('tag', ctx.tag);
|
||||
if (ctx.user) params.append('user', ctx.user);
|
||||
if (ctx.mime) params.append('mime', ctx.mime);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/ajax/items?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.html) {
|
||||
// Append new thumbnails
|
||||
postsContainer.insertAdjacentHTML('beforeend', data.html);
|
||||
|
||||
// Update state
|
||||
infiniteState.currentPage = data.currentPage;
|
||||
infiniteState.hasMore = data.hasMore;
|
||||
|
||||
// Update URL
|
||||
history.replaceState({}, '', buildUrl(infiniteState.currentPage));
|
||||
|
||||
// Update pagination display if exists
|
||||
const paginationLinks = document.querySelectorAll('.pagination .pagination-int-item, .pagination .btn');
|
||||
paginationLinks.forEach(el => {
|
||||
if (el.textContent.trim() == infiniteState.currentPage) {
|
||||
el.classList.add('disabled');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
infiniteState.hasMore = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Infinite scroll fetch error:', err);
|
||||
} finally {
|
||||
infiniteState.loading = false;
|
||||
if (foot) {
|
||||
foot.innerHTML = infiniteState.hasMore ? '▼' : '—';
|
||||
foot.style.color = 'transparent';
|
||||
}
|
||||
}
|
||||
else if(window.scrollY <= 0 && e.deltaY < 0) { // up
|
||||
if(elem = document.querySelector(".pagination > .prev:not(.disabled)")) {
|
||||
if(tts < scroll_treshold) {
|
||||
document.querySelector("nav.navbar").style.boxShadow = "0px 2px 0px var(--loading-indicator-color)";
|
||||
document.querySelector("nav.navbar").style.transition = ".2s ease-in-out";
|
||||
tts++;
|
||||
}
|
||||
else
|
||||
changePage(elem);
|
||||
}
|
||||
}
|
||||
else {
|
||||
tts = 0;
|
||||
document.querySelector("div#footbar").style.boxShadow = "unset";
|
||||
document.querySelector("div#footbar").style.color = "transparent";
|
||||
document.querySelector("nav.navbar").style.boxShadow = "unset";
|
||||
};
|
||||
|
||||
// Scroll detection - preload before reaching bottom
|
||||
const PRELOAD_OFFSET = 500; // pixels before bottom to trigger load
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
if (!document.querySelector('#main')) return;
|
||||
|
||||
const scrollPosition = window.innerHeight + window.scrollY;
|
||||
const pageHeight = document.querySelector('#main').offsetHeight;
|
||||
const distanceFromBottom = pageHeight - scrollPosition;
|
||||
|
||||
// Load more when within PRELOAD_OFFSET pixels of bottom
|
||||
if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) {
|
||||
loadMoreItems();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rmatch = /\/p\/(\d+?)/;
|
||||
if(document.referrer.match(rmatch) && document.location.href.match(rmatch))
|
||||
if(document.location.href.match(rmatch) < document.referrer.match(rmatch))
|
||||
document.body.scrollTop = document.body.scrollHeight;
|
||||
// </scroller>
|
||||
// </infinite-scroll>
|
||||
|
||||
// <swipe>
|
||||
const swipeRT = {
|
||||
@@ -309,6 +748,7 @@ window.requestAnimFrame = (function(){
|
||||
// </scroller>
|
||||
})();
|
||||
|
||||
|
||||
// disable default scroll event when mouse is on content div
|
||||
// this is useful for items that have a lot of tags for example: 12536
|
||||
const targetSelector = '.content';
|
||||
@@ -334,10 +774,13 @@ function init() {
|
||||
|
||||
window.addEventListener('load', init);
|
||||
|
||||
document.getElementById('sbtForm').addEventListener('submit', (e) => {
|
||||
const sbtForm = document.getElementById('sbtForm');
|
||||
if (sbtForm) {
|
||||
sbtForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById('sbtInput').value.trim();
|
||||
if (input) {
|
||||
window.location.href = `/tag/${encodeURIComponent(input)}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ const Cookie = {
|
||||
(() => {
|
||||
const acttheme = Cookie.get('theme') ?? "w0bm";
|
||||
const themecontainer = document.querySelector("li#themes > ul.dropdown-menu");
|
||||
|
||||
if (!themecontainer) return; // Theme menu not present on this page
|
||||
|
||||
const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase());
|
||||
if (acttheme !== document.documentElement.getAttribute("theme") && themes.includes(acttheme))
|
||||
document.documentElement.setAttribute("theme", acttheme);
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
(async () => {
|
||||
if(_addtag = document.querySelector("a#a_addtag")) {
|
||||
const postid = +document.querySelector("a.id-link").innerText;
|
||||
const poster = document.querySelector("a#a_username").innerText;
|
||||
let tags = [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2));
|
||||
// Helper to get dynamic context from the DOM
|
||||
const getContext = () => {
|
||||
const idLink = document.querySelector("a.id-link");
|
||||
if (!idLink) return null;
|
||||
return {
|
||||
postid: +idLink.innerText,
|
||||
poster: document.querySelector("a#a_username")?.innerText,
|
||||
tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2))
|
||||
};
|
||||
};
|
||||
|
||||
const queryapi = async (url, data, method = 'GET') => {
|
||||
let req;
|
||||
@@ -47,11 +53,19 @@
|
||||
});
|
||||
};
|
||||
|
||||
const addtagClick = (ae = false) => {
|
||||
if(ae)
|
||||
ae.preventDefault();
|
||||
const addtagClick = (e) => {
|
||||
if (e) e.preventDefault();
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid, tags } = ctx;
|
||||
|
||||
const insert = document.querySelector("a#a_addtag");
|
||||
// Check if input already exists to prevent duplicates
|
||||
if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) {
|
||||
insert.previousElementSibling.querySelector('input').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("badge", "badge-light", "mr-2");
|
||||
|
||||
@@ -73,8 +87,11 @@
|
||||
input.addEventListener("keyup", async e => {
|
||||
if (e.key === "Enter") {
|
||||
const tmptag = input.value?.trim();
|
||||
// Check fresh tags from DOM just in case? Or use captured tags?
|
||||
// Using captured 'tags' from when clicked is safe enough for immediate check.
|
||||
if (tags.includes(tmptag))
|
||||
return alert("tag already exists");
|
||||
|
||||
const res = await post("/api/v2/admin/" + postid + "/tags", {
|
||||
tagname: tmptag
|
||||
});
|
||||
@@ -82,10 +99,15 @@
|
||||
alert(res.msg);
|
||||
return false;
|
||||
}
|
||||
tags = res.tags.map(t => t.tag);
|
||||
// No need to update 'tags' local var, renderTags updates DOM, and next click reads DOM.
|
||||
renderTags(res.tags);
|
||||
addtagClick();
|
||||
|
||||
// Remove input and reset
|
||||
span.parentElement.removeChild(span);
|
||||
testList.innerText = "";
|
||||
|
||||
// Re-open input? Original code called addtagClick() again.
|
||||
addtagClick();
|
||||
}
|
||||
else if (e.key === "Escape") {
|
||||
span.parentElement.removeChild(span);
|
||||
@@ -125,14 +147,18 @@
|
||||
});
|
||||
|
||||
input.addEventListener("focusout", ie => {
|
||||
// Small delay to allow click events on suggestions or other checks?
|
||||
// Original code:
|
||||
if (input.value.length === 0)
|
||||
input.parentElement.parentElement.removeChild(input.parentElement);
|
||||
});
|
||||
};
|
||||
|
||||
const toggleEvent = async (e = false) => {
|
||||
if(e)
|
||||
e.preventDefault();
|
||||
const toggleEvent = async (e) => {
|
||||
if (e) e.preventDefault();
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid } = ctx;
|
||||
|
||||
const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', {
|
||||
method: 'PUT'
|
||||
@@ -141,7 +167,12 @@
|
||||
renderTags(res.tags);
|
||||
};
|
||||
|
||||
const toggleFavEvent = async e => {
|
||||
const toggleFavEvent = async (e) => {
|
||||
// e is the click event or undefined
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid } = ctx;
|
||||
|
||||
const res = await post('/api/v2/admin/togglefav', {
|
||||
postid: postid
|
||||
});
|
||||
@@ -176,24 +207,32 @@
|
||||
}
|
||||
};
|
||||
|
||||
_addtag.addEventListener("click", addtagClick);
|
||||
document.querySelector("a#a_toggle").addEventListener("click", toggleEvent);
|
||||
document.querySelector("svg#a_favo").addEventListener("click", toggleFavEvent);
|
||||
// Event Delegation
|
||||
document.addEventListener("click", e => {
|
||||
if (e.target.matches("a#a_addtag")) {
|
||||
addtagClick(e);
|
||||
} else if (e.target.matches("a#a_toggle")) {
|
||||
toggleEvent(e);
|
||||
} else if (e.target.closest("svg#a_favo")) {
|
||||
toggleFavEvent(e);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keyup", e => {
|
||||
if(e.target.tagName === "INPUT")
|
||||
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
|
||||
return;
|
||||
const ctx = getContext();
|
||||
if (!ctx) return; // Only trigger if on an item page
|
||||
|
||||
if (e.key === "p")
|
||||
toggleEvent();
|
||||
else if (e.key === "i")
|
||||
addtagClick();
|
||||
else if(e.key === "x")
|
||||
deleteButtonEvent();
|
||||
else if (e.key === "f")
|
||||
toggleFavEvent();
|
||||
});
|
||||
}
|
||||
|
||||
// Settings page logic (unchanged essentially, but kept inside IIFE scope)
|
||||
if (document.location.pathname === '/settings') {
|
||||
const saveAvatar = async e => {
|
||||
e.preventDefault();
|
||||
@@ -220,8 +259,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('input#s_avatar').addEventListener('click', saveAvatar);
|
||||
document.querySelector('input[name="i_avatar"]').addEventListener('keyup', async e => {
|
||||
const sAvatar = document.querySelector('input#s_avatar');
|
||||
if (sAvatar) sAvatar.addEventListener('click', saveAvatar);
|
||||
|
||||
const iAvatar = document.querySelector('input[name="i_avatar"]');
|
||||
if (iAvatar) iAvatar.addEventListener('keyup', async e => {
|
||||
if (e.key === 'Enter')
|
||||
await saveAvatar(e);
|
||||
});
|
||||
|
||||
@@ -32,7 +32,9 @@ export default new class {
|
||||
return (Math.round((b * 8 / s / 1e6) * 1e4) / 1e4);
|
||||
};
|
||||
timeAgo(date) {
|
||||
const { interval, epoch } = getDuration(~~((new Date() - new Date(date)) / 1e3));
|
||||
const duration = getDuration(~~((new Date() - new Date(date)) / 1e3));
|
||||
if (!duration) return "just now";
|
||||
const { interval, epoch } = duration;
|
||||
return `${interval} ${epoch}${interval === 1 ? "" : "s"} ago`;
|
||||
};
|
||||
md5(str) {
|
||||
|
||||
127
src/inc/routes/ajax.mjs
Normal file
127
src/inc/routes/ajax.mjs
Normal file
@@ -0,0 +1,127 @@
|
||||
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,
|
||||
fav: query.fav === 'true'
|
||||
});
|
||||
|
||||
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.
|
||||
// Templates use session.user for matching favorites. We must preserve it.
|
||||
// if (data.session.user) delete data.session.user; // REMOVED THIS
|
||||
} 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
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Infinite scroll endpoint for index thumbnails
|
||||
router.get(/\/ajax\/items/, async (req, res) => {
|
||||
let query = {};
|
||||
if (typeof req.url === 'string') {
|
||||
const parsedUrl = url.parse(req.url, true);
|
||||
query = parsedUrl.query;
|
||||
} else {
|
||||
query = req.url.qs || {};
|
||||
}
|
||||
|
||||
const page = parseInt(query.page) || 1;
|
||||
|
||||
const data = await f0cklib.getf0cks({
|
||||
page: page,
|
||||
tag: query.tag || null,
|
||||
user: query.user || null,
|
||||
mime: query.mime || null,
|
||||
mode: req.session.mode,
|
||||
session: !!req.session,
|
||||
fav: false
|
||||
});
|
||||
|
||||
if (!data.success) {
|
||||
return res.reply({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
success: false,
|
||||
html: '',
|
||||
hasMore: false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Render just the thumbnail items
|
||||
const itemsHtml = tpl.render('snippets/items-grid', {
|
||||
items: data.items,
|
||||
link: data.link
|
||||
});
|
||||
|
||||
// Render pagination
|
||||
const paginationHtml = tpl.render('snippets/pagination', {
|
||||
pagination: data.pagination,
|
||||
link: data.link
|
||||
});
|
||||
|
||||
const hasMore = data.pagination.next !== null;
|
||||
|
||||
return res.reply({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
html: itemsHtml,
|
||||
pagination: paginationHtml,
|
||||
hasMore: hasMore,
|
||||
nextPage: data.pagination.next,
|
||||
currentPage: data.pagination.page
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
};
|
||||
@@ -13,14 +13,18 @@ export default router => {
|
||||
group.get(/\/random(\/user\/.+|\/image|\/video|\/audio)?$/, async (req, res) => {
|
||||
const user = req.url.split[3] === "user" ? req.url.split[4] : "%";
|
||||
const mime = (allowedMimes.filter(n => req.url.split[3]?.startsWith(n))[0] ? req.url.split[3] : "") + "%";
|
||||
const tag = req.url.qs.tag || null;
|
||||
|
||||
const rows = await db`
|
||||
select *
|
||||
select "items".*
|
||||
from "items"
|
||||
left join tags_assign on tags_assign.item_id = items.id
|
||||
left join tags on tags.id = tags_assign.tag_id
|
||||
where
|
||||
mime ilike ${mime} and
|
||||
username ilike ${user} and
|
||||
active = 'true'
|
||||
${tag ? db`and tags.normalized ilike ${'%' + tag + '%'}` : db``}
|
||||
order by random()
|
||||
limit 1
|
||||
`;
|
||||
@@ -51,15 +55,13 @@ export default router => {
|
||||
where
|
||||
${db.unsafe(modequery)} and
|
||||
active = 'true'
|
||||
${
|
||||
opt.older
|
||||
${opt.older
|
||||
? db`and id <= ${opt.older}`
|
||||
: opt.newer
|
||||
? db`and id >= ${opt.newer}`
|
||||
: db``
|
||||
}
|
||||
order by id ${
|
||||
opt.newer
|
||||
order by id ${opt.newer
|
||||
? db`asc`
|
||||
: db`desc`
|
||||
}
|
||||
@@ -175,8 +177,7 @@ export default router => {
|
||||
}
|
||||
|
||||
const q = (await db`
|
||||
update "tags" set ${
|
||||
db({
|
||||
update "tags" set ${db({
|
||||
tag: newtag
|
||||
}, 'tag')
|
||||
}
|
||||
@@ -287,8 +288,7 @@ export default router => {
|
||||
else {
|
||||
// add fav
|
||||
await db`
|
||||
insert into "favorites" ${
|
||||
db({
|
||||
insert into "favorites" ${db({
|
||||
item_id: +postid,
|
||||
user_id: +req.session.id
|
||||
}, 'item_id', 'user_id')
|
||||
|
||||
41
src/inc/routes/tag_image.mjs
Normal file
41
src/inc/routes/tag_image.mjs
Normal file
@@ -0,0 +1,41 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export default (router, tpl) => {
|
||||
router.get(/^\/tag_image\/(?<tag>.+)$/, async (req, res) => {
|
||||
const tag = req.params.tag;
|
||||
|
||||
// Create a deterministic hash from the tag
|
||||
const hash = crypto.createHash('md5').update(tag).digest('hex');
|
||||
|
||||
// Generate colors from hash
|
||||
const c1 = '#' + hash.substring(0, 6);
|
||||
const c2 = '#' + hash.substring(6, 12);
|
||||
const c3 = '#' + hash.substring(12, 18);
|
||||
|
||||
// Generate some deterministic numbers for shapes
|
||||
const n1 = parseInt(hash.substring(18, 20), 16);
|
||||
const n2 = parseInt(hash.substring(20, 22), 16);
|
||||
|
||||
const svg = `
|
||||
<svg width="300" height="150" viewBox="0 0 300 150" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:${c1};stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:${c2};stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="300" height="150" fill="url(#grad)" />
|
||||
<circle cx="${n1}%" cy="${n2}%" r="${(n1 + n2) / 4}" fill="${c3}" fill-opacity="0.3" />
|
||||
<circle cx="${100 - n1}%" cy="${100 - n2}%" r="${(n1 + n2) / 3}" fill="${c3}" fill-opacity="0.2" />
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="sans-serif" font-size="24" fill="#fff" fill-opacity="0.9" font-weight="bold">${tag}</text>
|
||||
</svg>
|
||||
`.trim();
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
'Cache-Control': 'public, max-age=86400'
|
||||
});
|
||||
res.end(svg);
|
||||
});
|
||||
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>
|
||||
113
views/item.html
113
views/item.html
@@ -1,119 +1,10 @@
|
||||
@include(snippets/header)
|
||||
<canvas class="hidden-xs" id="bg"></canvas>
|
||||
|
||||
<div class="wrapper">
|
||||
<div id="main">
|
||||
|
||||
<div class="container">
|
||||
<div class="_204863">
|
||||
<div class="location">{{ (url.pathname) }}</div>
|
||||
<div class="gapLeft"></div>
|
||||
@if(session)
|
||||
<div class="gapRight">
|
||||
<svg class="iconset" id="a_favo"><use href="/s/img/iconset.svg#heart_{{ Object.values(item.favorites).filter(u => u.user == session.user)[0] ? 'solid' : 'regular' }}"></use></svg>
|
||||
<svg class="iconset" id="a_tfull"><use href="/s/img/iconset.svg#window-{{ fullscreen == 1 ? 'minimize' : 'maximize' }}"></use></svg>
|
||||
@if(session.admin)<svg class="iconset" id="a_delete"><use href="/s/img/iconset.svg#cross"></use></svg>@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="next-post">
|
||||
@if(pagination.prev)
|
||||
<div class="arrow-next">
|
||||
<a id="next" href="{{ link.main }}{{ pagination.prev }}"></a>
|
||||
</div>
|
||||
@else
|
||||
<div class="arrow-next">
|
||||
<a id="next" href="#" style="color: #ccc !important;"></a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="media-object">
|
||||
@if(item.mime.startsWith("video"))
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}" preload="auto" autoplay controls loop playsinline></video>
|
||||
</div>
|
||||
@elseif(item.mime.startsWith("audio"))
|
||||
<div class="embed-responsive embed-responsive-16by9" style="background: url('@if(item.coverart)//w0bm.com{{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;">
|
||||
<audio id="my-video" class="embed-responsive-item" autoplay controls loop src="{{ item.dest }}" data-setup="{}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" type="{{ item.mime }}"></audio>
|
||||
</div>
|
||||
@elseif(item.mime.startsWith("image"))
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<div class="embed-responsive-image" id="image-scroll">
|
||||
<a href="{{ item.dest }}" id="elfe" target="_blank"><img id="f0ck-image" class="img-fluid" src="{{ item.dest }}" loading="lazy" decoding="async"/></a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<h1>404 - Not f0cked</h1>
|
||||
@endif
|
||||
</div>
|
||||
<div class="previous-post">
|
||||
@if(pagination.next)
|
||||
<div class="arrow-prev">
|
||||
<a id="prev" href="{{ link.main }}{{ pagination.next }}"></a>
|
||||
</div>
|
||||
@else
|
||||
<div class="arrow-prev">
|
||||
<a id="prev" href="#" style="color: #ccc !important;"></a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="metadata">
|
||||
<div class="kontrollelement">
|
||||
<div class="einheit">
|
||||
@if(typeof pagination !== "undefined")
|
||||
<nav class="steuerung">
|
||||
@if(pagination.prev)
|
||||
<a id="" href="{{ link.main }}{{ pagination.prev }}">← next</a>
|
||||
@else
|
||||
<a id="" href="#" style="visibility: hidden">← next</a>
|
||||
@endif
|
||||
<span>|</span>
|
||||
<a id="random" class="" href="/random">
|
||||
<span>random</span>
|
||||
</a>
|
||||
<span>
|
||||
|
|
||||
</span>
|
||||
@if(pagination.next)
|
||||
<a id="" href="{{ link.main }}{{ pagination.next }}">prev →</a>
|
||||
@else
|
||||
<a id="" href="#" style="visibility: hidden">prev →</a>
|
||||
@endif
|
||||
</nav>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="blahlol">
|
||||
<span class="badge badge-dark">
|
||||
<a href="/{{ item.id }}" class="id-link">{{ item.id }}</a>
|
||||
@if(session)
|
||||
(<a id="a_username" href="/user/{{ user.name.toLowerCase() }}/f0cks@if(tmp.mime)/{{ tmp.mime }}@endif">{{ user.name }}</a>)
|
||||
@endif
|
||||
</span>
|
||||
<span class="badge badge-dark"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{ item.timestamp.timeago }}</time></span>
|
||||
<span class="badge badge-dark" id="tags">
|
||||
@if(typeof item.tags !== "undefined")
|
||||
@each(item.tags as tag)
|
||||
<span @if(session)tooltip="{{ tag.user }}"@endif class="badge {{ tag.badge }} mr-2">
|
||||
<a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(session.admin) <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>
|
||||
@include(item-partial)
|
||||
</div>
|
||||
</div>
|
||||
@include(snippets/footer)
|
||||
@@ -1,14 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en" theme="@if(typeof theme !== "undefined"){{ theme }}@endif" res="@if(typeof fullscreen !== "undefined"){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif">
|
||||
<html lang="en" theme="@if(typeof theme !== 'undefined'){{ theme }}@endif"
|
||||
res="@if(typeof fullscreen !== 'undefined'){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif">
|
||||
|
||||
<head>
|
||||
@if(typeof item !== "undefined")<title>f0bm - {{ item.id }}</title>@else<title>f0bm</title>@endif
|
||||
@if(typeof item !== 'undefined')<title>f0bm - {{ item.id }}</title>@else<title>f0bm</title>@endif
|
||||
<link rel="icon" type="image/gif" href="/s/img/favicon.png" />
|
||||
<link rel="stylesheet" href="/s/css/f0ck.css?v=@mtime(/public/s/css/f0ck.css)">
|
||||
<link rel="stylesheet" href="/s/css/w0bm.css?v=@mtime(/public/s/css/w0bm.css)">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
@if(typeof item !== "undefined")<link rel="canonical" href="https://w0bm.com/{{ item.id }}" />@endif
|
||||
@if(typeof item !== 'undefined')
|
||||
<link rel="canonical" href="https://w0bm.com/{{ item.id }}" />@endif
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- hier splitting betreiben -->
|
||||
<canvas class="hidden-xs" id="bg"></canvas>
|
||||
@include(snippets/navbar)
|
||||
7
views/snippets/items-grid.html
Normal file
7
views/snippets/items-grid.html
Normal file
@@ -0,0 +1,7 @@
|
||||
@each(items as item)
|
||||
<a href="{{ link.main }}{{ item.id }}" data-mime="{{ item.mime }}"
|
||||
data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}"
|
||||
style="background-image: url('/t/{{ item.id }}.webp')">
|
||||
<p></p>
|
||||
</a>
|
||||
@endeach
|
||||
@@ -2,36 +2,21 @@
|
||||
<!-- logged in -->
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="/"><span class="f0ck" width="" height="">w0bm.com</span></a>
|
||||
|
||||
<div class="navigation-links-guest">
|
||||
<ol>
|
||||
<a href="/tags">tags</a>
|
||||
<a href="/about">about</a>
|
||||
@if(!/^\/\d$/.test(url.pathname))
|
||||
<a href="/random">rand</a>
|
||||
<a href="/random" id="nav-random">rand</a>
|
||||
@endif
|
||||
</ol>
|
||||
</div>
|
||||
<!-- show pagination only for tags and main page -->
|
||||
@if(!/^\/\d$/.test(url.pathname))
|
||||
@if(!/^\/\d+$/.test(url.pathname))
|
||||
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
|
||||
<div class="pagination-container-fluid">
|
||||
<div class="pagination-wrapper">
|
||||
@if(typeof pagination !== "undefined")
|
||||
<nav class="pagination">
|
||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">«</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
|
||||
@include(snippets/pagination)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,36 +26,21 @@
|
||||
<!-- not logged in -->
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<a class="navbar-brand" href="/"><span class="f0ck" width="" height="">w0bm.com</span></a>
|
||||
|
||||
<div class="navigation-links-guest">
|
||||
<ol>
|
||||
<a href="/tags">tags</a>
|
||||
<a href="/about">about</a>
|
||||
@if(!/^\/\d$/.test(url.pathname))
|
||||
<a href="/random">rand</a>
|
||||
<a href="/random" id="nav-random">rand</a>
|
||||
@endif
|
||||
</ol>
|
||||
</div>
|
||||
<!-- show pagination only for tags and main page -->
|
||||
@if(!/^\/\d$/.test(url.pathname))
|
||||
@if(!/^\/\d+$/.test(url.pathname))
|
||||
<div class="collapse navbar-collapse show" id="navbarSupportedContent">
|
||||
<div class="pagination-container-fluid">
|
||||
<div class="pagination-wrapper">
|
||||
@if(typeof pagination !== "undefined")
|
||||
<nav class="pagination">
|
||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">«</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
|
||||
@include(snippets/pagination)
|
||||
</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
|
||||
@@ -2,24 +2,30 @@
|
||||
<div id="main">
|
||||
<div class="container">
|
||||
<h3 style="text-align: center;">☯</h3>
|
||||
<div class="tags">
|
||||
<div class="tags-grid">
|
||||
@if(session)
|
||||
@each(toptags_regged as toptag)
|
||||
<div class="tag badge badge-light mr-2">
|
||||
<div class="tagbox-body">
|
||||
<span class="toptag_id">{!! toptag.tag !!}</span>
|
||||
<span class="toptag_tag"><a href="/tag/{!! toptag.tag !!}">{{ toptag.total_items }}</a></span>
|
||||
<a href="/tag/{!! toptag.tag !!}" class="tag-card">
|
||||
<div class="tag-card-image">
|
||||
<img src="/tag_image/{!! toptag.tag !!}" loading="lazy" alt="{!! toptag.tag !!}">
|
||||
</div>
|
||||
<div class="tag-card-content">
|
||||
<span class="tag-name">#{!! toptag.tag !!}</span>
|
||||
<span class="tag-count">{{ toptag.total_items }} posts</span>
|
||||
</div>
|
||||
</a>
|
||||
@endeach
|
||||
@else
|
||||
@each(toptags as toptag)
|
||||
<div class="tag badge badge-light mr-2">
|
||||
<div class="tagbox-body">
|
||||
<span class="toptag_id">{!! toptag.tag !!}</span>
|
||||
<span class="toptag_tag"><a href="/tag/{!! toptag.tag !!}">{{ toptag.total_items }}</a></span>
|
||||
<a href="/tag/{!! toptag.tag !!}" class="tag-card">
|
||||
<div class="tag-card-image">
|
||||
<img src="/tag_image/{!! toptag.tag !!}" loading="lazy" alt="{!! toptag.tag !!}">
|
||||
</div>
|
||||
<div class="tag-card-content">
|
||||
<span class="tag-name">#{!! toptag.tag !!}</span>
|
||||
<span class="tag-count">{{ toptag.total_items }} posts</span>
|
||||
</div>
|
||||
</a>
|
||||
@endeach
|
||||
@endif
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user