gfdgfds
This commit is contained in:
@@ -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 });
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user