diff --git a/public/s/css/f0ck.css b/public/s/css/f0ck.css index 2673d42..49b4abb 100644 --- a/public/s/css/f0ck.css +++ b/public/s/css/f0ck.css @@ -1585,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 { @@ -2947,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; } @@ -3000,11 +2996,11 @@ button#togglebg { } .fader-in { - animation: fadeIn .8s steps(100) forwards; + opacity: 0.4 !important; } .fader-out { - animation: fadeOut .8s steps(100) forwards + opacity: 0 !important; } .settings { @@ -3052,75 +3048,76 @@ input#s_avatar { 0%, 100% { - opacity: 0.4; + 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; -} + /* Modern Tags Layout */ + .tags-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 20px; + padding: 20px 0; + } -.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 { + 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-image { - width: 100%; - height: 100px; - overflow: hidden; - position: relative; - background: #000; -} + .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 img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.5s; - opacity: 0.8; -} + .tag-card-image { + width: 100%; + height: 100px; + overflow: hidden; + position: relative; + background: #000; + } -.tag-card:hover .tag-card-image img { - transform: scale(1.1); - opacity: 1; -} + .tag-card-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.5s; + opacity: 0.8; + } -.tag-card-content { - padding: 15px; - display: flex; - flex-direction: column; - gap: 5px; -} + .tag-card:hover .tag-card-image img { + transform: scale(1.1); + opacity: 1; + } -.tag-name { - color: var(--white, #fff); - font-weight: bold; - font-size: 1.1em; - font-family: var(--font, monospace); -} + .tag-card-content { + padding: 15px; + display: flex; + flex-direction: column; + gap: 5px; + } -.tag-count { - color: #888; - font-size: 0.9em; -} \ No newline at end of file + .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; + } \ No newline at end of file diff --git a/public/s/js/f0ck.js b/public/s/js/f0ck.js index e015481..3e91a29 100644 --- a/public/s/js/f0ck.js +++ b/public/s/js/f0ck.js @@ -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,13 +36,27 @@ window.requestAnimFrame = (function () { } }); - const toggleBg = document.getElementById('togglebg'); - if (toggleBg) { - toggleBg.addEventListener('click', function (e) { - e.preventDefault(); - background = !background; - localStorage.setItem('background', background.toString()); - var canvas = document.getElementById('bg'); + + + if (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'); @@ -31,33 +64,102 @@ 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'); - 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) + 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; @@ -89,6 +191,10 @@ window.requestAnimFrame = (function () { // 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 @@ -99,12 +205,15 @@ window.requestAnimFrame = (function () { // // Extract context from Target URL first - let tag = null, user = null; + 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]); // 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 (inheritContext) { @@ -114,7 +223,10 @@ window.requestAnimFrame = (function () { } if (!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; + } } } // @@ -126,13 +238,14 @@ window.requestAnimFrame = (function () { 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); + 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(); @@ -187,13 +300,17 @@ window.requestAnimFrame = (function () { // If we inherited context, we should reflect it in the URL let pushUrl = `/${itemid}`; // Logic from ajax.mjs context reconstruction: - if (user) pushUrl = `/user/${user}/${itemid}`; // User takes precedence usually? Or strictly mutually exclusive in UI + 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"); @@ -266,12 +383,50 @@ window.requestAnimFrame = (function () { // Standard item links 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) => { - 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); + } }); // @@ -593,6 +748,7 @@ window.requestAnimFrame = (function () { // })(); + // 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'; diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs index cddbdbe..8a0c551 100644 --- a/src/inc/routes/ajax.mjs +++ b/src/inc/routes/ajax.mjs @@ -23,7 +23,8 @@ export default (router, tpl) => { url: contextUrl, user: query.user, tag: query.tag, - mime: query.mime + mime: query.mime, + fav: query.fav === 'true' }); if (!data.success) { @@ -38,10 +39,8 @@ export default (router, tpl) => { if (req.session) { data.session = { ...req.session }; // data.user comes from f0cklib (uploader). req.session.user is logged-in user string. - // If template engine confuses them, removing session.user from this context might help. - // item-partial doesn't use session.user. - // Note: If anything fails, it prints literal code, so we ensure no collision. - if (data.session.user) delete data.session.user; + // 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; } @@ -103,6 +102,12 @@ export default (router, tpl) => { 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({ @@ -110,6 +115,7 @@ export default (router, tpl) => { body: JSON.stringify({ success: true, html: itemsHtml, + pagination: paginationHtml, hasMore: hasMore, nextPage: data.pagination.next, currentPage: data.pagination.page diff --git a/views/item.html b/views/item.html index 7c182b3..4cd640e 100644 --- a/views/item.html +++ b/views/item.html @@ -1,5 +1,5 @@ @include(snippets/header) - +
diff --git a/views/snippets/header.html b/views/snippets/header.html index 0a9653b..aee4c89 100644 --- a/views/snippets/header.html +++ b/views/snippets/header.html @@ -1,14 +1,19 @@ - + + - @if(typeof item !== "undefined")f0bm - {{ item.id }}@elsef0bm@endif + @if(typeof item !== 'undefined')f0bm - {{ item.id }}@elsef0bm@endif - + - @if(typeof item !== "undefined")@endif + @if(typeof item !== 'undefined') + @endif + - @include(snippets/navbar) + + @include(snippets/navbar) \ No newline at end of file