This commit is contained in:
2026-09-20 21:43:02 +02:00
parent 5601c282b7
commit 4e857fd5ef
26 changed files with 3567 additions and 142 deletions
+356 -64
View File
@@ -1438,7 +1438,7 @@ window.cancelAnimFrame = (function () {
gridCacheMap.clear();
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isItemView = document.getElementById('prev') || document.getElementById('next') || /^\/\d+/.test(window.location.pathname);
const isItemView = document.getElementById('prev') || document.getElementById('next') || (typeof isItemPath === 'function' ? isItemPath(window.location.pathname) : (!window.location.pathname.startsWith('/4/') && /^\/\d+(?:[?#]|$)/.test(window.location.pathname)));
let reloadPromise = null;
if (isOnaraOpen) {
const bgUrl = window._onaraReturnUrl || getOnaraBaseUrl();
@@ -1502,7 +1502,7 @@ window.cancelAnimFrame = (function () {
gridCacheMap.clear();
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isItemView = document.getElementById('prev') || document.getElementById('next') || /^\/\d+/.test(window.location.pathname);
const isItemView = document.getElementById('prev') || document.getElementById('next') || (typeof isItemPath === 'function' ? isItemPath(window.location.pathname) : (!window.location.pathname.startsWith('/4/') && /^\/\d+(?:[?#]|$)/.test(window.location.pathname)));
let reloadPromise = null;
if (isOnaraOpen) {
const bgUrl = window._onaraReturnUrl || getOnaraBaseUrl();
@@ -2331,8 +2331,11 @@ window.cancelAnimFrame = (function () {
try {
const p = new URL(pathOrUrl, window.location.origin).pathname;
const parts = p.split('/').filter(Boolean);
if (parts[0] === '4') {
return parts.length >= 4 && /^\d+$/.test(parts[3]);
}
return !p.match(/\/p\//) && (
/^\/\d+/.test(p) ||
/^\/\d+(?:[?#]|$)/.test(p) ||
/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/.test(p) ||
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
(parts.length >= 2 && parts[0] === 'favs' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1])))
@@ -3334,7 +3337,16 @@ window.cancelAnimFrame = (function () {
return;
}
fetch(ajaxUrl, { credentials: 'include', priority: 'low', signal })
fetch(ajaxUrl, {
credentials: 'include',
priority: 'low',
headers: {
'X-Purpose': 'prefetch',
'Purpose': 'prefetch',
'Sec-Purpose': 'prefetch'
},
signal
})
.then(r => r.ok ? r.text() : null)
.then(rawText => {
if (!rawText || signal.aborted) return;
@@ -11084,6 +11096,11 @@ window.cancelAnimFrame = (function () {
// Immediately restore scrollability and hide modals
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
if (window.hideAllModals) window.hideAllModals();
const targetPath = new URL(url, window.location.origin).pathname;
if (targetPath.startsWith('/4/')) {
window.location.href = url;
return;
}
const willNavigateToItem = typeof isItemPath === 'function' ? isItemPath(url) : false;
if (!isOnaraActive() || !willNavigateToItem) {
if (window.closeOnaraModal) window.closeOnaraModal({ skipHistory: true });
@@ -11132,10 +11149,10 @@ window.cancelAnimFrame = (function () {
if (window.closeImageModal) window.closeImageModal();
// Auto-clear notification highlight if opening an item
const itemIdMatch = url.match(/\/(\d+)(\?.*|#.*)?$/);
const itemIdMatch = url.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:\?.*|#.*)?$/);
if (itemIdMatch) {
const itemId = itemIdMatch[1];
document.querySelectorAll(`a.thumb.has-notif[href$="/${itemId}"]`).forEach(el => {
document.querySelectorAll(`a.thumb.has-notif[href$="/${itemId}"], a.thumb.has-notif[data-item-id="${itemId}"]`).forEach(el => {
el.classList.remove('has-notif');
});
}
@@ -11174,12 +11191,7 @@ window.cancelAnimFrame = (function () {
const isSettings = pathname.match(/\/settings\/?(?:$|\?)/);
const isStatic = pathname.match(/\/(about|rules|terms|upload|subscriptions|stats|docs|discord|ranking|meme|memes)($|\/|\?)/);
const isUpload = pathname.match(/\/upload\/?(?:$|\?)/);
const parts = pathname.split('/').filter(Boolean);
const isItem = !pathname.match(/\/p\//) && (
pathname.match(/^\/\d+/) || pathname.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) ||
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1]))) ||
(parts.length >= 2 && parts[0] === 'favs' && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1])))
);
const isItem = typeof isItemPath === 'function' ? isItemPath(pathname) : (!pathname.startsWith('/4/') && /^\/\d+(?:[?#]|$)/.test(pathname));
const isMessages = !!pathname.match(/^\/messages(\/|$)/);
const isAbyss = !!pathname.match(/^\/abyss(\/|$|\?|#)/) || pathname === '/abyss';
const isGrid = !isProfile && !isUserHall && !isUserHalls && !isHall && !isHalls && !isTags && !isComments && !isNotifs && !isItem && !isAdmin && !isMod && !isSettings && !isStatic && !isUpload && !isMessages && !isAbyss;
@@ -12197,6 +12209,11 @@ window.cancelAnimFrame = (function () {
// Extract item ID from URL. Use the last numeric segment to avoid matching context IDs (like tag/1/...)
// Split path, filter numeric, pop last.
const isChanUrl = /^\/4\/([a-z0-9_]+)\/(\d+)\/(\d+)/i.test(new URL(url, window.location.origin).pathname);
if (isChanUrl) {
inheritContext = false;
}
const pathSegments = new URL(url, window.location.origin).pathname.split('/');
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
@@ -12265,7 +12282,7 @@ window.cancelAnimFrame = (function () {
// Prev/next arrows in item view always use explicit /h/foo/<id> or /tag/foo/<id> hrefs
// (via link.main), so they are unaffected by this guard.
// Note: Notification links call loadItemAjax(href, false) to skip inheritance directly.
if (inheritContext) {
if (inheritContext && !isChanUrl) {
if (!tag) {
const wTagMatch = window.location.href.match(/\/tag\/([^/?]+)/);
if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]);
@@ -12300,61 +12317,68 @@ window.cancelAnimFrame = (function () {
try {
// Construct AJAX URL
let ajaxUrl = `/ajax/item/${itemid}`;
const params = new URLSearchParams();
params.append('mode', window.activeMode);
const targetHash = new URL(url, window.location.origin).hash.replace(/^#/, '').trim();
if (targetHash && !targetHash.startsWith('c')) params.append('subf0ck', targetHash);
if (tag) params.append('tag', tag);
if (hall) params.append('hall', hall);
if (userHall && userHallOwner) {
params.append('userHall', userHall);
params.append('userHallOwner', userHallOwner);
} else if (user) {
params.append('user', user);
}
if (tagger) params.append('tagger', tagger);
if (isFavs) {
params.append('fav', 'true');
if (!user && window.f0ckGuestFavs) {
const guestIds = window.f0ckGuestFavs.get();
if (guestIds.length > 0) {
params.append('ids', guestIds.slice(0, 100).join(','));
if (isChanUrl) {
const urlObj = new URL(url, window.location.origin);
ajaxUrl = urlObj.pathname + (urlObj.search ? urlObj.search + '&ajax=1' : '?ajax=1');
} else {
const params = new URLSearchParams();
params.append('mode', window.activeMode);
const targetHash = new URL(url, window.location.origin).hash.replace(/^#/, '').trim();
if (targetHash && !targetHash.startsWith('c')) params.append('subf0ck', targetHash);
if (tag) params.append('tag', tag);
if (hall) params.append('hall', hall);
if (userHall && userHallOwner) {
params.append('userHall', userHall);
params.append('userHallOwner', userHallOwner);
} else if (user) {
params.append('user', user);
}
if (tagger) params.append('tagger', tagger);
if (isFavs) {
params.append('fav', 'true');
if (!user && window.f0ckGuestFavs) {
const guestIds = window.f0ckGuestFavs.get();
if (guestIds.length > 0) {
params.append('ids', guestIds.slice(0, 100).join(','));
}
}
}
}
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true');
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true');
if (isStrict || url.includes('strict=1') || window.location.search.includes('strict=1')) {
params.append('strict', '1');
}
// Note: mime is no longer appended to params, server reads from cookie automatically
// _t cache-buster removed — stable URL enables both HTTP and in-memory caching.
if (isStrict || url.includes('strict=1') || window.location.search.includes('strict=1')) {
params.append('strict', '1');
}
// Note: mime is no longer appended to params, server reads from cookie automatically
// _t cache-buster removed — stable URL enables both HTTP and in-memory caching.
const isRandom = document.cookie.includes('random_mode=1') || url.includes('random=1') || window.location.search.includes('random=1');
if (isRandom) {
params.append('random', '1');
}
const isRandom = document.cookie.includes('random_mode=1') || url.includes('random=1') || window.location.search.includes('random=1');
if (isRandom) {
params.append('random', '1');
}
// Explicit overrides for context (used for immediate mode switches)
if (options.forceParams) {
Object.keys(options.forceParams).forEach(key => {
params.set(key, options.forceParams[key]);
});
}
// Explicit overrides for context (used for immediate mode switches)
if (options.forceParams) {
Object.keys(options.forceParams).forEach(key => {
params.set(key, options.forceParams[key]);
});
}
if (useOnara) {
params.append('onara', '1');
}
if (useOnara) {
params.append('onara', '1');
}
if (params.toString() !== '') {
ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + params.toString();
if (params.toString() !== '') {
ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + params.toString();
}
}
// ── Stale-while-revalidate item cache ──────────────────────────────────
// cacheKey is the stable ajaxUrl (no _t), scoped to mode+context.
if (isChanUrl) {
options.skipCache = true;
}
const _itemCacheKey = ajaxUrl;
const _cachedItem = !options.skipCache ? itemCacheMap.get(_itemCacheKey) : null;
@@ -12524,7 +12548,7 @@ window.cancelAnimFrame = (function () {
window.f0ckDebug('[itemCache] Cache hit render complete, revalidating in background...');
// ── Background revalidation — silently refresh cache for next visit ──
fetch(ajaxUrl, { credentials: 'include' })
fetch(ajaxUrl, { credentials: 'include', keepalive: true })
.then(r => r.ok ? r.text() : null)
.then(freshText => {
if (!freshText) return;
@@ -12578,7 +12602,10 @@ window.cancelAnimFrame = (function () {
window.f0ckDebug(`[CLIENT_DEBUG] Using pre-fetched data (skipped network fetch)`);
} else {
// ── Normal fetch path ────────────────────────────────────────────
const response = await fetch(ajaxUrl, { credentials: 'include' });
const response = await fetch(ajaxUrl, {
credentials: 'include',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
});
const tHeaders = performance.now();
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
@@ -12802,7 +12829,10 @@ window.cancelAnimFrame = (function () {
const itemKey = (window.f0ckSession?.enable_item_slugs !== false && responseSlug) ? responseSlug : (responseSlug || itemid);
let pushUrl = `/${itemKey}`;
// Logic from ajax.mjs context reconstruction:
if (userHall && userHallOwner) {
if (isChanUrl) {
const u = new URL(url, window.location.origin);
pushUrl = u.pathname + u.search + u.hash;
} else if (userHall && userHallOwner) {
pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
} else if (user) {
pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`;
@@ -12815,13 +12845,13 @@ window.cancelAnimFrame = (function () {
// Append tagger filter so item nav stays in tagger context
if (tagger && tag) pushUrl += `?tagger=${encodeURIComponent(tagger)}`;
if (mime) {
if (!isChanUrl && mime) {
// If it already has itemKey at the end, insert mime before it
pushUrl = pushUrl.replace(new RegExp(`/${itemKey}$`), `/${mime}/${itemKey}`);
}
// Re-append hash if present
if (hash) pushUrl += hash;
if (hash && !isChanUrl) pushUrl += hash;
if (!options.keepMedia && !options.skipPush) {
// We overwrite proper URL even if the link clicked was "naked"
@@ -12844,7 +12874,16 @@ window.cancelAnimFrame = (function () {
if (window.initVisualizer) window.initVisualizer();
}
// Try to extract ID from response if possible or just use itemid
document.title = `${window.f0ckDomain} - ${itemKey}`;
if (isChanUrl) {
const chanM = new URL(url, window.location.origin).pathname.match(/^\/4\/([a-z0-9_]+)\/(\d+)\/(\d+)/i);
if (chanM) {
document.title = `/4/${chanM[1]}/${chanM[2]} #${chanM[3]} - ${window.f0ckDomain || 'f0ck'}`;
} else {
document.title = `${window.f0ckDomain} - #${itemid}`;
}
} else {
document.title = `${window.f0ckDomain} - ${itemKey}`;
}
window.f0ckDebug("AJAX load complete");
// Notify extensions — also triggers CommentSystem init which renders comments
@@ -13324,7 +13363,23 @@ window.cancelAnimFrame = (function () {
return false;
}
if (!isSpecialLink && (pathname === '/' || pathname.startsWith('/halls') || pathname.startsWith('/h/') || pathname.startsWith('/notifications') || pathname.startsWith('/tag') || pathname.startsWith('/user/') || pathname.match(/^\/(image|video|audio)/) || pathname.match(/\/p\/\d+/) || pathname.match(/^\/\d+/) || pathname.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) || pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes)/) || pathname.startsWith('/abyss'))) {
if (!isSpecialLink && (
(!pathname.startsWith('/4/') && (
pathname === '/' ||
pathname.startsWith('/halls') ||
pathname.startsWith('/h/') ||
pathname.startsWith('/notifications') ||
pathname.startsWith('/tag') ||
pathname.startsWith('/user/') ||
pathname.match(/^\/(image|video|audio)/) ||
pathname.match(/\/p\/\d+/) ||
pathname.match(/^\/\d+(?:[?#]|$)/) ||
pathname.match(/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/) ||
pathname.match(/^\/(about|rules|terms|upload|subscriptions|stats|docs|settings|admin|mod|ranking|messages|meme|memes)/) ||
pathname.startsWith('/abyss')
)) ||
(pathname.startsWith('/4/') && isItemPath(pathname))
)) {
e.preventDefault();
e.stopImmediatePropagation();
@@ -13348,7 +13403,244 @@ window.cancelAnimFrame = (function () {
}
}
if (target.closest('#togglebg')) {
window.openChanRehostModal = function({ itemId, isPlayerView = false, onSave = null }) {
if (!itemId) return;
let modal = document.getElementById('chan-rehost-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'chan-rehost-modal';
modal.className = 'modal-overlay chan-rehost-modal';
modal.style.display = 'none';
document.body.appendChild(modal);
}
modal.innerHTML = `
<div class="modal-content chan-rehost-modal-content">
<div class="chan-rehost-modal-header">
<h3 class="chan-rehost-modal-title">
<i class="fa-solid fa-cloud-arrow-up" style="color: #4ade80;"></i>
<span>Rehost Settings</span>
<a href="/${itemId}" target="_blank" class="chan-rehost-modal-post-link" title="Open post in new tab">#${itemId} <i class="fa-solid fa-arrow-up-right-from-square"></i></a>
</h3>
<button type="button" class="chan-rehost-modal-close" aria-label="Close">&times;</button>
</div>
<div class="chan-rehost-modal-body">
<div class="chan-rehost-form-group">
<label><i class="fa-solid fa-shield-halved"></i> Rating</label>
<div class="chan-rehost-ratings" id="chan-rehost-rating-group">
<button type="button" class="chan-rehost-pill untagged active" data-rating="untagged"><i class="fa-solid fa-question"></i> Untagged</button>
<button type="button" class="chan-rehost-pill sfw" data-rating="sfw"><i class="fa-solid fa-shield"></i> SFW</button>
<button type="button" class="chan-rehost-pill nsfw" data-rating="nsfw"><i class="fa-solid fa-eye-slash"></i> NSFW</button>
<button type="button" class="chan-rehost-pill nsfl" data-rating="nsfl"><i class="fa-solid fa-skull"></i> NSFL</button>
</div>
</div>
<div class="chan-rehost-form-group">
<label><i class="fa-solid fa-tags"></i> Tags (optional)</label>
<input type="text" id="chan-rehost-tags-input" class="chan-rehost-input" placeholder="e.g. music, webm, anime (comma-separated)" autocomplete="off" />
</div>
<div class="chan-rehost-form-group">
<label><i class="fa-solid fa-comment"></i> First Comment (optional)</label>
<textarea id="chan-rehost-comment-input" class="chan-rehost-input chan-rehost-textarea" placeholder="Add a comment on this post..." rows="2"></textarea>
</div>
</div>
<div class="chan-rehost-modal-actions">
<button type="button" class="btn btn-secondary chan-rehost-modal-cancel">Close</button>
${isPlayerView ? `<button type="button" class="btn btn-info chan-rehost-modal-save-view" style="background:#0284c7;color:#fff;border:none;border-radius:6px;padding:8px 14px;cursor:pointer;font-weight:600;"><i class="fa-solid fa-play"></i> Save & View Post</button>` : ''}
<button type="button" class="btn btn-primary chan-rehost-modal-save" style="background:#16a34a;color:#fff;border:none;border-radius:6px;padding:8px 14px;cursor:pointer;font-weight:600;"><i class="fa-solid fa-check"></i> Save Details</button>
</div>
</div>
`;
modal.style.display = 'flex';
document.body.classList.add('modal-open');
const ratingGroup = modal.querySelector('#chan-rehost-rating-group');
let selectedRating = 'untagged';
ratingGroup.addEventListener('click', (e) => {
const pill = e.target.closest('.chan-rehost-pill');
if (!pill) return;
ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => b.classList.remove('active'));
pill.classList.add('active');
selectedRating = pill.dataset.rating;
});
const tagsInput = modal.querySelector('#chan-rehost-tags-input');
const commentInput = modal.querySelector('#chan-rehost-comment-input');
// Pre-fill current rating and tags if available
fetch(`/api/v2/scroller/rehost/details/${itemId}`)
.then(r => r.json())
.then(data => {
if (data && data.success) {
if (data.rating) {
selectedRating = data.rating;
ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => {
b.classList.toggle('active', b.dataset.rating === data.rating);
});
}
if (data.tags && data.tags.length) {
tagsInput.value = data.tags.join(', ');
}
}
}).catch(() => {});
setTimeout(() => tagsInput.focus(), 100);
const closeModal = () => {
modal.style.display = 'none';
document.body.classList.remove('modal-open');
document.removeEventListener('keydown', handleKeydown);
};
const handleKeydown = (e) => {
if (e.key === 'Escape') closeModal();
};
document.addEventListener('keydown', handleKeydown);
modal.querySelector('.chan-rehost-modal-close').onclick = closeModal;
modal.querySelector('.chan-rehost-modal-cancel').onclick = closeModal;
modal.onclick = (e) => {
if (e.target === modal) closeModal();
};
const saveDetails = async (andView = false) => {
const saveBtn = modal.querySelector('.chan-rehost-modal-save');
const saveViewBtn = modal.querySelector('.chan-rehost-modal-save-view');
if (saveBtn) saveBtn.disabled = true;
if (saveViewBtn) saveViewBtn.disabled = true;
try {
const csrfToken = window.f0ckSession?.csrf_token || '';
const resp = await fetch('/api/v2/scroller/rehost/details', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
},
body: new URLSearchParams({
item_id: itemId,
rating: selectedRating,
tags: tagsInput.value.trim(),
comment: commentInput.value.trim()
})
});
const resData = await resp.json();
if (resData.success) {
if (typeof window.showToast === 'function') {
window.showToast('Details saved for #' + itemId + '!');
}
closeModal();
if (typeof onSave === 'function') {
onSave(resData);
}
if (andView) {
if (typeof window.loadItemAjax === 'function') {
window.loadItemAjax('/' + itemId);
} else {
window.location.href = '/' + itemId;
}
}
} else {
alert(resData.msg || 'Failed to save details');
if (saveBtn) saveBtn.disabled = false;
if (saveViewBtn) saveViewBtn.disabled = false;
}
} catch (err) {
console.error('[REHOST-MODAL] Save error:', err);
alert('Error saving details: network error');
if (saveBtn) saveBtn.disabled = false;
if (saveViewBtn) saveViewBtn.disabled = false;
}
};
modal.querySelector('.chan-rehost-modal-save').onclick = () => saveDetails(false);
if (isPlayerView && modal.querySelector('.chan-rehost-modal-save-view')) {
modal.querySelector('.chan-rehost-modal-save-view').onclick = () => saveDetails(true);
}
tagsInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveDetails(false);
}
});
};
if (target.closest('#chan-item-rehost-btn')) {
e.preventDefault();
const btn = target.closest('#chan-item-rehost-btn');
if (btn && !btn.classList.contains('loading') && !btn.classList.contains('rehosted')) {
const mediaUrl = btn.dataset.url;
if (mediaUrl) {
btn.classList.add('loading');
const origHtml = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Rehosting…';
(async () => {
try {
const csrfToken = window.f0ckSession?.csrf_token || '';
const resp = await fetch('/api/v2/scroller/rehost', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
},
body: new URLSearchParams({
url: mediaUrl,
original_filename: btn.dataset.filename || btn.dataset.title || '',
width: btn.dataset.width || '',
height: btn.dataset.height || ''
})
});
const data = await resp.json();
if (data.success && data.item_id) {
btn.classList.remove('loading');
btn.classList.add('rehosted');
btn.outerHTML = `
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;">
<i class="fa-solid fa-check"></i> #${data.item_id}
</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${data.item_id}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
<i class="fa-solid fa-pen"></i>
</button>
</span>
`;
if (typeof window.showToast === 'function') {
window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!');
}
if (typeof window.openChanRehostModal === 'function') {
window.openChanRehostModal({ itemId: data.item_id, isPlayerView: true });
}
} else {
btn.classList.remove('loading');
btn.innerHTML = origHtml;
alert(data.msg || data.message || 'Rehost failed');
}
} catch (err) {
console.error('[CHAN-REHOST] Error:', err);
btn.classList.remove('loading');
btn.innerHTML = origHtml;
alert('Rehost failed due to network error');
}
})();
}
}
} else if (target.closest('.chan-rehost-edit-btn')) {
e.preventDefault();
const editBtn = target.closest('.chan-rehost-edit-btn');
const itemId = editBtn?.dataset?.itemId;
if (itemId && typeof window.openChanRehostModal === 'function') {
const isPlayer = !!document.querySelector('.media-object');
window.openChanRehostModal({ itemId: Number(itemId), isPlayerView: isPlayer });
}
} else if (target.closest('#togglebg')) {
e.preventDefault();
window.toggleBackground();
} else if (target.closest('#toggleautoplay')) {