Files
f0ckm/views/chan/thread.html
T
2026-09-20 21:43:02 +02:00

856 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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)