From ed42b1595c92bb78c3c1a770605a31f0b60bb875 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Tue, 29 Sep 2026 19:48:29 +0200 Subject: [PATCH] fdsfads --- public/s/css/f0ckm.css | 578 +++++++++++++++++++++++++-------- public/s/js/comments.js | 45 ++- public/s/js/f0ckm.js | 255 ++++++++++++--- src/inc/routes/apiv2/index.mjs | 62 ++++ views/admin/users.html | 8 +- views/item-partial-legacy.html | 19 +- views/ranking.html | 1 - views/snippets/item-media.html | 11 +- views/user-partial.html | 8 +- 9 files changed, 760 insertions(+), 227 deletions(-) diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 1bb9212..5e72e26 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -3101,7 +3101,6 @@ body.sidebar-right-hidden #sidebar-drag-zone::after { .sidebar-video-card.touch-active { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 255, 255, 0.12); - transform: translateY(-1px); } .sidebar-video-card.sidebar-card-swapping { @@ -3134,10 +3133,6 @@ body.sidebar-right-hidden #sidebar-drag-zone::after { -webkit-user-select: none; } -.sidebar-video-card:hover .sidebar-video-thumb { - transform: scale(1.04); -} - .sidebar-video-badges { position: absolute; bottom: 6px; @@ -9983,12 +9978,6 @@ div.logwrap>p { } } -.admin-icon { - margin-right: 6px; - color: #ffeb3b; - font-size: 0.9em; -} - .topf0ckers { text-align: center; padding: 20px 0; @@ -11836,10 +11825,12 @@ video.autoplay-gif { align-items: center; justify-content: center; gap: 5px; - border-radius: 3px; + border-radius: 0; border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); background: var(--navigation-links-bg, rgba(255, 255, 255, 0.08)); color: var(--white, #fff); + box-shadow: none; + text-shadow: none; font-family: inherit; font-size: 13px; font-weight: bold; @@ -11895,20 +11886,29 @@ video.autoplay-gif { opacity: 0.9; } -/* Cancel buttons */ +/* Cancel buttons: regular theme buttons (style guide), accent on hover like the others */ .input-actions .cancel-reply, .input-actions .cancel-edit-btn { - background: #555; - color: #fff; - border: 1px solid #555; padding: 0 10px; } -.input-actions .cancel-reply:hover, -.input-actions .cancel-edit-btn:hover { - background: #666; - border-color: #666; - color: #fff; +/* Comment / DM input buttons: style guide (square, flat, theme colours, no glow) */ +.input-actions, +.input-actions * { + border-radius: 0 !important; +} + +.input-actions button:active { + opacity: 0.75; +} + +.input-actions button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +.emoji-picker { + box-shadow: none !important; } /* Light / Paper Theme */ @@ -15138,6 +15138,94 @@ html[theme='paper'] .badge.badge-untagged { cursor: pointer; } +/* ── #tags box (tags, rating badge, add-tag button): style guide (square, flat, theme colours, no glow) ── + Same rules as the .gapRight action row. Rating badges keep their rating colours. */ +span#tags, +span#tags *, +span#tags *::before, +span#tags *::after { + border-radius: 0 !important; +} + +span#tags :is(.badge, .rating-tag, .rating-label, .tag-btn, a, i) { + box-shadow: none !important; + text-shadow: none !important; +} + +/* The "new tag" highlight fades the background only: flat, no glow */ +span#tags .new-tag-glow { + box-shadow: none !important; +} + +/* One height for everything in the row: tags, rating badge, add-tag button and the tag input. Each used + to get its height from its own padding/borders (25.4 / 26.4 / 21 / 20px); now they share --tag-h + (the rating badge's height: 1.4em line + its label border) and centre their content inside it */ +span#tags { + --tag-h: calc(1.4em + 4px); +} + +span#tags .tags-inner > .badge, +span#tags .tag-btn, +span#tags .tag-ac-wrapper { + box-sizing: border-box; + height: var(--tag-h); + display: inline-flex; + align-items: center; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +span#tags .tag-controls { + padding-top: 2.5px; + padding-bottom: 2.5px; +} + +span#tags .tag-ac-wrapper form { + height: 100%; +} + +span#tags .tag-ac-input { + box-sizing: border-box; + height: 100%; +} + +span#tags .tag-btn { + min-width: 26px; + background: var(--navigation-links-bg, rgba(255, 255, 255, 0.08)); + border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); + color: var(--white, #fff) !important; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; +} + +span#tags .tag-btn:hover, +span#tags .tag-btn.is-active { + background: var(--nav-link-hover-bg, rgba(255, 255, 255, 0.15)); + border-color: var(--accent); + color: var(--accent) !important; +} + +/* Rating-coloured tag buttons keep their rating colour (hover still shows the accent border) */ +span#tags .tag-btn.is-sfw:not(:hover) { + color: #28a745 !important; +} + +span#tags .tag-btn.is-nsfw:not(:hover) { + color: #dc3545 !important; +} + +span#tags :is(.tag-btn, .rating-tag.can-cycle, .badge a):hover { + transform: none; +} + +span#tags :is(.tag-btn, .rating-tag.can-cycle):active { + opacity: 0.75; +} + +span#tags :is(.tag-btn, .rating-tag, .badge a):focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + .rules { @@ -17844,16 +17932,141 @@ i.iconset#a_oc { padding: 5px; } +/* Profile stat chips: style guide (square, flat, theme colours, no glow). !important beats the + per-theme shadow rules (amoled/light/paper) that would bring the shadow back */ .profile_head_user_stats > * { - background: var(--bg); - padding: 0px 5px 0px 5px; - border-radius: 5px; - box-shadow: 1px 1px 1px black; + /* Same 30px height as #edit-profile-btn (border included), text centred */ + box-sizing: border-box; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + line-height: 1; + white-space: nowrap; + background: var(--bg); + border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); + padding: 0 8px; + border-radius: 0 !important; + box-shadow: none !important; + text-shadow: none !important; } + +.profile_head_user_stats a { + color: var(--accent); +} + +/* Normal cursor on the chips; pointer only where something happens: the joined date (hover tooltip) + and the comment / tag / hall count links */ +.profile_head_user_stats > * { + cursor: default; +} + +.profile_head_user_stats > .stat-joined, +.profile_head_user_stats a { + cursor: pointer; +} + +/* Edit-profile toggle and edit-banner button on the profile banner: square, flat, theme colours, one + size (30x30); accent on hover and while editing. The banner button is positioned by its wrapper. */ +#edit-profile-btn { + position: absolute; + bottom: 5px; + left: 5px; + z-index: 20; +} + +#edit-profile-btn, +#inline-banner-edit-btn { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + padding: 0; + font-size: 13px; + line-height: 1; + background: var(--dropdown-bg, #232323); + border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); + color: var(--white, #fff); + border-radius: 0 !important; + box-shadow: none !important; + text-shadow: none !important; + cursor: pointer; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; +} + +#edit-profile-btn:hover, +#edit-profile-btn.is-active, +#inline-banner-edit-btn:hover { + background: var(--nav-link-hover-bg, #333); + border-color: var(--accent); + color: var(--accent); +} + +#edit-profile-btn:active, +#inline-banner-edit-btn:active { + opacity: 0.75; +} + +#edit-profile-btn:focus-visible, +#inline-banner-edit-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 1px; +} +/* Profile content (uploads / favourites panels): style guide (square, flat, theme colours, no glow). + Each section is a panel with a title bar; the thumbnails inside keep their own grid styling. */ +.user_content_wrapper { + gap: 8px; + padding: 8px; +} + +.user_content_wrapper > .user-uploads, +.user_content_wrapper > .favs { + min-width: 0; + background: var(--dropdown-bg, #232323); + border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); + border-radius: 0 !important; + box-shadow: none !important; +} + .uploads-header, .favs-header { - background: var(--black); - padding: 5px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + background: var(--bg, #111); + border-bottom: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); + padding: 6px 8px; + font-weight: bold; + border-radius: 0 !important; + box-shadow: none !important; + text-shadow: none !important; +} + +.uploads-header a, +.favs-header a { + color: var(--accent); + font-weight: normal; + text-decoration: none; +} + +.uploads-header a:hover, +.favs-header a:hover { + text-decoration: underline; +} + +.user_content_wrapper div.user-uploads div.posts, +.user_content_wrapper div.favs div.posts { + background: transparent; + padding: 6px; +} + +.user_content_wrapper .private-favs-msg { + padding: 12px 8px; + color: var(--text-muted, rgba(255, 255, 255, 0.6)); } @@ -22047,8 +22260,11 @@ div#flash, padding: 6px 12px; box-shadow: none !important; opacity: 0; + /* Hidden = really hidden: no Tab stops or screen-reader access to its buttons. visibility flips only + after the slide/fade out has finished (0.2s delay), and immediately when showing (see .visible) */ + visibility: hidden; pointer-events: none; - transition: transform 0.2s ease, opacity 0.2s ease; + transition: transform 0.2s ease, opacity 0.2s ease, visibility 0s linear 0.2s; user-select: none; font-family: var(--font, monospace) !important; max-width: 100vw; @@ -22057,7 +22273,9 @@ div#flash, #bulk-action-bar.visible { transform: translateX(-50%) translateY(0); opacity: 1; + visibility: visible; pointer-events: auto; + transition: transform 0.2s ease, opacity 0.2s ease, visibility 0s linear 0s; } .bulk-count-box { @@ -22085,79 +22303,79 @@ div#flash, } .bulk-actions-group { - display: flex; - align-items: center; + /* Equal columns: every action button is exactly as wide as the widest one */ + display: grid; + grid-auto-flow: column; + grid-auto-columns: 1fr; + align-items: stretch; gap: 6px; - flex-wrap: nowrap; + /* Sized to its content so 1fr resolves to the widest button, not to a share of the bar */ + width: max-content; } -.bulk-btn { +/* All bar buttons: style guide (square, flat, theme colours, no glow), one size, one look. + Delete / Add Tags / close used to have their own filled or red variants; they are plain now. */ +.bulk-btn, +.bulk-btn-primary, +.bulk-btn-danger, +.bulk-btn-close { + box-sizing: border-box; display: inline-flex; align-items: center; - gap: 5px; - padding: 5px 10px; + justify-content: center; + gap: 6px; + width: 100%; + height: 30px; + margin: 0; + padding: 0 10px; border-radius: 0 !important; font-size: 12px; + font-weight: normal !important; font-family: var(--font, monospace) !important; - border: 1px solid var(--nav-border-color, #444); - background: var(--nav-bg, #2b2b2b); - color: var(--white, #fff); + line-height: 1; + border: 1px solid var(--nav-border-color, #444) !important; + background: var(--navigation-links-bg, var(--nav-bg, #2b2b2b)) !important; + color: var(--white, #fff) !important; cursor: pointer; box-shadow: none !important; - transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease; + text-shadow: none !important; + filter: none !important; + transform: none !important; + transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease; white-space: nowrap; } -.bulk-btn:hover { - background: var(--nav-link-hover-bg, #333); - color: var(--accent, #9f0); - border-color: var(--accent, #9f0); - transform: none !important; +.bulk-btn:hover, +.bulk-btn:focus-visible, +.bulk-dropdown-wrap:has(.bulk-dropdown-menu.show) > .bulk-btn { + background: var(--nav-link-hover-bg, #333) !important; + color: var(--accent, #9f0) !important; + border-color: var(--accent, #9f0) !important; +} + +.bulk-btn:focus-visible { + outline: 2px solid var(--accent, #9f0); + outline-offset: 1px; } .bulk-btn:active { - transform: none !important; -} - -.bulk-btn-primary { - background: var(--accent, #9f0) !important; - color: var(--black, #000) !important; - border: 1px solid var(--accent, #9f0) !important; - font-weight: bold !important; - box-shadow: none !important; -} - -.bulk-btn-primary:hover { - filter: brightness(1.15); + opacity: 0.75; } +/* Close (x) sits outside the equal-width group: same height and look, square */ .bulk-btn-close { - padding: 5px 8px; - background: var(--nav-bg, #2b2b2b); - color: var(--white, #fff); - border: 1px solid var(--nav-border-color, #444); + width: 30px; + flex: 0 0 30px; + padding: 0; margin-right: 4px; } -.bulk-btn-close:hover { - background: #ff4444 !important; - color: #fff !important; - border-color: #ff4444 !important; +.bulk-btn .bulk-chevron { + font-size: 10px; } -.bulk-btn-danger { - background: transparent !important; - color: #ff5555 !important; - border: 1px solid #ff5555 !important; - font-weight: bold !important; - box-shadow: none !important; -} - -.bulk-btn-danger:hover { - background: #ff4444 !important; - color: #fff !important; - border-color: #ff4444 !important; - filter: none !important; +.bulk-dropdown-wrap { + display: flex; } /* Bulk rating dropdown wrapper */ @@ -22463,13 +22681,14 @@ div#flash, gap: 4px; } + /* Phones: icon-only buttons, still all the same size */ .bulk-btn { - padding: 5px 7px; - font-size: 11.5px; + padding: 0 8px; gap: 3px; } - .bulk-btn .btn-text-full { + .bulk-btn > span, + .bulk-btn .bulk-chevron { display: none; } @@ -22561,53 +22780,6 @@ div#flash, color: var(--accent); } -.album-counter-pill { - position: absolute; - top: 14px; - left: 50%; - transform: translateX(-50%); - pointer-events: none; - opacity: 0; - background: rgba(16, 16, 20, 0.75); - border: 1px solid rgba(255, 255, 255, 0.2); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - color: #fff; - padding: 5px 14px; - border-radius: 20px; - font-size: 0.85rem; - font-weight: 600; - display: flex; - align-items: center; - gap: 7px; - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4); - letter-spacing: 0.03em; - user-select: none; - transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); -} - -@media (hover: hover) { - .album-gallery-container:hover .album-counter-pill, - .album-gallery-container:focus-within .album-counter-pill { - opacity: 1; - pointer-events: auto; - } -} - -.album-gallery-container.v0ck_hover .album-counter-pill, -.album-gallery-container.strip-peek .album-counter-pill, -.album-gallery-container:has(.v0ck.v0ck_hover) .album-counter-pill, -.album-counter-pill.is-visible, -.album-counter-pill.strip-peek { - opacity: 1; - pointer-events: auto; -} - -.album-counter-pill i { - color: var(--accent); - font-size: 0.95rem; -} - .album-thumbnails-strip { position: absolute; bottom: 12px; @@ -22748,6 +22920,13 @@ div#flash, .album-gallery-container.is-video-active .album-thumbnails-strip, .album-gallery-container.is-audio-active .album-thumbnails-strip { + /* Follows the player controls: lifted above them only while they are shown (.v0ck_hover), back + down with them when they auto-hide, on the same 0.3s timing as the controls' slide */ + transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), bottom 0.3s ease; +} + +.album-gallery-container.is-video-active:has(.v0ck.v0ck_hover) .album-thumbnails-strip, +.album-gallery-container.is-audio-active:has(.v0ck.v0ck_hover) .album-thumbnails-strip { bottom: 58px; } @@ -22762,8 +22941,8 @@ div#flash, } @media (max-width: 768px) { - .album-gallery-container.is-video-active .album-thumbnails-strip, - .album-gallery-container.is-audio-active .album-thumbnails-strip { + .album-gallery-container.is-video-active:has(.v0ck.v0ck_hover) .album-thumbnails-strip, + .album-gallery-container.is-audio-active:has(.v0ck.v0ck_hover) .album-thumbnails-strip { bottom: 48px; } .album-gallery-container.is-youtube-active .album-thumbnails-strip { @@ -22855,11 +23034,6 @@ div#flash, .album-controls-overlay { padding: 0 8px; } - .album-counter-pill { - top: 10px; - padding: 3px 10px; - font-size: 0.78rem; - } .album-thumb-item { width: 36px; height: 36px; @@ -23202,4 +23376,138 @@ html.range-tap-lock input[type="range"].range-unlocked { a#nav-upload-link { user-select: none; -} \ No newline at end of file +} +/* Background toggle (l key / player button): the fade out matches the 1.5s fade in instead of the quick + 0.22s navigation fade-out. bg-toggle-fade is set by toggleBackground() while the background is off. */ +#bg.fader-out.bg-toggle-fade, +body.onara-modal-open #bg.bg-toggle-fade:is(.fader-in, .fader-out) { + transition: opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1), transform 0.6s cubic-bezier(0.45, 0, 0.2, 1) !important; +} + +/* ── Album thumbnail strip: style guide (square, flat, theme colours, no glow) ── + Same look as the bulk bar / tuner: theme panel background, nav border, accent for state. No blur, + no shadows, no hover/active scaling (the active thumb is marked by its accent border instead). + Positioning, show/hide and the mobile sizes are left to the rules above. */ +.album-thumbnails-strip, +.album-thumb-item, +.album-thumb-mime-badge { + border-radius: 0 !important; + box-shadow: none !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; +} + +.album-thumbnails-strip { + background: var(--dropdown-bg, #232323); + border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); + padding: 4px; + gap: 4px; +} + +.album-thumb-item { + border: 2px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); + opacity: 0.55; + transition: opacity 0.15s ease, border-color 0.15s ease; +} + +.album-thumb-item:hover, +.album-thumb-item:focus-visible { + opacity: 0.9; + transform: none; +} + +.album-thumb-item.active { + opacity: 1; + border-color: var(--accent, #9f0); + transform: none; +} + +.album-thumb-item:focus-visible { + outline: 2px solid var(--accent, #9f0); + outline-offset: 1px; +} + +.album-thumb-mime-badge { + background: var(--bg, #111); + color: var(--white, #fff); + border: 1px solid var(--nav-border-color, rgba(255, 255, 255, 0.15)); + bottom: 0; + right: 0; +} + +/* Dragging the album strip: grabbing cursor everywhere until the mouse is released (the thumbnails' + own pointer cursor would otherwise win, and leaving the strip would drop back to the page cursor) */ +html.album-strip-dragging, +html.album-strip-dragging * { + cursor: grabbing !important; + user-select: none !important; +} + +/* Legacy comment slide (comments.js slideLegacyComments): while it runs, the container's height comes + from --comments-slide-h (animated) and it clips its content, so the comments disappear exactly at its + top edge, right under the .blahlol box. !important + the extra class outrank layouts that pin the + container (Onara: height: auto !important; overflow: visible !important). Registered so the height + animates smoothly instead of jumping. */ +@property --comments-slide-h { + syntax: ''; + inherits: false; + initial-value: 0px; +} + +#comments-container.is-comments-sliding, +#onara-modal #comments-container.is-comments-sliding { + height: var(--comments-slide-h) !important; + min-height: 0 !important; + max-height: none !important; + overflow: hidden !important; + flex: 0 0 auto !important; +} + +/* Legacy .blahlol meta row (infobox off): ID / user / OC badge on the left, time (+ expiry) on the right */ +.blahlol-meta-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 6px 12px; + padding: 2px 6px; +} + +/* .blahlol hands down a 32px line-height (the time link has its own 28px): use the tags' 1.4 so the + row is as compact as the tag row below it */ +.blahlol-meta-row, +.blahlol-meta-row * { + line-height: 1.4; +} + +.blahlol-meta-row > .blahlol-id-badge { + display: inline-flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + min-width: 0; +} + +.blahlol-meta-row > .blahlol-meta-right { + display: inline-flex; + align-items: center; + gap: 6px; + margin-left: auto; +} + +/* Meta row: the time (smaller font) gets exactly the same box height as the user badge instead of a + line box from its own font size; the text is centred inside it */ +.blahlol-meta-row > .blahlol-id-badge, +.blahlol-meta-row > .blahlol-meta-right { + box-sizing: border-box; + height: 1.4em; +} + +.blahlol-meta-right > .badge, +.blahlol-meta-right .timestamp-link, +.blahlol-meta-right .timeago { + box-sizing: border-box; + display: inline-flex; + align-items: center; + height: 100%; +} diff --git a/public/s/js/comments.js b/public/s/js/comments.js index ea9eca1..8ab1910 100644 --- a/public/s/js/comments.js +++ b/public/s/js/comments.js @@ -4223,17 +4223,20 @@ class CommentSystem { 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'; + // Height + clipping go through a class and a registered custom property (see f0ckm.css): layouts + // like the Onara modal pin the container with `height: auto !important; overflow: visible + // !important`, which beats both an animated height and an inline overflow. The class rule is + // more specific and !important, and its height reads the variable the animation drives. + el.style.setProperty('--comments-slide-h', endH + 'px'); + el.classList.add('is-comments-sliding'); 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)]; + const anims = [el.animate([{ '--comments-slide-h': startH + 'px' }, { '--comments-slide-h': endH + 'px' }], opts)]; kids.forEach(k => anims.push(k.animate(frames(startH, endH), opts))); const slide = { dir: hide ? 'hide' : 'show', anims, el }; @@ -4241,8 +4244,8 @@ class CommentSystem { anims[0].onfinish = () => { if (this._legacySlide !== slide) return; this._legacySlide = null; - el.style.removeProperty('overflow'); - el.style.removeProperty('height'); + el.classList.remove('is-comments-sliding'); + el.style.removeProperty('--comments-slide-h'); if (!hide) { document.body.classList.remove('comments-sliding'); return; @@ -5005,6 +5008,28 @@ class CommentSystem { }, { once: true }); }; + // Bring the whole picker into view with some room below it. Measured once the enter animation has + // finished (mid-animation it is shifted up and slightly squashed, so it would look smaller than it + // is) and scrolled on whatever actually scrolls here (Onara modal, index container or the window). + const revealPicker = () => { + if (!picker || picker.style.display === 'none') return; + let done = false; + const run = () => { + if (done || !picker || picker.style.display === 'none') return; + done = true; + const MARGIN = 24; + const sc = window._f0ckScrollerFor ? window._f0ckScrollerFor(picker) : null; + const view = sc ? sc.getBoundingClientRect() : { top: 0, bottom: window.innerHeight }; + const r = picker.getBoundingClientRect(); + let delta = r.bottom + MARGIN - view.bottom; + // Never push the picker's own top out of view (very short viewports) + delta = Math.min(delta, r.top - view.top - 8); + if (delta > 0) (sc || window).scrollBy({ top: delta, behavior: 'smooth' }); + }; + picker.addEventListener('animationend', run, { once: true }); + setTimeout(run, 300); + }; + trigger.addEventListener('click', (e) => { e.preventDefault(); @@ -5028,9 +5053,7 @@ class CommentSystem { picker.offsetHeight; // reflow picker.style.animation = ''; trigger.classList.add('is-active'); - requestAnimationFrame(() => requestAnimationFrame(() => - picker.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) - )); + revealPicker(); } return; } @@ -5093,9 +5116,7 @@ class CommentSystem { } container.appendChild(picker); - requestAnimationFrame(() => requestAnimationFrame(() => - picker.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) - )); + revealPicker(); }); } diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 4b067df..9eb85e5 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -809,6 +809,36 @@ window.cancelAnimFrame = (function () { let video; let isNavigating = false; + // Next/prev presses that arrive while an item is still loading: +1 per next, -1 per prev. Each + // finished load immediately takes the next step (runNavQueue), so mashing next flies through items + // instead of dropping every press after the first. Capped so holding the key can't run forever. + let navQueue = 0; + const NAV_QUEUE_MAX = 25; + // Step inside the current item's album if there is a slide left in that direction. Returns true when + // it stepped (the post navigation must then not happen). + const albumNavStep = (direction) => { + const gal = window._currentActiveAlbumGallery; + if (!gal || typeof gal.getIndex !== 'function' || (gal.isAlive && !gal.isAlive())) return false; + const idx = gal.getIndex(); + const count = gal.getCount(); + if (!(count > 1)) return false; + if (direction === 'next' && idx < count - 1) { gal.next(); return true; } + if (direction === 'prev' && idx > 0) { gal.prev(); return true; } + return false; + }; + const runNavQueue = () => { + if (!navQueue || isNavigating) return; + const dir = navQueue > 0 ? 'next' : 'prev'; + const el = dir === 'next' + ? document.querySelector(".steuerung .nav-next:not([href='#']), .nav-next:not([href='#']), #next:not([href='#'])") + : document.querySelector(".steuerung .nav-prev:not([href='#']), .nav-prev:not([href='#']), #prev:not([href='#'])"); + if (!el || !el.href || el.href.endsWith('#')) { + navQueue = 0; // reached the end of the list + return; + } + navQueue += dir === 'next' ? -1 : 1; + el.click(); + }; const main = document.getElementById('main'); let posts = document.querySelector('.posts'); const navbar = document.querySelector("nav.navbar"); @@ -3149,6 +3179,10 @@ window.cancelAnimFrame = (function () { background = !background; window.background = background; localStorage.setItem('background', background ? 'true' : 'false'); + // Toggling (l key / player button): fade out exactly as slowly as it fades in. Plain fader-out is + // the quick navigation fade, so the toggle marks the canvas while the background is off. + const bgEl = document.getElementById('bg'); + if (bgEl) bgEl.classList.toggle('bg-toggle-fade', !background); window.initBackground(); // Update videoplayer toggle buttons if they exist @@ -3597,8 +3631,6 @@ window.cancelAnimFrame = (function () { const linkEl = container.querySelector('#elfe, .album-stage-link'); const prevBtn = container.querySelector('.album-btn-prev'); const nextBtn = container.querySelector('.album-btn-next'); - const currentIdxEl = container.querySelector('.album-current-idx'); - const totalCountEl = container.querySelector('.album-total-count'); const thumbItems = container.querySelectorAll('.album-thumb-item'); const stripEl = container.querySelector('.album-thumbnails-strip'); @@ -3613,7 +3645,6 @@ window.cancelAnimFrame = (function () { }, duration); } - if (totalCountEl) totalCountEl.textContent = albumData.length; let albumVideoV0ck = null; const initAlbumVideoV0ck = () => { @@ -3953,10 +3984,6 @@ window.cancelAnimFrame = (function () { linkEl.href = newSrc; } - if (currentIdxEl) { - currentIdxEl.textContent = currentIndex + 1; - } - thumbItems.forEach((btn, idx) => { const isActive = idx === currentIndex; btn.classList.toggle('active', isActive); @@ -4152,8 +4179,19 @@ window.cancelAnimFrame = (function () { }); } + // Arrived here by stepping backwards (prev from the following post): start on the LAST slide, so + // going back through albums mirrors going forward. Only when no specific slide was requested. + let enterFromEnd = false; + if (window._albumEnterFromEnd) { + window._albumEnterFromEnd = false; + if (!initialHash) { + currentIndex = albumData.length - 1; + enterFromEnd = true; + } + } + // Ensure proper initial media display (especially if initial item is video or subf0ck hash was requested) - showImage(currentIndex, 'none', !!initialHash); + showImage(currentIndex, 'none', !!initialHash || enterFromEnd); if (prevBtn) { prevBtn.addEventListener('click', (e) => { @@ -4182,6 +4220,10 @@ window.cancelAnimFrame = (function () { let lastX = 0; let lastTime = 0; + // Thumbnails are images inside buttons: without this the browser starts a native image drag on + // them (Firefox ignores -webkit-user-drag), which swallows the mousemoves the strip scrolls with + stripEl.addEventListener('dragstart', (e) => e.preventDefault()); + stripEl.addEventListener('mousedown', (e) => { if (e.button !== 0) return; isDown = true; @@ -4201,6 +4243,8 @@ window.cancelAnimFrame = (function () { if (Math.abs(walk) > 6) { hasDraggedStrip = true; stripEl.classList.add('is-dragging'); + // Grabbing cursor for the whole drag, also over thumbnails and outside the strip + document.documentElement.classList.add('album-strip-dragging'); e.preventDefault(); stripEl.scrollLeft = startScrollLeft - walk; @@ -4215,6 +4259,7 @@ window.cancelAnimFrame = (function () { if (!isDown) return; isDown = false; stripEl.classList.remove('is-dragging'); + document.documentElement.classList.remove('album-strip-dragging'); showStripPeek(6000); if (hasDraggedStrip && Math.abs(velX) > 0.15) { @@ -4393,6 +4438,10 @@ window.cancelAnimFrame = (function () { window._currentActiveAlbumGallery = { prev: () => showImage(currentIndex - 1, 'prev'), next: () => showImage(currentIndex + 1, 'next'), + // Used by next/prev post navigation to step through the slides first (no wrap-around there) + getIndex: () => currentIndex, + getCount: () => albumData.length, + isAlive: () => container.isConnected, showBySlug: (subKey) => { if (!subKey || !Array.isArray(albumData)) return false; const foundIdx = albumData.findIndex((item) => @@ -4437,7 +4486,6 @@ window.cancelAnimFrame = (function () { th.setAttribute('data-index', newIdx); }); - if (totalCountEl) totalCountEl.textContent = albumData.length; if (currentIndex >= albumData.length) currentIndex = albumData.length - 1; showImage(currentIndex, 'none', true); @@ -5502,6 +5550,21 @@ window.cancelAnimFrame = (function () { const startTime = performance.now(); const duration = 350; // ms + // Render the new background (scaled + blurred) ONCE into an offscreen canvas at canvas size. + // Each crossfade frame then only blends two small bitmaps instead of re-scaling and re-blurring + // the source (which may be a full-resolution album image) ~20 times. + let prerendered = null; + try { + prerendered = document.createElement('canvas'); + prerendered.width = targetW; + prerendered.height = targetH; + const pctx = prerendered.getContext('2d'); + pctx.filter = getCanvasFilter(); + drawCoverImage(pctx, sourceImg); + } catch (e) { + prerendered = null; + } + const step = (now) => { const elapsed = now - startTime; const progress = Math.min(1.0, elapsed / duration); @@ -5520,9 +5583,13 @@ window.cancelAnimFrame = (function () { // 2. Fade in new background smoothly over previous context.save(); context.globalAlpha = ease; - context.filter = getCanvasFilter(); try { - drawCoverImage(context, sourceImg); + if (prerendered) { + context.drawImage(prerendered, 0, 0, w, h); + } else { + context.filter = getCanvasFilter(); + drawCoverImage(context, sourceImg); + } } catch (e) {} context.restore(); @@ -5607,6 +5674,9 @@ window.cancelAnimFrame = (function () { let videoFadeStartTime = prevSnapshot ? performance.now() : 0; let _bgLastTs = 0; const animationLoop = (ts) => { + // Started directly (not from rAF) there is no timestamp: without one the fps throttle below compares + // against NaN and never throttles again for the rest of the loop + if (typeof ts !== 'number') ts = performance.now(); // Increment the per-frame ID so getEffectiveBackgroundTuning() cache invalidates correctly window._rafFrameId = ((window._rafFrameId || 0) + 1) & 0xFFFFFF; @@ -6169,21 +6239,25 @@ window.cancelAnimFrame = (function () { window.isCurrentlyInOnara = isCurrentlyInOnara; let _isAudioItemCache = { result: false, ts: 0 }; + // The album template always contains a hidden