diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 3855e31..1bb9212 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -2916,8 +2916,9 @@ body.sidebar-right-hidden #sidebar-drag-zone::after { overflow: visible !important; } + /* Same track count as the open state so the sidebar toggle can animate the columns (f0ckm.js) */ body.layout-legacy.sidebar-right-hidden .item-layout-container { - grid-template-columns: 1fr; + grid-template-columns: minmax(0, 1fr) 0px; } body.layout-legacy .item-layout-container .item-main-content { @@ -3473,6 +3474,7 @@ body.sidebar-right-hidden #sidebar-drag-zone::after { width: 320px; max-height: 80vh; overflow-y: auto; + scrollbar-width: none; background: rgba(18, 18, 22, 0.95); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); @@ -3495,19 +3497,17 @@ body.sidebar-right-hidden #sidebar-drag-zone::after { flex: 1; min-height: 0; overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: rgba(255, 255, 255, 0.15) transparent; + scrollbar-width: none; padding: 0; touch-action: pan-y; } -.sidebar-tuner-container::-webkit-scrollbar { - width: 5px; -} - -.sidebar-tuner-container::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.15); - border-radius: 4px; +/* Tuner scrolls (floating panel or sidebar container) but without a visible scrollbar */ +.sidebar-tuner-container::-webkit-scrollbar, +.f0ck-audio-tuner-panel::-webkit-scrollbar { + display: none; + width: 0; + height: 0; } .sidebar-tuner-container.active { @@ -4351,7 +4351,7 @@ html[theme='95'] .sidebar-video-title { font-size: 1em; border-bottom: 1px solid var(--nav-border-color); contain: layout style; - margin: 0px; + margin-bottom: 5px; } .sidebar-activity .comment-content-inner { @@ -4833,7 +4833,7 @@ body.layout-legacy .comment-input { } .admin-pin-btn:hover, -.admin-edit-btn:hover {^ +.admin-edit-btn:hover { color: var(--accent); } @@ -5418,6 +5418,14 @@ body.layout-legacy .blahlol { border-top: 1px solid var(--nav-border-color); } +/* Comments hidden ("c"): nothing below closes the box, so it gets its own bottom border. + comments-sliding = the comments are sliding up behind it or back down (comments.js); the border + stays for the whole slide in both directions */ +body.layout-legacy.comments-hidden .blahlol, +body.layout-legacy.comments-sliding .blahlol { + border-bottom: 1px solid var(--nav-border-color); +} + body.layout-legacy .comments-list:not(:has(.comment)) { border: none; } @@ -6540,9 +6548,9 @@ html[res="fullscreen"] span#favs { } /* Linux Firefox "thin" is often too small and hard to see. Use auto there. */ -html.is-linux.is-firefox *:not(html):not(body) { +/* html.is-linux.is-firefox *:not(html):not(body) { scrollbar-width: auto; -} +} */ /* Strictly hide root body and html scrollbar everywhere - eliminates gutter spacing */ html, @@ -10623,12 +10631,15 @@ div.favs div.posts { top: 0; left: 0; bottom: 0; - width: calc(100vw - var(--sidebar-w, 300px)); + /* Always full width, shifted left by half the sidebar so it stays centred under the content. The size + never changes with the sidebar, so the drawing is never stretched or redrawn: the sidebar toggle only + moves it (transform, GPU), see toggleSidebarRight() in f0ckm.js */ + width: 100vw; height: 100%; - transform: translate3d(0, 0, 0) scale(var(--bg-canvas-zoom, 1.38)); + transform: translate3d(calc(var(--sidebar-w, 300px) / -2), 0, 0) scale(var(--bg-canvas-zoom, 1.38)); transform-origin: center center; z-index: -2; - transition: opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s ease-in-out; + transition: opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1), transform 0.6s cubic-bezier(0.45, 0, 0.2, 1); opacity: var(--bg-canvas-opacity, 0.81); filter: var(--bg-canvas-filter, none); -webkit-filter: var(--bg-canvas-filter, none); @@ -15526,7 +15537,6 @@ body.layout-modern .tag-ac-input { min-height: 0; overflow-y: auto; overscroll-behavior: contain; - padding: 0 18px; scrollbar-width: none; } @@ -15540,7 +15550,6 @@ body.layout-modern .tag-ac-input { /* Above everything the form layers inside the panel (badges 5-100, tag input 9998, emoji picker 10000); all of it lives in the panel's own stacking context, so these numbers don't leak outside */ z-index: 10001; - margin: 0 -18px 12px; padding: 16px 62px 12px 18px; background: var(--bg, #111); border-bottom: 1px solid rgba(255, 255, 255, 0.1); @@ -15672,12 +15681,15 @@ nav.navbar #nav-upload-link.is-active { box-shadow: inset 0 -2px 0 var(--accent); } -nav.navbar #nav-upload-link.is-active i { +nav.navbar #nav-upload-link i { display: inline-block; - transform: rotate(180deg); transition: transform 0.15s; } +nav.navbar #nav-upload-link.is-active i { + transform: rotate(180deg); +} + /* Navbar entries (settings gear, user/anon menu "Settings") whose page is open in the page modal */ nav.navbar a[data-page-modal].is-active, nav.navbar a[data-page-modal].is-active i { @@ -21485,7 +21497,7 @@ body.onara-modal-open:not(.onara-modal-closing) #bg, body.onara-modal-open:not(.onara-modal-closing) #bg.fader-in { z-index: 51 !important; cursor: pointer !important; - transition: width 0.3s ease-in-out !important; + transition: transform 0.6s cubic-bezier(0.45, 0, 0.2, 1) !important; animation: none !important; opacity: var(--bg-canvas-opacity, 1.0) !important; } diff --git a/public/s/js/comments.js b/public/s/js/comments.js index 7fadcdd..ea9eca1 100644 --- a/public/s/js/comments.js +++ b/public/s/js/comments.js @@ -47,6 +47,9 @@ class CommentSystem { // inline script in header.html (prevents flash). Here we sync the // container-level class and display state to match. if (this.container) { + // A slide cut short by navigating away must not leave its border behind + this._legacySlide = null; + document.body.classList.remove('comments-sliding'); const isHidden = localStorage.getItem('comments_hidden') === 'true'; // Force show if hash is present if (window.location.hash && window.location.hash.startsWith('#c')) { @@ -4135,6 +4138,16 @@ class CommentSystem { document.body.classList.contains('comments-hidden') || localStorage.getItem('comments_hidden') === 'true'; + // Legacy layout: comments slide up behind the info box (and back down out of it) + const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (document.body.classList.contains('layout-legacy') && typeof this.container.animate === 'function' && !reduceMotion) { + // A slide still running decides the direction, since the classes only flip when it ends + const running = this._legacySlide && this._legacySlide.el === this.container ? this._legacySlide : null; + const hiddenNow = running ? running.dir === 'hide' : isHidden; + this.slideLegacyComments(!hiddenNow, layout, sidebar, siblings); + return; + } + if (isHidden) { // SHOW: expand grid first, then slide content in if (layout) layout.classList.remove('sidebar-hidden'); @@ -4170,6 +4183,83 @@ class CommentSystem { } } + // Legacy layout slide: the container shrinks from the bottom up while its content moves up by the same + // amount, so the comments look like they retract behind the box above them (reverse on show). + // Pressing again mid-slide turns it around from where it is. + slideLegacyComments(hide, layout, sidebar, siblings) { + const el = this.container; + const kids = Array.from(el.children); + // Curtain feel: slow, soft start, gentle settle at the end + const DURATION = 900; + const EASING = 'cubic-bezier(0.45, 0, 0.2, 1)'; + + let startH = null; + if (this._legacySlide) { + // Only turn around from mid-slide on the same item; a slide left over from a previous item is dropped + if (this._legacySlide.el === el) startH = el.getBoundingClientRect().height; + this._legacySlide.anims.forEach(a => a.cancel()); + this._legacySlide = null; + } + + if (!hide) { + // Put it back in the flow first (collapsed), so its full height can be measured + if (layout) layout.classList.remove('sidebar-hidden'); + document.body.classList.remove('sidebar-left-hidden', 'comments-hidden'); + // Border stays until the comments are fully down again + document.body.classList.add('comments-sliding'); + el.classList.remove('is-hidden', 'faded-out'); + el.style.removeProperty('display'); + if (sidebar) { + sidebar.classList.remove('is-hidden'); + sidebar.style.removeProperty('display'); + } + siblings.forEach(s => s.classList.remove('faded-out')); + localStorage.setItem('comments_hidden', 'false'); + if (startH === null) startH = 0; + } else { + // Mark state immediately so quick navigations stay hidden + localStorage.setItem('comments_hidden', 'true'); + document.body.classList.add('comments-sliding'); + if (startH === null) startH = el.getBoundingClientRect().height; + } + + el.style.height = 'auto'; + const fullH = el.scrollHeight; + const endH = hide ? 0 : fullH; + el.style.overflow = 'hidden'; + el.style.height = endH + 'px'; + + const frames = (h0, h1) => [{ transform: 'translateY(' + (h0 - fullH) + 'px)' }, { transform: 'translateY(' + (h1 - fullH) + 'px)' }]; + // A turnaround covers only part of the distance, so it takes proportionally less time (same speed) + const share = fullH > 0 ? Math.abs(endH - startH) / fullH : 1; + const opts = { duration: Math.round(DURATION * Math.max(0.3, Math.min(1, share))), easing: EASING }; + const anims = [el.animate([{ height: startH + 'px' }, { height: endH + 'px' }], opts)]; + kids.forEach(k => anims.push(k.animate(frames(startH, endH), opts))); + + const slide = { dir: hide ? 'hide' : 'show', anims, el }; + this._legacySlide = slide; + anims[0].onfinish = () => { + if (this._legacySlide !== slide) return; + this._legacySlide = null; + el.style.removeProperty('overflow'); + el.style.removeProperty('height'); + if (!hide) { + document.body.classList.remove('comments-sliding'); + return; + } + el.classList.add('faded-out', 'is-hidden'); + el.style.setProperty('display', 'none', 'important'); + siblings.forEach(s => s.classList.add('faded-out')); + if (sidebar) { + sidebar.classList.add('is-hidden'); + sidebar.style.setProperty('display', 'none', 'important'); + } + if (layout) layout.classList.add('sidebar-hidden'); + document.body.classList.add('sidebar-left-hidden', 'comments-hidden'); + document.body.classList.remove('comments-sliding'); + }; + } + escapeHtml(unsafe) { diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index eca2e1b..fd8dd7d 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -3745,6 +3745,7 @@ window.cancelAnimFrame = (function () { const newYtSrc = ytId ? `https://www.youtube.com/embed/${ytId}?enablejsapi=1` : newSrc; if (ytIframe.src !== newYtSrc) ytIframe.src = newYtSrc; ytIframe.style.display = 'block'; + if (window.stopVisualizer) window.stopVisualizer(); } else if (mime.startsWith('video/')) { container.classList.remove('is-youtube-active'); container.classList.add('is-video-active'); @@ -3770,6 +3771,7 @@ window.cancelAnimFrame = (function () { initAlbumVideoV0ck(); video = videoEl; + if (window.stopVisualizer) window.stopVisualizer(); if (window.initBackground) window.initBackground(); if (item.size && videoWrapper) { @@ -3943,6 +3945,8 @@ window.cancelAnimFrame = (function () { imgEl.classList.add('album-img-fade'); } video = null; + if (window.stopVisualizer) window.stopVisualizer(); + if (window.initBackground) window.initBackground(); } if (linkEl && !mime.startsWith('video/')) { @@ -5275,7 +5279,7 @@ window.cancelAnimFrame = (function () { } if (!elem) { const albumAudio = document.getElementById('f0ck-album-audio'); - if (albumAudio && albumAudio.src) { + if (albumAudio && albumAudio.src && !isInactiveAlbumAudio(albumAudio)) { elem = albumAudio; } } @@ -5528,6 +5532,16 @@ window.cancelAnimFrame = (function () { const itemId = window.getCurrentItemId(); const isDrawable = elem && elem.tagName === 'IMG'; + // Album image slides: the /t/ thumb belongs to the whole album, so draw the slide itself + if (isDrawable && elem.closest('.album-gallery-container')) { + if (elem.complete && elem.naturalWidth > 0) { + crossfadeToSource(elem); + } else { + elem.onload = () => crossfadeToSource(elem); + } + return; + } + if (itemId) { if (document.body.classList.contains('onara-modal-open')) { const onaraThumbImg = document.querySelector('.posts > a.thumb.onara-active img, .posts > a.thumb:focus img'); @@ -8825,6 +8839,20 @@ window.cancelAnimFrame = (function () { } }; + // Stop the audio visualizer loop so it no longer paints into #bg + window.stopVisualizer = () => { + if (visualizerRafId) { window.cancelAnimFrame(visualizerRafId); visualizerRafId = null; } + const bgCanvas = document.getElementById('bg'); + if (bgCanvas) bgCanvas.style.filter = ''; + }; + + // The album template always contains a hidden