gfdgfds
This commit is contained in:
+34
-22
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+133
-38
@@ -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 <audio>; it only counts while an audio slide is shown
|
||||
const isInactiveAlbumAudio = (el) => {
|
||||
if (!el || el.id !== 'f0ck-album-audio') return false;
|
||||
const albumContainer = el.closest('.album-gallery-container');
|
||||
return !albumContainer || !albumContainer.classList.contains('is-audio-active');
|
||||
};
|
||||
|
||||
window.initVisualizer = (targetAudio) => {
|
||||
let audioElement = targetAudio;
|
||||
if (!audioElement || !audioElement.isConnected) {
|
||||
@@ -8847,6 +8875,11 @@ window.cancelAnimFrame = (function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isInactiveAlbumAudio(audioElement)) {
|
||||
// Album is showing an image/video slide: the visualizer must not take over #bg
|
||||
window.stopVisualizer();
|
||||
return;
|
||||
}
|
||||
if (audioElement) {
|
||||
if (!audioElement.crossOrigin) {
|
||||
audioElement.crossOrigin = 'anonymous';
|
||||
@@ -17426,41 +17459,77 @@ window.cancelAnimFrame = (function () {
|
||||
// At ≤599px the sidebar is a pure overlay — never touch wrapper padding
|
||||
const animateWrappers = window.innerWidth > 599;
|
||||
|
||||
// Smoothness: the sidebar and #bg canvas animate via CSS transitions (compositor, minimal layout).
|
||||
// The content wrappers and Onara modal change width ONCE instead of every frame: before the slide
|
||||
// when opening, after it when closing. The strip the sidebar travels through is therefore always
|
||||
// empty background, so no thumbnails reflow behind it; the single reflow is masked with an opacity fade.
|
||||
const DURATION = 320;
|
||||
const EASING = 'cubic-bezier(0.32, 0.72, 0, 1)';
|
||||
// Smoothness: the sidebar, #bg canvas, drag pill, right-anchored bars, the Onara modal and the item
|
||||
// view's grid all move together, frame by frame, with one slow curtain-like easing (same feel as the
|
||||
// legacy comment slide). Only the thumbnail-grid wrappers (index pages) still change width ONCE
|
||||
// (before the slide when opening, after it when closing): their thumbnails would re-wrap into
|
||||
// different rows mid-slide, so that single reflow is masked with an opacity fade instead.
|
||||
const DURATION = 600;
|
||||
const EASING = 'cubic-bezier(0.45, 0, 0.2, 1)';
|
||||
const glide = DURATION + 'ms ' + EASING;
|
||||
const wrappers = getSidebarWrappers();
|
||||
const onaraModal = document.getElementById('onara-modal');
|
||||
const bgCanvas = document.getElementById('bg');
|
||||
const isEdgeWrapper = w => w.classList.contains('pagination-container-fluid') || w.classList.contains('admin-bar') || w.classList.contains('messages-convo-page');
|
||||
|
||||
// Elements positioned by `right`: glide to the new edge together with the sidebar (both directions)
|
||||
const moveEdges = (open) => {
|
||||
if (!animateWrappers) return;
|
||||
const val = open ? sidebarWidth + 'px' : '0px';
|
||||
const targets = wrappers ? Array.from(wrappers).filter(isEdgeWrapper) : [];
|
||||
if (onaraModal) targets.push(onaraModal);
|
||||
targets.forEach(el => {
|
||||
// Pin the current value first so the transition has a defined start
|
||||
el.style.setProperty('transition', 'none', 'important');
|
||||
el.style.setProperty('right', getComputedStyle(el).right, 'important');
|
||||
void el.offsetWidth;
|
||||
el.style.setProperty('transition', 'right ' + glide, 'important');
|
||||
el.style.setProperty('right', val, 'important');
|
||||
});
|
||||
};
|
||||
|
||||
// Padding-based wrappers (thumbnail grids): one-time reflow, softened
|
||||
const applyLayout = (open) => {
|
||||
// Let overlays that mirror the content area (page modal) resize at exactly the same moment
|
||||
window.dispatchEvent(new CustomEvent('f0ck:sidebar-right-layout', { detail: { open, width: sidebarWidth, overlay: !animateWrappers } }));
|
||||
if (!animateWrappers) return;
|
||||
wrappers.forEach(w => {
|
||||
const isRightEdge = w.classList.contains('pagination-container-fluid') || w.classList.contains('admin-bar');
|
||||
const isMessages = w.classList.contains('messages-convo-page');
|
||||
const prop = (isRightEdge || isMessages) ? 'right' : 'padding-right';
|
||||
if (isEdgeWrapper(w)) return;
|
||||
w.style.setProperty('transition', 'none', 'important');
|
||||
w.style.setProperty(prop, open ? sidebarWidth + 'px' : '0px', 'important');
|
||||
});
|
||||
if (onaraModal) {
|
||||
onaraModal.style.setProperty('transition', 'none', 'important');
|
||||
onaraModal.style.setProperty('right', open ? sidebarWidth + 'px' : '0px', 'important');
|
||||
}
|
||||
// #bg canvas is handled separately below — it transitions smoothly alongside the sidebar
|
||||
// Soften the one-time reflow
|
||||
wrappers.forEach(w => {
|
||||
if (w.classList.contains('pagination-container-fluid') || w.classList.contains('admin-bar')) return;
|
||||
w.style.setProperty('padding-right', open ? sidebarWidth + 'px' : '0px', 'important');
|
||||
if (typeof w.animate === 'function') w.animate([{ opacity: 0.55 }, { opacity: 1 }], { duration: 220, easing: 'ease-out' });
|
||||
});
|
||||
if (typeof window.redrawBgCanvas === 'function') requestAnimationFrame(() => window.redrawBgCanvas());
|
||||
};
|
||||
|
||||
// Opening: make room first, then slide in over empty space
|
||||
// Item view: animate the grid's sidebar track so media and comments glide with the sidebar instead
|
||||
// of jumping when the class flips at the end. The target comes from the current track sizes
|
||||
// (last track = sidebar column, the one before it = flexible content column) rather than from
|
||||
// flipping the class early, which would kick off unrelated CSS transitions.
|
||||
let gridAnim = null;
|
||||
const itemLayout = animateWrappers && !document.body.classList.contains('onara-modal-open')
|
||||
? document.querySelector('.item-layout-container') : null;
|
||||
if (itemLayout && typeof itemLayout.animate === 'function') {
|
||||
const from = getComputedStyle(itemLayout).gridTemplateColumns;
|
||||
const tracks = from && from !== 'none' ? from.trim().split(/\s+/) : [];
|
||||
const px = tracks.map(t => (/^-?[\d.]+px$/.test(t) ? parseFloat(t) : NaN));
|
||||
if (tracks.length >= 2 && px.every(n => !isNaN(n))) {
|
||||
const last = px.length - 1;
|
||||
const newLast = isHidden ? sidebarWidth : 0;
|
||||
if (Math.abs(px[last] - newLast) > 0.5) {
|
||||
const to = px.slice();
|
||||
to[last - 1] = Math.max(0, px[last - 1] + (px[last] - newLast));
|
||||
to[last] = newLast;
|
||||
itemLayout.style.setProperty('transition', 'none', 'important');
|
||||
gridAnim = itemLayout.animate(
|
||||
[{ gridTemplateColumns: px.map(n => n + 'px').join(' ') }, { gridTemplateColumns: to.map(n => n + 'px').join(' ') }],
|
||||
{ duration: DURATION, easing: EASING, fill: 'forwards' }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveEdges(isHidden);
|
||||
// Opening: make room in thumbnail grids first, then slide in over empty space
|
||||
if (isHidden) applyLayout(true);
|
||||
|
||||
// The drag-zone pill follows the sidebar edge (small fixed element: cheap to animate)
|
||||
@@ -17477,22 +17546,17 @@ window.cancelAnimFrame = (function () {
|
||||
edgeZone.style.pointerEvents = 'all';
|
||||
}
|
||||
|
||||
// Commit the starting width for #bg so the transition has a defined origin
|
||||
if (bgCanvas && animateWrappers) {
|
||||
const bgStartWidth = isHidden ? '100vw' : `calc(100vw - ${sidebarWidth}px)`;
|
||||
bgCanvas.style.setProperty('width', bgStartWidth, 'important');
|
||||
}
|
||||
|
||||
sidebar.style.willChange = 'transform';
|
||||
void sidebar.offsetWidth; // commit the start state before transitions begin
|
||||
sidebar.style.setProperty('transition', `transform ${DURATION}ms ${EASING}`, 'important');
|
||||
sidebar.style.transform = `translateX(${targetTranslate}px)`;
|
||||
|
||||
// #bg canvas width transitions in sync with the sidebar slide (position:fixed, no layout cost)
|
||||
// #bg canvas: same size in both states, it only shifts to stay centred under the content. Moving it
|
||||
// (transform, GPU) instead of resizing it means no stretched frame mid-slide and no redraw at the end.
|
||||
if (bgCanvas && animateWrappers) {
|
||||
const bgTargetWidth = isHidden ? `calc(100vw - ${sidebarWidth}px)` : '100vw';
|
||||
bgCanvas.style.setProperty('transition', `width ${DURATION}ms ${EASING}`, 'important');
|
||||
bgCanvas.style.setProperty('width', bgTargetWidth, 'important');
|
||||
const bgShift = isHidden ? -sidebarWidth / 2 : 0;
|
||||
bgCanvas.style.setProperty('transition', `transform ${DURATION}ms ${EASING}`, 'important');
|
||||
bgCanvas.style.setProperty('transform', `translate3d(${bgShift}px, 0, 0) scale(var(--bg-canvas-zoom, 1.38))`, 'important');
|
||||
}
|
||||
|
||||
let settled = false;
|
||||
@@ -17522,16 +17586,16 @@ window.cancelAnimFrame = (function () {
|
||||
}
|
||||
if (bgCanvas) {
|
||||
bgCanvas.style.removeProperty('transition');
|
||||
bgCanvas.style.removeProperty('width');
|
||||
bgCanvas.style.removeProperty('transform');
|
||||
}
|
||||
wrappers.forEach(w => {
|
||||
w.style.removeProperty('transition');
|
||||
w.style.removeProperty('padding-right');
|
||||
w.style.removeProperty('right');
|
||||
});
|
||||
if (typeof window.redrawBgCanvas === 'function') {
|
||||
window.redrawBgCanvas();
|
||||
}
|
||||
// The class now yields the columns the animation ended on: hand back to the stylesheet
|
||||
if (gridAnim) gridAnim.cancel();
|
||||
if (itemLayout) itemLayout.style.removeProperty('transition');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -22685,6 +22749,12 @@ document.addEventListener('click', (e) => {
|
||||
// Trackpad sideways swipes (deltaX dominant) are left to the browser.
|
||||
(() => {
|
||||
const WHEEL_X_SEL = '.f0ck-tuner-subtabs-nav, .f0ck-tuner-mode-bar, #settings-quicknav';
|
||||
const WHEEL_X_SPEED = 0.45; // fraction of the wheel delta applied per notch
|
||||
const WHEEL_X_EASE = 0.12; // share of the remaining distance covered per frame
|
||||
const stopGlide = (bar) => {
|
||||
if (bar._wheelRaf) cancelAnimationFrame(bar._wheelRaf);
|
||||
bar._wheelRaf = null;
|
||||
};
|
||||
document.addEventListener('wheel', (e) => {
|
||||
if (e.ctrlKey || e.defaultPrevented) return; // ctrl+wheel = zoom
|
||||
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
||||
@@ -22695,7 +22765,9 @@ document.addEventListener('click', (e) => {
|
||||
const dir = Math.sign(e.deltaY);
|
||||
// The wheel stays with the bar while the pointer is on it (no page scroll)
|
||||
e.preventDefault();
|
||||
if ((dir < 0 && bar.scrollLeft <= 0) || (dir > 0 && bar.scrollLeft >= max - 1)) {
|
||||
// While a glide is running, measure against where it is heading, not where it currently is
|
||||
const pos = bar._wheelRaf ? bar._wheelTarget : bar.scrollLeft;
|
||||
if ((dir < 0 && pos <= 0) || (dir > 0 && pos >= max - 1)) {
|
||||
// At the end: short rubber-band stretch past the edge, then ease back. A running stretch is not
|
||||
// restarted, so a continuous wheel spin reads as one bounce instead of a jitter.
|
||||
const items = Array.from(bar.children).filter(el => typeof el.animate === 'function' && el.offsetParent !== null);
|
||||
@@ -22722,7 +22794,30 @@ document.addEventListener('click', (e) => {
|
||||
}
|
||||
// deltaMode 1 = lines (Firefox mouse wheels), 2 = pages
|
||||
const amount = Math.abs(e.deltaMode === 1 ? e.deltaY * 16 : e.deltaMode === 2 ? e.deltaY * bar.clientWidth : e.deltaY);
|
||||
bar.scrollLeft = Math.max(0, Math.min(max, bar.scrollLeft + dir * amount));
|
||||
// Slower than a raw wheel step, and eased: each notch moves the target, the bar glides after it
|
||||
bar._wheelTarget = Math.max(0, Math.min(max, pos + dir * amount * WHEEL_X_SPEED));
|
||||
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
bar.scrollLeft = bar._wheelTarget;
|
||||
return;
|
||||
}
|
||||
if (!bar._wheelRaf) {
|
||||
if (!bar._wheelCancelBound) {
|
||||
// Grabbing the bar (touch/pen/mouse) hands control back to the user
|
||||
bar.addEventListener('pointerdown', () => stopGlide(bar), { passive: true });
|
||||
bar._wheelCancelBound = true;
|
||||
}
|
||||
const step = () => {
|
||||
const diff = bar._wheelTarget - bar.scrollLeft;
|
||||
if (Math.abs(diff) < 0.5) {
|
||||
bar.scrollLeft = bar._wheelTarget;
|
||||
bar._wheelRaf = null;
|
||||
return;
|
||||
}
|
||||
bar.scrollLeft += diff * WHEEL_X_EASE;
|
||||
bar._wheelRaf = requestAnimationFrame(step);
|
||||
};
|
||||
bar._wheelRaf = requestAnimationFrame(step);
|
||||
}
|
||||
}, { passive: false });
|
||||
})();
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
@endif
|
||||
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
||||
@if(enable_comments)
|
||||
<i class="iconset {{ isSubscribed ? 'fa-solid' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
|
||||
<i class="iconset {{ isSubscribed ? 'fa-solid active' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
|
||||
@endif
|
||||
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
|
||||
@if(halls_enabled)
|
||||
@@ -212,7 +212,7 @@
|
||||
@endif
|
||||
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
||||
@if(enable_comments)
|
||||
<i class="iconset {{ isSubscribed ? 'fa-solid' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
|
||||
<i class="iconset {{ isSubscribed ? 'fa-solid active' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
|
||||
@endif
|
||||
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
|
||||
@if(halls_enabled)
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
@if(session)
|
||||
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
||||
@if(enable_comments)
|
||||
<i class="iconset {{ isSubscribed ? 'fa-solid' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
|
||||
<i class="iconset {{ isSubscribed ? 'fa-solid active' : 'fa-regular' }} fa-bell" id="subscribe-btn" data-item-id="{{ item.id }}" title="{{ isSubscribed ? 'Subscribed' : 'Subscribe' }}"></i>
|
||||
@endif
|
||||
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
|
||||
@if(can_manage_item)
|
||||
|
||||
Reference in New Issue
Block a user