diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 61d7795..d4e6187 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -15688,93 +15688,48 @@ body.scroller-active #gchat-reopen-bubble { /* ============================================= NOTIFICATION THUMBNAIL BLUR - Blur is driven by the current mode filter set - on . - No extra settings required. + JS swaps img src to /t/{id}_blur.webp and adds + .notif-thumb-blurred. CSS only handles the label + overlay and hover/reveal transitions. ============================================= */ -/* SFW mode: blur NSFW and NSFL thumbnails */ -html[data-notif-filter="sfw"] .notif-thumb[data-mode="nsfw"] img, -html[data-notif-filter="sfw"] .notif-thumb[data-mode="nsfl"] img { - filter: blur(8px) !important; - transform: scale(1.08) !important; - transition: filter 0.2s ease, transform 0.2s ease !important; +/* Blurred thumb: position for overlay */ +.notif-thumb.notif-thumb-blurred { + position: relative; } -/* NSFW mode: blur NSFL thumbnails */ -html[data-notif-filter="nsfw"] .notif-thumb[data-mode="nsfl"] img { - filter: blur(8px) !important; - transform: scale(1.08) !important; - transition: filter 0.2s ease, transform 0.2s ease !important; +/* Overlay label */ +.notif-thumb.notif-thumb-blurred::after { + content: attr(data-mode); + text-transform: uppercase; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-weight: 800; + font-size: 10px; + letter-spacing: 1.5px; + text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9); + background: rgba(10, 10, 12, 0.45); + color: #ffffff; + pointer-events: none; + transition: opacity 0.15s ease; + z-index: 2; } -/* NSFL mode: blur SFW and NSFW thumbnails */ -html[data-notif-filter="nsfl"] .notif-thumb[data-mode="sfw"] img, -html[data-notif-filter="nsfl"] .notif-thumb[data-mode="nsfw"] img { - filter: blur(8px) !important; - transform: scale(1.08) !important; - transition: filter 0.2s ease, transform 0.2s ease !important; +/* NSFL label gets a red tint */ +.notif-thumb.notif-thumb-blurred[data-mode="nsfl"]::after { + color: #ff3b30; } -/* Overlay label — SFW mode */ -html[data-notif-filter="sfw"] .notif-thumb[data-mode="nsfw"], -html[data-notif-filter="sfw"] .notif-thumb[data-mode="nsfl"], -html[data-notif-filter="nsfw"] .notif-thumb[data-mode="nsfl"], -html[data-notif-filter="nsfl"] .notif-thumb[data-mode="sfw"], -html[data-notif-filter="nsfl"] .notif-thumb[data-mode="nsfw"] { - position: relative !important; +/* Hide overlay on hover (JS handles the actual src swap) */ +.notif-thumb.notif-thumb-blurred:hover::after { + opacity: 0; } -html[data-notif-filter="sfw"] .notif-thumb[data-mode="nsfw"]::after, -html[data-notif-filter="sfw"] .notif-thumb[data-mode="nsfl"]::after, -html[data-notif-filter="nsfw"] .notif-thumb[data-mode="nsfl"]::after, -html[data-notif-filter="nsfl"] .notif-thumb[data-mode="sfw"]::after, -html[data-notif-filter="nsfl"] .notif-thumb[data-mode="nsfw"]::after { - content: attr(data-mode) !important; - text-transform: uppercase !important; - position: absolute !important; - inset: 0 !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - font-weight: 800 !important; - font-size: 10px !important; - letter-spacing: 1.5px !important; - text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9) !important; - background: rgba(10, 10, 12, 0.55) !important; - color: #ffffff !important; - pointer-events: none !important; - opacity: 1 !important; - transition: opacity 0.2s ease !important; - z-index: 2 !important; -} - -html[data-notif-filter="sfw"] .notif-thumb[data-mode="nsfl"]::after, -html[data-notif-filter="nsfw"] .notif-thumb[data-mode="nsfl"]::after { color: #ff3b30 !important; } - -/* Reveal on hover — only when hovering directly on the thumb */ -.notif-thumb[data-mode]:hover img { - filter: none !important; - transform: scale(1.1) !important; -} -.notif-thumb[data-mode]:hover::after { - opacity: 0 !important; -} - -/* Revealed state (set by JS tap-to-reveal for mobile) */ -.notif-thumb[data-mode].revealed img { - filter: none !important; -} -.notif-thumb[data-mode].revealed::after { - opacity: 0 !important; - pointer-events: none !important; -} - -/* ALL mode: no blur on notification thumbnails */ -html.mode-all .notif-thumb[data-mode] img { - filter: none !important; - transform: none !important; -} -html.mode-all .notif-thumb[data-mode]::after { - display: none !important; +/* Revealed state: hide overlay */ +.notif-thumb.notif-thumb-blurred.revealed::after { + opacity: 0; + pointer-events: none; } \ No newline at end of file diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index afce3e8..4f47644 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -505,19 +505,64 @@ window.cancelAnimFrame = (function () { } // Track active mode on for notification thumbnail blur. - // data-notif-filter is set to the current mode string so CSS can blur - // thumbnails whose rating doesn't match the active filter. + // data-notif-filter is set to the current mode string and also + // drives src-swapping to pre-blurred thumbnails (/t/{id}_blur.webp). + const _notifBlurShouldBlur = (itemMode, activeMode) => { + if (activeMode === 3) return false; // ALL mode: never blur + if (activeMode === 0) return itemMode === 'nsfw' || itemMode === 'nsfl'; // SFW filter + if (activeMode === 1) return itemMode === 'nsfl'; // NSFW filter + if (activeMode === 4) return itemMode === 'sfw' || itemMode === 'nsfw'; // NSFL filter + return false; + }; + + window.applyNotifThumbBlur = (container) => { + const root = container || document; + const mode = window.activeMode ?? 3; + root.querySelectorAll('.notif-thumb[data-mode]').forEach(thumb => { + const img = thumb.querySelector('img'); + if (!img) return; + const itemMode = thumb.dataset.mode; + if (!itemMode || thumb.classList.contains('revealed')) return; + + if (_notifBlurShouldBlur(itemMode, mode)) { + if (!img.dataset.origSrc) { + // Extract numeric item ID from any thumbnail path variant: + // /t/{id}.webp, /mod/pending/t/{id}.webp, /mod/deleted/t/{id}.webp + const idMatch = img.src.match(/\/t\/(\d+)\.webp/); + if (idMatch) { + // Always use the canonical /t/{id}.webp as the sharp src (hover target) + img.dataset.origSrc = `/t/${idMatch[1]}.webp`; + img.src = `/t/${idMatch[1]}_blur.webp`; + thumb.classList.add('notif-thumb-blurred'); + } + } + } else { + // Restore original thumbnail + if (img.dataset.origSrc) { + img.src = img.dataset.origSrc; + delete img.dataset.origSrc; + thumb.classList.remove('notif-thumb-blurred'); + } + } + }); + }; + + const _updateNotifFilterClass = (mode) => { const modeNames = { 0: 'sfw', 1: 'nsfw', 2: 'untagged', 3: 'all', 4: 'nsfl' }; htmlEl.setAttribute('data-notif-filter', modeNames[mode] || 'all'); - // Keep mode-all class in sync (used by existing CSS override) if (mode === 3) htmlEl.classList.add('mode-all'); else htmlEl.classList.remove('mode-all'); + // Re-apply blur src swaps whenever mode changes + window.applyNotifThumbBlur(); }; _updateNotifFilterClass(window.activeMode ?? 3); document.addEventListener('f0ck:modeChanged', (e) => { _updateNotifFilterClass(e.detail?.mode ?? 3); }); + // Apply to server-rendered /notifications page on load + document.addEventListener('DOMContentLoaded', () => window.applyNotifThumbBlur()); + // ---- Multi-select Rating Toggles ---- // Reads/writes a `ratings` cookie (e.g. "sfw|untagged") and syncs with server via /mode/3 (ALL). @@ -6596,6 +6641,12 @@ class NotificationSystem { e.preventDefault(); e.stopPropagation(); tappedThumb.classList.add('revealed'); + tappedThumb.classList.remove('notif-thumb-blurred'); + const img = tappedThumb.querySelector('img'); + if (img && img.dataset.origSrc) { + img.src = img.dataset.origSrc; + delete img.dataset.origSrc; + } return; } } @@ -6716,6 +6767,25 @@ class NotificationSystem { } }); + // Hover src-swap: show sharp thumbnail on hover, restore blur on mouseout + const _notifThumbHoverIn = (e) => { + const thumb = e.target.closest('.notif-thumb.notif-thumb-blurred'); + if (!thumb || thumb.classList.contains('revealed')) return; + const img = thumb.querySelector('img'); + if (img && img.dataset.origSrc) img.src = img.dataset.origSrc; + }; + const _notifThumbHoverOut = (e) => { + const thumb = e.target.closest('.notif-thumb.notif-thumb-blurred'); + if (!thumb || thumb.classList.contains('revealed')) return; + const img = thumb.querySelector('img'); + if (img && img.dataset.origSrc) { + const idMatch = img.dataset.origSrc.match(/\/t\/(\d+)\.webp/); + if (idMatch) img.src = `/t/${idMatch[1]}_blur.webp`; + } + }; + document.addEventListener('mouseover', _notifThumbHoverIn); + document.addEventListener('mouseout', _notifThumbHoverOut); + // Close on clicking 'View all' or 'Manage subscriptions' this.dropdown.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => { @@ -6938,6 +7008,7 @@ class NotificationSystem { return; } this.list.innerHTML = Sanitizer.clean(items.map(n => this.renderItem(n)).join('')); + if (window.applyNotifThumbBlur) window.applyNotifThumbBlur(this.list); } renderHistoryItem(n) { diff --git a/views/notifications.html b/views/notifications.html index 3bc3332..cb137c0 100644 --- a/views/notifications.html +++ b/views/notifications.html @@ -39,6 +39,7 @@ const data = await res.json(); if (data.success) { container.innerHTML = data.html || '