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
+855
View File
@@ -0,0 +1,855 @@
@include(snippets/header)
<div class="pagewrapper">
<div id="main" class="chan-page chan-thread-page">
<!-- Top Header Navigation -->
<div class="chan-header-bar chan-thread-header">
<div class="chan-thread-nav-row">
<a href="/4/{{ board }}/catalogue" class="chan-back-btn">
<i class="fa-solid fa-arrow-left"></i> /{{ board }}/ Catalogue
</a>
<div class="chan-thread-actions">
@if(first_media_no)
<a href="/4/{{ board }}/{{ tid }}/{{ first_media_no }}" class="chan-action-btn primary" title="Open thread media in player view">
<i class="fa-solid fa-play"></i> Player View
</a>
@endif
<a href="/scroller#{{ board }}/{{ tid }}" class="chan-action-btn" title="Open in vertical video scroller">
<i class="fa-solid fa-mobile-screen"></i> Scroller
</a>
<a href="https://boards.4chan.org/{{ board }}/thread/{{ tid }}" target="_blank" rel="noopener noreferrer" class="chan-action-btn" title="View original thread on 4chan">
<i class="fa-solid fa-arrow-up-right-from-square"></i> 4chan.org
</a>
</div>
</div>
<div class="chan-thread-title-row">
<span class="chan-clover-icon"><i class="fa-solid fa-clover"></i></span>
<div class="chan-thread-title-info">
<h2>
@if(op.sub)
{{ op.sub }}
@else
Thread #{{ tid }}
@endif
</h2>
<div class="chan-thread-meta">
<span class="chan-meta-item"><i class="fa-solid fa-comments"></i> {{ posts.length }} posts</span>
<span class="chan-meta-item"><i class="fa-solid fa-image"></i> {{ media_count }} media files</span>
@if(rehosts_count > 0)
<span class="chan-meta-item" style="color: #4ade80;"><i class="fa-solid fa-check"></i> {{ rehosts_count }} rehosted</span>
@endif
</div>
</div>
<!-- Filter Toggle: All vs Media Only vs Rehosted -->
<div class="chan-filter-toggle">
<button type="button" class="chan-toggle-btn active" id="filter-all-btn">All Posts ({{ posts.length }})</button>
<button type="button" class="chan-toggle-btn" id="filter-media-btn">Media Only ({{ media_count }})</button>
@if(rehosts_count > 0)
<button type="button" class="chan-toggle-btn" id="filter-rehosted-btn" style="color: #4ade80;"><i class="fa-solid fa-check"></i> Rehosted ({{ rehosts_count }})</button>
@endif
</div>
</div>
</div>
<!-- Posts Stream -->
<div class="chan-posts-stream" id="chan-posts-stream">
@each(posts as p)
<article class="chan-post-card {{ p.no === tid ? 'is-op' : 'is-reply' }} {{ p.has_media ? 'has-media' : 'no-media' }} {{ p.local_id ? 'is-rehosted' : '' }}" id="p{{ p.no }}" data-no="{{ p.no }}" data-has-media="{{ p.has_media ? 1 : 0 }}" data-is-rehosted="{{ p.local_id ? 1 : 0 }}">
<!-- Post Header -->
<div class="chan-post-header">
<span class="chan-post-name">{{ p.name }}</span>
@if(p.trip)
<span class="chan-post-trip">{{ p.trip }}</span>
@endif
<time class="chan-post-time" title="{{ p.date_str }}">{{ p.timeago }}</time>
<a href="#p{{ p.no }}" class="chan-post-no">No. {{ p.no }}</a>
@if(p.local_id)
<a href="/{{ p.local_id }}" target="_blank" class="chan-post-rehosted-pill" title="Already rehosted on f0ckm as Post #{{ p.local_id }}">
<i class="fa-solid fa-check"></i> #{{ p.local_id }}
</a>
@endif
@if(p.sub && p.no !== tid)
<span class="chan-reply-sub">{{ p.sub }}</span>
@endif
@if(p.has_media)
<div class="chan-post-media-badge">
<i class="fa-solid fa-paperclip"></i>
<span>{{ p.ext.toUpperCase().replace('.', '') }}</span>
</div>
@endif
</div>
<!-- Post Content (Media + Comment) -->
<div class="chan-post-content">
@if(p.has_media)
<div class="chan-media-wrapper">
<div class="chan-thumb-box">
<a href="/4/{{ board }}/{{ tid }}/{{ p.no }}" class="chan-thumb-link" title="Open in player view">
@if(p.thumb)
<img src="{{ p.thumb }}" alt="Media #{{ p.no }}" class="chan-thumb-img" loading="lazy" />
@else
<div class="chan-no-thumb"><i class="fa-solid fa-file"></i></div>
@endif
@if(p.is_video)
<span class="chan-video-indicator"><i class="fa-solid fa-play"></i></span>
@endif
@if(p.local_id)
<span class="chan-thumb-rehost-badge" title="Rehosted on f0ckm as Post #{{ p.local_id }}"><i class="fa-solid fa-check"></i> #{{ p.local_id }}</span>
@endif
</a>
</div>
<div class="chan-file-info">
<div class="chan-filename-row">
<a href="{{ p.external_media_url }}" target="_blank" rel="noopener noreferrer" class="chan-file-download" title="Open full media in new tab">
<i class="fa-solid fa-arrow-up-right-from-square"></i>
<span class="chan-file-title">{{ p.filename || ('File ' + p.tim + p.ext) }}</span>
</a>
</div>
<div class="chan-file-meta-row">
@if(p.fsize)<span>{{ p.fsize }}</span>@endif
@if(p.w && p.h)<span>{{ p.w }}×{{ p.h }}</span>@endif
</div>
<!-- Rehost & Favorite actions -->
<div class="chan-rehost-container" id="rehost-box-{{ p.no }}">
@if(p.local_id)
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;flex:1;">
<a href="/{{ p.local_id }}" class="chan-rehost-btn rehosted" target="_blank">
<i class="fa-solid fa-check"></i> Rehosted #{{ p.local_id }}
</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ p.local_id }}" title="Edit rating, tags & comment" style="padding:4px 7px;border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;font-size:0.75rem;">
<i class="fa-solid fa-pen"></i>
</button>
</span>
@else
<button type="button" class="chan-rehost-btn" data-url="{{ p.external_media_url }}" data-board="{{ board }}" data-tid="{{ tid }}" data-post="{{ p.no }}" data-filename="{{ p.filename || '' }}" data-width="{{ p.w || '' }}" data-height="{{ p.h || '' }}">
<i class="fa-solid fa-cloud-arrow-up"></i> Rehost
</button>
@endif
<button type="button" class="chan-card-fav-btn {{ p.user_has_favorited ? 'favorited' : '' }}" data-post="{{ p.no }}" data-local-id="{{ p.local_id || '' }}" data-url="{{ p.external_media_url }}" data-board="{{ board }}" data-tid="{{ tid }}" data-filename="{{ p.filename || '' }}" data-width="{{ p.w || '' }}" data-height="{{ p.h || '' }}" title="{{ p.user_has_favorited ? 'Remove from favorites' : 'Favorite' }}">
<i class="fa-{{ p.user_has_favorited ? 'solid' : 'regular' }} fa-heart"></i>
</button>
</div>
</div>
</div>
@endif
<!-- Post Comment -->
@if(p.com_raw)
<div class="chan-post-comment">
{{ p.com_formatted }}
</div>
@endif
</div>
</article>
@endeach
</div>
</div>
</div>
<style>
.chan-thread-page {
max-width: 1200px;
margin: 0 auto;
padding: 20px 15px 80px;
}
.chan-thread-header {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 25px;
background: var(--bg-card, rgba(25, 25, 25, 0.85));
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
border-radius: 12px;
padding: 16px 20px;
backdrop-filter: blur(8px);
}
.chan-thread-nav-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.chan-back-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.06);
color: var(--text-main, #fff);
border: 1px solid rgba(255, 255, 255, 0.1);
text-decoration: none;
font-weight: 600;
font-size: 0.88rem;
transition: all 0.2s;
}
.chan-back-btn:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.2);
}
.chan-thread-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.chan-action-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 600;
background: rgba(255, 255, 255, 0.06);
color: var(--text-muted, #aaa);
border: 1px solid rgba(255, 255, 255, 0.08);
text-decoration: none;
transition: all 0.2s;
}
.chan-action-btn:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
.chan-action-btn.primary {
background: rgba(74, 222, 128, 0.15);
color: #4ade80;
border-color: rgba(74, 222, 128, 0.3);
}
.chan-action-btn.primary:hover {
background: #4ade80;
color: #111;
}
.chan-thread-title-row {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
justify-content: space-between;
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding-top: 14px;
}
.chan-thread-title-info h2 {
font-size: 1.35rem;
font-weight: 800;
margin: 0 0 4px;
color: #fff;
}
.chan-thread-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.82rem;
color: #888;
}
.chan-meta-item {
display: inline-flex;
align-items: center;
gap: 5px;
}
/* Filter toggle */
.chan-filter-toggle {
display: inline-flex;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 3px;
gap: 3px;
}
.chan-toggle-btn {
background: none;
border: none;
padding: 5px 12px;
border-radius: 6px;
font-size: 0.82rem;
font-weight: 600;
color: #aaa;
cursor: pointer;
transition: all 0.2s;
}
.chan-toggle-btn.active {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
/* Posts stream */
.chan-posts-stream {
display: flex;
flex-direction: column;
gap: 12px;
}
.chan-post-card {
background: var(--bg-card, rgba(25, 25, 25, 0.85));
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
border-radius: 10px;
padding: 14px 18px;
transition: border-color 0.2s, background-color 0.2s;
}
.chan-post-card.is-rehosted {
border-color: rgba(74, 222, 128, 0.3);
background: rgba(74, 222, 128, 0.03);
}
.chan-post-card.is-op {
border-left: 4px solid #4ade80;
background: rgba(30, 30, 30, 0.95);
}
.chan-post-card:target {
border-color: #4ade80;
background: rgba(74, 222, 128, 0.05);
}
.chan-post-rehosted-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 7px;
border-radius: 4px;
background: rgba(74, 222, 128, 0.2);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.4);
font-size: 0.75rem;
font-weight: 700;
text-decoration: none;
transition: all 0.2s;
}
.chan-post-rehosted-pill:hover {
background: #4ade80;
color: #111;
}
.chan-thumb-rehost-badge {
position: absolute;
top: 6px;
left: 6px;
background: rgba(16, 185, 129, 0.92);
color: #fff;
font-size: 0.68rem;
font-weight: 700;
padding: 2px 6px;
border-radius: 4px;
backdrop-filter: blur(4px);
display: inline-flex;
align-items: center;
gap: 3px;
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
z-index: 2;
}
.chan-post-header {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
font-size: 0.82rem;
margin-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
padding-bottom: 6px;
}
.chan-post-name {
font-weight: 700;
color: #10b981;
}
.chan-post-trip {
color: #888;
font-weight: 600;
}
.chan-post-time {
color: #777;
}
.chan-post-no {
color: #aaa;
text-decoration: none;
font-family: monospace;
}
.chan-post-no:hover {
color: #4ade80;
text-decoration: underline;
}
.chan-reply-sub {
color: #fbbf24;
font-weight: 600;
}
.chan-post-media-badge {
margin-left: auto;
font-size: 0.75rem;
font-weight: 600;
color: #888;
display: inline-flex;
align-items: center;
gap: 4px;
}
.chan-post-body {
display: flex;
gap: 18px;
align-items: flex-start;
}
@media (max-width: 640px) {
.chan-post-body {
flex-direction: column;
}
}
/* Media Box */
.chan-media-box {
display: flex;
flex-direction: column;
gap: 8px;
width: 200px;
flex-shrink: 0;
}
.chan-media-thumb-wrap {
position: relative;
width: 100%;
aspect-ratio: 16 / 10;
background: #000;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.chan-media-link {
display: block;
width: 100%;
height: 100%;
}
.chan-media-thumb {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.2s;
}
.chan-media-thumb-wrap:hover .chan-media-thumb {
transform: scale(1.05);
}
.chan-media-play-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.35);
opacity: 0;
transition: opacity 0.2s;
color: #fff;
font-size: 1.5rem;
}
.chan-media-thumb-wrap:hover .chan-media-play-overlay {
opacity: 1;
}
.chan-media-details {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.75rem;
}
.chan-media-filename {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chan-media-filename a {
color: #aaa;
text-decoration: none;
}
.chan-media-filename a:hover {
color: #4ade80;
}
.chan-media-meta {
color: #777;
display: flex;
gap: 8px;
}
/* Rehost button */
.chan-rehost-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 6px;
font-size: 0.78rem;
font-weight: 600;
background: rgba(74, 222, 128, 0.15);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.3);
cursor: pointer;
text-decoration: none;
transition: all 0.2s;
width: 100%;
flex: 1;
justify-content: center;
box-sizing: border-box;
}
.chan-rehost-btn:hover {
background: #4ade80;
color: #111;
}
.chan-rehost-btn.rehosted {
background: rgba(74, 222, 128, 0.2);
color: #4ade80;
cursor: default;
}
.chan-rehost-btn.loading {
opacity: 0.6;
pointer-events: none;
}
.chan-rehost-container {
display: flex;
gap: 6px;
align-items: center;
width: 100%;
}
.chan-card-fav-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 28px;
width: 28px;
min-width: 28px;
padding: 0;
border-radius: 6px;
font-size: 0.85rem;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #888;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
}
.chan-card-fav-btn:hover {
background: rgba(255, 75, 75, 0.15);
border-color: rgba(255, 75, 75, 0.4);
color: #ff4b4b;
}
.chan-card-fav-btn.favorited {
background: rgba(255, 75, 75, 0.2);
border-color: rgba(255, 75, 75, 0.5);
color: #ff4b4b;
}
.chan-card-fav-btn.loading {
opacity: 0.6;
pointer-events: none;
}
/* Comments & Greentext */
.chan-post-comment {
color: #ddd;
font-size: 0.88rem;
line-height: 1.5;
word-break: break-word;
flex: 1;
}
.chan-quote,
.chan-post-comment .quote {
color: #789922;
}
.chan-ref,
.chan-post-comment .quotelink {
color: #d00;
text-decoration: underline;
font-family: monospace;
cursor: pointer;
}
.chan-ref:hover,
.chan-post-comment .quotelink:hover {
color: #ff3333;
}
.chan-post-comment s,
.chan-post-comment .spoiler {
background: #333;
color: #333;
padding: 0 3px;
border-radius: 2px;
text-decoration: none;
cursor: pointer;
transition: color 0.15s;
}
.chan-post-comment s:hover,
.chan-post-comment .spoiler:hover {
color: #fff;
}
/* Quote preview popup */
.chan-quote-preview-popup {
position: fixed;
z-index: 99999;
max-width: 480px;
background: #1e1e1e;
border: 1px solid #4ade80;
border-radius: 8px;
padding: 12px 14px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
pointer-events: none;
color: #eee;
font-size: 0.85rem;
}
</style>
<script>
(() => {
// Toggle filter: All vs Media Only
const filterAllBtn = document.getElementById('filter-all-btn');
const filterMediaBtn = document.getElementById('filter-media-btn');
const filterRehostedBtn = document.getElementById('filter-rehosted-btn');
const posts = document.querySelectorAll('.chan-post-card');
if (filterAllBtn && filterMediaBtn) {
filterAllBtn.addEventListener('click', () => {
filterAllBtn.classList.add('active');
filterMediaBtn.classList.remove('active');
if (filterRehostedBtn) filterRehostedBtn.classList.remove('active');
posts.forEach(p => p.style.display = 'block');
});
filterMediaBtn.addEventListener('click', () => {
filterMediaBtn.classList.add('active');
filterAllBtn.classList.remove('active');
if (filterRehostedBtn) filterRehostedBtn.classList.remove('active');
posts.forEach(p => {
p.style.display = p.dataset.hasMedia === '1' ? 'block' : 'none';
});
});
if (filterRehostedBtn) {
filterRehostedBtn.addEventListener('click', () => {
filterRehostedBtn.classList.add('active');
filterAllBtn.classList.remove('active');
filterMediaBtn.classList.remove('active');
posts.forEach(p => {
p.style.display = p.dataset.isRehosted === '1' ? 'block' : 'none';
});
});
}
}
// Inline Rehosting
document.addEventListener('click', async (e) => {
const btn = e.target.closest('.chan-rehost-btn:not(.rehosted)');
if (!btn) return;
e.preventDefault();
const url = btn.dataset.url;
const board = btn.dataset.board;
const tid = btn.dataset.tid;
const postNo = btn.dataset.post;
const filename = btn.dataset.filename;
if (!url) return;
btn.classList.add('loading');
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Rehosting…';
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,
original_filename: filename || '',
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-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">' +
'<a href="/' + data.item_id + '" class="chan-rehost-btn rehosted" target="_blank">' +
'<i class="fa-solid fa-check"></i> Rehosted #' + data.item_id + '</a>' +
'<button type="button" class="chan-rehost-edit-btn" data-item-id="' + data.item_id + '" title="Edit rating, tags & comment" style="padding:4px 7px;border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;font-size:0.75rem;">' +
'<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: false });
}
} else {
btn.classList.remove('loading');
btn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Retry';
alert(data.msg || data.message || 'Rehosting failed');
}
} catch (err) {
console.error('[CHAN] Rehost error:', err);
btn.classList.remove('loading');
btn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Error';
}
});
document.addEventListener('click', (e) => {
const editBtn = e.target.closest('.chan-rehost-edit-btn');
if (editBtn) {
e.preventDefault();
const itemId = editBtn.dataset.itemId;
if (itemId && typeof window.openChanRehostModal === 'function') {
window.openChanRehostModal({ itemId: Number(itemId), isPlayerView: false });
}
}
});
// Favorite from Thread Card (Auto-rehost if needed, then toggle favorite)
document.addEventListener('click', async (e) => {
const favBtn = e.target.closest('.chan-card-fav-btn');
if (!favBtn || favBtn.classList.contains('loading')) return;
e.preventDefault();
if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) {
if (typeof window.flashMessage === 'function') window.flashMessage('Anonymous favoriting is disabled.', 3000, 'error');
return;
}
if (!window.f0ckSession || !window.f0ckSession.user) {
if (typeof window.flashMessage === 'function') window.flashMessage('Login to favorite posts', 3000, 'warning');
return;
}
const postNo = favBtn.dataset.post;
let localId = favBtn.dataset.localId;
const mediaUrl = favBtn.dataset.url;
favBtn.classList.add('loading');
const origHtml = favBtn.innerHTML;
favBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
try {
// If not yet rehosted, auto-rehost first
if (!localId && mediaUrl) {
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: mediaUrl,
original_filename: favBtn.dataset.filename || '',
width: favBtn.dataset.width || '',
height: favBtn.dataset.height || ''
})
});
const rehostData = await rehostResp.json();
if (rehostData.success && rehostData.item_id) {
localId = rehostData.item_id;
favBtn.dataset.localId = localId;
// Update rehost box
const rehostBox = document.getElementById('rehost-box-' + postNo);
if (rehostBox) {
const rehostBtn = rehostBox.querySelector('.chan-rehost-btn');
if (rehostBtn) {
rehostBtn.outerHTML = '<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;flex:1;">' +
'<a href="/' + localId + '" class="chan-rehost-btn rehosted" target="_blank">' +
'<i class="fa-solid fa-check"></i> Rehosted #' + localId + '</a>' +
'<button type="button" class="chan-rehost-edit-btn" data-item-id="' + localId + '" title="Edit rating, tags & comment" style="padding:4px 7px;border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;font-size:0.75rem;">' +
'<i class="fa-solid fa-pen"></i></button></span>';
}
}
// Add thumb badge if missing
const postCard = favBtn.closest('.chan-post');
const thumbLink = postCard?.querySelector('.chan-thumb-link');
if (thumbLink && !thumbLink.querySelector('.chan-thumb-rehost-badge')) {
const badge = document.createElement('span');
badge.className = 'chan-thumb-rehost-badge';
badge.title = 'Rehosted on f0ckm as Post #' + localId;
badge.innerHTML = '<i class="fa-solid fa-check"></i> #' + localId;
thumbLink.appendChild(badge);
}
} else {
throw new Error(rehostData.msg || 'Rehost failed');
}
}
// Now toggle favorite
const csrfToken = window.f0ckSession?.csrf_token || '';
const favResp = await fetch('/api/v2/togglefav', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
},
body: JSON.stringify({ postid: Number(localId), chan_url: mediaUrl })
});
const favData = await favResp.json();
favBtn.classList.remove('loading');
if (favData.success) {
const isNowFav = !!favData.favorited;
favBtn.classList.toggle('favorited', isNowFav);
favBtn.innerHTML = '<i class="fa-' + (isNowFav ? 'solid' : 'regular') + ' fa-heart"></i>';
favBtn.title = isNowFav ? 'Remove from favorites' : 'Favorite';
if (typeof window.flashMessage === 'function') {
window.flashMessage(isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES');
}
} else {
favBtn.innerHTML = origHtml;
if (typeof window.flashMessage === 'function') {
window.flashMessage(favData.msg || 'Failed to update favorite', 3000, 'error');
}
}
} catch (err) {
console.error('[CHAN-CARD-FAV] Error:', err);
favBtn.classList.remove('loading');
favBtn.innerHTML = origHtml;
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite', 3000, 'error');
}
}
});
// Quote hover preview
let previewEl = null;
document.addEventListener('mouseover', (e) => {
const ref = e.target.closest('.chan-ref, .quotelink');
if (!ref) return;
const targetPostNo = ref.dataset.post || ref.getAttribute('href')?.replace(/^#p?/, '');
if (!targetPostNo) return;
const targetCard = document.getElementById('p' + targetPostNo);
if (!targetCard) return;
if (!previewEl) {
previewEl = document.createElement('div');
previewEl.className = 'chan-quote-preview-popup';
document.body.appendChild(previewEl);
}
previewEl.innerHTML = targetCard.innerHTML;
previewEl.style.display = 'block';
const rect = ref.getBoundingClientRect();
let top = rect.bottom + 8;
let left = rect.left;
if (left + 480 > window.innerWidth) left = window.innerWidth - 490;
if (top + 200 > window.innerHeight) top = rect.top - 210;
previewEl.style.top = Math.max(10, top) + 'px';
previewEl.style.left = Math.max(10, left) + 'px';
});
document.addEventListener('mouseout', (e) => {
const ref = e.target.closest('.chan-ref, .quotelink');
if (ref && previewEl) {
previewEl.style.display = 'none';
}
});
})();
</script>
@include(snippets/footer)