bvcx
This commit is contained in:
@@ -7317,6 +7317,20 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.location a,
|
||||
.location .location-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.location a:hover,
|
||||
.location .location-link:hover {
|
||||
color: #4ade80;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.item_title,
|
||||
.item-title,
|
||||
.item-itle {
|
||||
@@ -22424,4 +22438,160 @@ div#flash,
|
||||
.audio-track-artist {
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chan Rehost Details Modal */
|
||||
.chan-rehost-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.82);
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 100200;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.chan-rehost-modal-content {
|
||||
background: #18181b;
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
border-radius: 12px;
|
||||
width: 92%;
|
||||
max-width: 480px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.7);
|
||||
padding: 22px 24px;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.chan-rehost-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
.chan-rehost-modal-title {
|
||||
margin: 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.chan-rehost-modal-post-link {
|
||||
font-size: 0.85rem;
|
||||
color: #4ade80;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.chan-rehost-modal-post-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chan-rehost-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
line-height: 1;
|
||||
}
|
||||
.chan-rehost-modal-close:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.chan-rehost-form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.chan-rehost-form-group label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
margin-bottom: 7px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.chan-rehost-ratings {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.chan-rehost-pill {
|
||||
flex: 1;
|
||||
padding: 8px 4px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.chan-rehost-pill:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
.chan-rehost-pill.active.untagged {
|
||||
background: #3f3f46;
|
||||
border-color: #71717a;
|
||||
color: #fff;
|
||||
}
|
||||
.chan-rehost-pill.active.sfw {
|
||||
background: #15803d;
|
||||
border-color: #22c55e;
|
||||
color: #fff;
|
||||
}
|
||||
.chan-rehost-pill.active.nsfw {
|
||||
background: #b91c1c;
|
||||
border-color: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
.chan-rehost-pill.active.nsfl {
|
||||
background: #450a0a;
|
||||
border-color: #991b1b;
|
||||
color: #fca5a5;
|
||||
}
|
||||
.chan-rehost-input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
padding: 9px 12px;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 0.88rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.chan-rehost-input:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
.chan-rehost-textarea {
|
||||
resize: vertical;
|
||||
min-height: 56px;
|
||||
}
|
||||
.chan-rehost-modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.chan-rehost-edit-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.chan-rehost-edit-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
}
|
||||
+67
-5
@@ -6,7 +6,7 @@
|
||||
const infoEl = document.querySelector("#a_info");
|
||||
const idLinkEl = document.querySelector("a.id-link");
|
||||
|
||||
const rawId = commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText;
|
||||
const rawId = favoEl?.dataset?.localId || commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText;
|
||||
if (!rawId) return null;
|
||||
|
||||
const tagsContainer = document.querySelector("#tags");
|
||||
@@ -292,17 +292,79 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
const favoBtns = document.querySelectorAll("#a_favo");
|
||||
const firstFavoBtn = favoBtns[0];
|
||||
const chanRehostBtn = document.querySelector('#chan-item-rehost-btn');
|
||||
const isChan = firstFavoBtn?.dataset?.isChan === 'true' || !!chanRehostBtn;
|
||||
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
|
||||
let localId = firstFavoBtn?.dataset?.localId;
|
||||
|
||||
// If viewing un-rehosted 4chan post, auto-rehost first
|
||||
if (isChan && !localId && chanUrl) {
|
||||
favoBtns.forEach(btn => {
|
||||
btn.className = 'iconset fa-solid fa-spinner fa-spin';
|
||||
});
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || '';
|
||||
const rehostResp = 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: chanUrl,
|
||||
original_filename: firstFavoBtn?.dataset?.filename || chanRehostBtn?.dataset?.filename || '',
|
||||
width: firstFavoBtn?.dataset?.width || chanRehostBtn?.dataset?.width || '',
|
||||
height: firstFavoBtn?.dataset?.height || chanRehostBtn?.dataset?.height || ''
|
||||
})
|
||||
});
|
||||
const rehostData = await rehostResp.json();
|
||||
if (rehostData.success && rehostData.item_id) {
|
||||
localId = rehostData.item_id;
|
||||
favoBtns.forEach(btn => {
|
||||
btn.dataset.itemId = localId;
|
||||
btn.dataset.localId = localId;
|
||||
});
|
||||
const commentsEl = document.querySelector("#comments-container");
|
||||
if (commentsEl) commentsEl.dataset.itemId = localId;
|
||||
const infoEl = document.querySelector("#a_info");
|
||||
if (infoEl) infoEl.dataset.itemId = localId;
|
||||
|
||||
if (chanRehostBtn) {
|
||||
chanRehostBtn.classList.add('rehosted');
|
||||
chanRehostBtn.outerHTML = `
|
||||
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
|
||||
<a href="/${localId}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #${localId})" 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> #${localId}</a>
|
||||
<button type="button" class="chan-rehost-edit-btn" data-item-id="${localId}" 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>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
throw new Error(rehostData.msg || 'Rehost failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CHAN-FAV-ADMIN] Auto-rehost error:', err);
|
||||
favoBtns.forEach(btn => {
|
||||
btn.className = 'iconset fa-regular fa-heart';
|
||||
});
|
||||
if (typeof window.flashMessage === 'function') {
|
||||
window.flashMessage('Failed to favorite post', 3000, 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid } = ctx;
|
||||
const postid = localId ? Number(localId) : ctx.postid;
|
||||
|
||||
// Read state BEFORE the API call so we have a fallback
|
||||
const favoBtns = document.querySelectorAll("#a_favo");
|
||||
const wasAlreadyFav = favoBtns.length > 0 && favoBtns[0].classList.contains('fa-solid');
|
||||
|
||||
try {
|
||||
const res = await post('/api/v2/togglefav', {
|
||||
postid: postid
|
||||
postid: postid,
|
||||
chan_url: chanUrl
|
||||
});
|
||||
if (res && res.success) {
|
||||
if (window.invalidateItemCache) {
|
||||
|
||||
+356
-64
@@ -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">×</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')) {
|
||||
|
||||
@@ -1936,10 +1936,6 @@
|
||||
const origClass = icon.className;
|
||||
icon.className = 'fa-solid fa-spinner';
|
||||
|
||||
let rating = applied.mode === 0 ? 'sfw' : (applied.mode === 1 ? 'nsfw' : (applied.mode === 4 ? 'nsfl' : 'sfw'));
|
||||
if (item.external_board === 'wsg') rating = 'sfw';
|
||||
else if (item.external_board === 'gif') rating = 'nsfw';
|
||||
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || window.scrollerCsrf || '';
|
||||
const resp = await fetch('/api/v2/scroller/rehost', {
|
||||
@@ -1950,9 +1946,6 @@
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
url: item.external_media_url || item.dest,
|
||||
rating: rating,
|
||||
tags: '',
|
||||
comment: `Rehosted from 4chan thread: ${applied.externalUrl || 'unknown'}`,
|
||||
...(item.original_filename ? { original_filename: item.original_filename } : {})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1559,9 +1559,11 @@
|
||||
if (txt) ids.add(txt);
|
||||
}
|
||||
|
||||
// 2. From URL pathname (/tag/:tag/:slugOrId, /user/:user/favs/:slugOrId, /user/:user/:slugOrId, /:slugOrId)
|
||||
// 2. From URL pathname (/tag/:tag/:slugOrId, /user/:user/favs/:slugOrId, /user/:user/:slugOrId, /:slugOrId, /4/:board/:thread/:post)
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
if (segments.length >= 2 && segments[0] === 'tag') {
|
||||
if (segments.length >= 4 && segments[0] === '4') {
|
||||
ids.add(decodeURIComponent(segments[3]).trim());
|
||||
} else if (segments.length >= 2 && segments[0] === 'tag') {
|
||||
if (segments[2]) {
|
||||
ids.add(decodeURIComponent(segments[2]).trim());
|
||||
}
|
||||
@@ -1576,7 +1578,7 @@
|
||||
ids.add(decodeURIComponent(segments[2]).trim());
|
||||
}
|
||||
} else if (segments.length === 1) {
|
||||
const forbidden = ['s', 'b', 't', 'ca', 'a', 'login', 'register', 'settings', 'about', 'terms', 'rules', 'api', 'logout', 'auth', 'admin', 'mod', 'comments', 'notifications', 'feed', 'upload', 'tags', 'halls', 'ranking', 'abyss', 'random', 'scroller', 'p'];
|
||||
const forbidden = ['s', 'b', 't', 'ca', 'a', '4', 'login', 'register', 'settings', 'about', 'terms', 'rules', 'api', 'logout', 'auth', 'admin', 'mod', 'comments', 'notifications', 'feed', 'upload', 'tags', 'halls', 'ranking', 'abyss', 'random', 'scroller', 'p'];
|
||||
if (!forbidden.includes(segments[0])) {
|
||||
ids.add(decodeURIComponent(segments[0]).trim());
|
||||
}
|
||||
@@ -2089,20 +2091,232 @@
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
// ── Dedicated 4chan Thread Feed (mirrors Tag/Favs Feed for /4/:board/:thread context) ─────
|
||||
let currentChanBoard = null;
|
||||
let currentChanThread = null;
|
||||
let chanFeedLoaded = false;
|
||||
let chanFeedLoading = false;
|
||||
|
||||
const getCurrentChanContext = () => {
|
||||
const m = window.location.pathname.match(/^\/4\/([a-z0-9_]+)\/(\d+)(?:\/(\d+))?/i);
|
||||
if (m) {
|
||||
return {
|
||||
board: m[1].toLowerCase(),
|
||||
thread: m[2],
|
||||
post: m[3] || null
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const highlightActiveChanCard = (scrollIntoView = false) => {
|
||||
const chanCtx = getCurrentChanContext();
|
||||
const container = document.getElementById('sidebar-chan-container');
|
||||
if (!container || !chanCtx) return;
|
||||
|
||||
const currentPost = chanCtx.post || '';
|
||||
let matchedCard = null;
|
||||
|
||||
container.querySelectorAll('.sidebar-video-card').forEach(card => {
|
||||
const cardId = card.dataset.id ? String(card.dataset.id).trim() : '';
|
||||
const isMatch = (currentPost && cardId === currentPost);
|
||||
if (isMatch) {
|
||||
card.classList.add('active-tag-item');
|
||||
if (!matchedCard) matchedCard = card;
|
||||
} else {
|
||||
card.classList.remove('active-tag-item');
|
||||
}
|
||||
});
|
||||
|
||||
if (matchedCard && scrollIntoView) {
|
||||
const isChanTabActive = document.querySelector('.sidebar-tab.active')?.dataset.tab === 'chan' &&
|
||||
container.style.display !== 'none';
|
||||
if (isChanTabActive) {
|
||||
try {
|
||||
const cr = container.getBoundingClientRect();
|
||||
const mr = matchedCard.getBoundingClientRect();
|
||||
const targetScrollTop = container.scrollTop + (mr.top - cr.top) - (container.clientHeight / 2) + (mr.height / 2);
|
||||
container.scrollTo({ top: Math.max(0, targetScrollTop), behavior: 'smooth' });
|
||||
} catch (_) {
|
||||
matchedCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadChanFeed = async (reset = false) => {
|
||||
const chanCtx = getCurrentChanContext();
|
||||
const container = document.getElementById('sidebar-chan-container');
|
||||
const itemsContainer = container?.querySelector('.sidebar-chan-items-container');
|
||||
if (!container || !itemsContainer || chanFeedLoading) return;
|
||||
if (!chanCtx) return;
|
||||
|
||||
chanFeedLoading = true;
|
||||
|
||||
if (reset) {
|
||||
itemsContainer.innerHTML = `
|
||||
<div class="sidebar-loading-state">
|
||||
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
<span>Loading thread media...</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/v2/chan/${encodeURIComponent(chanCtx.board)}/${encodeURIComponent(chanCtx.thread)}/media`, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
const data = await res.json();
|
||||
const items = data.items || data.media || [];
|
||||
|
||||
if (data.success && items.length > 0) {
|
||||
chanFeedLoaded = true;
|
||||
|
||||
// Update header
|
||||
const countEl = container.querySelector('.sidebar-chan-count');
|
||||
if (countEl) countEl.textContent = `(${items.length})`;
|
||||
|
||||
const nameEl = container.querySelector('.sidebar-chan-name');
|
||||
if (nameEl) nameEl.textContent = `/${chanCtx.board}/${chanCtx.thread}`;
|
||||
|
||||
const overviewBtn = container.querySelector('.sidebar-chan-overview-btn');
|
||||
if (overviewBtn) {
|
||||
overviewBtn.href = `/4/${chanCtx.board}/${chanCtx.thread}`;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
items.forEach(v => {
|
||||
const isActive = chanCtx.post && String(v.no) === String(chanCtx.post);
|
||||
const activeClass = isActive ? ' active-tag-item' : '';
|
||||
const targetHref = `/4/${chanCtx.board}/${chanCtx.thread}/${v.no}`;
|
||||
const formatBadge = v.ext ? `<span class="sidebar-media-format-badge">${escapeHtml(v.ext.replace('.', '').toUpperCase())}</span>` : '';
|
||||
const displayTitle = v.filename ? `${v.filename}` : `#${v.no}`;
|
||||
const overlayIcon = v.is_video ? 'fa-solid fa-play' : 'fa-solid fa-image';
|
||||
const fsizeStr = v.fsize ? `${(v.fsize / 1024 / 1024).toFixed(1)} MB` : '';
|
||||
|
||||
html += `
|
||||
<div class="sidebar-video-card${activeClass}" data-id="${v.no}">
|
||||
<a href="${targetHref}" class="sidebar-video-link" data-inherit-context="false">
|
||||
<div class="sidebar-video-thumb-wrap">
|
||||
<img src="${escapeHtml(v.thumb)}" class="sidebar-video-thumb" alt="${escapeHtml(displayTitle)}" loading="lazy" draggable="false" onerror="this.style.display='none'; if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');" />
|
||||
<div class="sidebar-media-placeholder hidden">
|
||||
<i class="${overlayIcon}"></i>
|
||||
</div>
|
||||
<div class="sidebar-video-badges">
|
||||
${formatBadge}
|
||||
<span class="sidebar-video-badge" style="background: rgba(74, 222, 128, 0.2); color: #4ade80;">4CHAN</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-video-details">
|
||||
<div class="sidebar-video-info">
|
||||
<div class="sidebar-video-title" title="${escapeHtml(displayTitle)}">${escapeHtml(displayTitle)}</div>
|
||||
<div class="sidebar-video-channel">
|
||||
<span class="sidebar-video-user" style="color: #4ade80;">#${v.no}</span>
|
||||
</div>
|
||||
<div class="sidebar-video-meta">
|
||||
${fsizeStr ? `<span class="sidebar-video-time">${escapeHtml(fsizeStr)}</span>` : ''}
|
||||
${v.w && v.h ? `<span>${fsizeStr ? '• ' : ''}${v.w}x${v.h}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
itemsContainer.innerHTML = html;
|
||||
setTimeout(() => highlightActiveChanCard(true), 60);
|
||||
} else if (reset) {
|
||||
itemsContainer.innerHTML = `
|
||||
<div class="sidebar-empty-state" style="text-align:center;padding:30px 15px;color:var(--text-muted,#888);font-size:0.88em;">
|
||||
<i class="fa-solid fa-clover" style="font-size:1.8em;opacity:0.4;margin-bottom:8px;display:block;color:#4ade80;"></i>
|
||||
No media found in thread
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CHAN-FEED] Failed:', err);
|
||||
if (reset) {
|
||||
itemsContainer.innerHTML = `<div class="sidebar-empty-state" style="text-align:center;padding:25px;color:var(--danger,#ff4444);font-size:0.88em;">Failed to load thread media</div>`;
|
||||
}
|
||||
} finally {
|
||||
chanFeedLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateChanTabVisibility = (forceSwitchToChan = false) => {
|
||||
const chanCtx = getCurrentChanContext();
|
||||
const chanBtn = document.getElementById('sidebar-tab-chan');
|
||||
if (!chanBtn) return;
|
||||
|
||||
if (chanCtx) {
|
||||
chanBtn.style.display = 'inline-flex';
|
||||
chanBtn.title = `/${chanCtx.board}/${chanCtx.thread}`;
|
||||
|
||||
if (currentChanBoard !== chanCtx.board || currentChanThread !== chanCtx.thread) {
|
||||
currentChanBoard = chanCtx.board;
|
||||
currentChanThread = chanCtx.thread;
|
||||
chanFeedLoaded = false;
|
||||
userManuallySelectedNonTagTab = false;
|
||||
switchSidebarTab('chan');
|
||||
} else {
|
||||
if (forceSwitchToChan || (!userManuallySelectedNonTagTab && document.querySelector('.sidebar-tab.active')?.dataset.tab !== 'chan')) {
|
||||
switchSidebarTab('chan');
|
||||
} else {
|
||||
highlightActiveChanCard(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
chanBtn.style.display = 'none';
|
||||
currentChanBoard = null;
|
||||
currentChanThread = null;
|
||||
chanFeedLoaded = false;
|
||||
|
||||
const activeTab = document.querySelector('.sidebar-tab.active')?.dataset.tab;
|
||||
if (activeTab === 'chan') {
|
||||
if (getCurrentTag()) {
|
||||
switchSidebarTab('tag');
|
||||
} else {
|
||||
switchSidebarTab('recommendations');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const bindChanContainerEvents = () => {
|
||||
const container = document.getElementById('sidebar-chan-container');
|
||||
if (!container || container.dataset.eventsBound === 'true') return;
|
||||
container.dataset.eventsBound = 'true';
|
||||
container.addEventListener('click', (e) => {
|
||||
const link = e.target.closest('a.sidebar-video-link');
|
||||
if (link && link.href) {
|
||||
e.preventDefault();
|
||||
container.querySelectorAll('.sidebar-video-card').forEach(c => c.classList.remove('active-tag-item'));
|
||||
const card = link.closest('.sidebar-video-card');
|
||||
if (card) card.classList.add('active-tag-item');
|
||||
if (typeof window.loadItemAjax === 'function') {
|
||||
window.loadItemAjax(link.href, false);
|
||||
} else {
|
||||
window.location.href = link.href;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const switchSidebarTab = (tabName) => {
|
||||
if (window.clearHoverPreview) {
|
||||
window.clearHoverPreview();
|
||||
}
|
||||
const normalizedTab = (tabName === 'videos' || tabName === 'recommendations')
|
||||
? 'recommendations'
|
||||
: (tabName === 'tag' ? 'tag' : (tabName === 'favs' ? 'favs' : tabName));
|
||||
: (tabName === 'tag' ? 'tag' : (tabName === 'favs' ? 'favs' : (tabName === 'chan' ? 'chan' : tabName)));
|
||||
const tabs = document.querySelectorAll('.sidebar-tab');
|
||||
const contents = document.querySelectorAll('.sidebar-tab-content');
|
||||
|
||||
tabs.forEach(t => {
|
||||
const tTab = (t.dataset.tab === 'videos' || t.dataset.tab === 'recommendations')
|
||||
? 'recommendations'
|
||||
: (t.dataset.tab === 'tag' ? 'tag' : (t.dataset.tab === 'favs' ? 'favs' : t.dataset.tab));
|
||||
: (t.dataset.tab === 'tag' ? 'tag' : (t.dataset.tab === 'favs' ? 'favs' : (t.dataset.tab === 'chan' ? 'chan' : t.dataset.tab)));
|
||||
if (tTab === normalizedTab) {
|
||||
t.classList.add('active');
|
||||
} else {
|
||||
@@ -2113,7 +2327,7 @@
|
||||
contents.forEach(c => {
|
||||
const cTab = (c.dataset.tabContent === 'videos' || c.dataset.tabContent === 'recommendations')
|
||||
? 'recommendations'
|
||||
: (c.dataset.tabContent === 'tag' ? 'tag' : (c.dataset.tabContent === 'favs' ? 'favs' : c.dataset.tabContent));
|
||||
: (c.dataset.tabContent === 'tag' ? 'tag' : (c.dataset.tabContent === 'favs' ? 'favs' : (c.dataset.tabContent === 'chan' ? 'chan' : c.dataset.tabContent)));
|
||||
if (cTab === normalizedTab) {
|
||||
c.classList.add('active');
|
||||
c.style.display = 'block';
|
||||
@@ -2143,6 +2357,12 @@
|
||||
} else {
|
||||
setTimeout(() => highlightActiveFavsCard(true), 60);
|
||||
}
|
||||
} else if (normalizedTab === 'chan') {
|
||||
if (!chanFeedLoaded) {
|
||||
loadChanFeed(true);
|
||||
} else {
|
||||
setTimeout(() => highlightActiveChanCard(true), 60);
|
||||
}
|
||||
}
|
||||
};
|
||||
window.switchSidebarTab = switchSidebarTab;
|
||||
@@ -2154,7 +2374,7 @@
|
||||
const btn = e.target.closest('.sidebar-tab');
|
||||
if (btn && btn.dataset.tab) {
|
||||
e.preventDefault();
|
||||
if (btn.dataset.tab !== 'tag' && btn.dataset.tab !== 'favs') {
|
||||
if (btn.dataset.tab !== 'tag' && btn.dataset.tab !== 'favs' && btn.dataset.tab !== 'chan') {
|
||||
userManuallySelectedNonTagTab = true;
|
||||
} else {
|
||||
userManuallySelectedNonTagTab = false;
|
||||
@@ -2170,7 +2390,9 @@
|
||||
savedTab = localStorage.getItem('sidebar_active_tab') || 'comments';
|
||||
} catch (_) {}
|
||||
|
||||
if (getCurrentFavsUser()) {
|
||||
if (getCurrentChanContext()) {
|
||||
switchSidebarTab('chan');
|
||||
} else if (getCurrentFavsUser()) {
|
||||
switchSidebarTab('favs');
|
||||
} else if (getCurrentTag()) {
|
||||
switchSidebarTab('tag');
|
||||
@@ -2410,22 +2632,28 @@
|
||||
initTagOrderButton();
|
||||
bindTagContainerEvents();
|
||||
bindFavsContainerEvents();
|
||||
bindChanContainerEvents();
|
||||
updateTagTabVisibility();
|
||||
updateFavsTabVisibility();
|
||||
updateChanTabVisibility();
|
||||
document.addEventListener('f0ck:contentLoaded', () => {
|
||||
updateTagTabVisibility();
|
||||
updateFavsTabVisibility();
|
||||
updateChanTabVisibility();
|
||||
setTimeout(() => {
|
||||
highlightActiveTagCard(true);
|
||||
highlightActiveFavsCard(true);
|
||||
highlightActiveChanCard(true);
|
||||
}, 60);
|
||||
});
|
||||
window.addEventListener('popstate', () => {
|
||||
setTimeout(() => {
|
||||
updateTagTabVisibility();
|
||||
updateFavsTabVisibility();
|
||||
updateChanTabVisibility();
|
||||
highlightActiveTagCard(true);
|
||||
highlightActiveFavsCard(true);
|
||||
highlightActiveChanCard(true);
|
||||
}, 60);
|
||||
});
|
||||
bindRecommendationEvents();
|
||||
|
||||
+67
-5
@@ -6,7 +6,7 @@
|
||||
const infoEl = document.querySelector("#a_info");
|
||||
const idLinkEl = document.querySelector("a.id-link");
|
||||
|
||||
const rawId = commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText;
|
||||
const rawId = favoEl?.dataset?.localId || commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText;
|
||||
if (!rawId) return null;
|
||||
|
||||
const tagsContainer = document.querySelector("#tags");
|
||||
@@ -256,17 +256,79 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
const favoBtns = document.querySelectorAll("#a_favo");
|
||||
const firstFavoBtn = favoBtns[0];
|
||||
const chanRehostBtn = document.querySelector('#chan-item-rehost-btn');
|
||||
const isChan = firstFavoBtn?.dataset?.isChan === 'true' || !!chanRehostBtn;
|
||||
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
|
||||
let localId = firstFavoBtn?.dataset?.localId;
|
||||
|
||||
// If viewing un-rehosted 4chan post, auto-rehost first
|
||||
if (isChan && !localId && chanUrl) {
|
||||
favoBtns.forEach(btn => {
|
||||
btn.className = 'iconset fa-solid fa-spinner fa-spin';
|
||||
});
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || '';
|
||||
const rehostResp = 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: chanUrl,
|
||||
original_filename: firstFavoBtn?.dataset?.filename || chanRehostBtn?.dataset?.filename || '',
|
||||
width: firstFavoBtn?.dataset?.width || chanRehostBtn?.dataset?.width || '',
|
||||
height: firstFavoBtn?.dataset?.height || chanRehostBtn?.dataset?.height || ''
|
||||
})
|
||||
});
|
||||
const rehostData = await rehostResp.json();
|
||||
if (rehostData.success && rehostData.item_id) {
|
||||
localId = rehostData.item_id;
|
||||
favoBtns.forEach(btn => {
|
||||
btn.dataset.itemId = localId;
|
||||
btn.dataset.localId = localId;
|
||||
});
|
||||
const commentsEl = document.querySelector("#comments-container");
|
||||
if (commentsEl) commentsEl.dataset.itemId = localId;
|
||||
const infoEl = document.querySelector("#a_info");
|
||||
if (infoEl) infoEl.dataset.itemId = localId;
|
||||
|
||||
if (chanRehostBtn) {
|
||||
chanRehostBtn.classList.add('rehosted');
|
||||
chanRehostBtn.outerHTML = `
|
||||
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
|
||||
<a href="/${localId}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #${localId})" 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> #${localId}</a>
|
||||
<button type="button" class="chan-rehost-edit-btn" data-item-id="${localId}" 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>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
throw new Error(rehostData.msg || 'Rehost failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CHAN-FAV] Auto-rehost error:', err);
|
||||
favoBtns.forEach(btn => {
|
||||
btn.className = 'iconset fa-regular fa-heart';
|
||||
});
|
||||
if (typeof window.flashMessage === 'function') {
|
||||
window.flashMessage('Failed to favorite post', 3000, 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid } = ctx;
|
||||
const postid = localId ? Number(localId) : ctx.postid;
|
||||
|
||||
// Read state BEFORE the API call so we have a fallback
|
||||
const favoBtns = document.querySelectorAll("#a_favo");
|
||||
const wasAlreadyFav = favoBtns.length > 0 && favoBtns[0].classList.contains('fa-solid');
|
||||
|
||||
try {
|
||||
const res = await post('/api/v2/togglefav', {
|
||||
postid: postid
|
||||
postid: postid,
|
||||
chan_url: chanUrl
|
||||
});
|
||||
if (res && res.success) {
|
||||
if (window.invalidateItemCache) {
|
||||
|
||||
Reference in New Issue
Block a user