Compare commits

...

9 Commits

Author SHA1 Message Date
x
224064d0ca fixing background visibility and states 2026-01-23 18:37:44 +01:00
x
3ee28fd0b7 Merge branch 'f0bm' into eins-f0bm 2026-01-23 17:03:13 +01:00
eins
964284d5c9 REAL seamless now! 2026-01-23 15:53:45 +00:00
x
9a03d5f697 adding generic tag cards 2026-01-23 16:53:19 +01:00
eins
9b1041dda7 first version of infinite scrolling 2026-01-23 15:45:28 +00:00
4bc8b8f436 Merge pull request 'fixed issues with the random button and hotkeys' (#2) from eins/f0bm:f0bm into f0bm
Reviewed-on: #2
2026-01-23 14:53:20 +00:00
eins
45f9345e9c fix r key on tag overview 2026-01-23 14:46:39 +00:00
eins
c74e5a7402 fixed scrolling in overview 2026-01-23 14:39:42 +00:00
eins
6799ec1567 fixed issues with the random button and hotkeys 2026-01-23 14:07:52 +00:00
13 changed files with 705 additions and 240 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ deleted/b
deleted/ca deleted/ca
deleted/t deleted/t
tmp/* tmp/*
tools

View File

@@ -1197,7 +1197,9 @@ body {
overscroll-behavior-y: contain; overscroll-behavior-y: contain;
overflow: unset; overflow: unset;
font-size: 14px; font-size: 14px;
height: /* 100%; */auto; height:
/* 100%; */
auto;
} }
.wrapper { .wrapper {
@@ -1318,6 +1320,13 @@ div.posts>a:hover::after {
grid-template-columns: auto 1fr 0fr; grid-template-columns: auto 1fr 0fr;
justify-content: start; justify-content: start;
border-bottom: 1px solid var(--nav-border-color); 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 { .navbar-brand {
@@ -1424,7 +1433,7 @@ ul.navbar-nav li.nav-item {
} }
.nav-link[data-toggle="dropdown"].ddcontent::after { .nav-link[data-toggle="dropdown"].ddcontent::after {
content: "\00a0("attr(content) ")\00a0\25bc"; content: "\00a0(" attr(content) ")\00a0\25bc";
} }
.nav-link[data-toggle="dropdown"]:not(.ddcontent)::after { .nav-link[data-toggle="dropdown"]:not(.ddcontent)::after {
@@ -1576,14 +1585,10 @@ span.placeholder {
} }
@media (max-width: 1056px) { @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 { .navbar-brand {
grid-area: f0ck; /* maintained for potential other uses or reset */
} }
.pagination-container-fluid { .pagination-container-fluid {
@@ -1608,7 +1613,7 @@ span.placeholder {
} }
@media (max-width: 1325px) { @media (max-width: 1325px) {
/* ranking page - idea */ /* ranking page - idea */
/* .ranking { /* .ranking {
grid-template-columns: 1fr 1fr !important; grid-template-columns: 1fr 1fr !important;
} */ } */
@@ -1783,8 +1788,9 @@ span.placeholder {
.index-container { .index-container {
width: 100%; width: 100%;
padding: 5px; padding: 5px;
/* background-color: var(--navigation-links-bg); /* background-color: var(--navigation-links-bg);
*/} */
}
@media (min-width: 361px) { @media (min-width: 361px) {
.embed-responsive-image { .embed-responsive-image {
@@ -2779,7 +2785,9 @@ ul.navbar-nav-guests li.nav-item {
margin-right: .5rem; 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-right: .5rem;
padding-left: .5rem; padding-left: .5rem;
} }
@@ -2792,7 +2800,7 @@ ul.navbar-nav-guests li.nav-item {
margin: 0; margin: 0;
padding: 5px; padding: 5px;
} }
ul.navbar-nav-guests li.nav-item { ul.navbar-nav-guests li.nav-item {
margin-right: unset; margin-right: unset;
} }
@@ -2812,16 +2820,20 @@ ul.navbar-nav-guests li.nav-item {
/* Pagination Responsiveness */ /* Pagination Responsiveness */
@media (max-width: 799px) { @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-right: 2px;
padding-left: 2px; padding-left: 2px;
} }
.pagination > a, .pagination > span { .pagination>a,
.pagination>span {
margin-right: 2px; margin-right: 2px;
margin-left: 2px; margin-left: 2px;
} }
.pagination { .pagination {
justify-content: center !important; justify-content: center !important;
} }
@@ -2830,15 +2842,19 @@ ul.navbar-nav-guests li.nav-item {
/* fadeIn effect */ /* fadeIn effect */
@keyframes fadeIn { @keyframes fadeIn {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
img#f0ck-image, div.imageDoor, div.posts a, video { img#f0ck-image,
animation: 1s ease-out 0s 1 fadeInFX; div.imageDoor,
div.posts a,
video {
animation: 1s ease-out 0s 1 fadeInFX;
} }
/* f0ckgle */ /* f0ckgle */
@@ -2867,7 +2883,7 @@ img#f0ck-image, div.imageDoor, div.posts a, video {
background: var(--nav-bg); background: var(--nav-bg);
padding: 5px; padding: 5px;
} }
.profile_head_avatar { .profile_head_avatar {
margin: 0; margin: 0;
} }
@@ -2892,11 +2908,13 @@ img#f0ck-image, div.imageDoor, div.posts a, video {
padding: 5px; padding: 5px;
} }
.f0cks h5, .favs h5 { .f0cks h5,
.favs h5 {
background: var(--dropdown-bg); background: var(--dropdown-bg);
} }
.f0cks-header, .favs-header { .f0cks-header,
.favs-header {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
background: var(--img-border-color); background: var(--img-border-color);
@@ -2925,7 +2943,7 @@ div.favs div.posts {
filter: blur(100px); filter: blur(100px);
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
z-index: 0; z-index: 0;
transition: 2s ease; transition: opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 0.2; opacity: 0.2;
} }
@@ -2940,46 +2958,50 @@ button#togglebg {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% {
100% {
opacity: 1; opacity: 1;
} }
} }
@keyframes fadeOutFX { @keyframes fadeOutFX {
0% { 0% {
opacity: 1; opacity: 1;
} }
100% {
100% {
opacity: 0; opacity: 0;
} }
} }
@keyframes fadeIn { @keyframes fadeIn {
0% { 0% {
opacity: 0; opacity: 0;
} }
100% { 100% {
opacity: 0.2; opacity: 0.2;
} }
} }
@keyframes fadeOut { @keyframes fadeOut {
0% { 0% {
opacity: 0.2; opacity: 0.2;
} }
100% {
opacity: 0; 100% {
} opacity: 0;
}
} }
.fader-in { .fader-in {
animation: fadeIn .8s steps(100) forwards; opacity: 0.4 !important;
} }
.fader-out { .fader-out {
animation: fadeOut .8s steps(100) forwards opacity: 0 !important;
} }
.settings { .settings {
display: grid; display: grid;
@@ -3004,6 +3026,7 @@ input#s_avatar {
#s_avatar:hover { #s_avatar:hover {
background: #ffffff0f; background: #ffffff0f;
} }
.theforceofthree { .theforceofthree {
display: grid; display: grid;
grid-template-columns: 0.4fr 1fr 0.4fr; grid-template-columns: 0.4fr 1fr 0.4fr;
@@ -3011,4 +3034,90 @@ input#s_avatar {
.upload { .upload {
padding: 10px; 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;
}

View File

@@ -130,12 +130,12 @@
return false; return false;
} }
renderTags(res.tags); renderTags(res.tags);
span.parentElement.removeChild(span); if (span.parentElement) span.parentElement.removeChild(span);
testList.innerText = ""; testList.innerText = "";
addtagClick(); addtagClick();
} }
else if (e.key === "Escape") { else if (e.key === "Escape") {
span.parentElement.removeChild(span); if (span.parentElement) span.parentElement.removeChild(span);
testList.innerText = ""; testList.innerText = "";
} }
else { else {

View File

@@ -8,6 +8,25 @@ window.requestAnimFrame = (function () {
(() => { (() => {
let video; 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")) { if (elem = document.querySelector("#my-video")) {
video = new v0ck(elem); video = new v0ck(elem);
document.addEventListener("keydown", e => { document.addEventListener("keydown", e => {
@@ -17,13 +36,27 @@ window.requestAnimFrame = (function () {
} }
}); });
const toggleBg = document.getElementById('togglebg');
if (toggleBg) {
toggleBg.addEventListener('click', function (e) { if (elem !== null) {
e.preventDefault(); // ... existing code ...
background = !background; }
localStorage.setItem('background', background.toString()); }
var canvas = document.getElementById('bg');
// 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) { if (background) {
canvas.classList.add('fader-in'); canvas.classList.add('fader-in');
canvas.classList.remove('fader-out'); canvas.classList.remove('fader-out');
@@ -31,33 +64,102 @@ window.requestAnimFrame = (function () {
canvas.classList.add('fader-out'); canvas.classList.add('fader-out');
canvas.classList.remove('fader-in'); canvas.classList.remove('fader-in');
} }
animationLoop();
});
}
if (elem !== null) { const context = canvas.getContext('2d');
if (localStorage.getItem('background') == undefined) { const cw = canvas.width = canvas.clientWidth | 0;
localStorage.setItem('background', 'true'); const ch = canvas.height = canvas.clientHeight | 0;
}
var background = localStorage.getItem('background') === 'true'; const animationLoop = () => {
var canvas = document.getElementById('bg'); if (elem.paused || elem.ended || !background)
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; return;
context.drawImage(video, 0, 0, cw, ch); context.drawImage(elem, 0, 0, cw, ch);
window.requestAnimFrame(animationLoop); window.requestAnimFrame(animationLoop);
} }
elem.addEventListener('play', 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; let tt = false;
const stimeout = 500; const stimeout = 500;
@@ -68,6 +170,18 @@ window.requestAnimFrame = (function () {
} }
}; };
// 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) => { const loadItemAjax = async (url, inheritContext = true) => {
console.log("loadItemAjax called with:", url, "inheritContext:", inheritContext); console.log("loadItemAjax called with:", url, "inheritContext:", inheritContext);
// Show loading indicator // Show loading indicator
@@ -77,6 +191,10 @@ window.requestAnimFrame = (function () {
// Extract item ID from URL. Regex now handles query params, hashes, and trailing slashes. // Extract item ID from URL. Regex now handles query params, hashes, and trailing slashes.
const match = url.match(/\/(\d+)(?:\/|#|\?|$)/); 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) { if (!match) {
console.warn("loadItemAjax: No ID match found in URL", url); console.warn("loadItemAjax: No ID match found in URL", url);
// fallback for weird/external links // fallback for weird/external links
@@ -87,12 +205,15 @@ window.requestAnimFrame = (function () {
// <context-preservation> // <context-preservation>
// Extract context from Target URL first // Extract context from Target URL first
let tag = null, user = null; let tag = null, user = null, isFavs = false;
const tagMatch = url.match(/\/tag\/([^/]+)/); const tagMatch = url.match(/\/tag\/([^/]+)/);
if (tagMatch) tag = decodeURIComponent(tagMatch[1]); if (tagMatch) tag = decodeURIComponent(tagMatch[1]);
const userMatch = url.match(/\/user\/([^/]+)/); 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 (userMatch) {
user = decodeURIComponent(userMatch[1]);
if (url.includes(`/user/${userMatch[1]}/favs`)) isFavs = true;
}
// If missing and inheritContext is true, check Window Location // If missing and inheritContext is true, check Window Location
if (inheritContext) { if (inheritContext) {
@@ -102,7 +223,10 @@ window.requestAnimFrame = (function () {
} }
if (!user) { if (!user) {
const wUserMatch = window.location.href.match(/\/user\/([^/]+)/); const wUserMatch = window.location.href.match(/\/user\/([^/]+)/);
if (wUserMatch) user = decodeURIComponent(wUserMatch[1]); if (wUserMatch) {
user = decodeURIComponent(wUserMatch[1]);
if (window.location.href.includes(`/user/${wUserMatch[1]}/favs`)) isFavs = true;
}
} }
} }
// </context-preservation> // </context-preservation>
@@ -114,13 +238,14 @@ window.requestAnimFrame = (function () {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (tag) params.append('tag', tag); if (tag) params.append('tag', tag);
if (user) params.append('user', user); if (user) params.append('user', user);
if (isFavs) params.append('fav', 'true');
if ([...params].length > 0) { if ([...params].length > 0) {
ajaxUrl += '?' + params.toString(); ajaxUrl += '?' + params.toString();
} }
console.log("Fetching:", ajaxUrl); console.log("Fetching:", ajaxUrl);
const response = await fetch(ajaxUrl); const response = await fetch(ajaxUrl, { credentials: 'include' });
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`); if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
const rawText = await response.text(); const rawText = await response.text();
@@ -140,7 +265,7 @@ window.requestAnimFrame = (function () {
html = rawText; html = rawText;
} }
let container = document.querySelector('.container'); let container = document.querySelector('#main .container');
if (!container && document.querySelector('.index-container')) { if (!container && document.querySelector('.index-container')) {
// Transition from Index to Item View // Transition from Index to Item View
@@ -148,13 +273,19 @@ window.requestAnimFrame = (function () {
main.innerHTML = '<div class="container"></div>'; main.innerHTML = '<div class="container"></div>';
container = main.querySelector('.container'); container = main.querySelector('.container');
} else if (container) { } else if (container) {
// Already in Item View, clear usage // Check if we are on Tags Overview logic (which reuses .container)
const oldContent = container.querySelector('.content'); const tagsOverview = container.querySelector('.tags');
const oldMetadata = container.querySelector('.metadata'); if (tagsOverview) {
const oldHeader = container.querySelector('._204863'); container.innerHTML = '';
if (oldHeader) oldHeader.remove(); } else {
if (oldContent) oldContent.remove(); // Already in Item View, clear usage
if (oldMetadata) oldMetadata.remove(); 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); container.insertAdjacentHTML('beforeend', html);
@@ -169,13 +300,17 @@ window.requestAnimFrame = (function () {
// If we inherited context, we should reflect it in the URL // If we inherited context, we should reflect it in the URL
let pushUrl = `/${itemid}`; let pushUrl = `/${itemid}`;
// Logic from ajax.mjs context reconstruction: // Logic from ajax.mjs context reconstruction:
if (user) pushUrl = `/user/${user}/${itemid}`; // User takes precedence usually? Or strictly mutually exclusive in UI if (user) {
pushUrl = `/user/${user}/${itemid}`;
if (isFavs) pushUrl = `/user/${user}/favs/${itemid}`;
}
else if (tag) pushUrl = `/tag/${tag}/${itemid}`; else if (tag) pushUrl = `/tag/${tag}/${itemid}`;
// We overwrite proper URL even if the link clicked was "naked" // We overwrite proper URL even if the link clicked was "naked"
history.pushState({}, '', pushUrl); history.pushState({}, '', pushUrl);
setupMedia(); setupMedia();
if (window.initBackground) window.initBackground();
// Try to extract ID from response if possible or just use itemid // Try to extract ID from response if possible or just use itemid
document.title = `f0bm - ${itemid}`; document.title = `f0bm - ${itemid}`;
if (navbar) navbar.classList.remove("pbwork"); if (navbar) navbar.classList.remove("pbwork");
@@ -187,13 +322,12 @@ window.requestAnimFrame = (function () {
}; };
const changePage = (e, pbwork = true) => { const changePage = (e, pbwork = true) => {
if (e.tagName === 'A') { if (pbwork) {
e.preventDefault(); const nav = document.querySelector("nav.navbar");
loadItemAjax(e.href); if (nav) nav.classList.add("pbwork");
} else {
pbwork && document.querySelector("nav.navbar").classList.add("pbwork");
!tt && (tt = setTimeout(() => e.click(), stimeout));
} }
// Trigger native click for navigation
e.click();
}; };
// Intercept clicks // Intercept clicks
@@ -208,10 +342,10 @@ window.requestAnimFrame = (function () {
return; return;
} }
const link = e.target.closest('#next, #prev, #random, .id-link, .nav-next, .nav-prev'); 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) { if (link && link.href && link.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
// Special check for random // Special check for random
if (link.id === 'random') { if (link.id === 'random' || link.id === 'nav-random') {
e.preventDefault(); e.preventDefault();
const nav = document.querySelector("nav.navbar"); const nav = document.querySelector("nav.navbar");
if (nav) nav.classList.add("pbwork"); if (nav) nav.classList.add("pbwork");
@@ -240,18 +374,59 @@ window.requestAnimFrame = (function () {
window.location.href = link.href; window.location.href = link.href;
} }
}) })
.catch(() => window.location.href = link.href); .catch((err) => {
console.error("Random fetch failed:", err);
window.location.href = link.href;
});
return; return;
} }
// Standard item links // Standard item links
e.preventDefault(); e.preventDefault();
loadItemAjax(link.href, true); 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) => { window.addEventListener('popstate', (e) => {
loadItemAjax(window.location.href, true); 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> // <keybindings>
@@ -261,7 +436,7 @@ window.requestAnimFrame = (function () {
"a": clickOnElementBinding("#next"), "a": clickOnElementBinding("#next"),
"ArrowRight": clickOnElementBinding("#prev"), "ArrowRight": clickOnElementBinding("#prev"),
"d": clickOnElementBinding("#prev"), "d": clickOnElementBinding("#prev"),
"r": clickOnElementBinding("#random"), "r": clickOnElementBinding("#random, #nav-random"),
" ": clickOnElementBinding("#f0ck-image") " ": clickOnElementBinding("#f0ck-image")
}; };
document.addEventListener("keydown", e => { document.addEventListener("keydown", e => {
@@ -327,59 +502,116 @@ window.requestAnimFrame = (function () {
} }
// </image-responsive> // </image-responsive>
// <scroller> // <infinite-scroll>
let tts = 0; const postsContainer = document.querySelector("div.posts");
const scroll_treshold = 1; if (postsContainer) {
if ([...document.querySelectorAll("div.posts")].length === 1) { // Infinite scroll state
document.addEventListener("wheel", e => { 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)';
}
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 ? '&#9660;' : '&#8212;';
foot.style.color = 'transparent';
}
}
};
// Scroll detection - preload before reaching bottom
const PRELOAD_OFFSET = 500; // pixels before bottom to trigger load
window.addEventListener("scroll", () => {
if (!document.querySelector('#main')) return; if (!document.querySelector('#main')) return;
if (Math.ceil(window.innerHeight + window.scrollY) >= document.querySelector('#main').offsetHeight && e.deltaY > 0) { // down const scrollPosition = window.innerHeight + window.scrollY;
if (elem = document.querySelector(".pagination > .next:not(.disabled)")) { const pageHeight = document.querySelector('#main').offsetHeight;
if (tts < scroll_treshold) { const distanceFromBottom = pageHeight - scrollPosition;
const foot = document.querySelector("div#footbar");
if (foot) { // Load more when within PRELOAD_OFFSET pixels of bottom
foot.style.boxShadow = "inset 0px 4px 0px var(--footbar-color)"; if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) {
foot.style.color = "var(--footbar-color)"; loadMoreItems();
}
tts++;
}
else
changePage(elem);
}
}
else if (window.scrollY <= 0 && e.deltaY < 0) { // up
if (elem = document.querySelector(".pagination > .prev:not(.disabled)")) {
if (tts < scroll_treshold) {
const nav = document.querySelector("nav.navbar");
if (nav) {
nav.style.boxShadow = "0px 2px 0px var(--loading-indicator-color)";
nav.style.transition = ".2s ease-in-out";
}
tts++;
}
else
changePage(elem);
}
}
else {
tts = 0;
const foot = document.querySelector("div#footbar");
if (foot) {
foot.style.boxShadow = "unset";
foot.style.color = "transparent";
}
const nav = document.querySelector("nav.navbar");
if (nav) nav.style.boxShadow = "unset";
} }
}); });
} }
// </infinite-scroll>
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>
// <swipe> // <swipe>
const swipeRT = { const swipeRT = {
@@ -516,6 +748,7 @@ window.requestAnimFrame = (function () {
// </scroller> // </scroller>
})(); })();
// disable default scroll event when mouse is on content div // disable default scroll event when mouse is on content div
// this is useful for items that have a lot of tags for example: 12536 // this is useful for items that have a lot of tags for example: 12536
const targetSelector = '.content'; const targetSelector = '.content';
@@ -541,10 +774,13 @@ function init() {
window.addEventListener('load', init); window.addEventListener('load', init);
document.getElementById('sbtForm').addEventListener('submit', (e) => { const sbtForm = document.getElementById('sbtForm');
e.preventDefault(); if (sbtForm) {
const input = document.getElementById('sbtInput').value.trim(); sbtForm.addEventListener('submit', (e) => {
if (input) { e.preventDefault();
window.location.href = `/tag/${encodeURIComponent(input)}`; const input = document.getElementById('sbtInput').value.trim();
} if (input) {
}); window.location.href = `/tag/${encodeURIComponent(input)}`;
}
});
}

View File

@@ -1,10 +1,10 @@
const Cookie = { const Cookie = {
get: name => { get: name => {
const c = document.cookie.match(`(?:(?:^|.*; *)${name} *= *([^;]*).*$)|^.*$`)[1]; const c = document.cookie.match(`(?:(?:^|.*; *)${name} *= *([^;]*).*$)|^.*$`)[1];
if(c) return decodeURIComponent(c); if (c) return decodeURIComponent(c);
}, },
set: (name, value, opts = {}) => { set: (name, value, opts = {}) => {
if(opts.days) { if (opts.days) {
opts['max-age'] = opts.days * 60 * 60 * 24; opts['max-age'] = opts.days * 60 * 60 * 24;
delete opts.days; delete opts.days;
} }
@@ -17,8 +17,11 @@ const Cookie = {
(() => { (() => {
const acttheme = Cookie.get('theme') ?? "w0bm"; const acttheme = Cookie.get('theme') ?? "w0bm";
const themecontainer = document.querySelector("li#themes > ul.dropdown-menu"); 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()); const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase());
if(acttheme !== document.documentElement.getAttribute("theme") && themes.includes(acttheme)) if (acttheme !== document.documentElement.getAttribute("theme") && themes.includes(acttheme))
document.documentElement.setAttribute("theme", acttheme); document.documentElement.setAttribute("theme", acttheme);
[...themecontainer.querySelectorAll("li > a")].forEach(t => t.addEventListener("click", e => { [...themecontainer.querySelectorAll("li > a")].forEach(t => t.addEventListener("click", e => {
e.preventDefault(); e.preventDefault();
@@ -30,15 +33,15 @@ const Cookie = {
})); }));
document.addEventListener("keydown", e => { document.addEventListener("keydown", e => {
if(e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA")
return; return;
const acttheme = Cookie.get('theme') ?? "w0bm"; const acttheme = Cookie.get('theme') ?? "w0bm";
const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase()); const themes = [...themecontainer.querySelectorAll("li > a")].map(t => t.innerText.toLowerCase());
const k = e.key; const k = e.key;
if(k === "t") { if (k === "t") {
e.preventDefault(); e.preventDefault();
let i = themes.indexOf(acttheme); let i = themes.indexOf(acttheme);
if(++i >= themes.length) if (++i >= themes.length)
i = 0; i = 0;
document.documentElement.setAttribute("theme", themes[i]); document.documentElement.setAttribute("theme", themes[i]);
document.querySelector("#themes > a").setAttribute("content", themes[i]); document.querySelector("#themes > a").setAttribute("content", themes[i]);
@@ -46,10 +49,10 @@ const Cookie = {
} }
}); });
if(tbuttonfull = document.querySelector('svg#a_tfull')) { if (tbuttonfull = document.querySelector('svg#a_tfull')) {
tbuttonfull.addEventListener('click', e => { tbuttonfull.addEventListener('click', e => {
let f = Cookie.get('fullscreen'); let f = Cookie.get('fullscreen');
if(f == 1) { if (f == 1) {
Cookie.set('fullscreen', 0); Cookie.set('fullscreen', 0);
document.querySelector('html').setAttribute('res', ''); document.querySelector('html').setAttribute('res', '');
tbuttonfull.innerHTML = `<use href="/s/img/iconset.svg#window-maximize"></use>`; tbuttonfull.innerHTML = `<use href="/s/img/iconset.svg#window-maximize"></use>`;

View File

@@ -23,7 +23,8 @@ export default (router, tpl) => {
url: contextUrl, url: contextUrl,
user: query.user, user: query.user,
tag: query.tag, tag: query.tag,
mime: query.mime mime: query.mime,
fav: query.fav === 'true'
}); });
if (!data.success) { if (!data.success) {
@@ -38,10 +39,8 @@ export default (router, tpl) => {
if (req.session) { if (req.session) {
data.session = { ...req.session }; data.session = { ...req.session };
// data.user comes from f0cklib (uploader). req.session.user is logged-in user string. // 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. // Templates use session.user for matching favorites. We must preserve it.
// item-partial doesn't use session.user. // if (data.session.user) delete data.session.user; // REMOVED THIS
// Note: If anything fails, it prints literal code, so we ensure no collision.
if (data.session.user) delete data.session.user;
} else { } else {
data.session = false; data.session = false;
} }
@@ -64,5 +63,65 @@ export default (router, tpl) => {
}); });
}); });
// 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; return router;
}; };

View File

@@ -3,7 +3,7 @@ import db from '../../sql.mjs';
import lib from '../../lib.mjs'; import lib from '../../lib.mjs';
import search from '../../routeinc/search.mjs'; import search from '../../routeinc/search.mjs';
const allowedMimes = [ "audio", "image", "video", "%" ]; const allowedMimes = ["audio", "image", "video", "%"];
export default router => { export default router => {
router.group(/^\/api\/v2/, group => { router.group(/^\/api\/v2/, group => {
group.get(/$/, (req, res) => { group.get(/$/, (req, res) => {
@@ -13,24 +13,28 @@ export default router => {
group.get(/\/random(\/user\/.+|\/image|\/video|\/audio)?$/, async (req, res) => { group.get(/\/random(\/user\/.+|\/image|\/video|\/audio)?$/, async (req, res) => {
const user = req.url.split[3] === "user" ? req.url.split[4] : "%"; 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 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` const rows = await db`
select * select "items".*
from "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 where
mime ilike ${mime} and mime ilike ${mime} and
username ilike ${user} and username ilike ${user} and
active = 'true' active = 'true'
${tag ? db`and tags.normalized ilike ${'%' + tag + '%'}` : db``}
order by random() order by random()
limit 1 limit 1
`; `;
return res.json({ return res.json({
success: rows.length > 0, success: rows.length > 0,
items: rows.length > 0 ? rows[0] : [] items: rows.length > 0 ? rows[0] : []
}); });
}); });
group.get(/\/items\/get/, async (req, res) => { group.get(/\/items\/get/, async (req, res) => {
let eps = 150; let eps = 150;
@@ -51,17 +55,15 @@ export default router => {
where where
${db.unsafe(modequery)} and ${db.unsafe(modequery)} and
active = 'true' active = 'true'
${ ${opt.older
opt.older ? db`and id <= ${opt.older}`
? db`and id <= ${opt.older}` : opt.newer
: opt.newer ? db`and id >= ${opt.newer}`
? db`and id >= ${opt.newer}` : db``
: db`` }
} order by id ${opt.newer
order by id ${ ? db`asc`
opt.newer : db`desc`
? db`asc`
: db`desc`
} }
limit ${eps} limit ${eps}
`).sort((a, b) => b.id - a.id); `).sort((a, b) => b.id - a.id);
@@ -73,10 +75,10 @@ export default router => {
items: rows items: rows
}, 200); }, 200);
}); });
group.get(/\/item\/[0-9]+$/, async (req, res) => { group.get(/\/item\/[0-9]+$/, async (req, res) => {
const id = +req.url.split[3]; const id = +req.url.split[3];
const item = await db` const item = await db`
select * select *
from "items" from "items"
@@ -97,14 +99,14 @@ export default router => {
order by id desc order by id desc
limit 1 limit 1
`; `;
if(item.length === 0) { if (item.length === 0) {
return res.json({ return res.json({
success: false, success: false,
msg: 'no items found' msg: 'no items found'
}); });
} }
const rows = { const rows = {
...item[0], ...item[0],
...{ ...{
@@ -118,11 +120,11 @@ export default router => {
rows rows
}); });
}); });
group.get(/\/user\/.*(\/\d+)?$/, async (req, res) => { group.get(/\/user\/.*(\/\d+)?$/, async (req, res) => {
const user = req.url.split[3]; const user = req.url.split[3];
const eps = +req.url.split[4] || 50; const eps = +req.url.split[4] || 50;
const rows = db` const rows = db`
select id, mime, size, src, stamp, userchannel, username, usernetwork select id, mime, size, src, stamp, userchannel, username, usernetwork
from "items" from "items"
@@ -130,7 +132,7 @@ export default router => {
order by stamp desc order by stamp desc
limit ${+eps} limit ${+eps}
`; `;
return res.json({ return res.json({
success: rows.length > 0, success: rows.length > 0,
items: rows.length > 0 ? rows : [] items: rows.length > 0 ? rows : []
@@ -140,7 +142,7 @@ export default router => {
// tags lol // tags lol
group.put(/\/admin\/tags\/(?<tagname>.*)/, lib.loggedin, async (req, res) => { group.put(/\/admin\/tags\/(?<tagname>.*)/, lib.loggedin, async (req, res) => {
if(!req.params.tagname || !req.post.newtag) { if (!req.params.tagname || !req.post.newtag) {
return res.json({ return res.json({
success: false, success: false,
msg: 'missing tagname or newtag', msg: 'missing tagname or newtag',
@@ -154,7 +156,7 @@ export default router => {
const tagname = decodeURIComponent(req.params.tagname); const tagname = decodeURIComponent(req.params.tagname);
const newtag = req.post.newtag; const newtag = req.post.newtag;
if(['sfw', 'nsfw'].includes(tagname) || ['sfw', 'nsfw'].includes(newtag)) { if (['sfw', 'nsfw'].includes(tagname) || ['sfw', 'nsfw'].includes(newtag)) {
return res.json({ return res.json({
msg: 'f0ck you' msg: 'f0ck you'
}, 405); // method not allowed }, 405); // method not allowed
@@ -166,8 +168,8 @@ export default router => {
where tag = ${tagname} where tag = ${tagname}
limit 1 limit 1
`)[0]; `)[0];
if(!tmptag) { if (!tmptag) {
return res.json({ return res.json({
success: false, success: false,
msg: 'no tag found' msg: 'no tag found'
@@ -175,10 +177,9 @@ export default router => {
} }
const q = (await db` const q = (await db`
update "tags" set ${ update "tags" set ${db({
db({ tag: newtag
tag: newtag }, 'tag')
}, 'tag')
} }
where tag = ${tagname} where tag = ${tagname}
returning * returning *
@@ -195,7 +196,7 @@ export default router => {
const searchString = req.url.qs.q; const searchString = req.url.qs.q;
if(searchString?.length <= 1) { if (searchString?.length <= 1) {
reply.error = 'too short lol'; reply.error = 'too short lol';
return res.json(reply); return res.json(reply);
} }
@@ -212,7 +213,7 @@ export default router => {
`; `;
reply.success = true; reply.success = true;
reply.suggestions = search(q, searchString); reply.suggestions = search(q, searchString);
} catch(err) { } catch (err) {
reply.error = err.msg; reply.error = err.msg;
} }
@@ -220,7 +221,7 @@ export default router => {
}); });
group.post(/\/admin\/deletepost$/, lib.auth, async (req, res) => { group.post(/\/admin\/deletepost$/, lib.auth, async (req, res) => {
if(!req.post.postid) { if (!req.post.postid) {
return res.json({ return res.json({
success: false, success: false,
msg: 'no postid' msg: 'no postid'
@@ -228,7 +229,7 @@ export default router => {
} }
const id = +req.post.postid; const id = +req.post.postid;
if(id <= 1) { if (id <= 1) {
return res.json({ return res.json({
success: false success: false
}); });
@@ -243,7 +244,7 @@ export default router => {
limit 1 limit 1
`; `;
if(f0ck.length === 0) { if (f0ck.length === 0) {
return res.json({ return res.json({
success: false, success: false,
msg: `f0ck ${id}: f0ck not found` msg: `f0ck ${id}: f0ck not found`
@@ -251,15 +252,15 @@ export default router => {
} }
await db`update "items" set active = 'false' where id = ${id}`; await db`update "items" set active = 'false' where id = ${id}`;
await fs.copyFile(`./public/b/${f0ck[0].dest}`, `./deleted/b/${f0ck[0].dest}`).catch(_=>{});
await fs.copyFile(`./public/t/${id}.webp`, `./deleted/t/${id}.webp`).catch(_=>{});
await fs.unlink(`./public/b/${f0ck[0].dest}`).catch(_=>{});
await fs.unlink(`./public/t/${id}.webp`).catch(_=>{});
if(f0ck[0].mime.startsWith('audio')) { await fs.copyFile(`./public/b/${f0ck[0].dest}`, `./deleted/b/${f0ck[0].dest}`).catch(_ => { });
await fs.copyFile(`./public/ca/${id}.webp`, `./deleted/ca/${id}.webp`).catch(_=>{}); await fs.copyFile(`./public/t/${id}.webp`, `./deleted/t/${id}.webp`).catch(_ => { });
await fs.unlink(`./public/ca/${id}.webp`).catch(_=>{}); await fs.unlink(`./public/b/${f0ck[0].dest}`).catch(_ => { });
await fs.unlink(`./public/t/${id}.webp`).catch(_ => { });
if (f0ck[0].mime.startsWith('audio')) {
await fs.copyFile(`./public/ca/${id}.webp`, `./deleted/ca/${id}.webp`).catch(_ => { });
await fs.unlink(`./public/ca/${id}.webp`).catch(_ => { });
} }
res.json({ res.json({
@@ -269,14 +270,14 @@ export default router => {
group.post(/\/admin\/togglefav$/, lib.loggedin, async (req, res) => { group.post(/\/admin\/togglefav$/, lib.loggedin, async (req, res) => {
const postid = +req.post.postid; const postid = +req.post.postid;
let favs = await db` let favs = await db`
select user_id select user_id
from "favorites" from "favorites"
where item_id = ${+postid} where item_id = ${+postid}
`; `;
if(Object.values(favs).filter(u => u.user_id === req.session.id)[0]) { if (Object.values(favs).filter(u => u.user_id === req.session.id)[0]) {
// del fav // del fav
await db` await db`
delete from "favorites" delete from "favorites"
@@ -287,11 +288,10 @@ export default router => {
else { else {
// add fav // add fav
await db` await db`
insert into "favorites" ${ insert into "favorites" ${db({
db({ item_id: +postid,
item_id: +postid, user_id: +req.session.id
user_id: +req.session.id }, 'item_id', 'user_id')
}, 'item_id', 'user_id')
} }
`; `;
} }
@@ -310,7 +310,7 @@ export default router => {
favs favs
}); });
}); });
}); });
return router; return router;

View 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;
};

View File

@@ -1,5 +1,5 @@
@include(snippets/header) @include(snippets/header)
<canvas class="hidden-xs" id="bg"></canvas>
<div class="wrapper"> <div class="wrapper">
<div id="main"> <div id="main">

View File

@@ -1,14 +1,19 @@
<!doctype html> <!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> <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="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/f0ck.css?v=@mtime(/public/s/css/f0ck.css)">
<link rel="stylesheet" href="/s/css/w0bm.css?v=@mtime(/public/s/css/w0bm.css)"> <link rel="stylesheet" href="/s/css/w0bm.css?v=@mtime(/public/s/css/w0bm.css)">
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <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> </head>
<body> <body>
<!-- hier splitting betreiben --> <!-- hier splitting betreiben -->
@include(snippets/navbar) <canvas class="hidden-xs" id="bg"></canvas>
@include(snippets/navbar)

View 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

View File

@@ -2,18 +2,17 @@
<!-- 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" id="nav-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 -->
@if(!/^\/\d$/.test(url.pathname)) @if(!/^\/\d+$/.test(url.pathname))
<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">
@@ -27,18 +26,17 @@
<!-- not logged in --> <!-- not 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" id="nav-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 -->
@if(!/^\/\d$/.test(url.pathname)) @if(!/^\/\d+$/.test(url.pathname))
<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">

View File

@@ -2,27 +2,33 @@
<div id="main"> <div id="main">
<div class="container"> <div class="container">
<h3 style="text-align: center;"></h3> <h3 style="text-align: center;"></h3>
<div class="tags"> <div class="tags-grid">
@if(session) @if(session)
@each(toptags_regged as toptag) @each(toptags_regged as toptag)
<div class="tag badge badge-light mr-2"> <a href="/tag/{!! toptag.tag !!}" class="tag-card">
<div class="tagbox-body"> <div class="tag-card-image">
<span class="toptag_id">{!! toptag.tag !!}</span> <img src="/tag_image/{!! toptag.tag !!}" loading="lazy" alt="{!! toptag.tag !!}">
<span class="toptag_tag"><a href="/tag/{!! toptag.tag !!}">{{ toptag.total_items }}</a></span>
</div> </div>
</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 @endeach
@else @else
@each(toptags as toptag) @each(toptags as toptag)
<div class="tag badge badge-light mr-2"> <a href="/tag/{!! toptag.tag !!}" class="tag-card">
<div class="tagbox-body"> <div class="tag-card-image">
<span class="toptag_id">{!! toptag.tag !!}</span> <img src="/tag_image/{!! toptag.tag !!}" loading="lazy" alt="{!! toptag.tag !!}">
<span class="toptag_tag"><a href="/tag/{!! toptag.tag !!}">{{ toptag.total_items }}</a></span>
</div> </div>
</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 @endeach
@endif @endif
</div> </div>
</div> </div>
</div> </div>
@include(snippets/footer) @include(snippets/footer)