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
+113 -10
View File
@@ -396,18 +396,48 @@
}
};
const deleteButtonEvent = async e => {
if (e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
const ctx = getContext();
if (!ctx) return;
const { postid, poster, authorId } = ctx;
const deleteSubItemEvent = async (subf0ck, postid) => {
if (!subf0ck || !postid) return;
if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded');
const subDesc = subf0ck.display_index
? `Slide #${subf0ck.display_index}`
: `Slide (${subf0ck.slug || subf0ck.id})`;
ModAction.confirm(
'Delete Slide from Album',
`Are you sure you want to delete <strong style="color:#d9534f">${subDesc}</strong> from this album?<br><small style="opacity:0.8;">The rest of the album will remain intact.</small>`,
async (reason) => {
const res = await post("/api/v2/admin/delete-album-item", {
postid: postid,
sub_id: subf0ck.id,
sub_slug: subf0ck.slug,
order_index: subf0ck.order_index,
reason: reason
});
if (!res.success) {
throw new Error(res.msg || 'Failed to delete album item');
}
if (res.post_deleted) {
if (window.flashMessage) window.flashMessage('Album deleted (no remaining items)', 2500, 'info');
const mediaObj = document.querySelector('.media-object');
if (mediaObj) {
mediaObj.innerHTML = '<div style="padding: 100px; text-align: center; color: #d9534f;"><h1>Album Deleted</h1><p>The album has been removed.</p></div>';
}
} else {
if (window.albumGallery && typeof window.albumGallery.removeSubf0ck === 'function') {
window.albumGallery.removeSubf0ck(subf0ck.id || subf0ck.slug || subf0ck.order_index);
} else {
window.location.reload();
}
if (window.flashMessage) window.flashMessage('Slide deleted from album', 2500, 'success');
}
},
{ allowEmpty: window.f0ckSession?.is_admin, confirmText: 'Delete Slide' }
);
};
const deleteEntirePost = (postid, poster, authorId) => {
const i18n = window.f0ckI18n || {};
const confirmTitle = i18n.item_delete_title || 'Delete Item';
const posterStr = poster
@@ -438,6 +468,68 @@
}, { allowEmpty: window.f0ckSession?.is_admin });
};
const deleteButtonEvent = async e => {
if (e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
const ctx = getContext();
if (!ctx) return;
const { postid, poster, authorId } = ctx;
if (typeof ModAction === 'undefined') return alert('Error: ModAction module not loaded');
const isAlbum = !!(window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function');
const curSub = isAlbum ? window.albumGallery.getCurrentSubf0ck() : null;
if (isAlbum && curSub) {
const subDesc = curSub.display_index ? `Slide ${curSub.display_index}` : 'Current Slide';
const choiceHtml = `
<div style="margin-bottom: 15px; font-size: 1.05rem;">
This post is an album containing multiple slides. What would you like to delete?
</div>
<div style="display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;">
<button type="button" id="btn-choice-delete-sub" class="btn btn-warning" style="padding: 8px 16px; font-weight: 600; cursor: pointer;">
<i class="fa-solid fa-trash-can"></i> Delete ${subDesc} Only
</button>
<button type="button" id="btn-choice-delete-album" class="btn btn-danger" style="padding: 8px 16px; font-weight: 600; cursor: pointer;">
<i class="fa-solid fa-layer-group"></i> Delete Entire Album
</button>
</div>
`;
ModAction.confirm('Delete Options', choiceHtml, () => {}, {
hideReason: true,
hideConfirm: true,
unsafeContent: true,
cancelText: 'Cancel'
});
setTimeout(() => {
const modal = document.getElementById('mod-action-modal');
if (!modal) return;
const subBtn = modal.querySelector('#btn-choice-delete-sub');
const albumBtn = modal.querySelector('#btn-choice-delete-album');
if (subBtn) {
subBtn.onclick = () => {
modal.style.display = 'none';
deleteSubItemEvent(curSub, postid);
};
}
if (albumBtn) {
albumBtn.onclick = () => {
modal.style.display = 'none';
deleteEntirePost(postid, poster, authorId);
};
}
}, 50);
return;
}
deleteEntirePost(postid, poster, authorId);
};
let tmptt = null;
const editTagEvent = async e => {
e.preventDefault();
@@ -504,6 +596,17 @@
addtagClick(e);
} else if (target.closest("#a_delete")) {
deleteButtonEvent(e);
} else if (target.closest(".album-sub-delete-btn, #a_delete_sub")) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const ctx = getContext();
const curSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function')
? window.albumGallery.getCurrentSubf0ck()
: null;
if (ctx && curSub) {
deleteSubItemEvent(curSub, ctx.postid);
}
} else if (target.matches('#tags .badge > a[href*="/tag/"]')) {
editTagEvent(e);
} else if (target.closest('.admin-deltag') || target.closest('.removetag')) {
+568 -969
View File
File diff suppressed because it is too large Load Diff
+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)
};
+294
View File
@@ -0,0 +1,294 @@
var CATEGORY_META = {
wrong_rating: { label: 'Wrong Rating', bg: '#ffc107', color: '#000' },
spam: { label: 'Spam', bg: '#6c757d', color: '#fff' },
duplicate: { label: 'Duplicate', bg: '#17a2b8', color: '#fff' },
copyright: { label: 'Copyright', bg: '#fd7e14', color: '#fff' },
illegal: { label: 'Illegal', bg: '#dc3545', color: '#fff' },
other: { label: 'Other', bg: '#495057', color: '#fff' }
};
function catBadge(c) {
var m = CATEGORY_META[c] || { label: c, bg: '#444', color: '#fff' };
return '<span class="rp-cat" style="background:' + m.bg + ';color:' + m.color + ';">' + m.label + '</span>';
}
function rpEsc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function relTime(d) {
var diff = (Date.now() - new Date(d)) / 1000;
if (diff < 60) return 'just now';
if (diff < 3600) return Math.floor(diff/60) + 'm ago';
if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
return new Date(d).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
}
window.currentPage = window.currentPage || 1;
window.loadReports = async function(page) {
page = page || 1;
window.currentPage = page;
var status = document.getElementById('report-status-filter').value;
var feed = document.getElementById('reports-feed');
var pag = document.getElementById('reports-pagination');
feed.innerHTML = '<div class="rp-state-msg"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</div>';
pag.innerHTML = '';
try {
var res = await fetch('/api/v2/mod/reports?status=' + status + '&page=' + page);
var data = await res.json();
if (!data.success) {
feed.innerHTML = '<div class="rp-state-msg" style="color:#dc3545;">Error: ' + rpEsc(data.msg) + '</div>';
return;
}
window.currentReports = data.reports;
window.emojiMap = new Map();
if (data.emojis) data.emojis.forEach(function(e) { window.emojiMap.set(e.name.toLowerCase(), e.url); });
if (!data.reports.length) {
feed.innerHTML = '<div class="rp-state-msg">No reports found.</div>';
return;
}
feed.innerHTML = '';
var isAdmin = window.f0ckSession && window.f0ckSession.admin;
data.reports.forEach(function(r) {
var card = document.createElement('div');
var cats = Array.isArray(r.categories) ? r.categories : [];
card.className = 'rp-card' + (cats.indexOf('illegal') !== -1 ? ' illegal-flag' : '');
var statusBadge = '<span class="rp-status-badge rp-status-' + status + '">' + status + '</span>';
var reporter = r.reporter_name
? '<a href="/user/' + rpEsc(r.reporter_name) + '" class="rp-reporter-link">' + rpEsc(r.reporter_name) + '</a>' + (r.reporter_ip ? ' <span class="rp-reporter-ip">(' + rpEsc(r.reporter_ip) + ')</span>' : '')
: '<span style="color:#666;font-style:italic;">Guest' + (r.reporter_ip ? ' (' + rpEsc(r.reporter_ip) + ')' : '') + '</span>';
var targetHtml = '';
var itemLink = '';
if (r.comment_id) {
targetHtml = '<span style="color:#888;font-size:0.85em;">comment #' + r.comment_id + '</span>';
if (r.resolved_item_id) itemLink = '<a href="/' + r.resolved_item_id + '" target="_blank" class="rp-open-link"><i class="fa-solid fa-arrow-up-right-from-square"></i> item #' + r.resolved_item_id + '</a>';
} else if (r.resolved_item_id) {
targetHtml = '<span style="color:#888;font-size:0.85em;">item</span>';
itemLink = '<a href="/' + r.resolved_item_id + '" target="_blank" class="rp-open-link"><i class="fa-solid fa-arrow-up-right-from-square"></i> #' + r.resolved_item_id + '</a>';
} else if (r.reported_user_name) {
targetHtml = 'user <a href="/user/' + rpEsc(r.reported_user_name) + '" class="rp-target-link">' + rpEsc(r.reported_user_name) + '</a>';
}
var previewHtml = '';
var isItem = !!r.resolved_item_id && r.resolved_item_dest;
var isComment = !!r.comment_id;
if (isItem && !isComment) {
var mime = r.resolved_item_mime || '';
var src = '/b/' + r.resolved_item_dest;
var href = '/' + r.resolved_item_id;
if (mime === 'video/youtube') {
var ytId = r.resolved_item_dest.replace('yt:', '');
previewHtml = '<div class="rp-preview"><img src="https://img.youtube.com/vi/' + ytId + '/mqdefault.jpg" loading="lazy"><a href="' + href + '" target="_blank" class="rp-preview-link"><i class="fa-brands fa-youtube"></i></a></div>';
} else if (mime.indexOf('image/') === 0) {
previewHtml = '<div class="rp-preview"><img src="' + src + '" loading="lazy"><a href="' + href + '" target="_blank" class="rp-preview-link"><i class="fa-solid fa-expand"></i></a></div>';
} else if (mime.indexOf('video/') === 0) {
previewHtml = '<div class="rp-preview"><video src="' + src + '" muted playsinline preload="metadata"></video><a href="' + href + '" target="_blank" class="rp-preview-link"><i class="fa-solid fa-play"></i></a></div>';
} else if (mime.indexOf('audio/') === 0) {
previewHtml = '<div class="rp-preview"><div class="rp-no-preview"><i class="fa-solid fa-music"></i></div><a href="' + href + '" target="_blank" class="rp-preview-link"><i class="fa-solid fa-expand"></i></a></div>';
} else {
previewHtml = '<div class="rp-preview"><div class="rp-no-preview"><i class="fa-solid fa-file"></i></div><a href="' + href + '" target="_blank" class="rp-preview-link"><i class="fa-solid fa-expand"></i></a></div>';
}
} else if (isComment) {
var body = (r.comment_body || '[deleted]').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
if (window.emojiMap) {
body = body.replace(/:([a-z0-9_]+):/g, function(match, code) {
var url = window.emojiMap.get(code.toLowerCase());
return url ? '<img src="' + url + '" style="height:18px;vertical-align:middle;" title=":' + code + ':">' : match;
});
}
previewHtml = '<div class="rp-preview" style="background:rgba(0,0,0,0.4);width:180px;min-width:180px;align-items:flex-start;padding:12px;overflow-y:auto;font-size:0.78em;color:#ccc;font-family:monospace;line-height:1.5;white-space:pre-wrap;height:140px;">' + body + '</div>';
} else {
previewHtml = '<div class="rp-preview"><div class="rp-no-preview"><i class="fa-solid fa-user"></i></div></div>';
}
var catsHtml = cats.length ? '<div class="rp-cats">' + cats.map(catBadge).join('') + '</div>' : '';
var reasonHtml = r.reason ? '<div class="rp-reason">' + rpEsc(r.reason) + '</div>' : '';
var actions = '';
if (status === 'pending') {
actions += '<button class="rp-btn rp-btn-resolve" onclick="window.resolveReport(' + r.id + ',\'resolved\')"><i class="fa-solid fa-check"></i> Resolve</button>';
actions += '<button class="rp-btn rp-btn-reject" onclick="window.resolveReport(' + r.id + ',\'rejected\')"><i class="fa-solid fa-xmark"></i> Reject</button>';
actions += '<span class="rp-sep"></span>';
}
if (isItem && !isComment) {
var isUnav = r.resolved_item_visibility === 3;
actions += '<button class="rp-btn rp-btn-delete" onclick="window.adminDeleteItem(' + r.resolved_item_id + ')"><i class="fa-solid fa-trash"></i> Delete</button>';
actions += '<button class="rp-btn ' + (isUnav ? 'rp-btn-avail' : 'rp-btn-unavail') + '" onclick="window.modToggleUnavailable(' + r.resolved_item_id + ',' + (r.resolved_item_visibility||0) + ')">' + (isUnav ? '<i class="fa-solid fa-eye"></i> Restore' : '<i class="fa-solid fa-ban"></i> 451') + '</button>';
}
if (isComment) {
actions += '<button class="rp-btn rp-btn-delete" onclick="window.adminDeleteComment(' + r.comment_id + ')"><i class="fa-solid fa-trash"></i> Del Comment</button>';
}
if (r.reported_user_id && (isAdmin || !r.reported_user_is_admin)) {
var who = r.reported_user_name ? rpEsc(r.reported_user_name) : 'user';
actions += '<span class="rp-sep"></span>';
actions += '<button class="rp-btn rp-btn-warn" onclick="window.modWarnUser(' + r.reported_user_id + ')"><i class="fa-solid fa-triangle-exclamation"></i> Warn ' + who + '</button>';
actions += '<button class="rp-btn rp-btn-ban" onclick="window.adminBanUser(' + r.reported_user_id + ')"><i class="fa-solid fa-gavel"></i> Ban ' + who + '</button>';
} else if (!r.reported_user_id) {
actions += '<span class="rp-anon-note">Anonymous reporter</span>';
}
if (r.reporter_id && r.reporter_name) {
actions += '<button class="rp-btn rp-btn-secondary" onclick="window.modWarnUser(' + r.reporter_id + ')">Warn Reporter</button>';
}
var ts = new Date(r.created_at).toLocaleString();
var rt = relTime(r.created_at);
var resolverHtml = (status !== 'pending' && r.resolver_name)
? '<span style="font-size:0.78em;color:#888;margin-left:6px;"><i class="fa-solid fa-' + (status === 'resolved' ? 'check' : 'xmark') + '" style="margin-right:3px;color:' + (status === 'resolved' ? '#28a745' : '#6c757d') + ';"></i>by <a href="/user/' + rpEsc(r.resolver_name) + '" style="color:#888;font-weight:600;text-decoration:none;">' + rpEsc(r.resolver_name) + '</a></span>'
: '';
card.innerHTML =
'<div class="rp-card-bar">' +
'<div class="rp-card-bar-left">' +
'<span class="rp-card-id">#' + r.id + '</span>' +
statusBadge +
resolverHtml +
'<span style="font-size:0.83em;color:#aaa;margin-left:6px;">from ' + reporter + '</span>' +
(targetHtml ? '<span style="font-size:0.83em;color:#777;">&rarr; ' + targetHtml + '</span>' : '') +
itemLink +
'</div>' +
'<span class="rp-card-time" title="' + rpEsc(ts) + '">' + rt + '</span>' +
'</div>' +
'<div class="rp-card-body">' +
previewHtml +
'<div class="rp-info">' + catsHtml + reasonHtml + '</div>' +
'</div>' +
'<div class="rp-card-actions">' + actions + '</div>';
feed.appendChild(card);
});
pag.innerHTML = '';
if (data.pages > 1) {
if (data.page > 1)
pag.innerHTML += '<button onclick="window.loadReports(' + (data.page-1) + ')"><i class="fa-solid fa-chevron-left"></i> Prev</button>';
pag.innerHTML += '<span class="rp-page-info">Page ' + data.page + ' of ' + data.pages + '</span>';
if (data.page < data.pages)
pag.innerHTML += '<button onclick="window.loadReports(' + (data.page+1) + ')">Next <i class="fa-solid fa-chevron-right"></i></button>';
}
} catch(e) {
feed.innerHTML = '<div class="rp-state-msg" style="color:#dc3545;"><i class="fa-solid fa-circle-exclamation"></i> Network error</div>';
}
};
window.resolveReport = function(id, action) {
var label = action === 'resolved' ? 'Resolve' : 'Reject';
var desc = action === 'resolved'
? 'Mark report #' + id + ' as resolved.'
: 'Reject report #' + id + '. The content will remain as-is.';
window.ModAction.confirm(label + ' Report #' + id, desc, async function() {
var params = new URLSearchParams();
params.append('action', action);
var csrfToken = window.f0ckSession && window.f0ckSession.csrf_token;
var res = await fetch('/api/v2/mod/reports/' + id + '/resolve', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': csrfToken
},
body: params
});
var data = await res.json();
if (data.success) {
window.loadReports(window.currentPage);
if (window.NotificationSystemInstance && typeof window.NotificationSystemInstance.pollDebounced === 'function')
window.NotificationSystemInstance.pollDebounced();
} else {
throw new Error(data.msg || 'Failed to update report');
}
}, { hideReason: true });
};
window.adminDeleteComment = function(id) {
window.ModAction.confirm('Delete Comment #' + id, 'Are you sure you want to delete this comment? This action is permanent.', async (reason) => {
var params = new URLSearchParams();
params.append('reason', reason);
var res = await fetch('/api/comments/' + id + '/delete', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params });
var data = await res.json();
if (data.success) { if (window.showFlash) window.showFlash('comment deleted', 'success'); }
else throw new Error(data.msg || 'Unknown error');
});
};
window.adminDeleteItem = function(id) {
window.ModAction.confirm('Delete Item #' + id, 'Are you sure you want to delete this item? This action is permanent.', async (reason) => {
var params = new URLSearchParams();
params.append('postid', id); params.append('reason', reason);
var res = await fetch('/api/v2/admin/deletepost', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params });
var data = await res.json();
if (data.success) { if (window.showFlash) window.showFlash('item deleted', 'success'); }
else throw new Error(data.msg || 'Unknown error');
});
};
window.modToggleUnavailable = function(id, currentVis) {
var willBeUnavailable = currentVis !== 3;
var targetVis = willBeUnavailable ? 3 : 0;
var actionText = willBeUnavailable ? 'Make Unavailable (HTTP 451)' : 'Make Available (Public)';
window.ModAction.confirm('Item Visibility', actionText + ' for item #' + id + '?', async () => {
var params = new URLSearchParams();
params.append('postid', id); params.append('id', id); params.append('visibility', targetVis);
var csrfToken = window.f0ckSession && window.f0ckSession.csrf_token;
var res = await fetch('/api/v2/item/visibility', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrfToken }, body: params });
var data = await res.json();
if (data.success) {
if (window.showFlash) window.showFlash(willBeUnavailable ? 'Item marked unavailable (451)' : 'Item restored to public', 'success');
var item = window.currentReports.find(function(x) { return x.resolved_item_id === id; });
if (item) item.resolved_item_visibility = targetVis;
window.loadReports(window.currentPage);
} else throw new Error(data.msg || 'Failed to update visibility');
});
};
window.modWarnUser = function(userId) {
window.ModAction.confirm('Warn User ID ' + userId, '', async (reason) => {
var params = new URLSearchParams();
params.append('user_id', userId); params.append('reason', reason);
var res = await fetch('/api/v2/mod/warnings/issue', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params });
var data = await res.json();
if (data.success) { if (window.showFlash) window.showFlash('user has been warned', 'success'); }
else throw new Error(data.msg || 'Unknown error');
});
};
window.adminBanUser = function(userId) {
var isAdmin = window.f0ckSession && window.f0ckSession.admin;
var promptHtml =
'<p>This will restrict the user from accessing their account and performing most actions.</p>' +
'<div style="margin-top:10px;"><label>Ban Duration:</label>' +
'<select id="ban-duration-select" class="form-control" style="margin-top:5px;">' +
(isAdmin ? '<option value="permanent">Permanent</option>' : '') +
'<option value="1">1 Hour</option><option value="6">6 Hours</option><option value="24">24 Hours (1 Day)</option>' +
(!isAdmin ? '<option value="48">48 Hours (2 Days)</option>' : '') +
(isAdmin ? '<option value="168">168 Hours (1 Week)</option>' : '') +
(isAdmin ? '<option value="720">720 Hours (1 Month)</option>' : '') +
'</select></div>';
window.ModAction.confirm('Ban User ID ' + userId, promptHtml, async (reason) => {
var duration = document.getElementById('ban-duration-select').value;
var params = new URLSearchParams();
params.append('user_id', userId); params.append('reason', reason); params.append('duration', duration);
var res = await fetch('/api/v2/admin/ban', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: params });
var data = await res.json();
if (data.success) { if (window.showFlash) window.showFlash('User banned cleanly.', 'success'); }
else throw new Error(data.msg || 'Unknown error');
});
};
(function() {
var filter = document.getElementById('report-status-filter');
if (filter) filter.onchange = function() { window.loadReports(1); };
window.loadReports(1);
})();
+2 -2
View File
@@ -3654,8 +3654,8 @@
// Tab type arrays
const SCROLLER_USER_TYPES = ['comment_reply', 'subscription', 'mention', 'upload_comment'];
const SCROLLER_SYSTEM_TYPES = ['approve', 'deny', 'item_deleted', 'upload_success', 'upload_error', 'admin_pending', 'report', 'warning'];
let sActiveTab = 'user';
const sActiveTabEl = sNotifDropdown ? sNotifDropdown.querySelector('.notif-tab.active') : null;
let sActiveTab = sActiveTabEl ? sActiveTabEl.dataset.tab : ((window.f0ckEnableComments === false) ? 'system' : 'user');
let sCachedNotifs = [];
if (sNotifBtn && sNotifDropdown) {
+1 -1
View File
@@ -1170,7 +1170,7 @@
const renderVideoCard = (video, options = {}) => {
const videoKey = (window.f0ckSession?.enable_item_slugs !== false && video.slug) ? video.slug : video.id;
const rClass = video.rating_class || 'untagged';
const rClass = video.rating_class || (video.tag_id == 1 ? 'sfw' : (video.tag_id == 2 ? 'nsfw' : ((video.tag_id == 3 || video.tag_id == window.f0ckSession?.nsfl_tag_id) ? 'nsfl' : 'untagged')));
const blurNsfw = localStorage.getItem('blurNsfw') === 'true';
const blurNsfl = localStorage.getItem('blurNsfl') === 'true';
const blurSfw = localStorage.getItem('blurSfw') === 'true';
+26 -6
View File
@@ -815,13 +815,33 @@ class v0ck {
// Attempt autoplay and show overlay if blocked
const shouldAutoplay = !isBlurredDetail && window.f0ckSession?.disable_autoplay !== true;
if (shouldAutoplay) {
const playPromise = togglePlay();
if (playPromise !== undefined) {
playPromise.catch(() => {
if (!video.paused) {
player.classList.remove('v0ck_initial');
} else {
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise.then(() => {
player.classList.remove('v0ck_initial');
}).catch((err) => {
if (err && err.name === 'AbortError') {
const onCanPlay = () => {
video.removeEventListener('canplay', onCanPlay);
if (video.paused) {
video.play().then(() => {
player.classList.remove('v0ck_initial');
}).catch(() => {
player.classList.add('v0ck_initial');
});
}
};
video.addEventListener('canplay', onCanPlay, { once: true });
} else {
player.classList.add('v0ck_initial');
}
});
} else if (video.paused) {
player.classList.add('v0ck_initial');
});
} else if (video.paused) {
player.classList.add('v0ck_initial');
}
}
} else {
player.classList.add('v0ck_initial');