bvcx
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user