alotta good shit

This commit is contained in:
2026-09-19 06:20:37 +02:00
parent 74f7884525
commit 1477d56658
45 changed files with 4363 additions and 2069 deletions
+460 -38
View File
@@ -96,6 +96,8 @@ window.cancelAnimFrame = (function () {
// 3. Fallback to URL pathname if on an item page
const path = window.location.pathname;
if (path.includes('/admin/') || path.includes('/mod/') || path.includes('/settings') || path.includes('/user/')) return null;
// Exclude pagination paths like /p/2, /tag/foo/p/3 — the trailing digit is a page number, not an item ID
if (/\/p\/\d+\/?$/.test(path)) return null;
const match = path.match(/\/(\d+)\/?$/);
if (match) return match[1];
@@ -207,7 +209,7 @@ window.cancelAnimFrame = (function () {
console.log(`[RANDOM-POOL] nsfp_version changed (${window._randomPoolNsfpVersion}${newNsfpVersion}), invalidating pool`);
}
_shuffleArray(data.items);
window._randomPool = { items: data.items, total: data.total, sampled: data.sampled };
window._randomPool = { items: data.items, total: data.total, sampled: data.sampled, idMap: data.id_map || {} };
window._randomPoolContext = contextKey;
window._randomPoolCursor = 0;
window._randomPoolNsfpVersion = newNsfpVersion;
@@ -1894,7 +1896,24 @@ window.cancelAnimFrame = (function () {
}
};
const updateOnaraActiveItem = (itemid, url, forceScroll = false, slug = null) => {
const getExtFromMime = (mime) => {
if (!mime || typeof mime !== 'string') return '';
const sub = mime.split('/')[1] || mime;
return sub
.replace('youtube', 'yt')
.replace('x-shockwave-flash', 'flash')
.replace('vnd.adobe.flash.movie', 'flash')
.replace('x-zip-compressed', 'zip')
.replace('x-rar-compressed', 'rar')
.replace('vnd.rar', 'rar')
.replace('x-7z-compressed', '7z')
.replace('x-tar', 'tar')
.replace('x-bzip2', 'bz2')
.replace('x-xz', 'xz')
.toUpperCase();
};
const updateOnaraActiveItem = (itemid, url, forceScroll = false, slug = null, extraMeta = null) => {
if (!isOnaraActive()) return null;
// Clear onara-active from any and all elements to guarantee only 1 item is selected
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
@@ -1912,6 +1931,12 @@ window.cancelAnimFrame = (function () {
if (!targetThumb && 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}."]`);
}
if (!targetThumb) {
const altNumericId = window._randomPool?.idMap?.[slug] || window._randomPool?.idMap?.[itemid];
if (altNumericId) {
targetThumb = document.querySelector(`.posts > a.thumb[data-item-id="${altNumericId}"], .posts > a.thumb[href$="/${altNumericId}"], .posts > a.thumb[data-bg*="/${altNumericId}."]`);
}
}
// Blur any other active element so browser native focus cannot highlight a second thumbnail
if (document.activeElement && document.activeElement !== targetThumb && typeof document.activeElement.blur === 'function') {
@@ -1920,25 +1945,180 @@ window.cancelAnimFrame = (function () {
if (targetThumb) {
targetThumb.classList.add('onara-active');
if (extraMeta) {
if (extraMeta.mime && !targetThumb.dataset.mime) {
targetThumb.dataset.mime = extraMeta.mime;
targetThumb.dataset.ext = getExtFromMime(extraMeta.mime);
}
if (extraMeta.user && !targetThumb.dataset.user) {
targetThumb.dataset.user = extraMeta.user;
}
if (extraMeta.dest && !targetThumb.dataset.file) {
targetThumb.dataset.file = String(extraMeta.dest).replace(/^\/b\//, '');
}
}
scrollOnaraThumbIntoView(targetThumb, forceScroll);
}
return targetThumb;
};
window.updateOnaraActiveItem = updateOnaraActiveItem;
const resolveItemThumbInfo = (itemid, slug, extraMeta = null) => {
let numericId = extraMeta?.numericId || null;
let thumb = extraMeta?.thumb || null;
let mode = extraMeta?.mode || null;
let mime = extraMeta?.mime || null;
let user = extraMeta?.user || null;
let dest = extraMeta?.dest || null;
if (!numericId) {
if (typeof itemid === 'number' || (typeof itemid === 'string' && /^\d+$/.test(itemid))) {
numericId = itemid;
} else if (typeof slug === 'number' || (typeof slug === 'string' && /^\d+$/.test(slug))) {
numericId = slug;
} else if (window._randomPool?.idMap) {
if (slug && window._randomPool.idMap[slug]) {
numericId = window._randomPool.idMap[slug];
} else if (itemid && window._randomPool.idMap[itemid]) {
numericId = window._randomPool.idMap[itemid];
}
}
if (!numericId && typeof window.getCurrentItemId === 'function') {
const cur = window.getCurrentItemId();
if (cur && /^\d+$/.test(cur)) numericId = cur;
}
}
// Recover mime, user, dest from mounted Onara DOM if not passed in extraMeta
if (!mime) {
const mimeEl = document.querySelector('#onara-item-mount #info-file-mime, #onara-item-mount [data-mime], #info-file-mime');
if (mimeEl) {
mime = mimeEl.getAttribute('data-mime') || mimeEl.textContent?.trim() || null;
}
if (!mime) {
const mediaEl = document.querySelector('#onara-item-mount video, #onara-item-mount audio, #onara-item-mount img');
if (mediaEl) {
if (mediaEl.tagName === 'VIDEO') mime = 'video/mp4';
else if (mediaEl.tagName === 'AUDIO') mime = 'audio/mp3';
else if (mediaEl.tagName === 'IMG') {
const src = mediaEl.src || '';
if (src.endsWith('.png')) mime = 'image/png';
else if (src.endsWith('.gif')) mime = 'image/gif';
else if (src.endsWith('.webp')) mime = 'image/webp';
else mime = 'image/jpeg';
}
}
}
}
if (!user) {
const userEl = document.querySelector('#onara-item-mount #a_username, #a_username');
if (userEl) {
user = userEl.getAttribute('data-username') || userEl.textContent?.trim() || null;
}
}
if (!dest) {
const directLinkEl = document.querySelector('#onara-item-mount #info-file-direct-link, #info-file-direct-link');
if (directLinkEl) {
dest = directLinkEl.getAttribute('href') || null;
}
}
if (!thumb && numericId) {
thumb = `/t/${numericId}.webp`;
} else if (!thumb && typeof itemid === 'string' && /^\d+$/.test(itemid)) {
thumb = `/t/${itemid}.webp`;
}
if (!mode) {
if (typeof window.activeMode === 'string' && ['sfw', 'nsfw', 'nsfl'].includes(window.activeMode)) {
mode = window.activeMode;
} else {
mode = 'sfw';
}
}
return { numericId, thumb, mode, mime, user, dest };
};
const createSynthThumb = (itemid, url, slug, extraMeta = null) => {
const { numericId, thumb, mode, mime, user, dest } = resolveItemThumbInfo(itemid, slug, extraMeta);
const synthThumb = document.createElement('a');
synthThumb.href = url || `/${slug || itemid}`;
synthThumb.className = 'thumb lazy-thumb onara-active onara-synth-thumb loaded';
if (numericId) {
synthThumb.dataset.itemId = numericId;
}
if (mime) {
synthThumb.dataset.mime = mime;
synthThumb.dataset.ext = getExtFromMime(mime);
}
if (user) {
synthThumb.dataset.user = user;
}
if (dest) {
synthThumb.dataset.file = String(dest).replace(/^\/b\//, '');
}
const finalThumbUrl = thumb || (numericId ? `/t/${numericId}.webp` : `/t/${itemid}.webp`);
synthThumb.dataset.bg = finalThumbUrl;
synthThumb.setAttribute('data-mode', mode || 'sfw');
synthThumb.dataset.size = '1';
synthThumb.style.setProperty('--thumb-bg', `url('${finalThumbUrl}')`);
synthThumb.innerHTML = '<div class="thumb-indicators"></div><div class="thumb-select-check"><i class="fa-solid fa-check"></i></div><p></p>';
return synthThumb;
};
let _onaraSyncSeq = 0;
const syncOnaraBackgroundGrid = async (itemid, url, knownPage = null, slug = null) => {
const syncOnaraBackgroundGrid = async (itemid, url, knownPage = null, slug = null, extraMeta = null) => {
if (!isOnaraActive()) return;
const isRandom = document.cookie.includes('random_mode=1') || (url && url.includes('random=1')) || window.location.search.includes('random=1');
// Fast path: if target item is already in current background DOM (and not a temporary placeholder), just highlight and ensure visibility
const existingThumb = updateOnaraActiveItem(itemid, url, false, slug);
const existingThumb = updateOnaraActiveItem(itemid, url, false, slug, extraMeta);
if (existingThumb && !existingThumb.classList.contains('onara-synth-thumb')) {
return;
}
if (existingThumb && existingThumb.classList.contains('onara-synth-thumb')) {
if (isRandom) {
if (extraMeta) {
if (extraMeta.mime && !existingThumb.dataset.mime) {
existingThumb.dataset.mime = extraMeta.mime;
existingThumb.dataset.ext = getExtFromMime(extraMeta.mime);
}
if (extraMeta.user && !existingThumb.dataset.user) {
existingThumb.dataset.user = extraMeta.user;
}
if (extraMeta.dest && !existingThumb.dataset.file) {
existingThumb.dataset.file = String(extraMeta.dest).replace(/^\/b\//, '');
}
}
scrollOnaraThumbIntoView(existingThumb, false);
return;
}
existingThumb.remove();
}
const currentPosts = document.querySelector('.posts');
// In random (ZOMG) mode, if posts already exist in background, don't fetch page 1 (which wipes the grid with random items).
// Instead, seamlessly insert the synthetic thumb at a random position in the existing grid.
if (isRandom && currentPosts && currentPosts.children.length > 0) {
currentPosts.querySelectorAll('.onara-synth-thumb').forEach(el => el.remove());
const synthThumb = createSynthThumb(itemid, url, slug, extraMeta);
const children = Array.from(currentPosts.children);
const randIdx = Math.floor(Math.random() * (children.length + 1));
if (randIdx < children.length) {
currentPosts.insertBefore(synthThumb, children[randIdx]);
} else {
currentPosts.appendChild(synthThumb);
}
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
scrollOnaraThumbIntoView(synthThumb, true);
return;
}
const currentSeq = ++_onaraSyncSeq;
const urlObj = new URL(url || window.location.href, window.location.origin);
const feedBasePath = urlObj.pathname.replace(/\/+$/, '').replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '') || '/';
@@ -2067,16 +2247,21 @@ window.cancelAnimFrame = (function () {
}
}
const thumb = updateOnaraActiveItem(itemid, url, true, slug);
const currentPosts = document.querySelector('.posts');
if (!thumb && currentPosts) {
const synthThumb = document.createElement('a');
synthThumb.href = url;
synthThumb.className = 'thumb lazy-thumb onara-active onara-synth-thumb';
synthThumb.dataset.bg = `/t/${itemid}.webp`;
synthThumb.dataset.size = '1';
synthThumb.innerHTML = '<div class="thumb-indicators"></div><p></p>';
currentPosts.prepend(synthThumb);
const thumb = updateOnaraActiveItem(itemid, url, true, slug, extraMeta);
const currentPostsFallback = document.querySelector('.posts');
if (!thumb && currentPostsFallback) {
const synthThumb = createSynthThumb(itemid, url, slug, extraMeta);
if (isRandom && currentPostsFallback.children.length > 0) {
const children = Array.from(currentPostsFallback.children);
const randIdx = Math.floor(Math.random() * (children.length + 1));
if (randIdx < children.length) {
currentPostsFallback.insertBefore(synthThumb, children[randIdx]);
} else {
currentPostsFallback.appendChild(synthThumb);
}
} else {
currentPostsFallback.prepend(synthThumb);
}
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
scrollOnaraThumbIntoView(synthThumb, true);
}
@@ -3130,7 +3315,11 @@ window.cancelAnimFrame = (function () {
};
const initAlbumGallery = () => {
const container = document.querySelector('.album-gallery-container');
const onaraMount = document.getElementById('onara-item-mount');
const isOnara = (typeof isOnaraActive === 'function' && isOnaraActive()) || document.body.classList.contains('onara-modal-open');
const container = (isOnara && onaraMount)
? (onaraMount.querySelector('.album-gallery-container') || document.querySelector('.album-gallery-container'))
: document.querySelector('.album-gallery-container');
if (!container) {
window._currentActiveAlbumGallery = null;
return;
@@ -3413,8 +3602,24 @@ window.cancelAnimFrame = (function () {
if (isAutoplayAllowed()) {
const playPromise = videoEl.play();
if (playPromise !== undefined) {
playPromise.catch(() => {
playerWrap.classList.add('v0ck_initial');
playPromise.then(() => {
playerWrap.classList.remove('v0ck_initial');
}).catch((err) => {
if (err.name === 'AbortError') {
const onCanPlay = () => {
videoEl.removeEventListener('canplay', onCanPlay);
if (videoEl.paused) {
videoEl.play().then(() => {
playerWrap.classList.remove('v0ck_initial');
}).catch(() => {
playerWrap.classList.add('v0ck_initial');
});
}
};
videoEl.addEventListener('canplay', onCanPlay, { once: true });
} else {
playerWrap.classList.add('v0ck_initial');
}
});
}
} else {
@@ -3509,8 +3714,24 @@ window.cancelAnimFrame = (function () {
if (isAutoplayAllowed()) {
const playPromise = audioEl.play();
if (playPromise !== undefined) {
playPromise.catch(() => {
playerWrap.classList.add('v0ck_initial');
playPromise.then(() => {
playerWrap.classList.remove('v0ck_initial');
}).catch((err) => {
if (err.name === 'AbortError') {
const onCanPlay = () => {
audioEl.removeEventListener('canplay', onCanPlay);
if (audioEl.paused) {
audioEl.play().then(() => {
playerWrap.classList.remove('v0ck_initial');
}).catch(() => {
playerWrap.classList.add('v0ck_initial');
});
}
};
audioEl.addEventListener('canplay', onCanPlay, { once: true });
} else {
playerWrap.classList.add('v0ck_initial');
}
});
}
} else {
@@ -4006,6 +4227,39 @@ window.cancelAnimFrame = (function () {
updateInfoModal: updateInfoModal,
updateAlbumTags: updateAlbumTags,
getCurrentSubf0ck: () => albumData[currentIndex],
removeSubf0ck: (subIdentifier) => {
let idx = -1;
if (typeof subIdentifier === 'number' && subIdentifier >= 0 && subIdentifier < albumData.length) {
idx = subIdentifier;
} else if (subIdentifier !== undefined && subIdentifier !== null) {
idx = albumData.findIndex(s => s && (s.id == subIdentifier || s.slug == subIdentifier || s.subf0ck_id == subIdentifier));
} else {
idx = currentIndex;
}
if (idx === -1) idx = currentIndex;
if (idx < 0 || idx >= albumData.length) return false;
albumData.splice(idx, 1);
if (albumData.length <= 1) {
window.location.reload();
return true;
}
const thumbs = container.querySelectorAll('.album-thumb-item');
if (thumbs[idx]) {
thumbs[idx].remove();
}
const updatedThumbs = container.querySelectorAll('.album-thumb-item');
updatedThumbs.forEach((th, newIdx) => {
th.setAttribute('data-index', newIdx);
});
if (totalCountEl) totalCountEl.textContent = albumData.length;
if (currentIndex >= albumData.length) currentIndex = albumData.length - 1;
showImage(currentIndex, 'none', true);
return true;
},
isHovered: false
};
window.albumGallery = window._currentActiveAlbumGallery;
@@ -4037,7 +4291,15 @@ window.cancelAnimFrame = (function () {
const setupMedia = () => {
window._currentActiveAlbumGallery = null;
window.albumGallery = null;
const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video");
const onaraMount = document.getElementById('onara-item-mount');
const isOnara = (typeof isOnaraActive === 'function' && isOnaraActive()) || document.body.classList.contains('onara-modal-open');
let elem = null;
if (isOnara && onaraMount) {
elem = onaraMount.querySelector("#my-video, audio#my-video");
}
if (!elem) {
elem = document.querySelector("#main #my-video, #main audio#my-video") || document.querySelector("#my-video, audio#my-video");
}
if (elem) {
video = new v0ck(elem);
} else {
@@ -12049,8 +12311,16 @@ window.cancelAnimFrame = (function () {
onaraMount.innerHTML = '';
}
_container = onaraMount;
updateOnaraActiveItem(itemid, url, false, _cachedItem.slug);
syncOnaraBackgroundGrid(itemid, url, _cachedItem.page, _cachedItem.slug);
const cachedExtraMeta = {
numericId: _cachedItem.numericId,
thumb: _cachedItem.thumb,
mode: _cachedItem.mode,
mime: _cachedItem.mime,
user: _cachedItem.user,
dest: _cachedItem.dest
};
updateOnaraActiveItem(itemid, url, false, _cachedItem.slug, cachedExtraMeta);
syncOnaraBackgroundGrid(itemid, url, _cachedItem.page, _cachedItem.slug, cachedExtraMeta);
} else {
_container = document.querySelector('#main .container') || (document.getElementById('main')?.classList.contains('item-view') ? document.getElementById('main') : null);
const _isStructuralPage = !!document.querySelector('.pagewrapper');
@@ -12214,6 +12484,8 @@ window.cancelAnimFrame = (function () {
const tStart = performance.now();
let html, paginationHtml, responseSlug = null, responsePage = null;
let responseNumericId = null, responseThumb = null, responseMode = null;
let responseMime = null, responseUser = null, responseDest = null;
// ── Pre-fetched data path (merged random + item load) ──────────────
if (options.prefetchedData) {
@@ -12228,6 +12500,12 @@ window.cancelAnimFrame = (function () {
paginationHtml = data.pagination;
responseSlug = data.slug || data.item?.slug || null;
responsePage = data.page || null;
responseNumericId = data.numeric_id || data.id || data.item?.id || null;
responseThumb = data.thumb || data.item?.thumb || (responseNumericId ? `/t/${responseNumericId}.webp` : null);
responseMode = data.mode ?? data.tag_id ?? data.item?.tag_id ?? null;
responseMime = data.mime || data.item?.matching_sub_mime || data.item?.mime || null;
responseUser = data.user || data.username || data.item?.author_display_name || data.item?.display_name || data.item?.username || null;
responseDest = data.dest || data.file || data.item?.matching_sub_dest || data.item?.dest || null;
}
window.f0ckDebug(`[CLIENT_DEBUG] Using pre-fetched data (skipped network fetch)`);
} else {
@@ -12259,6 +12537,12 @@ window.cancelAnimFrame = (function () {
paginationHtml = data.pagination;
responseSlug = data.slug || data.item?.slug || null;
responsePage = data.page || null;
responseNumericId = data.numeric_id || data.id || data.item?.id || null;
responseThumb = data.thumb || data.item?.thumb || (responseNumericId ? `/t/${responseNumericId}.webp` : null);
responseMode = data.mode ?? data.tag_id ?? data.item?.tag_id ?? null;
responseMime = data.mime || data.item?.matching_sub_mime || data.item?.mime || null;
responseUser = data.user || data.username || data.item?.author_display_name || data.item?.display_name || data.item?.username || null;
responseDest = data.dest || data.file || data.item?.matching_sub_dest || data.item?.dest || null;
} else {
html = rawText;
}
@@ -12270,7 +12554,18 @@ window.cancelAnimFrame = (function () {
// ── Store in item cache (stale-while-revalidate) ───────────────────────
if (html && !options.noCacheStore) {
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, page: responsePage, ts: Date.now() });
itemCacheMap.set(_itemCacheKey, {
html,
slug: responseSlug,
page: responsePage,
numericId: responseNumericId,
thumb: responseThumb,
mode: responseMode,
mime: responseMime,
user: responseUser,
dest: responseDest,
ts: Date.now()
});
if (itemCacheMap.size > ITEM_CACHE_MAX) {
// Evict oldest entry
itemCacheMap.delete(itemCacheMap.keys().next().value);
@@ -12290,8 +12585,16 @@ window.cancelAnimFrame = (function () {
onaraMount.innerHTML = '';
}
container = onaraMount;
updateOnaraActiveItem(itemid, url, false, responseSlug);
syncOnaraBackgroundGrid(itemid, url, responsePage, responseSlug);
const currentExtraMeta = {
numericId: responseNumericId,
thumb: responseThumb,
mode: responseMode,
mime: responseMime,
user: responseUser,
dest: responseDest
};
updateOnaraActiveItem(itemid, url, false, responseSlug, currentExtraMeta);
syncOnaraBackgroundGrid(itemid, url, responsePage, responseSlug, currentExtraMeta);
} else {
container = document.querySelector('#main .container') || (document.getElementById('main') && document.getElementById('main').classList.contains('item-view') ? document.getElementById('main') : null);
const isStructuralPage = !!document.querySelector('.pagewrapper');
@@ -12731,7 +13034,11 @@ window.cancelAnimFrame = (function () {
// Background grid sync: pass targetUrl and null so actual page is looked up and synced
if (isOnaraActive()) {
syncOnaraBackgroundGrid(pick, targetUrl, null, pick);
const numericId = window._randomPool?.idMap?.[pick] || (/^\d+$/.test(pick) ? pick : null);
syncOnaraBackgroundGrid(pick, targetUrl, null, pick, {
numericId,
thumb: numericId ? `/t/${numericId}.webp` : null
});
}
return;
}
@@ -12802,7 +13109,14 @@ window.cancelAnimFrame = (function () {
}
loadItemAjax(targetUrl, true, { transition: 'fade-zoom', prefetchedData: data });
if (isOnaraActive() && data.id) {
syncOnaraBackgroundGrid(data.id, targetUrl, data.page || null, targetKey);
syncOnaraBackgroundGrid(data.id, targetUrl, data.page || null, targetKey, {
numericId: data.numeric_id || data.id,
thumb: data.thumb || `/t/${data.numeric_id || data.id}.webp`,
mode: data.mode ?? data.tag_id,
mime: data.mime,
user: data.user || data.username,
dest: data.dest
});
}
} else {
// No items found — restore UI, don't redirect
@@ -16580,7 +16894,8 @@ class NotificationSystem {
this.retryCount = 0;
this.maxRetries = 20; // Increased retries
this.pendingNotifIds = new Set(); // item IDs notified before thumbnail was in the grid
this.activeTab = 'user'; // 'user' or 'system'
const activeTabEl = this.dropdown ? this.dropdown.querySelector('.notif-tab.active') : null;
this.activeTab = activeTabEl ? activeTabEl.dataset.tab : ((window.f0ckEnableComments === false) ? 'system' : 'user');
this._cachedUser = [];
this._cachedSystem = [];
@@ -16906,15 +17221,18 @@ class NotificationSystem {
if (!delId) return;
window.f0ckDebug(`[SSE] Item deleted: ${delId}`);
// Remove from main grid — a.thumb is the anchor, li is its parent card
const thumb = document.querySelector(`a.thumb[href$="/${delId}"], a.lazy-thumb[href$="/${delId}"]`);
if (thumb) {
// Remove from main grid — prefer data-item-id (works with slugs too), fall back to href match
const gridCards = document.querySelectorAll(
`a.thumb[data-item-id="${delId}"], a.lazy-thumb[data-item-id="${delId}"], ` +
`a.thumb[href$="/${delId}"], a.lazy-thumb[href$="/${delId}"]`
);
gridCards.forEach(thumb => {
const card = thumb.closest('li') || thumb;
card.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
card.style.opacity = '0';
card.style.transform = 'scale(0.95)';
setTimeout(() => card.remove(), 300);
}
});
// If currently viewing this item, navigate to next item using soft AJAX nav
const currentItemId = (typeof window.getCurrentItemId === 'function' ? window.getCurrentItemId() : null) || window.currentItemId;
@@ -17014,6 +17332,33 @@ class NotificationSystem {
document.dispatchEvent(new CustomEvent('f0ck:global_chat_topic', { detail: data.data }));
} else if (data.type === 'global_chat_presence') {
document.dispatchEvent(new CustomEvent('f0ck:global_chat_presence', { detail: data.data }));
} else if (data.type === 'brand_image') {
window.f0ckDebug(`[SSE] Brand image update received:`, data.data?.url);
if (data.data?.url) {
const src = data.data.url;
// Update the randomizeLogo pool so clicking the brand always uses the current image
window.f0ckBrandImages = [src];
const logos = document.querySelectorAll('#navbar-logo');
if (logos.length > 0) {
logos.forEach(el => { el.src = src; el.style.display = ''; });
} else {
// Logo element doesn't exist yet (no image was set before) — create it
document.querySelectorAll('a.navbar-brand').forEach(brandLink => {
const textNode = Array.from(brandLink.childNodes).find(n => n.nodeType === Node.TEXT_NODE);
const img = document.createElement('img');
img.id = 'navbar-logo';
img.src = src;
img.alt = document.title;
img.style.cssText = 'max-height:40px;vertical-align:middle;max-width:180px;width:auto;';
if (textNode) brandLink.insertBefore(img, textNode);
else brandLink.prepend(img);
});
}
} else {
window.f0ckBrandImages = [];
document.querySelectorAll('#navbar-logo').forEach(el => el.remove());
}
}
} catch (err) {
console.error('SSE data parse error', err);
@@ -19113,6 +19458,9 @@ class ModAction {
confirmBtn.innerText = options.confirmText || (hideReason ? (i18n.confirm_yes || 'Yes') : (i18n.confirm_btn || 'Confirm'));
cancelBtn.innerText = options.cancelText || (hideReason ? (i18n.confirm_no || 'No') : (i18n.cancel_btn || 'Cancel'));
confirmBtn.style.display = options.hideConfirm ? 'none' : '';
cancelBtn.style.display = options.hideCancel ? 'none' : '';
const close = () => {
modal.style.display = 'none';
cleanup();
@@ -19149,6 +19497,8 @@ class ModAction {
const cleanup = () => {
confirmBtn.onclick = null;
cancelBtn.onclick = null;
confirmBtn.style.display = '';
cancelBtn.style.display = '';
if (enterHandler) reasonEl.removeEventListener('keydown', enterHandler);
confirmBtn.disabled = false;
};
@@ -19813,6 +20163,11 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
// Helper to reset all category checkboxes
const resetReportCategories = () => {
document.querySelectorAll('.report-cat-check').forEach(cb => { cb.checked = false; });
};
// Open item report
document.addEventListener('click', (e) => {
const itemBtn = e.target.closest('.report-item-btn');
@@ -19822,6 +20177,7 @@ document.addEventListener('DOMContentLoaded', () => {
reportCommentInput.value = '';
reportUserInput.value = '';
reportReason.value = '';
resetReportCategories();
clearReportError();
reportModal.style.display = 'flex';
document.body.classList.add('modal-open');
@@ -19836,6 +20192,7 @@ document.addEventListener('DOMContentLoaded', () => {
reportCommentInput.value = commentBtn.dataset.id;
reportUserInput.value = '';
reportReason.value = '';
resetReportCategories();
clearReportError();
reportModal.style.display = 'flex';
document.body.classList.add('modal-open');
@@ -19850,6 +20207,7 @@ document.addEventListener('DOMContentLoaded', () => {
reportCommentInput.value = '';
reportUserInput.value = userBtn.dataset.userId;
reportReason.value = '';
resetReportCategories();
clearReportError();
reportModal.style.display = 'flex';
document.body.classList.add('modal-open');
@@ -19861,6 +20219,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (e.target.matches('#report-cancel') || e.target.id === 'report-modal') {
reportModal.style.display = 'none';
document.body.classList.remove('modal-open');
resetReportCategories();
clearReportError();
if (_reportRcWidgetId !== null && window.grecaptcha) {
try { grecaptcha.reset(_reportRcWidgetId); } catch(e) {}
@@ -19871,8 +20230,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (e.target.matches('#report-submit')) {
clearReportError();
const reason = reportReason.value.trim();
if (!reason) {
showReportError((window.f0ckI18n && window.f0ckI18n.reason_required) || 'Please provide a reason.', reportReason);
const checkedCategories = Array.from(document.querySelectorAll('.report-cat-check:checked')).map(cb => cb.value);
if (!reason && checkedCategories.length === 0) {
showReportError((window.f0ckI18n && window.f0ckI18n.reason_required) || 'Please select at least one reason or provide a description.', reportReason);
return;
}
@@ -19894,7 +20255,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (reportItemInput.value) payload.append('item_id', reportItemInput.value);
if (reportCommentInput.value) payload.append('comment_id', reportCommentInput.value);
if (reportUserInput.value) payload.append('reported_user_id', reportUserInput.value);
payload.append('reason', reason);
if (reason) payload.append('reason', reason);
if (checkedCategories.length) payload.append('categories', checkedCategories.join(','));
if (rcToken) payload.append('g-recaptcha-response', rcToken);
const submitBtn = e.target;
@@ -19916,6 +20278,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.showFlash) window.showFlash((window.f0ckI18n && window.f0ckI18n.report_success) || 'Report submitted successfully.', 'success');
// Reset fields for future reports
reportReason.value = '';
resetReportCategories();
} else {
const errMsg = data.msg || (window.f0ckI18n && window.f0ckI18n.report_error) || 'An error occurred.';
showReportError(errMsg);
@@ -22312,6 +22675,9 @@ window.BulkSelection = (() => {
const bar = document.createElement('div');
bar.id = 'bulk-action-bar';
bar.innerHTML = `
<button type="button" class="bulk-btn bulk-btn-close" id="bulk-btn-close" title="Cancel selection (Esc)">
<i class="fa-solid fa-xmark"></i>
</button>
<div class="bulk-count-box">
<i class="fa-solid fa-check-double" style="color: var(--accent);"></i>
<span class="bulk-count-num">0</span>
@@ -22344,13 +22710,14 @@ window.BulkSelection = (() => {
</button>
</div>
</div>
<button type="button" class="bulk-btn bulk-btn-danger" id="bulk-btn-delete" title="Delete selected items">
<i class="fa-solid fa-trash-can"></i>
<span class="btn-text-full">Delete</span>
</button>
<button type="button" class="bulk-btn" id="bulk-btn-select-all" title="Select all loaded items">
<i class="fa-regular fa-square-check"></i>
<span class="btn-text-full">Select All</span>
</button>
<button type="button" class="bulk-btn bulk-btn-close" id="bulk-btn-close" title="Cancel selection (Esc)">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
`;
document.body.appendChild(bar);
@@ -22406,6 +22773,7 @@ window.BulkSelection = (() => {
document.getElementById('bulk-btn-select-all')?.addEventListener('click', () => selectAllLoaded());
document.getElementById('bulk-btn-close')?.addEventListener('click', () => exitSelectionMode());
document.getElementById('bulk-btn-delete')?.addEventListener('click', () => executeBulkDelete());
// Wire up Modal events
document.getElementById('bulk-modal-close-btn')?.addEventListener('click', () => closeTagModal());
@@ -22779,6 +23147,59 @@ window.BulkSelection = (() => {
}
}
function executeBulkDelete() {
const itemIds = Array.from(selectedIds);
if (itemIds.length === 0) return;
if (typeof ModAction === 'undefined') {
return window.flashMessage?.('Error: ModAction module not loaded', 3000, 'error');
}
const i18n = window.f0ckI18n || {};
const title = i18n.item_delete_title || 'Delete Item';
const msg = `Are you sure you want to delete <strong>${itemIds.length} selected item(s)</strong>? This cannot be undone.`;
ModAction.confirm(title, msg, async (reason) => {
let deleted = 0;
let failed = 0;
for (const id of itemIds) {
try {
const res = await fetch('/api/v2/admin/deletepost', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify({ postid: id, reason: reason || 'Bulk delete' })
});
const data = await res.json();
if (data.success) {
deleted++;
const thumb = document.querySelector(`a.thumb[data-item-id="${id}"], a.lazy-thumb[data-item-id="${id}"], a.thumb[href$="/${id}"], a.lazy-thumb[href$="/${id}"]`);
if (thumb) {
const card = thumb.closest('li') || thumb;
card.style.transition = 'opacity 0.25s ease, transform 0.25s ease';
card.style.opacity = '0';
card.style.transform = 'scale(0.92)';
setTimeout(() => card.remove(), 260);
}
} else {
failed++;
console.warn(`[BULK_DELETE] Failed to delete item ${id}:`, data.msg);
}
} catch (err) {
failed++;
console.error(`[BULK_DELETE] Error deleting item ${id}:`, err);
}
}
if (failed > 0) throw new Error(`Deleted ${deleted} item(s), ${failed} failed`);
flash(`Deleted ${deleted} item(s)`);
exitSelectionMode();
}, { allowEmpty: window.f0ckSession?.is_admin, unsafeContent: true });
}
// --- Event Listeners Initialization ---
function init() {
ensureDOMElements();
@@ -22979,6 +23400,7 @@ window.BulkSelection = (() => {
openTagModal,
closeTagModal,
executeBulkRating,
executeBulkDelete,
selectAllLoaded,
getSelectedIds: () => Array.from(selectedIds)
};