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 += `