From 2a6e0f44f3e90141177f020f6c21a81ffbbcc9cb Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Sat, 22 Aug 2026 20:02:28 +0200 Subject: [PATCH] fdsa --- public/s/css/f0ckm.css | 414 +++++++++++++++++++++++- public/s/js/f0ckm.js | 260 +++++++++++++-- src/inc/routeinc/f0cklib.mjs | 57 +++- src/inc/routes/ajax.mjs | 39 ++- src/inc/routes/index.mjs | 40 ++- views/index-partial.html | 41 ++- views/snippets/excluded-tags-modal.html | 195 ++++++++--- views/snippets/filter-bar.html | 1 + views/snippets/items-grid.html | 40 ++- 9 files changed, 989 insertions(+), 98 deletions(-) create mode 100644 views/snippets/filter-bar.html diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index e49e905..7932cd1 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -8203,15 +8203,16 @@ input#s_avatar { #search-close, #excluded-tags-close { - position: absolute; - top: 20px; - right: 30px; - color: var(--white); - font-size: 2rem; + position: relative; + top: auto; + right: auto; + color: var(--white, #fff); + font-size: 1.5rem; cursor: pointer; font-family: sans-serif; opacity: 0.7; transition: opacity 0.2s; + line-height: 1; } #search-close:hover, @@ -8219,6 +8220,106 @@ input#s_avatar { opacity: 1; } +/* Filter Modal Card & Body Structure */ +#excluded-tags-overlay .filter-modal-container { + background: var(--nav-bg, rgba(18, 18, 20, 0.96)); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 14px; + width: 100%; + max-width: 600px; + max-height: 85vh; + display: flex; + flex-direction: column; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.7); + padding: 0; + overflow: hidden; + color: var(--white, #fff); + position: relative; +} + +.filter-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.02); +} + +.filter-modal-title { + margin: 0; + font-size: 1.15em; + font-weight: 600; + color: var(--white, #fff); + display: flex; + align-items: center; + gap: 10px; +} + +.filter-modal-body { + padding: 20px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 18px; +} + +.modal-filter-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.modal-filter-label { + font-size: 0.82em; + text-transform: uppercase; + letter-spacing: 0.5px; + color: rgba(255, 255, 255, 0.6); + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + margin: 0; +} + +.filter-timeframe-subgroup { + display: none; +} + +.filter-timeframe-subgroup.visible { + display: block; +} + +.filter-modal-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(0, 0, 0, 0.25); +} + +.filter-apply-btn { + background: var(--accent, #1fb2b0); + color: var(--bg, #000); + border: none; + border-radius: 6px; + padding: 8px 18px; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; +} + +.filter-apply-btn:hover { + filter: brightness(1.15); + transform: translateY(-1px); +} + /* Delete Tag Modal */ .modal-overlay { position: fixed; @@ -17549,4 +17650,305 @@ a.lazy-thumb.filtered-upload-ghost:hover::after { .item-main-content { padding-left: 15px; padding-right: 15px; -} \ No newline at end of file +} + +/* ========================================================================== + MAINPAGE OVERHAUL: FILTER BAR & MULTI-LAYOUT PRESENTATIONS + ========================================================================== */ + +.mainpage-filter-bar { + position: sticky; + top: calc(var(--navbar-h, 50px) + 8px); + z-index: 100; + display: flex; + flex-direction: column; + gap: 10px; + background: var(--nav-bg, rgba(18, 18, 20, 0.92)); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + padding: 12px 16px; + margin-bottom: 18px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.mainpage-filter-bar:hover { + border-color: rgba(255, 255, 255, 0.15); +} + +.filter-bar-row { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px 18px; + width: 100%; +} + +.filter-bar-row.row-secondary { + padding-top: 10px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.filter-group-left { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px 18px; +} + +.filter-group-right { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} + +.filter-section { + display: flex; + align-items: center; + gap: 8px; +} + +.filter-label { + font-size: 0.78rem; + font-weight: 600; + color: var(--text-muted, #888); + text-transform: uppercase; + letter-spacing: 0.5px; + display: flex; + align-items: center; + gap: 4px; +} + +.filter-btn-group { + display: inline-flex; + background: rgba(0, 0, 0, 0.3); + border-radius: 6px; + padding: 2px; + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.filter-btn { + background: transparent; + border: none; + color: var(--text-muted, #aaa); + padding: 4px 10px; + font-size: 0.82rem; + font-weight: 500; + border-radius: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 5px; + transition: all 0.15s ease; +} + +.filter-btn:hover { + color: #fff; + background: rgba(255, 255, 255, 0.08); +} + +.filter-btn.active { + background: var(--accent, #1fb2b0); + color: #000; + font-weight: 700; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); +} + +.filter-toggle-pill { + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--text-muted, #aaa); + padding: 4px 10px; + font-size: 0.8rem; + border-radius: 20px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 5px; + transition: all 0.15s ease; +} + +.filter-toggle-pill:hover { + border-color: var(--accent, #1fb2b0); + color: #fff; +} + +.filter-toggle-pill.active { + background: rgba(31, 178, 176, 0.2); + border-color: var(--accent, #1fb2b0); + color: var(--accent, #1fb2b0); + font-weight: 600; +} + +.filter-toggle-pill.rating-sfw.active { + background: rgba(34, 197, 94, 0.2); + border-color: #22c55e; + color: #4ade80; +} + +.filter-toggle-pill.rating-nsfw.active { + background: rgba(249, 115, 22, 0.2); + border-color: #f97316; + color: #fb923c; +} + +.filter-toggle-pill.rating-nsfl.active { + background: rgba(239, 68, 68, 0.2); + border-color: #ef4444; + color: #f87171; +} + +.filter-toggle-pill.rating-untagged.active { + background: rgba(168, 85, 247, 0.2); + border-color: #a855f7; + color: #c084fc; +} + +.filter-timeframe { + display: none; +} + +.filter-timeframe.visible { + display: flex; +} + +.filter-reset-btn { + background: transparent; + border: none; + color: #888; + font-size: 0.78rem; + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + transition: color 0.2s ease; + + margin-left: auto; +} + +.filter-reset-btn:hover { + color: #ef4444; +} + +/* === LAYOUT MODES === */ + +/* Masonry Layout */ +.posts.layout-masonry { + column-count: 5; + column-gap: 10px; + display: block; +} + +@media (max-width: 1400px) { .posts.layout-masonry { column-count: 4; } } +@media (max-width: 1000px) { .posts.layout-masonry { column-count: 3; } } +@media (max-width: 650px) { .posts.layout-masonry { column-count: 2; } } + +.posts.layout-masonry .thumb { + break-inside: avoid; + margin-bottom: 10px; + display: block; + width: 100% !important; + height: auto !important; +} + +/* Stream / Vertical Feed Layout */ +.posts.layout-stream { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + width: 100%; + max-width: 680px; + margin: 0 auto; +} + +.stream-card { + width: 100%; + background: var(--nav-bg, #181818); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + transition: transform 0.2s ease, border-color 0.2s ease; +} + +.stream-card:hover { + border-color: rgba(255, 255, 255, 0.18); +} + +.stream-card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + background: rgba(0, 0, 0, 0.2); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.stream-author { + font-weight: 600; + font-size: 0.95rem; + color: #fff; + text-decoration: none; +} + +.stream-author:hover { + text-decoration: underline; +} + +.stream-meta { + display: flex; + align-items: center; + gap: 8px; +} + +.stream-media-wrapper { + position: relative; + display: block; + width: 100%; + background: #000; + text-align: center; +} + +.stream-media-img { + max-width: 100%; + max-height: 550px; + object-fit: contain; + display: block; + margin: 0 auto; +} + +.stream-media-badge { + position: absolute; + top: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.7); + color: #fff; + font-size: 0.7rem; + font-weight: 700; + padding: 3px 7px; + border-radius: 4px; + backdrop-filter: blur(4px); +} + +.stream-card-footer { + padding: 10px 14px; + background: rgba(0, 0, 0, 0.2); + border-top: 1px solid rgba(255, 255, 255, 0.05); + display: flex; + justify-content: flex-end; +} + +.stream-action-btn { + color: var(--accent, #1fb2b0); + text-decoration: none; + font-size: 0.85rem; + font-weight: 600; +} + +.stream-action-btn:hover { + text-decoration: underline; +} diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index caa87e6..7bfa40c 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -692,18 +692,18 @@ window.cancelAnimFrame = (function () { else if (window.activeMode === 4) activeRatings = ['nsfl']; else if (window.activeMode === 2) activeRatings = ['untagged']; } - const selector = document.getElementById('rating-selector'); - if (!selector) return; - selector.querySelectorAll('.rating-toggle-btn').forEach(btn => { - const r = btn.dataset.rating; - if (!r) return; // ALL button handled separately - btn.classList.toggle('active', activeRatings.includes(r)); + const containers = document.querySelectorAll('#rating-selector, .rating-group'); + containers.forEach(container => { + container.querySelectorAll('.rating-toggle-btn').forEach(btn => { + const r = btn.dataset.rating; + if (!r) return; // ALL button handled separately + btn.classList.toggle('active', activeRatings.includes(r)); + }); + container.querySelectorAll('.rating-toggle-all').forEach(allBtn => { + allBtn.classList.toggle('active', (getRatingsCookie().length === 0 && window.activeMode === 3) || activeRatings.length === 0); + }); }); - // ALL button: active when ratings cookie is empty/absent (server mode is the authority) - const allBtn = document.getElementById('rating-btn-all'); - if (allBtn) { - allBtn.classList.toggle('active', (getRatingsCookie().length === 0 && window.activeMode === 3) || activeRatings.length === 0); - } + if (window.updateFilterBadge) window.updateFilterBadge(); }; // Wire up rating toggle buttons @@ -2280,8 +2280,10 @@ window.cancelAnimFrame = (function () { } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initRuffle, { once: true }); + document.addEventListener('DOMContentLoaded', () => { if (window.initMainpageFilterBar) window.initMainpageFilterBar(); }); } else { initRuffle(); + if (window.initMainpageFilterBar) window.initMainpageFilterBar(); } document.addEventListener('f0ck:contentLoaded', initRuffle); @@ -2313,6 +2315,186 @@ window.cancelAnimFrame = (function () { new ResizeObserver(window.syncNavbarHeight).observe(_navEl); } + // ── Global Filter Bar Event Delegation ────────────────────────────────── + document.addEventListener('click', (e) => { + // 1. Layout button click (grid, masonry, stream) + const layoutBtn = e.target.closest('.layout-btn'); + if (layoutBtn) { + e.preventDefault(); + const layout = layoutBtn.dataset.layout; + const container = layoutBtn.closest('.filter-modal-container, #mainpage-filter-bar, .modal-filter-group') || document; + container.querySelectorAll('.layout-btn').forEach(b => b.classList.toggle('active', b === layoutBtn)); + + const postsContainer = document.getElementById('posts-container'); + if (postsContainer) { + postsContainer.className = `posts layout-${layout} grid-transition show`; + postsContainer.id = 'posts-container'; + postsContainer.dataset.layout = layout; + } + + document.cookie = `layout=${layout}; path=/; max-age=31536000`; + const u = new URL(window.location.href); + if (!layout || layout === 'grid') u.searchParams.delete('layout'); + else u.searchParams.set('layout', layout); + + const fromModal = !!layoutBtn.closest('#excluded-tags-overlay'); + if (fromModal) window._keepFilterModal = true; + + if (window.gridCacheMap) window.gridCacheMap.clear(); + if (typeof loadPageAjax === 'function') { + const p = loadPageAjax(u.pathname + u.search, true, { bypassCache: true }); + if (fromModal) Promise.resolve(p).finally(() => { window._keepFilterModal = false; }); + } else { + window.location.href = u.pathname + u.search; + } + return; + } + + // 2. Sort button click + const sortBtn = e.target.closest('.sort-btn'); + if (sortBtn) { + e.preventDefault(); + const sort = sortBtn.dataset.sort; + const container = sortBtn.closest('.filter-modal-container, #mainpage-filter-bar, .modal-filter-group') || document; + container.querySelectorAll('.sort-btn').forEach(b => b.classList.toggle('active', b === sortBtn)); + + const timeframeSec = document.getElementById('filter-timeframe-section'); + if (timeframeSec) { + timeframeSec.classList.toggle('visible', ['popular', 'top', 'xd', 'comments'].includes(sort)); + } + + document.cookie = `sort=${sort}; path=/; max-age=31536000`; + const u = new URL(window.location.href); + if (!sort || sort === 'newest') u.searchParams.delete('sort'); + else u.searchParams.set('sort', sort); + + const fromModal = !!sortBtn.closest('#excluded-tags-overlay'); + if (fromModal) window._keepFilterModal = true; + + if (window.gridCacheMap) window.gridCacheMap.clear(); + if (typeof loadPageAjax === 'function') { + const p = loadPageAjax(u.pathname + u.search, true, { bypassCache: true }); + if (fromModal) Promise.resolve(p).finally(() => { window._keepFilterModal = false; }); + } else { + window.location.href = u.pathname + u.search; + } + return; + } + + // 3. Timeframe button click + const tfBtn = e.target.closest('.timeframe-btn'); + if (tfBtn) { + e.preventDefault(); + const timeframe = tfBtn.dataset.timeframe; + const container = tfBtn.closest('.filter-modal-container, #mainpage-filter-bar, .modal-filter-group') || document; + container.querySelectorAll('.timeframe-btn').forEach(b => b.classList.toggle('active', b === tfBtn)); + + document.cookie = `timeframe=${timeframe}; path=/; max-age=31536000`; + const u = new URL(window.location.href); + if (!timeframe || timeframe === 'all') u.searchParams.delete('timeframe'); + else u.searchParams.set('timeframe', timeframe); + + const fromModal = !!tfBtn.closest('#excluded-tags-overlay'); + if (fromModal) window._keepFilterModal = true; + + if (window.gridCacheMap) window.gridCacheMap.clear(); + if (typeof loadPageAjax === 'function') { + const p = loadPageAjax(u.pathname + u.search, true, { bypassCache: true }); + if (fromModal) Promise.resolve(p).finally(() => { window._keepFilterModal = false; }); + } else { + window.location.href = u.pathname + u.search; + } + return; + } + + // 4. MIME / Media type pill click (image, video, audio, flash) + const mimePill = e.target.closest('.mime-pill'); + if (mimePill) { + e.preventDefault(); + mimePill.classList.toggle('active'); + + const container = mimePill.closest('.filter-modal-container, #mainpage-filter-bar, .mime-group') || document; + const activeMimes = []; + container.querySelectorAll('.mime-pill.active').forEach(p => { + if (p.dataset.mime) activeMimes.push(p.dataset.mime); + }); + + const mimeVal = activeMimes.join(','); + document.cookie = `mime=${encodeURIComponent(mimeVal)}; path=/; max-age=31536000`; + + const u = new URL(window.location.href); + if (activeMimes.length > 0) u.searchParams.set('mime', mimeVal); + else u.searchParams.delete('mime'); + + const fromModal = !!mimePill.closest('#excluded-tags-overlay'); + if (fromModal) window._keepFilterModal = true; + + if (window.gridCacheMap) window.gridCacheMap.clear(); + if (typeof loadPageAjax === 'function') { + const p = loadPageAjax(u.pathname + u.search, true, { bypassCache: true }); + if (fromModal) Promise.resolve(p).finally(() => { window._keepFilterModal = false; }); + } else { + window.location.href = u.pathname + u.search; + } + return; + } + + // 5. Generic attribute toggle pill click (OC, Discussed) + const togglePill = e.target.closest('.filter-toggle-pill[data-toggle]'); + if (togglePill) { + e.preventDefault(); + togglePill.classList.toggle('active'); + const paramName = togglePill.dataset.toggle; + const val = togglePill.classList.contains('active') ? '1' : null; + + const u = new URL(window.location.href); + if (val) u.searchParams.set(paramName, val); + else u.searchParams.delete(paramName); + + const fromModal = !!togglePill.closest('#excluded-tags-overlay'); + if (fromModal) window._keepFilterModal = true; + + if (window.gridCacheMap) window.gridCacheMap.clear(); + if (typeof loadPageAjax === 'function') { + const p = loadPageAjax(u.pathname + u.search, true, { bypassCache: true }); + if (fromModal) Promise.resolve(p).finally(() => { window._keepFilterModal = false; }); + } else { + window.location.href = u.pathname + u.search; + } + return; + } + + // 6. Reset button click + const resetBtn = e.target.closest('#filter-reset-btn'); + if (resetBtn) { + e.preventDefault(); + document.cookie = 'ratings=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + document.cookie = 'mime=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + document.cookie = 'layout=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + document.cookie = 'sort=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + document.cookie = 'timeframe=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + document.cookie = 'mode=3; path=/; max-age=31536000'; + window.activeMode = 3; + if (window.syncRatingButtonUI) window.syncRatingButtonUI(); + if (window.updateFilterBadge) window.updateFilterBadge(); + + const u = new URL(window.location.href); + ['layout', 'sort', 'timeframe', 'oc', 'has_comments', 'pinned', 'ratings', 'mime', 'mode'].forEach(p => u.searchParams.delete(p)); + + const fromModal = !!resetBtn.closest('#excluded-tags-overlay'); + if (fromModal) window._keepFilterModal = true; + + if (window.gridCacheMap) window.gridCacheMap.clear(); + if (typeof loadPageAjax === 'function') { + const p = loadPageAjax(u.pathname + u.search, true, { bypassCache: true }); + if (fromModal) Promise.resolve(p).finally(() => { window._keepFilterModal = false; }); + } else { + window.location.href = u.pathname + u.search; + } + return; + } + }); + // Sidebar state init — apply persisted hidden/visible state on load window.initSidebarRightToggle = () => { // One-time migration: clear the old mobile-default-hidden value so existing @@ -2590,13 +2772,21 @@ window.cancelAnimFrame = (function () { canvas.classList.add('fader-out'); } - // Rescue existing pagination container before wiping main, so it never disappears from the DOM + // Rescue existing pagination container before wiping main, so it never disappears const existingPagContainer = main.querySelector('.pagination-container-fluid'); // Clear entire main content and create fresh grid structure (with sidebar for index) const indexWrapper = document.createElement('div'); indexWrapper.className = 'index-layout-wrapper'; - indexWrapper.innerHTML = '
'; + const indexContainer = document.createElement('div'); + indexContainer.className = 'index-container'; + const currentLayout = urlObj.searchParams.get('layout') || (document.cookie.match(/layout=([^;]+)/)?.[1]) || 'grid'; + const postsElem = document.createElement('div'); + postsElem.className = `posts layout-${currentLayout} grid-transition`; + postsElem.id = 'posts-container'; + postsElem.dataset.layout = currentLayout; + indexContainer.appendChild(postsElem); + indexWrapper.appendChild(indexContainer); main.innerHTML = ''; main.appendChild(indexWrapper); @@ -2667,14 +2857,20 @@ window.cancelAnimFrame = (function () { if (isFav) ajaxUrl += `&fav=true`; if (mime) ajaxUrl += `&mime=${encodeURIComponent(mime)}`; + urlObj.searchParams.forEach((val, key) => { + if (!ajaxUrl.includes(`${key}=`)) { + ajaxUrl += `&${key}=${encodeURIComponent(val)}`; + } + }); + // Preserve tagger filter from URL query string const taggerMatch = urlObj.searchParams.get('tagger'); - if (taggerMatch) ajaxUrl += `&tagger=${encodeURIComponent(taggerMatch)}`; + if (taggerMatch && !ajaxUrl.includes('tagger=')) ajaxUrl += `&tagger=${encodeURIComponent(taggerMatch)}`; const isRandom = document.cookie.includes('random_mode=1') || url.includes('random=1') || window.location.search.includes('random=1'); - if (isRandom) ajaxUrl += `&random=1`; + if (isRandom && !ajaxUrl.includes('random=')) ajaxUrl += `&random=1`; const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true'); - if (isStrict || url.includes('strict=1') || window.location.search.includes('strict=1')) { + if ((isStrict || url.includes('strict=1') || window.location.search.includes('strict=1')) && !ajaxUrl.includes('strict=')) { ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + 'strict=1'; } @@ -3096,6 +3292,11 @@ window.cancelAnimFrame = (function () { if (data.success) { if (replace) { + const currentLayout = urlObj.searchParams.get('layout') || (document.cookie.match(/layout=([^;]+)/)?.[1]) || 'grid'; + posts.id = 'posts-container'; + posts.className = `posts layout-${currentLayout} grid-transition show`; + posts.dataset.layout = currentLayout; + // Atomic replacement to prevent "jumping" posts.innerHTML = data.html; window.updateVisitIndicators(); @@ -3115,7 +3316,7 @@ window.cancelAnimFrame = (function () { posts.classList.add('show'); }); - // Update header (h2) for tags/users + // Update header (h2) for tags/users/mainpage const container = document.querySelector('.index-container'); if (container && data.titleHtml !== undefined) { const oldH2 = container.querySelector('h2'); @@ -5615,8 +5816,6 @@ window.cancelAnimFrame = (function () { } }; const initExcludedTagsModal = () => { - if (window.f0ckSession && !window.f0ckSession.logged_in) return; - const overlay = document.getElementById('excluded-tags-overlay'); if (!overlay) return; @@ -5625,7 +5824,6 @@ window.cancelAnimFrame = (function () { const list = document.getElementById('nav_excluded_tags_list'); const suggestions = document.getElementById('nav_exclude_suggestions'); - const filterBtn = document.getElementById('nav-filter-btn'); const toggleModal = (show) => { if (show) { @@ -5635,11 +5833,11 @@ window.cancelAnimFrame = (function () { document.body.style.overflow = 'hidden'; renderTags(); if (window.syncRatingButtonUI) window.syncRatingButtonUI(); - if (window.innerWidth > 768) input.focus(); + if (input && window.innerWidth > 768) input.focus(); } else { overlay.classList.remove('visible'); document.body.style.overflow = ''; - suggestions.style.display = 'none'; + if (suggestions) suggestions.style.display = 'none'; setTimeout(() => { overlay.style.display = 'none'; }, 200); @@ -5647,6 +5845,11 @@ window.cancelAnimFrame = (function () { }; const renderTags = async () => { + if (!list) return; + if (window.f0ckSession && !window.f0ckSession.logged_in) { + list.innerHTML = ''; + return; + } try { const res = await fetch('/api/v2/settings/excluded_tags'); const data = await res.json(); @@ -5680,22 +5883,17 @@ window.cancelAnimFrame = (function () { } }; - if (filterBtn) { - filterBtn.addEventListener('click', (e) => { - e.preventDefault(); - toggleModal(true); - }); - } - - // Delegated handler: [data-action="open-filter-modal"] links (e.g. error page hint) + // Delegated handler: open modal on nav-filter-btn or [data-action="open-filter-modal"] document.addEventListener('click', (e) => { - const trigger = e.target.closest('[data-action="open-filter-modal"]'); + const trigger = e.target.closest('#nav-filter-btn, [data-action="open-filter-modal"]'); if (!trigger) return; e.preventDefault(); toggleModal(true); }); close.addEventListener('click', () => toggleModal(false)); + const doneBtn = document.getElementById('filter-modal-done-btn'); + if (doneBtn) doneBtn.addEventListener('click', () => toggleModal(false)); overlay.addEventListener('click', (e) => { if (e.target === overlay) toggleModal(false); }); diff --git a/src/inc/routeinc/f0cklib.mjs b/src/inc/routeinc/f0cklib.mjs index 5f3627c..7d6fa23 100644 --- a/src/inc/routeinc/f0cklib.mjs +++ b/src/inc/routeinc/f0cklib.mjs @@ -75,7 +75,7 @@ const flashMimes = Object.entries(cfg.mimes || {}).filter(([, ext]) => ext === ' const COUNT_CACHE_TTL_MS = 90_000; const countCache = new Map(); // key → { total, expiresAt } -function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger }) { +function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger, sort, timeframe, oc, pinned, hasComments }) { return JSON.stringify([ modequery, tag ?? '', @@ -88,7 +88,12 @@ function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, ex newerThan ?? '', minXd, userHallObj?.id ?? '', - tagger ?? '' + tagger ?? '', + sort ?? '', + timeframe ?? '', + oc ? 1 : 0, + pinned ? 1 : 0, + hasComments ? 1 : 0 ]); } @@ -235,7 +240,7 @@ async function checkFavoritesAccess(rawUser, { session, user_id, is_admin } = {} } export default { - getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, is_admin, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => { + getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, is_admin, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger, sort, timeframe, oc, pinned, hasComments } = {}) => { if (fav && rawUser) { const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin }); if (isPrivate && !isAllowed) { @@ -296,11 +301,43 @@ export default { // xD filter: use materialized items.xd_score column (kept live by trigger) for fast indexed lookup const xdFilter = minXd > 0 ? db`and items.xd_score >= ${minXd}` : db``; + // Advanced filters + const ocFilter = (oc === true || oc === '1' || oc === 'true') ? db`and items.is_oc = true` : db``; + const pinnedFilter = (pinned === true || pinned === '1' || pinned === 'true') ? db`and items.is_pinned = true` : db``; + const hasCommentsFilter = (hasComments === true || hasComments === '1' || hasComments === 'true') + ? db`and exists (select 1 from comments where item_id = items.id and is_deleted = false)` + : db``; + + let timeframeFilter = db``; + if (timeframe && timeframe !== 'all') { + let seconds = 0; + if (timeframe === 'day') seconds = 86400; + else if (timeframe === 'week') seconds = 86400 * 7; + else if (timeframe === 'month') seconds = 86400 * 30; + else if (timeframe === 'year') seconds = 86400 * 365; + if (seconds > 0) { + const minStamp = Math.floor(Date.now() / 1000) - seconds; + timeframeFilter = db`and items.stamp >= ${minStamp}`; + } + } + + const effectiveSort = sort || (random ? 'random' : 'newest'); + let orderSQL = db`items.is_pinned desc, items.id desc`; + if (effectiveSort === 'oldest') { + orderSQL = db`items.is_pinned desc, items.id asc`; + } else if (effectiveSort === 'popular' || effectiveSort === 'top' || effectiveSort === 'xd') { + orderSQL = db`items.is_pinned desc, items.xd_score desc, items.id desc`; + } else if (effectiveSort === 'comments') { + orderSQL = db`items.is_pinned desc, (select count(*) from comments where item_id = items.id and is_deleted = false) desc, items.id desc`; + } else if (effectiveSort === 'random') { + orderSQL = db`random()`; + } + const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : []; const isStrict = strictParams.length > 0; const tagger = rawTagger ? lib.escapeLike(rawTagger) : null; - const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner, tagger }; + const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner, tagger, sort: effectiveSort, timeframe, oc, pinned, hasComments }; const modequery = computeBaseMode(mode, ratings, session); let tagFilter = db``; @@ -363,7 +400,7 @@ export default { ? db`and (coalesce(items.visibility, 0) = 0 or lower(items.username) = ${session.user.toLowerCase()})` : db`and coalesce(items.visibility, 0) = 0`); - const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger }); + const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger, sort: effectiveSort, timeframe, oc, pinned, hasComments }); let total = getCachedCount(cacheKey); if (total === null) { @@ -386,6 +423,10 @@ export default { ${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``} ${newerThan ? db`and items.id > ${newerThan}` : db``} ${xdFilter} + ${ocFilter} + ${pinnedFilter} + ${hasCommentsFilter} + ${timeframeFilter} `; total = Number(totalRows[0].total); if (total > 0) setCachedCount(cacheKey, total); @@ -429,8 +470,12 @@ export default { ${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``} ${newerThan ? db`and items.id > ${newerThan}` : db``} ${xdFilter} + ${ocFilter} + ${pinnedFilter} + ${hasCommentsFilter} + ${timeframeFilter} ${fav ? db`group by items.id, items.is_pinned` : db``} - order by ${random ? db`random()` : db`items.is_pinned desc, items.id desc`} + order by ${orderSQL} offset ${newerThan ? 0 : offset} limit ${eps} `; diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs index 531c6dc..73e5b89 100644 --- a/src/inc/routes/ajax.mjs +++ b/src/inc/routes/ajax.mjs @@ -207,9 +207,16 @@ export default (router, tpl) => { const page = parseInt(query.page) || 1; const isRandom = query.random === '1' || req.cookies.random_mode === '1'; - const ratingsRaw = req.cookies.ratings; + const ratingsRaw = query.ratings || req.cookies.ratings; const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null; + const sortParam = query.sort || req.cookies.sort || 'newest'; + const timeframeParam = query.timeframe || req.cookies.timeframe || 'all'; + const ocParam = query.oc === '1' || query.oc === 'true'; + const pinnedParam = query.pinned === '1' || query.pinned === 'true'; + const hasCommentsParam = query.has_comments === '1' || query.has_comments === 'true' || query.hasComments === 'true'; + const layoutParam = query.layout || req.cookies.layout || 'grid'; + const data = await f0cklib.getf0cks({ page: page, tag: query.tag || null, @@ -219,6 +226,11 @@ export default (router, tpl) => { userHallOwner: query.userHallOwner || null, mime: query.mime || (req.cookies.mime || null), mode: query.mode !== undefined ? +query.mode : req.mode, + sort: sortParam, + timeframe: timeframeParam, + oc: ocParam, + pinned: pinnedParam, + hasComments: hasCommentsParam, ratings: ratingsArr, session: req.session, user_id: req.session?.id, @@ -260,7 +272,8 @@ export default (router, tpl) => { // Render just the thumbnail items const itemsHtml = tpl.render('snippets/items-grid', { items: data.items, - link: data.link + link: data.link, + layout: layoutParam }, req); // Render pagination @@ -283,6 +296,27 @@ export default (router, tpl) => { session: (req.session && req.session.user) ? { ...req.session } : false }, req); + // Render filter bar + const activeRatings = ratingsArr || ['sfw', 'nsfw', 'untagged']; + const activeMimes = (query.mime || req.cookies.mime || '').split(',').filter(Boolean); + + const filterBarHtml = tpl.render('snippets/filter-bar', { + layout: layoutParam, + sort: sortParam, + timeframe: timeframeParam, + oc: ocParam ? '1' : '0', + pinned: pinnedParam ? '1' : '0', + hasComments: hasCommentsParam ? '1' : '0', + rating_sfw: activeRatings.includes('sfw'), + rating_nsfw: activeRatings.includes('nsfw'), + rating_nsfl: activeRatings.includes('nsfl'), + rating_untagged: activeRatings.includes('untagged'), + mime_image: activeMimes.includes('image'), + mime_video: activeMimes.includes('video'), + mime_audio: activeMimes.includes('audio'), + mime_flash: activeMimes.includes('flash') + }, req); + const hasMore = data.pagination.next !== null; return res.reply({ @@ -296,6 +330,7 @@ export default (router, tpl) => { success: true, html: itemsHtml, titleHtml: titleHtml, + filterBarHtml: filterBarHtml, pagination: paginationHtml, hasMore: hasMore, nextPage: data.pagination.next, diff --git a/src/inc/routes/index.mjs b/src/inc/routes/index.mjs index dda4a91..89f4f87 100644 --- a/src/inc/routes/index.mjs +++ b/src/inc/routes/index.mjs @@ -227,6 +227,16 @@ export default (router, tpl) => { // Decode tag param once — browsers send title%3A... on hard reload, title:... via AJAX const reqTag = req.params.tag ? decodeURIComponent(req.params.tag) : req.params.tag; + const layout = req.query?.layout || req.url.qs?.layout || req.cookies.layout || req.session?.layout || 'grid'; + const sort = req.query?.sort || req.url.qs?.sort || req.cookies.sort || 'newest'; + const timeframe = req.query?.timeframe || req.url.qs?.timeframe || 'all'; + const oc = req.query?.oc || req.url.qs?.oc || null; + const pinned = req.query?.pinned || req.url.qs?.pinned || null; + const hasComments = req.query?.has_comments || req.url.qs?.has_comments || null; + + const ratingsRaw = req.query?.ratings || req.url.qs?.ratings || req.cookies.ratings; + const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; + const data = await (req.params.itemid ? f0cklib.getf0ck : f0cklib.getf0cks)({ user: req.params.user, tag: reqTag, @@ -236,7 +246,7 @@ export default (router, tpl) => { hall: req.params.hall, fav: req.params.mode == 'favs', mode: req.mode, - ratings: (() => { if (req.mode === 2 || req.mode === 3) return null; const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(), + ratings: ratingsArr, session: req.session, user_id: req.session?.id, is_admin: req.session?.admin, @@ -247,8 +257,34 @@ export default (router, tpl) => { random: req.cookies.random_mode === '1', minXdScore: req.params.itemid ? 0 : (req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0)), lang: req.lang, - tagger: req.url.qs?.tagger || null + tagger: req.url.qs?.tagger || null, + sort, + timeframe, + oc, + pinned, + hasComments }); + const activeRatings = ratingsArr || ['sfw', 'nsfw', 'untagged']; + + const rawMimeVal = req.query?.mime || req.url.qs?.mime || req.cookies.mime || req.params.mime || ''; + const activeMimes = String(rawMimeVal).split(',').filter(Boolean); + + data.layout = layout; + data.sort = sort; + data.timeframe = timeframe; + data.oc = oc; + data.pinned = pinned; + data.hasComments = hasComments; + + data.rating_sfw = activeRatings.includes('sfw'); + data.rating_nsfw = activeRatings.includes('nsfw'); + data.rating_nsfl = activeRatings.includes('nsfl'); + data.rating_untagged = activeRatings.includes('untagged'); + + data.mime_image = activeMimes.includes('image'); + data.mime_video = activeMimes.includes('video'); + data.mime_audio = activeMimes.includes('audio'); + data.mime_flash = activeMimes.includes('flash'); console.log(`[DEBUG] Checking strict mode: query=${req.query?.strict}, session=${req.session?.strict_mode}, effective=${!!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode)}`); console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`); diff --git a/views/index-partial.html b/views/index-partial.html index 9441f1f..6300223 100644 --- a/views/index-partial.html +++ b/views/index-partial.html @@ -1,8 +1,45 @@
@include(snippets/page-title) -
+ +
@each(items as item) + + @if(layout == 'stream') + +
+
+ + {!! item.display_name || item.username !!} + +
+ @if(item.is_pinned) + Pinned + @endif + @if(item.is_oc) + OC + @endif + @if(enable_xd_score && item.xd_tier > 0) + xD: {{ item.xd_score }} + @endif + +
+
+ + + Media thumbnail + {{ item.mime.split('/')[1].replace('youtube', 'YT').replace('x-shockwave-flash', 'FLASH').toUpperCase() }} + + + +
+ + @else +
@if(item.is_pinned) @@ -17,6 +54,8 @@

+ @endif + @endeach
diff --git a/views/snippets/excluded-tags-modal.html b/views/snippets/excluded-tags-modal.html index 63dd2e8..78633c4 100644 --- a/views/snippets/excluded-tags-modal.html +++ b/views/snippets/excluded-tags-modal.html @@ -1,56 +1,153 @@