From ba5bc18b98dcc2027140a4e601d86d0cc714fe60 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Sun, 13 Sep 2026 21:55:23 +0200 Subject: [PATCH] gfsd --- migrations/add_albums.sql | 11 + public/s/css/f0ckm.css | 283 +++++++++++++- public/s/css/upload.css | 23 ++ public/s/css/v0ck.css | 12 + public/s/js/admin.js | 66 +++- public/s/js/f0ckm.js | 638 +++++++++++++++++++++++++++----- public/s/js/sidebar-activity.js | 18 +- public/s/js/upload.js | 19 +- public/s/js/user.js | 30 +- public/s/js/v0ck.js | 31 +- src/inc/lib.mjs | 66 +++- src/inc/locales/de.json | 4 +- src/inc/locales/en.json | 4 +- src/inc/locales/nl.json | 4 +- src/inc/locales/zange.json | 4 +- src/inc/queue.mjs | 10 +- src/inc/routeinc/f0cklib.mjs | 293 +++++++++++++-- src/inc/routes/ajax.mjs | 3 +- src/inc/routes/apiv2/index.mjs | 81 ++-- src/inc/routes/apiv2/tags.mjs | 235 ++++++++---- src/inc/routes/index.mjs | 3 +- src/inc/routes/mod.mjs | 11 + src/inc/routes/static.mjs | 48 +++ src/index.mjs | 11 + src/upload_handler.mjs | 52 ++- views/index-partial.html | 4 +- views/item-partial-legacy.html | 4 +- views/item-partial-modern.html | 4 +- views/snippets/footer.html | 1 + views/snippets/item-media.html | 24 +- views/snippets/items-grid.html | 4 +- views/user-partial.html | 8 +- 32 files changed, 1739 insertions(+), 270 deletions(-) diff --git a/migrations/add_albums.sql b/migrations/add_albums.sql index 7b2079d..2bc3bf7 100644 --- a/migrations/add_albums.sql +++ b/migrations/add_albums.sql @@ -21,3 +21,14 @@ CREATE TABLE IF NOT EXISTS public.album_items ( CREATE INDEX IF NOT EXISTS idx_album_items_item_id ON public.album_items(item_id, order_index ASC); CREATE INDEX IF NOT EXISTS idx_album_items_slug ON public.album_items(slug); + +CREATE TABLE IF NOT EXISTS public.album_items_tags_assign ( + album_item_id INTEGER NOT NULL REFERENCES public.album_items(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES public.tags(id) ON DELETE CASCADE, + user_id INTEGER REFERENCES public."user"(id) ON DELETE SET DEFAULT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + PRIMARY KEY (album_item_id, tag_id) +); + +CREATE INDEX IF NOT EXISTS idx_album_items_tags_assign_tag_id ON public.album_items_tags_assign(tag_id); +CREATE INDEX IF NOT EXISTS idx_album_items_tags_assign_album_item_id ON public.album_items_tags_assign(album_item_id); diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index 93a0a64..9bcb4ea 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -3160,6 +3160,50 @@ body.sidebar-right-hidden #sidebar-drag-zone { text-shadow: 0 0 16px rgba(var(--accent-rgb, 153, 255, 0), 0.35); } +.sidebar-media-placeholder.audio.has-cover { + background: radial-gradient(circle at center, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0.82) 85%), var(--nav-bg, #1e1e1e); +} + +.audio-cover-circle { + display: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 155px; + height: 155px; + max-width: calc(100% - 40px); + max-height: calc(100% - 40px); + aspect-ratio: 1 / 1; + border-radius: 50%; + background-size: cover; + background-position: center; + background-repeat: no-repeat; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.88), 0 0 35px rgba(var(--accent-rgb, 153, 255, 0), 0.28), inset 0 0 20px rgba(0, 0, 0, 0.65); + border: 2px solid rgba(var(--accent-rgb, 153, 255, 0), 0.4); + overflow: hidden; + pointer-events: none; + z-index: 1; + will-change: transform, box-shadow, border-color; +} + +.audio-cover-circle::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + background: radial-gradient(circle at center, transparent 72%, rgba(0, 0, 0, 0.35) 92%, rgba(0, 0, 0, 0.75) 100%); + pointer-events: none; +} + +.sidebar-media-placeholder.audio.has-cover .audio-cover-circle { + display: block; +} + +.sidebar-media-placeholder.audio.has-cover i { + display: none !important; +} + .sidebar-media-placeholder.hidden { display: none !important; } @@ -3170,12 +3214,223 @@ body.sidebar-right-hidden #sidebar-drag-zone { border-radius: inherit; } +.album-thumb-item > .sidebar-media-placeholder.audio { + z-index: 1; + font-size: 1.25rem; + border-radius: inherit; +} + .thumb:has(.sidebar-media-placeholder), .thumb.thumb-fallback:has(.sidebar-media-placeholder) { opacity: 1 !important; background-image: none !important; } +.v0ck > .sidebar-media-placeholder.audio, +.album-audio-wrapper > .sidebar-media-placeholder.audio, +.embed-responsive > .sidebar-media-placeholder.audio { + z-index: 0; + pointer-events: none; + font-size: clamp(2rem, 5.5vw, 3.8rem); + border-radius: inherit; +} + +.v0ck:fullscreen > .sidebar-media-placeholder.audio, +.v0ck.v0ck_fullscreen > .sidebar-media-placeholder.audio, +#main:fullscreen .v0ck.v0ck_fullscreen > .sidebar-media-placeholder.audio, +#onara-modal:fullscreen .v0ck.v0ck_fullscreen > .sidebar-media-placeholder.audio { + font-size: clamp(3rem, 7.5vw, 5.5rem); +} + +.v0ck:fullscreen .audio-cover-circle, +.v0ck.v0ck_fullscreen .audio-cover-circle, +#main:fullscreen .v0ck.v0ck_fullscreen .audio-cover-circle, +#onara-modal:fullscreen .v0ck.v0ck_fullscreen .audio-cover-circle { + width: clamp(260px, 56vmin, 560px); + height: clamp(260px, 56vmin, 560px); + max-width: calc(100% - 80px); + max-height: calc(100% - 100px); +} + +.sidebar-media-placeholder.audio i { + position: relative; + z-index: 2; + display: inline-block; + transform-origin: center center; + will-change: transform, filter; + transition: transform 0.015s linear; +} + +/* Audio Reactivity Tuner Overlay */ +.f0ck-tuner-toggle { + position: fixed; + bottom: 65px; + right: 20px; + z-index: 99998; + background: var(--nav-bg, #1a1a1a); + color: var(--accent, #99ff00); + border: 1.5px solid var(--accent, #99ff00); + border-radius: 20px; + padding: 6px 13px; + font-size: 11px; + font-weight: 700; + cursor: pointer; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.65); + display: flex; + align-items: center; + gap: 6px; + transition: transform 0.15s ease, background-color 0.15s ease; +} + +.f0ck-tuner-toggle:hover { + transform: scale(1.05); + background: rgba(var(--accent-rgb, 153, 255, 0), 0.18); +} + +.f0ck-audio-tuner-panel { + position: fixed; + bottom: 110px; + right: 20px; + z-index: 99999; + width: 320px; + max-height: 80vh; + overflow-y: auto; + background: rgba(18, 18, 22, 0.95); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(var(--accent-rgb, 153, 255, 0), 0.45); + border-radius: 12px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.9); + padding: 14px 16px; + color: #e6e6e6; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 11px; +} + +.f0ck-audio-tuner-panel.hidden { + display: none !important; +} + +.f0ck-tuner-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + padding-bottom: 8px; +} + +.f0ck-tuner-section-title { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--accent, #99ff00); + margin: 12px 0 6px 0; + padding-bottom: 3px; + border-bottom: 1px solid rgba(var(--accent-rgb, 153, 255, 0), 0.25); +} + +.f0ck-tuner-section-title:first-child { + margin-top: 0; +} + +.f0ck-tuner-title { + font-weight: 700; + font-size: 13px; + color: var(--accent, #99ff00); + display: flex; + align-items: center; + gap: 6px; +} + +.f0ck-tuner-close { + background: none; + border: none; + color: #888; + font-size: 14px; + cursor: pointer; + padding: 2px 6px; +} + +.f0ck-tuner-close:hover { + color: #fff; +} + +.f0ck-tuner-row { + margin-bottom: 10px; +} + +.f0ck-tuner-row-label { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + color: #bbb; + font-size: 11px; +} + +.f0ck-tuner-row-label span:last-child { + font-weight: 700; + color: var(--accent, #99ff00); + font-family: monospace; +} + +.f0ck-tuner-row input[type="range"] { + width: 100%; + accent-color: var(--accent, #99ff00); + cursor: pointer; + margin: 0; + height: 4px; + display: block; +} + +.f0ck-tuner-footer { + display: flex; + gap: 8px; + margin-top: 14px; + border-top: 1px solid rgba(255, 255, 255, 0.1); + padding-top: 10px; +} + +.f0ck-tuner-footer button { + flex: 1; + padding: 8px 10px; + font-size: 11px; + font-weight: 700; + border-radius: 6px; + cursor: pointer; + transition: filter 0.15s ease, background 0.15s ease; +} + +.f0ck-tuner-btn-copy { + background: var(--accent, #99ff00); + color: #000; + border: none; + display: flex; + align-items: center; + justify-content: center; + gap: 5px; +} + +.f0ck-tuner-btn-copy:hover { + filter: brightness(1.15); +} + +.f0ck-tuner-btn-reset { + background: rgba(255, 255, 255, 0.08); + color: #ccc; + border: 1px solid rgba(255, 255, 255, 0.18); + display: flex; + align-items: center; + justify-content: center; + gap: 5px; +} + +.f0ck-tuner-btn-reset:hover { + background: rgba(255, 255, 255, 0.15); + color: #fff; +} + .sidebar-video-details { display: flex; margin-top: 8px; @@ -8347,16 +8602,18 @@ video.emoji { /* visualizer */ -canvas { +canvas.audio-visualizer, +.v0ck canvas.audio-visualizer, +.embed-responsive canvas.audio-visualizer { position: absolute; - top: 1px; - /* This removes the ugly 1px line seen at the bottom when you hover over the audio element */ + top: 0; left: 0; bottom: 0; right: 0; height: 100%; width: 100%; pointer-events: none; + z-index: 1 !important; } /* tags */ @@ -13617,6 +13874,26 @@ body.layout-legacy .navbar-header { flex-shrink: 0; } +.subf0ck-tag-scope { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-muted, #888); + margin-bottom: 6px; + display: flex; + align-items: center; + gap: 5px; +} + +.subf0ck-tag-scope code { + font-size: 11px; + color: #fff; + background: rgba(255, 255, 255, 0.1); + padding: 1px 4px; + border-radius: 3px; +} + /* ===== layout-modern: flexible by default, viewport-locked for item view ===== */ @media (min-width: 1000px) { body.layout-modern { diff --git a/public/s/css/upload.css b/public/s/css/upload.css index 5b47ed0..f981924 100644 --- a/public/s/css/upload.css +++ b/public/s/css/upload.css @@ -2012,3 +2012,26 @@ opacity: 0.7; } +.album-stage-tags-wrap { + width: 100%; + margin-top: 4px; +} + +.album-stage-tags-input { + width: 100%; + background: rgba(0, 0, 0, 0.45); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 4px; + padding: 3px 6px; + font-size: 0.68rem; + color: #fff; + outline: none; + box-sizing: border-box; + transition: border-color 0.2s ease, background 0.2s ease; +} + +.album-stage-tags-input:focus { + border-color: var(--accent); + background: rgba(0, 0, 0, 0.65); +} + diff --git a/public/s/css/v0ck.css b/public/s/css/v0ck.css index 7415c74..2b09610 100644 --- a/public/s/css/v0ck.css +++ b/public/s/css/v0ck.css @@ -52,6 +52,18 @@ display: none; } +.v0ck canvas.audio-visualizer { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + height: 100%; + width: 100%; + pointer-events: none; + z-index: 1 !important; +} + .v0ck_overlay { pointer-events: none; position: absolute; diff --git a/public/s/js/admin.js b/public/s/js/admin.js index ee8743f..ddfd6f8 100644 --- a/public/s/js/admin.js +++ b/public/s/js/admin.js @@ -12,8 +12,14 @@ const tagsContainer = document.querySelector("#tags"); const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null; const usernameEl = document.querySelector("a#a_username"); + const currentSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') + ? window.albumGallery.getCurrentSubf0ck() + : null; + const subf0ck_id = currentSub ? (currentSub.slug || currentSub.id) : (tagsContainer?.dataset?.subf0ckSlug || tagsContainer?.dataset?.subf0ckId || null); + return { postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(), + subf0ck_id, // data-username holds the raw DB username; data-author-id holds the user's numeric ID. // Never fall back to innerText — it may be a display name or the literal string 'unknown'. poster: (usernameEl?.dataset?.username || '').trim() || null, @@ -81,7 +87,7 @@ contentEl.textContent = tag.tag; } else { contentEl = document.createElement("a"); - contentEl.href = `/tag/${tag.normalized}`; + contentEl.href = "/tag/" + tag.normalized; contentEl.style = "color: inherit !important"; contentEl.textContent = tag.tag; } @@ -102,19 +108,27 @@ span.classList.add('can-cycle'); } } else if (!window.f0ckSession?.is_anonymized && window.f0ckSession?.logged_in && !window.f0ckSession?.is_anon && (tag.display_name || tag.user)) { - span.setAttribute('tooltip', tag.display_name || tag.user); + span.setAttribute("tooltip", tag.display_name || tag.user); } span.appendChild(contentEl); + if (!isRating && tag.can_exclude) { + const excludeBtn = document.createElement("button"); + excludeBtn.type = "button"; + excludeBtn.className = "tag-exclude-btn"; + excludeBtn.setAttribute("title", tag.is_excluded ? "Excluded (click to unexclude)" : "Exclude tag"); + excludeBtn.setAttribute("aria-label", "Exclude tag"); + excludeBtn.innerHTML = ``; + span.appendChild(excludeBtn); + } + if (!isRating) { const delbutton = document.createElement("a"); - delbutton.innerHTML = ''; - delbutton.href = "javascript:void(0)"; - // Class for delegation + delbutton.href = "#"; delbutton.classList.add("admin-deltag", "removetag"); - - span.appendChild(document.createTextNode('\u00A0')); + delbutton.innerHTML = ''; + span.appendChild(document.createTextNode(" ")); span.appendChild(delbutton); } @@ -188,7 +202,7 @@ e.stopImmediatePropagation(); const ctx = getContext(); if (!ctx) return; - const { postid } = ctx; + const { postid, subf0ck_id } = ctx; let target = e.target; if (target.nodeType === 3) target = target.parentElement; @@ -202,8 +216,12 @@ if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded'); ModAction.confirm((window.f0ckI18n && window.f0ckI18n.tag_delete_title) || 'Delete Tag', `${(window.f0ckI18n && window.f0ckI18n.tag_delete_confirm) || 'Are you sure you want to delete the tag'} ${tagname}?`, async (reason) => { - // Send reason via query param for DELETE request - const res = await (await fetch("/api/v2/tags/" + postid + "/" + encodeURIComponent(tagname) + (reason ? "?reason=" + encodeURIComponent(reason) : ""), { + const qs = new URLSearchParams(); + if (reason) qs.set('reason', reason); + if (subf0ck_id) qs.set('subf0ck_id', subf0ck_id); + const qsStr = qs.toString(); + + const res = await (await fetch("/api/v2/tags/" + postid + "/" + encodeURIComponent(tagname) + (qsStr ? "?" + qsStr : ""), { method: 'DELETE', headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token } })).json(); @@ -211,6 +229,10 @@ if (!res.success) { throw new Error(res.msg || "Error deleting tag"); } + if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') { + const cur = window.albumGallery.getCurrentSubf0ck(); + if (cur && res.tags) cur.tags = res.tags; + } renderTags(res.tags); if (window.flashMessage) window.flashMessage((window.f0ckI18n?.tag_deleted_success) || 'Tag deleted', 2500, 'success'); }, { allowEmpty: window.f0ckSession?.is_admin }); @@ -220,7 +242,7 @@ if (e) e.preventDefault(); const ctx = getContext(); if (!ctx) return; - const { postid, tags } = ctx; + const { postid, subf0ck_id, tags } = ctx; const anchor = document.querySelector("a#a_addtag"); if (!anchor) return; @@ -229,13 +251,27 @@ existingTags: tags, anchorEl: anchor, onSubmit: async (tag) => { - const res = await post("/api/v2/tags/" + postid, { tagname: tag }); - if (res.success && window.invalidateItemCache) { - window.invalidateItemCache(postid); + const payload = { tagname: tag }; + if (subf0ck_id) payload.subf0ck_id = subf0ck_id; + const res = await post("/api/v2/tags/" + postid, payload); + if (res.success) { + if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') { + const cur = window.albumGallery.getCurrentSubf0ck(); + if (cur && res.tags) cur.tags = res.tags; + } + if (window.invalidateItemCache) { + window.invalidateItemCache(postid); + } } return res; }, - renderTags + renderTags: (newTags, hl) => { + if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') { + const cur = window.albumGallery.getCurrentSubf0ck(); + if (cur) cur.tags = newTags; + } + renderTags(newTags, hl); + } }); }; diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 3fa0075..9e8fc65 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -374,27 +374,49 @@ window.cancelAnimFrame = (function () { // Update audio cover in player if viewing this audio item const audioCover = document.getElementById('f0ck-audio-cover'); if (audioCover && currentId === idStr) { + const parent = audioCover.parentElement; + const audioEl = document.querySelector('audio#my-video'); + let ph = parent ? parent.querySelector(':scope > .sidebar-media-placeholder.audio') : null; + if (parent && !ph) { + ph = document.createElement('div'); + ph.className = 'sidebar-media-placeholder audio'; + ph.innerHTML = ''; + parent.prepend(ph); + } + let coverCircle = ph ? ph.querySelector('.audio-cover-circle') : null; if (hasCoverart === true || (hasCoverart === undefined && audioCover.src && audioCover.src.includes('/ca/'))) { const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`; audioCover.src = coverUrl; - const parent = audioCover.parentElement; if (parent) { - parent.style.background = `url('${coverUrl}') no-repeat center / contain black`; + parent.style.background = 'none'; + if (ph) { + if (!coverCircle) { + coverCircle = document.createElement('div'); + coverCircle.className = 'audio-cover-circle'; + ph.insertBefore(coverCircle, ph.firstChild); + } + coverCircle.style.backgroundImage = `url('${coverUrl}')`; + ph.classList.add('has-cover'); + ph.style.display = ''; + } } - const audioEl = document.querySelector('audio#my-video'); if (audioEl) { audioEl.setAttribute('poster', coverUrl); } } else if (hasCoverart === false) { - const fallbackUrl = '/s/img/audio.webp'; - audioCover.src = fallbackUrl; - const parent = audioCover.parentElement; + audioCover.removeAttribute('src'); if (parent) { - parent.style.background = `url('${fallbackUrl}') no-repeat center / contain black`; + parent.style.background = 'none'; + if (ph) { + if (coverCircle) { + coverCircle.style.backgroundImage = 'none'; + } + ph.classList.remove('has-cover'); + ph.style.display = ''; + } } - const audioEl = document.querySelector('audio#my-video'); if (audioEl) { - audioEl.setAttribute('poster', fallbackUrl); + audioEl.removeAttribute('poster'); } } } @@ -1684,10 +1706,10 @@ window.cancelAnimFrame = (function () { } catch {} } if (!targetThumb && slug) { - targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"]`); + targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"], .posts > a.thumb[href*="/${slug}#"]`); } if (!targetThumb && itemid) { - targetThumb = document.querySelector(`.posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[data-bg*="/${itemid}."]`); + targetThumb = document.querySelector(`.posts > a.thumb[data-item-id="${itemid}"], .posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[href*="/${itemid}#"], .posts > a.thumb[data-bg*="/${itemid}."]`); } // Blur any other active element so browser native focus cannot highlight a second thumbnail @@ -1856,7 +1878,8 @@ window.cancelAnimFrame = (function () { const gridObj = new URL(window._onaraCurrentGridUrl, window.location.origin); const gridBasePath = gridObj.pathname.replace(/\/p\/\d+/, '').replace(/\/+$/, ''); if (gridBasePath === path) { - return gridObj.pathname + (gridObj.search || '') + (gridObj.hash || ''); + const safeGridHash = (gridObj.hash && !gridObj.hash.match(/^#[a-zA-Z0-9_-]{1,60}$/)) ? gridObj.hash : ''; + return gridObj.pathname + (gridObj.search || '') + safeGridHash; } } catch {} } @@ -1872,7 +1895,8 @@ window.cancelAnimFrame = (function () { path = (path ? path : '') + '/p/' + currentPage; } if (!path) path = '/'; - return path + (urlObj.search || '') + (urlObj.hash || ''); + const safeHash = (urlObj.hash && !urlObj.hash.match(/^#[a-zA-Z0-9_-]{1,60}$/)) ? urlObj.hash : ''; + return path + (urlObj.search || '') + safeHash; } catch { return '/'; } @@ -2731,7 +2755,18 @@ window.cancelAnimFrame = (function () { window._currentActiveAlbumGallery = null; return; } - if (container._f0ckAlbumInit) return; + + const getHashSubf0ckId = () => { + return (window.location.hash || '').replace(/^#/, '').trim(); + }; + + if (container._f0ckAlbumInit) { + const initialHash = getHashSubf0ckId() || container.getAttribute('data-requested-subf0ck') || ''; + if (initialHash && window._currentActiveAlbumGallery && typeof window._currentActiveAlbumGallery.showBySlug === 'function') { + window._currentActiveAlbumGallery.showBySlug(initialHash); + } + return; + } container._f0ckAlbumInit = true; let albumData = null; @@ -2789,12 +2824,15 @@ window.cancelAnimFrame = (function () { let currentIndex = 0; - const getHashSubf0ckId = () => { - return (window.location.hash || '').replace(/^#/, '').trim(); - }; - const initialHash = getHashSubf0ckId() || container.getAttribute('data-requested-subf0ck') || ''; if (initialHash) { + if (!getHashSubf0ckId()) { + history.replaceState(null, '', window.location.pathname + window.location.search + '#' + initialHash); + } + const locEl = document.querySelector('.location'); + if (locEl && !locEl.textContent.includes('#')) { + locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + initialHash; + } const foundIdx = albumData.findIndex(item => String(item.slug || '') === initialHash || String(item.subf0ck_id || '') === initialHash || @@ -2904,6 +2942,10 @@ window.cancelAnimFrame = (function () { if (updateHash && subKey) { history.replaceState(null, '', window.location.pathname + window.location.search + '#' + subKey); } + const locEl = document.querySelector('.location'); + if (locEl && subKey) { + locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + subKey; + } // Pause any playing audio or video if (videoEl && !videoEl.paused) { @@ -2962,16 +3004,42 @@ window.cancelAnimFrame = (function () { if (audioWrapper) { audioWrapper.style.display = 'block'; - const coverUrl = item.coverart || item.thumb || '/s/img/audio.webp'; - audioWrapper.style.backgroundImage = `url('${coverUrl}')`; - audioWrapper.style.backgroundRepeat = 'no-repeat'; - audioWrapper.style.backgroundPosition = 'center'; - audioWrapper.style.backgroundSize = 'contain'; - audioWrapper.style.backgroundColor = 'black'; + const hasCover = item.has_coverart && item.coverart && !item.coverart.includes('audio.webp'); + const coverUrl = hasCover ? (item.coverart || item.thumb) : null; + let ph = audioWrapper.querySelector(':scope > .sidebar-media-placeholder.audio'); + if (!ph) { + ph = document.createElement('div'); + ph.className = 'sidebar-media-placeholder audio'; + ph.innerHTML = ''; + audioWrapper.prepend(ph); + } + let coverCircle = ph.querySelector('.audio-cover-circle'); + if (!coverCircle) { + coverCircle = document.createElement('div'); + coverCircle.className = 'audio-cover-circle'; + ph.insertBefore(coverCircle, ph.firstChild); + } + audioWrapper.style.backgroundImage = 'none'; + audioWrapper.style.backgroundColor = 'transparent'; + if (coverUrl) { + coverCircle.style.backgroundImage = `url('${coverUrl}')`; + ph.classList.add('has-cover'); + } else { + coverCircle.style.backgroundImage = 'none'; + ph.classList.remove('has-cover'); + } + ph.style.display = ''; } if (audioEl) { audioEl.style.display = 'block'; + const hasCover = item.has_coverart && item.coverart && !item.coverart.includes('audio.webp'); + const coverUrl = hasCover ? (item.coverart || item.thumb) : null; + if (coverUrl) { + audioEl.setAttribute('poster', coverUrl); + } else { + audioEl.removeAttribute('poster'); + } if (audioEl.src !== newSrc && !audioEl.src.endsWith(newSrc)) { audioEl.src = newSrc; @@ -2981,6 +3049,12 @@ window.cancelAnimFrame = (function () { initAlbumAudioV0ck(); video = audioEl; + if (window.initVisualizer) window.initVisualizer(); + + audioEl.addEventListener('play', () => audioWrapper?.classList.add('is-playing')); + audioEl.addEventListener('pause', () => audioWrapper?.classList.remove('is-playing')); + audioEl.addEventListener('ended', () => audioWrapper?.classList.remove('is-playing')); + if (item.size && audioWrapper) { const dlBtn = audioWrapper.querySelector('#v0ck_download'); if (dlBtn) dlBtn.textContent = `Download (${item.size})`; @@ -3041,11 +3115,44 @@ window.cancelAnimFrame = (function () { }); updateInfoModal(); + updateAlbumTags(); preload(currentIndex + 1); preload(currentIndex - 1); }; + const updateAlbumTags = () => { + const sub = albumData[currentIndex]; + if (!sub) return; + + const tagsContainer = document.querySelector('#tags'); + if (!tagsContainer) return; + + const subSlug = sub.slug || sub.subf0ck_id || sub.id; + tagsContainer.dataset.subf0ckId = String(sub.id); + tagsContainer.dataset.subf0ckSlug = String(subSlug); + tagsContainer.dataset.subf0ckIndex = String(currentIndex); + + let tagScopeEl = document.querySelector('#subf0ck-tag-scope'); + if (!tagScopeEl) { + const sidebarCont = document.querySelector('.sidebar-tags-container'); + if (sidebarCont) { + tagScopeEl = document.createElement('div'); + tagScopeEl.id = 'subf0ck-tag-scope'; + tagScopeEl.className = 'subf0ck-tag-scope'; + sidebarCont.insertBefore(tagScopeEl, tagsContainer); + } + } + if (tagScopeEl) { + tagScopeEl.innerHTML = ` Subf0ck ${subSlug} (${currentIndex + 1}/${albumData.length}):`; + } + + const subTags = Array.isArray(sub.tags) ? sub.tags : []; + if (typeof window.renderTags === 'function') { + window.renderTags(subTags); + } + }; + const updateInfoModal = () => { const modal = document.getElementById('info-modal'); if (!modal) return; @@ -3168,63 +3275,81 @@ window.cancelAnimFrame = (function () { let stripTimeout = null; container.addEventListener('click', (e) => { if (stripEl && !e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn')) { - stripEl.classList.add('is-visible'); - clearTimeout(stripTimeout); + stripEl.classList.add('strip-peek'); + if (stripTimeout) clearTimeout(stripTimeout); stripTimeout = setTimeout(() => { - stripEl.classList.remove('is-visible'); - }, 3500); + stripEl.classList.remove('strip-peek'); + }, 3000); } }); - // Hashchange listener for forward/backward browser navigation - const hashChangeHandler = () => { - const newHash = getHashSubf0ckId(); - if (!newHash) return; - const targetIdx = albumData.findIndex(item => - String(item.slug || '') === newHash || - String(item.subf0ck_id || '') === newHash || - String(item.id) === newHash || - String(item.order_index + 1) === newHash - ); - if (targetIdx !== -1 && targetIdx !== currentIndex) { - showImage(targetIdx, 'none', false); - } - }; - window.addEventListener('hashchange', hashChangeHandler); - - // Touch swipe support on album container - let touchStartX = 0; - let touchStartY = 0; + // Handle touch swipe for mobile gallery navigation + let touchStartX = null; + let touchStartY = null; container.addEventListener('touchstart', (e) => { - if (e.target.closest('.v0ck_player_controls, .v0ck_settings_menu, input[type="range"]')) return; - if (e.touches && e.touches.length === 1) { + if (e.touches.length === 1) { touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; } }, { passive: true }); container.addEventListener('touchend', (e) => { - if (e.target.closest('.v0ck_player_controls, .v0ck_settings_menu, input[type="range"]')) return; - if (e.changedTouches && e.changedTouches.length === 1) { - const diffX = e.changedTouches[0].clientX - touchStartX; - const diffY = e.changedTouches[0].clientY - touchStartY; - if (Math.abs(diffX) > 40 && Math.abs(diffX) > Math.abs(diffY) * 1.5) { - if (diffX > 0) { - showImage(currentIndex - 1, 'prev'); - } else { - showImage(currentIndex + 1, 'next'); - } + if (touchStartX === null || touchStartY === null) return; + const touchEndX = e.changedTouches[0].clientX; + const touchEndY = e.changedTouches[0].clientY; + const diffX = touchEndX - touchStartX; + const diffY = touchEndY - touchStartY; + + // Minimum swipe distance of 50px and predominantly horizontal + if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY) * 1.5) { + if (diffX < 0) { + showImage(currentIndex + 1, 'next'); + } else { + showImage(currentIndex - 1, 'prev'); } } + touchStartX = null; + touchStartY = null; }, { passive: true }); + // Handle hash change if user navigates back/forward to another subf0ck + window.addEventListener('hashchange', () => { + const newHash = getHashSubf0ckId(); + if (!newHash) return; + const foundIdx = albumData.findIndex((item) => + String(item.slug || '') === newHash || + String(item.subf0ck_id || '') === newHash || + String(item.id) === newHash || + String(item.order_index + 1) === newHash + ); + if (foundIdx !== -1 && foundIdx !== currentIndex) { + showImage(foundIdx, 'none', false); + } + }); + window._currentActiveAlbumGallery = { prev: () => showImage(currentIndex - 1, 'prev'), next: () => showImage(currentIndex + 1, 'next'), + showBySlug: (subKey) => { + if (!subKey || !Array.isArray(albumData)) return false; + const foundIdx = albumData.findIndex((item) => + String(item.slug || '') === String(subKey) || + String(item.subf0ck_id || '') === String(subKey) || + String(item.id) === String(subKey) || + String(item.order_index + 1) === String(subKey) + ); + if (foundIdx !== -1) { + showImage(foundIdx, 'none', true); + return true; + } + return false; + }, updateInfoModal: updateInfoModal, + updateAlbumTags: updateAlbumTags, getCurrentSubf0ck: () => albumData[currentIndex], isHovered: false }; + window.albumGallery = window._currentActiveAlbumGallery; container.addEventListener('mouseenter', () => { if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = true; @@ -3234,8 +3359,25 @@ window.cancelAnimFrame = (function () { }); }; + const syncLocationSubf0ck = () => { + const layout = document.querySelector('.item-layout-container'); + const reqSub = layout ? (layout.getAttribute('data-requested-subf0ck') || '') : ''; + const hash = (window.location.hash || '').replace(/^#/, '').trim(); + const targetSub = hash || reqSub; + if (targetSub) { + if (!hash && reqSub) { + history.replaceState(null, '', window.location.pathname + window.location.search + '#' + reqSub); + } + const locEl = document.querySelector('.location'); + if (locEl && !locEl.textContent.includes('#')) { + locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + targetSub; + } + } + }; + const setupMedia = () => { window._currentActiveAlbumGallery = null; + window.albumGallery = null; const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video"); if (elem) { video = new v0ck(elem); @@ -3243,8 +3385,12 @@ window.cancelAnimFrame = (function () { video = null; } initAlbumGallery(); + syncLocationSubf0ck(); }; - document.addEventListener('f0ck:contentLoaded', initAlbumGallery); + document.addEventListener('f0ck:contentLoaded', () => { + initAlbumGallery(); + syncLocationSubf0ck(); + }); const initOnaraInitialState = () => { if (!isOnaraActive()) return; @@ -3524,13 +3670,155 @@ window.cancelAnimFrame = (function () { }; + // Audio Visualizer Reactivity Tuner + const DEFAULT_AUDIO_TUNING = { + bassGain: 0.5, + bassPower: 2.0, + scaleBounce: 0.4, + bounceBoost: 2.5, + glowIntensity: 250, + attackSpeed: 1.0, + releaseSpeed: 0.6, + coverSize: 155, + barHeight: 0.4, + smoothing: 0.84 + }; + + let savedTuning = null; + try { + const raw = localStorage.getItem('f0ck_audio_tuning'); + if (raw) savedTuning = JSON.parse(raw); + } catch (e) {} + + window.audioVisualizerTuning = Object.assign({}, DEFAULT_AUDIO_TUNING, savedTuning || {}); + + const initAudioTunerUI = () => { + if (document.getElementById('f0ck-audio-tuner-panel')) return; + + const panel = document.createElement('div'); + panel.id = 'f0ck-audio-tuner-panel'; + panel.className = 'f0ck-audio-tuner-panel hidden'; + + const sliders = [ + // Disc & Glow Section + { section: 'Cover Art & Glow Reactivity', key: 'scaleBounce', label: 'Disc Bounce (Scale)', min: 0.00, max: 5.00, step: 0.05, unit: 'x' }, + { section: 'Cover Art & Glow Reactivity', key: 'bounceBoost', label: 'Bounce Reactivity Boost', min: 1.0, max: 30.0, step: 0.5, unit: 'x' }, + { section: 'Cover Art & Glow Reactivity', key: 'bassGain', label: 'Bass Gain (Multiplier)', min: 0.5, max: 6.0, step: 0.1, unit: 'x' }, + { section: 'Cover Art & Glow Reactivity', key: 'bassPower', label: 'Sensitivity Curve (Power)', min: 0.30, max: 2.00, step: 0.02, unit: '' }, + { section: 'Cover Art & Glow Reactivity', key: 'glowIntensity', label: 'Glow Aura Reach', min: 10, max: 250, step: 5, unit: 'px' }, + { section: 'Cover Art & Glow Reactivity', key: 'attackSpeed', label: 'Attack Speed (Snap)', min: 0.10, max: 1.00, step: 0.02, unit: '' }, + { section: 'Cover Art & Glow Reactivity', key: 'releaseSpeed', label: 'Release Speed (Decay)', min: 0.05, max: 0.60, step: 0.01, unit: '' }, + { section: 'Cover Art & Glow Reactivity', key: 'coverSize', label: 'Cover Disc Diameter', min: 120, max: 450, step: 5, unit: 'px' }, + + // Visualizer Bars Section + { section: 'Bottom Visualizer Bars', key: 'barHeight', label: 'Visualizer Bar Height', min: 0.05, max: 2.00, step: 0.05, unit: 'x' }, + { section: 'Bottom Visualizer Bars', key: 'smoothing', label: 'Visualizer Bar Smoothing', min: 0.10, max: 0.95, step: 0.02, unit: '' } + ]; + + let rowsHtml = ''; + let currentSection = ''; + sliders.forEach(s => { + if (s.section && s.section !== currentSection) { + currentSection = s.section; + rowsHtml += `
${currentSection}
`; + } + const val = window.audioVisualizerTuning[s.key] !== undefined ? window.audioVisualizerTuning[s.key] : DEFAULT_AUDIO_TUNING[s.key]; + rowsHtml += ` +
+
+ ${s.label} + ${val}${s.unit} +
+ +
+ `; + }); + + panel.innerHTML = ` +
+ Audio Reactivity Tuner + +
+
+ ${rowsHtml} +
+ + `; + + const toggleBtn = document.createElement('button'); + toggleBtn.id = 'f0ck-tuner-toggle'; + toggleBtn.type = 'button'; + toggleBtn.className = 'f0ck-tuner-toggle'; + toggleBtn.innerHTML = ' Tuner'; + toggleBtn.title = 'Open Live Audio Reactivity Tuner'; + + document.body.appendChild(panel); + document.body.appendChild(toggleBtn); + + toggleBtn.addEventListener('click', () => { + panel.classList.toggle('hidden'); + }); + + panel.querySelector('#f0ck-tuner-close').addEventListener('click', () => { + panel.classList.add('hidden'); + }); + + sliders.forEach(s => { + const input = panel.querySelector(`#input-${s.key}`); + const valEl = panel.querySelector(`#val-${s.key}`); + input.addEventListener('input', () => { + const parsed = parseFloat(input.value); + window.audioVisualizerTuning[s.key] = parsed; + valEl.textContent = `${parsed}${s.unit}`; + try { + localStorage.setItem('f0ck_audio_tuning', JSON.stringify(window.audioVisualizerTuning)); + } catch (e) {} + }); + }); + + panel.querySelector('#f0ck-tuner-copy').addEventListener('click', () => { + const copyBtn = panel.querySelector('#f0ck-tuner-copy'); + const text = JSON.stringify(window.audioVisualizerTuning, null, 2); + navigator.clipboard.writeText(text).then(() => { + copyBtn.innerHTML = ' Copied!'; + if (typeof window.flashMessage === 'function') { + window.flashMessage('Settings copied to clipboard! Paste them into the chat.', 3500, 'success'); + } + setTimeout(() => { + copyBtn.innerHTML = ' Copy Settings'; + }, 2000); + }); + }); + + panel.querySelector('#f0ck-tuner-reset').addEventListener('click', () => { + Object.assign(window.audioVisualizerTuning, DEFAULT_AUDIO_TUNING); + try { + localStorage.removeItem('f0ck_audio_tuning'); + } catch (e) {} + sliders.forEach(s => { + const input = panel.querySelector(`#input-${s.key}`); + const valEl = panel.querySelector(`#val-${s.key}`); + input.value = DEFAULT_AUDIO_TUNING[s.key]; + valEl.textContent = `${DEFAULT_AUDIO_TUNING[s.key]}${s.unit}`; + }); + if (typeof window.flashMessage === 'function') { + window.flashMessage('Tuner reset to defaults', 2000, 'info'); + } + }); + }; + window.initVisualizer = () => { - const audioElement = document.querySelector("audio"); + const audioElement = document.querySelector("audio#my-video, audio#f0ck-album-audio, audio"); if (audioElement) { + // Ensure Tuner UI is mounted + initAudioTunerUI(); + // Cleanup existing visualizer if (visualizerRafId) window.cancelAnimFrame(visualizerRafId); - const existingCanvas = document.querySelector(".v0ck > canvas.audio-visualizer"); - if (existingCanvas) existingCanvas.remove(); + document.querySelectorAll("canvas.audio-visualizer").forEach(c => c.remove()); const canvas = document.createElement("canvas"); canvas.className = "audio-visualizer"; @@ -3538,39 +3826,195 @@ window.cancelAnimFrame = (function () { canvas.width = 1920; canvas.height = 1080; - setTimeout(() => { - const v0ckContainer = document.querySelector(".v0ck"); - if (v0ckContainer) v0ckContainer.insertAdjacentElement("afterbegin", canvas); - }, 400); + const attachCanvas = () => { + const v0ckContainer = audioElement.closest('.v0ck') || audioElement.parentElement || document.querySelector('.v0ck'); + if (v0ckContainer && !v0ckContainer.contains(canvas)) { + const controls = v0ckContainer.querySelector('.v0ck_player_controls'); + if (controls) { + v0ckContainer.insertBefore(canvas, controls); + } else { + v0ckContainer.appendChild(canvas); + } + } + }; + + attachCanvas(); + setTimeout(attachCanvas, 50); + setTimeout(attachCanvas, 250); + setTimeout(attachCanvas, 600); if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } - - const analyser = audioCtx.createAnalyser(); - analyser.fftSize = 2048; - try { - const source = audioCtx.createMediaElementSource(audioElement); - source.connect(analyser); - source.connect(audioCtx.destination); - } catch (e) { - console.warn("Visualizer Source creation failed (already connected?):", e); + let source = audioElement._mediaElementSource; + let analyser = audioElement._audioAnalyser; + + if (!source) { + try { + source = audioCtx.createMediaElementSource(audioElement); + audioElement._mediaElementSource = source; + } catch (e) { + console.warn("Visualizer Source creation failed:", e); + } + } + + const cfgInit = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; + if (!analyser) { + analyser = audioCtx.createAnalyser(); + analyser.fftSize = 2048; + analyser.smoothingTimeConstant = cfgInit.smoothing !== undefined ? cfgInit.smoothing : 0.80; + audioElement._audioAnalyser = analyser; + if (source) { + source.connect(analyser); + source.connect(audioCtx.destination); + } + } else { + analyser.fftSize = 2048; + analyser.smoothingTimeConstant = cfgInit.smoothing !== undefined ? cfgInit.smoothing : 0.80; } let data = new Uint8Array(analyser.frequencyBinCount); + let noteIcon = null; + let smoothScale = 1; + let smoothGlow = 0; + const draw = (data) => { - data = [...data]; ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent") || "#9f0"; - data.forEach((value, i) => { - const percent = value / 256; - const height = (canvas.height * percent / 2) - 40; - const offset = canvas.height - height - 1; - const barWidth = canvas.width / analyser.frequencyBinCount; - ctx.fillRect(i * barWidth, offset, barWidth, height); - }); + const accent = getComputedStyle(document.body).getPropertyValue("--accent")?.trim() || "#99ff00"; + ctx.fillStyle = accent; + const count = analyser.frequencyBinCount; + const barWidth = canvas.width / count; + const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING; + const barMult = cfg.barHeight !== undefined ? cfg.barHeight : 0.85; + + for (let i = 0; i < count; i++) { + const value = data[i]; + if (value > 0) { + const percent = (value / 256) * barMult; + const height = (canvas.height * percent / 2) - 30; + if (height > 0) { + const offset = canvas.height - height - 1; + ctx.fillRect(i * barWidth, offset, barWidth, height); + } + } + } + + if (cfg.smoothing !== undefined && analyser.smoothingTimeConstant !== cfg.smoothing) { + analyser.smoothingTimeConstant = cfg.smoothing; + } + + // Animate the music note and/or cover art circle to react dynamically to the music + const v0ckContainer = audioElement.closest('.v0ck') || audioElement.parentElement || document.querySelector('.v0ck'); + const ph = v0ckContainer ? v0ckContainer.querySelector('.sidebar-media-placeholder.audio') : null; + if (!noteIcon || !noteIcon.isConnected) { + noteIcon = ph ? ph.querySelector('i') : null; + } + const coverCircle = ph ? ph.querySelector('.audio-cover-circle') : null; + + if (coverCircle && cfg.coverSize) { + coverCircle.style.width = cfg.coverSize + 'px'; + coverCircle.style.height = cfg.coverSize + 'px'; + } + + if (noteIcon || coverCircle) { + if (!audioElement.paused && audioCtx.state === 'running') { + // Bass energy from kick/sub-bass bins (1 to 10): peak transient + body + let bassMax = 0; + let bassSum = 0; + const bassBins = Math.min(10, count); + for (let b = 1; b <= bassBins; b++) { + const val = data[b]; + if (val > bassMax) bassMax = val; + bassSum += val; + } + const bassPeak = bassMax / 255; + const bassAvg = bassSum / (bassBins * 255); + // Kick-driven punch: transient peak dominant for instant beat response + const kickEnergy = Math.min(1, (bassPeak * 0.75 + bassAvg * 0.25) * (cfg.bassGain || 3.0)); + + // Midrange melodic energy + let midMax = 0; + let midSum = 0; + const midStart = 11; + const midEnd = Math.min(32, count); + for (let m = midStart; m < midEnd; m++) { + const val = data[m]; + if (val > midMax) midMax = val; + midSum += val; + } + const midEnergy = Math.min(1, ((midMax / 255) * 0.65 + (midSum / ((midEnd - midStart) * 255)) * 0.35) * 2.2); + + // High-frequency snap (snares, hi-hats, percs) + let highMax = 0; + const highStart = 33; + const highEnd = Math.min(70, count); + for (let h = highStart; h < highEnd; h++) { + if (data[h] > highMax) highMax = data[h]; + } + const highEnergy = Math.min(1, (highMax / 255) * 2.0); + + // Dynamic punch with power curve + const punch = Math.pow(kickEnergy, (cfg.bassPower !== undefined ? cfg.bassPower : 1.5)); + const bounceMultiplier = cfg.bounceBoost !== undefined ? cfg.bounceBoost : 1.5; + // Target scale jumps based on slider and bounce boost + const targetScale = 1 + (punch * (cfg.scaleBounce !== undefined ? cfg.scaleBounce : 0.50) * bounceMultiplier) + (midEnergy * 0.05); + const targetGlow = punch * (cfg.glowIntensity !== undefined ? cfg.glowIntensity : 20) + midEnergy * 20; + + // Attack & release smoothing + const cfgAttack = cfg.attackSpeed !== undefined ? cfg.attackSpeed : 0.85; + const cfgRelease = cfg.releaseSpeed !== undefined ? cfg.releaseSpeed : 0.16; + const attackSpeed = targetScale > smoothScale ? cfgAttack : cfgRelease; + smoothScale += (targetScale - smoothScale) * attackSpeed; + smoothGlow += (targetGlow - smoothGlow) * attackSpeed; + + // Rhythmic tilt based on balance + const tilt = (midEnergy - kickEnergy * 0.6) * 18; + + if (noteIcon) { + noteIcon.style.transform = `scale(${smoothScale.toFixed(3)}) rotate(${tilt.toFixed(2)}deg)`; + noteIcon.style.filter = `drop-shadow(0 0 ${Math.round(14 + smoothGlow * 0.5)}px rgba(var(--accent-rgb, 153, 255, 0), ${(0.45 + (smoothScale - 1) * 1.5).toFixed(2)}))`; + } + + if (coverCircle) { + // Direct scale bounce: the disc visibly thumps like a speaker cone + const circleScale = smoothScale; + const glow1 = Math.round(16 + smoothGlow * 1.1); + const glow2 = Math.round(38 + smoothGlow * 2.3); + const glow3 = Math.round(75 + smoothGlow * 3.6); + const alpha1 = Math.min(0.98, (0.42 + (smoothScale - 1) * 2.4)).toFixed(2); + const alpha2 = Math.min(0.85, (0.24 + (smoothScale - 1) * 1.8)).toFixed(2); + const alpha3 = Math.min(0.60, (0.10 + (smoothScale - 1) * 1.2)).toFixed(2); + coverCircle.style.transform = `translate(-50%, -50%) scale(${circleScale.toFixed(3)})`; + coverCircle.style.boxShadow = `0 14px 45px rgba(0, 0, 0, 0.9), 0 0 ${glow1}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha1}), 0 0 ${glow2}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha2}), 0 0 ${glow3}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha3}), inset 0 0 20px rgba(0, 0, 0, 0.65)`; + coverCircle.style.borderColor = `rgba(var(--accent-rgb, 153, 255, 0), ${(0.42 + (smoothScale - 1) * 2.0).toFixed(2)})`; + } + } else { + // Decay smoothly back to neutral state when paused or idle + if (smoothScale > 1.002) { + smoothScale += (1 - smoothScale) * 0.16; + if (noteIcon) { + noteIcon.style.transform = `scale(${smoothScale.toFixed(3)})`; + noteIcon.style.filter = ''; + } + if (coverCircle) { + coverCircle.style.transform = `translate(-50%, -50%) scale(${smoothScale.toFixed(3)})`; + } + } else if (smoothScale !== 1) { + smoothScale = 1; + if (noteIcon) { + noteIcon.style.transform = ''; + noteIcon.style.filter = ''; + } + if (coverCircle) { + coverCircle.style.transform = 'translate(-50%, -50%)'; + coverCircle.style.boxShadow = ''; + coverCircle.style.borderColor = ''; + } + } + } + } }; const loopingFunction = () => { @@ -3581,11 +4025,19 @@ window.cancelAnimFrame = (function () { visualizerRafId = requestAnimationFrame(loopingFunction); - audioElement.onplay = () => { - if (audioCtx.state === 'suspended') { + const resumeAudio = () => { + if (audioCtx && audioCtx.state === 'suspended') { audioCtx.resume(); } }; + + audioElement.addEventListener('play', resumeAudio); + audioElement.addEventListener('playing', resumeAudio); + const playerWrap = audioElement.closest('.v0ck') || audioElement.parentElement; + if (playerWrap) { + playerWrap.addEventListener('click', resumeAudio, { passive: true }); + playerWrap.addEventListener('pointerdown', resumeAudio, { passive: true }); + } } }; @@ -5339,6 +5791,8 @@ window.cancelAnimFrame = (function () { const params = new URLSearchParams(); params.append('mode', window.activeMode); + const targetHash = new URL(url, window.location.origin).hash.replace(/^#/, '').trim(); + if (targetHash) params.append('subf0ck', targetHash); if (tag) params.append('tag', tag); if (hall) params.append('hall', hall); if (userHall && userHallOwner) { @@ -6586,13 +7040,18 @@ window.cancelAnimFrame = (function () { ? window.getCsrfToken() : ((window.f0ckSession && window.f0ckSession.csrf_token) || document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''); + const currentSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') + ? window.albumGallery.getCurrentSubf0ck() + : null; + const subf0ck_id = currentSub ? (currentSub.slug || currentSub.id) : (ratingEl.dataset.subf0ckSlug || ratingEl.dataset.subf0ckId || document.querySelector('#tags')?.dataset?.subf0ckSlug || null); + fetch(`/api/v2/item/${postid}/rating`, { method: 'POST', headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf }, - body: JSON.stringify({ rating: targetRating, csrf_token: csrf }) + body: JSON.stringify({ rating: targetRating, subf0ck_id, csrf_token: csrf }) }) .then(r => r.json()) .then(res => { @@ -6611,6 +7070,9 @@ window.cancelAnimFrame = (function () { if (ratingEl._lastCycleReqId !== reqId) return; // ignore stale responses if (res.success) { + if (res.tags && currentSub) { + currentSub.tags = res.tags; + } if (res.tags && window.renderTags) { window.renderTags(res.tags); } diff --git a/public/s/js/sidebar-activity.js b/public/s/js/sidebar-activity.js index 9eec414..66c14ed 100644 --- a/public/s/js/sidebar-activity.js +++ b/public/s/js/sidebar-activity.js @@ -1169,9 +1169,10 @@ const isVideo = mime.startsWith('video/'); const isImage = mime.startsWith('image/'); - let thumbUrl = `/t/${video.id}.webp`; + let thumbUrl = video.thumb || video.matching_sub_thumb || `/t/${video.id}.webp`; if (isBlurred) { - thumbUrl = `/t/${video.id}_blur.webp`; + const baseThumb = thumbUrl.replace(/\.webp$/, ''); + thumbUrl = `${baseThumb}_blur.webp`; } if (window.applyThumbCacheBust) thumbUrl = window.applyThumbCacheBust(thumbUrl); @@ -1207,7 +1208,9 @@ ? `xD ${video.xd_score}` : ''; - const ext = (video.mime ? video.mime.split('/')[1] : '') + const activeMime = video.matching_sub_mime || video.mime || ''; + const activeDest = video.matching_sub_dest || video.dest || ''; + const ext = (activeMime ? activeMime.split('/')[1] : '') .replace('jpeg', 'jpg') .replace('x-shockwave-flash', 'flash') .replace('x-flac', 'flac') @@ -1253,15 +1256,16 @@ const tagContext = options.tag || null; const isStrict = isStrictMode(); const strictSuffix = (isTagFeed && isStrict) ? '?strict=1' : ''; + const targetSubHash = video.target_subf0ck_slug ? `#${video.target_subf0ck_slug}` : ''; const targetHref = (isTagFeed && tagContext) - ? `/tag/${encodeURIComponent(tagContext).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${videoKey}${strictSuffix}` - : `/${videoKey}`; + ? `/tag/${encodeURIComponent(tagContext).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${videoKey}${strictSuffix}${targetSubHash}` + : `/${videoKey}${targetSubHash}`; const activeClass = options.isActive ? ' active-tag-item' : ''; return ` -