feat(chan): represent replies/backlinks and fix styling for reply cards with media

This commit is contained in:
2026-09-26 15:42:45 +02:00
parent 27f5e3563f
commit 62f0fbde63
2 changed files with 520 additions and 151 deletions
+473 -146
View File
@@ -56,7 +56,7 @@
<!-- 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 }}">
<article class="chan-post-card {{ p.no === tid ? 'is-op' : 'is-reply' }} {{ p.has_media ? 'has-media' : 'no-media' }} {{ p.local_id ? 'is-rehosted' : '' }} {{ p.is_onara_active ? 'onara-active active-thread-post' : '' }}" 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">
@@ -74,6 +74,16 @@
@if(p.sub && p.no !== tid)
<span class="chan-reply-sub">{{ p.sub }}</span>
@endif
@if(p.replies && p.replies.length > 0)
<div class="chan-post-backlinks" title="{{ p.replies.length }} {{ p.replies.length === 1 ? 'reply' : 'replies' }}">
<span class="chan-backlinks-icon"><i class="fa-solid fa-reply"></i></span>
<span class="chan-backlinks-items">
@each(p.replies as rNo)
<a href="#p{{ rNo }}" class="chan-ref chan-backlink" data-post="{{ rNo }}">&gt;&gt;{{ rNo }}</a>
@endeach
</span>
</div>
@endif
@if(p.has_media)
<div class="chan-post-media-badge">
<i class="fa-solid fa-paperclip"></i>
@@ -87,7 +97,7 @@
@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">
<a href="/4/{{ board }}/{{ tid }}/{{ p.no }}" class="chan-thumb-link {{ p.is_onara_active ? 'onara-active' : '' }}" title="Open in player view">
@if(p.thumb)
<img src="{{ p.thumb }}" alt="Media #{{ p.no }}" class="chan-thumb-img" loading="lazy" />
@else
@@ -121,7 +131,7 @@
<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;">
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ p.local_id }}" title="Edit rating" 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>
@@ -293,7 +303,8 @@
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;
transition: border-color 0.2s, background-color 0.2s, box-shadow 0.2s;
scroll-margin-top: 80px;
}
.chan-post-card.is-rehosted {
border-color: rgba(74, 222, 128, 0.3);
@@ -303,9 +314,64 @@
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-card.is-reply {
background: rgba(24, 24, 28, 0.8);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.chan-post-card.is-reply:hover {
border-color: rgba(255, 255, 255, 0.12);
}
.chan-post-card.is-reply.has-media {
background: rgba(26, 26, 31, 0.88);
border-color: rgba(255, 255, 255, 0.08);
}
.chan-post-card:target,
.chan-post-card.active-thread-post,
.chan-post-card.onara-active {
border-color: #4ade80 !important;
background: rgba(74, 222, 128, 0.08) !important;
box-shadow: 0 0 0 2px rgba(74, 222, 128, 0.5), 0 0 20px rgba(74, 222, 128, 0.25) !important;
position: relative;
z-index: 5;
}
@keyframes chanHighlightPulse {
0% {
border-color: #4ade80 !important;
background: rgba(74, 222, 128, 0.18) !important;
box-shadow: 0 0 0 3px #4ade80, 0 0 30px rgba(74, 222, 128, 0.6) !important;
}
50% {
border-color: #4ade80 !important;
background: rgba(74, 222, 128, 0.10) !important;
box-shadow: 0 0 0 2px rgba(74, 222, 128, 0.7), 0 0 20px rgba(74, 222, 128, 0.4) !important;
}
100% {
border-color: rgba(74, 222, 128, 0.4) !important;
background: rgba(74, 222, 128, 0.04) !important;
box-shadow: 0 0 0 1px rgba(74, 222, 128, 0.3) !important;
}
}
.chan-post-card.chan-flash-highlight {
animation: chanHighlightPulse 1.8s ease-out forwards !important;
position: relative;
z-index: 6;
}
.chan-thumb-link.onara-active .chan-thumb-img {
box-shadow: 0 0 0 2px #4ade80, 0 0 15px rgba(74, 222, 128, 0.6) !important;
}
body.onara-modal-open:not(.onara-modal-closing) .chan-post-card:not(.onara-active):not(.active-thread-post) {
opacity: var(--onara-grid-dim-opacity, 0.5) !important;
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important;
}
body.onara-modal-open:not(.onara-modal-closing) .chan-post-card.onara-active,
body.onara-modal-open:not(.onara-modal-closing) .chan-post-card.active-thread-post {
opacity: 1 !important;
border-color: #4ade80 !important;
box-shadow: 0 0 0 3px #4ade80, 0 0 25px rgba(74, 222, 128, 0.5) !important;
position: relative !important;
z-index: 52 !important;
transition: box-shadow 0.28s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease !important;
}
.chan-post-rehosted-pill {
@@ -389,85 +455,151 @@
gap: 4px;
}
.chan-post-body {
/* Post Content Layout (Media + Comment) */
.chan-post-content {
display: flex;
gap: 18px;
gap: 16px;
align-items: flex-start;
}
@media (max-width: 640px) {
.chan-post-body {
.chan-post-content {
flex-direction: column;
gap: 12px;
}
}
/* Media Box */
.chan-media-box {
/* Media Wrapper */
.chan-media-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
width: 200px;
width: 170px;
flex-shrink: 0;
}
.chan-media-thumb-wrap {
.chan-post-card.is-reply .chan-media-wrapper {
width: 160px;
}
.chan-post-card.is-op .chan-media-wrapper {
width: 220px;
}
@media (max-width: 640px) {
.chan-media-wrapper,
.chan-post-card.is-reply .chan-media-wrapper,
.chan-post-card.is-op .chan-media-wrapper {
width: 100%;
max-width: 260px;
}
}
/* Thumb Box & Link */
.chan-thumb-box {
position: relative;
width: 100%;
aspect-ratio: 16 / 10;
background: #000;
background: #0d0d0d;
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;
.chan-thumb-link {
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.35);
opacity: 0;
transition: opacity 0.2s;
color: #fff;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
text-decoration: none;
background: #0d0d0d;
}
.chan-thumb-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.25s ease;
}
.chan-thumb-link:hover .chan-thumb-img {
transform: scale(1.05);
}
.chan-no-thumb {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #444;
font-size: 1.5rem;
background: #111;
}
.chan-media-thumb-wrap:hover .chan-media-play-overlay {
opacity: 1;
/* Video Indicator Overlay */
.chan-video-indicator {
position: absolute;
bottom: 6px;
right: 6px;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
pointer-events: none;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
z-index: 2;
}
.chan-media-details {
/* File Info (filename, dimensions, size) */
.chan-file-info {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.75rem;
width: 100%;
}
.chan-media-filename {
white-space: nowrap;
.chan-filename-row {
display: flex;
align-items: center;
overflow: hidden;
}
.chan-file-download {
display: inline-flex;
align-items: center;
gap: 5px;
color: #999;
text-decoration: none;
font-size: 0.73rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
transition: color 0.15s ease;
}
.chan-media-filename a {
color: #aaa;
text-decoration: none;
}
.chan-media-filename a:hover {
.chan-file-download:hover {
color: #4ade80;
text-decoration: underline;
}
.chan-media-meta {
color: #777;
.chan-file-download i {
font-size: 0.68rem;
flex-shrink: 0;
opacity: 0.7;
}
.chan-file-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chan-file-meta-row {
display: flex;
align-items: center;
gap: 8px;
color: #666;
font-size: 0.7rem;
font-family: monospace;
}
/* Rehost button */
@@ -510,6 +642,18 @@
align-items: center;
width: 100%;
}
.chan-rehosted-wrap {
display: inline-flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
}
.chan-rehosted-wrap .chan-rehost-btn {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chan-card-fav-btn {
display: inline-flex;
align-items: center;
@@ -554,16 +698,66 @@
.chan-post-comment .quote {
color: #789922;
}
/* Backlinks in post header */
.chan-post-backlinks {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 6px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 6px;
padding: 2px 7px;
font-size: 0.78rem;
max-width: 100%;
}
.chan-backlinks-icon {
color: #71717a;
font-size: 0.72rem;
display: inline-flex;
align-items: center;
user-select: none;
}
.chan-backlinks-icon i {
transform: scaleX(-1);
}
.chan-backlinks-items {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 6px;
}
.chan-backlink {
color: #f87171 !important;
text-decoration: none !important;
font-family: monospace;
font-size: 0.78rem;
cursor: pointer;
padding: 1px 5px;
border-radius: 4px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
transition: all 0.15s ease;
line-height: 1.3;
}
.chan-backlink:hover {
color: #fff !important;
background: rgba(239, 68, 68, 0.35) !important;
border-color: rgba(239, 68, 68, 0.6) !important;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.35);
}
.chan-ref,
.chan-post-comment .quotelink {
color: #d00;
color: #f87171;
text-decoration: underline;
font-family: monospace;
cursor: pointer;
transition: color 0.15s ease;
}
.chan-ref:hover,
.chan-post-comment .quotelink:hover {
color: #ff3333;
color: #ff4d4d;
}
.chan-post-comment s,
.chan-post-comment .spoiler {
@@ -584,15 +778,41 @@
.chan-quote-preview-popup {
position: fixed;
z-index: 99999;
max-width: 480px;
background: #1e1e1e;
width: max-content;
max-width: 520px;
max-height: 80vh;
overflow-y: auto;
background: rgba(22, 22, 24, 0.98);
border: 1px solid #4ade80;
border-radius: 8px;
padding: 12px 14px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
border-radius: 10px;
padding: 12px 16px;
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.8), 0 0 20px rgba(74, 222, 128, 0.2);
pointer-events: none;
color: #eee;
font-size: 0.85rem;
backdrop-filter: blur(12px);
}
.chan-quote-preview-popup .chan-post-header {
margin-bottom: 8px;
padding-bottom: 4px;
}
.chan-quote-preview-popup .chan-rehost-container {
display: none !important;
}
.chan-quote-preview-popup .chan-media-wrapper {
margin-bottom: 8px;
}
.chan-quote-preview-popup .chan-thumb-box {
max-width: 120px;
max-height: 120px;
}
.chan-quote-preview-popup .chan-thumb-img {
max-width: 120px;
max-height: 120px;
object-fit: cover;
}
.chan-quote-preview-popup .chan-post-backlinks {
display: none !important;
}
</style>
@@ -672,7 +892,7 @@
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;">' +
'<button type="button" class="chan-rehost-edit-btn" data-item-id="' + data.item_id + '" title="Edit rating" 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>';
const postCard = document.getElementById('p' + postNo) || btn.closest('.chan-post-card');
@@ -692,7 +912,9 @@
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 });
const postCardEl = document.getElementById('p' + postNo) || postCard;
const anchor = (postCardEl ? (postCardEl.querySelector('.chan-rehost-edit-btn') || postCardEl.querySelector('.chan-rehosted-wrap')) : null) || document.getElementById('rehost-box-' + postNo);
window.openChanRehostModal({ itemId: data.item_id, anchorEl: anchor });
}
} else {
btn.classList.remove('loading');
@@ -713,7 +935,7 @@
e.stopImmediatePropagation();
const itemId = editBtn.dataset.itemId;
if (itemId && typeof window.openChanRehostModal === 'function') {
window.openChanRehostModal({ itemId: Number(itemId), isPlayerView: false });
window.openChanRehostModal({ itemId: Number(itemId), anchorEl: editBtn });
}
}
});
@@ -738,103 +960,178 @@
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>';
const wasFaved = favBtn.classList.contains('favorited');
const isNowFav = !wasFaved;
try {
// If not yet rehosted, auto-rehost first
if (!localId && mediaUrl) {
// 1. Instantly apply client UI
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');
}
if (navigator.vibrate) navigator.vibrate(50);
// 2. Perform server sync in background
(async () => {
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" 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 = document.getElementById('p' + postNo) || favBtn.closest('.chan-post-card');
if (postCard) {
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);
}
const timeEl = postCard.querySelector('.chan-post-time, .timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.setAttribute('title', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
}
}
} else {
throw new Error(rehostData.msg || 'Rehost failed');
}
}
// Now sync favorite to server
const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', {
const favResp = await fetch('/api/v2/togglefav', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Type': 'application/json',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
},
body: new URLSearchParams({
url: mediaUrl,
original_filename: favBtn.dataset.filename || '',
width: favBtn.dataset.width || '',
height: favBtn.dataset.height || ''
body: JSON.stringify({
postid: Number(localId),
chan_url: mediaUrl,
action: isNowFav ? 'add' : 'delete',
favorited: isNowFav
})
});
const rehostData = await rehostResp.json();
if (rehostData.success && rehostData.item_id) {
localId = rehostData.item_id;
favBtn.dataset.localId = localId;
const favData = await favResp.json();
// 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 = document.getElementById('p' + postNo) || favBtn.closest('.chan-post-card');
if (postCard) {
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);
}
const timeEl = postCard.querySelector('.chan-post-time, .timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.setAttribute('title', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
}
}
if (favData.success) {
const finalIsFav = (favData.favorited !== undefined) ? !!favData.favorited : isNowFav;
favBtn.classList.toggle('favorited', finalIsFav);
favBtn.innerHTML = '<i class="fa-' + (finalIsFav ? 'solid' : 'regular') + ' fa-heart"></i>';
favBtn.title = finalIsFav ? 'Remove from favorites' : 'Favorite';
} else {
throw new Error(rehostData.msg || 'Rehost failed');
// Rollback on server rejection
favBtn.classList.toggle('favorited', wasFaved);
favBtn.innerHTML = '<i class="fa-' + (wasFaved ? 'solid' : 'regular') + ' fa-heart"></i>';
favBtn.title = wasFaved ? 'Remove from favorites' : 'Favorite';
if (typeof window.flashMessage === 'function') {
window.flashMessage(favData.msg || 'Failed to update favorite', 3000, 'error');
}
}
} catch (err) {
console.error('[CHAN-CARD-FAV] Error:', err);
// Rollback on network/rehost error
favBtn.classList.toggle('favorited', wasFaved);
favBtn.innerHTML = '<i class="fa-' + (wasFaved ? 'solid' : 'regular') + ' fa-heart"></i>';
favBtn.title = wasFaved ? 'Remove from favorites' : 'Favorite';
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite', 3000, 'error');
}
}
})();
});
// 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');
// Flash highlight a post card
const highlightPost = (el) => {
if (!el) return;
el.classList.remove('chan-flash-highlight');
void el.offsetWidth;
el.classList.add('chan-flash-highlight');
setTimeout(() => {
el.classList.remove('chan-flash-highlight');
}, 2000);
};
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');
// Quote click navigation (smooth jump + flash highlight)
document.addEventListener('click', (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;
e.preventDefault();
if (previewEl) previewEl.style.display = 'none';
// If card is hidden due to active filter ("Media" or "Rehosted"), reset to "All"
if (targetCard.style.display === 'none') {
const filterAllBtn = document.getElementById('filter-all-btn');
if (filterAllBtn) filterAllBtn.click();
}
history.pushState(null, '', '#p' + targetPostNo);
try {
targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
} catch (_) {
targetCard.scrollIntoView();
}
highlightPost(targetCard);
});
// Handle browser back/forward buttons navigating hash
window.addEventListener('hashchange', () => {
if (window.location.hash && window.location.hash.startsWith('#p')) {
const el = document.querySelector(window.location.hash);
if (el) {
if (el.style.display === 'none') {
const filterAllBtn = document.getElementById('filter-all-btn');
if (filterAllBtn) filterAllBtn.click();
}
} else {
favBtn.innerHTML = origHtml;
if (typeof window.flashMessage === 'function') {
window.flashMessage(favData.msg || 'Failed to update favorite', 3000, 'error');
try {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} catch (_) {
el.scrollIntoView();
}
}
} 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');
highlightPost(el);
}
}
});
@@ -860,13 +1157,23 @@
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;
const popHeight = previewEl.offsetHeight || 150;
const popWidth = previewEl.offsetWidth || 380;
previewEl.style.top = Math.max(10, top) + 'px';
previewEl.style.left = Math.max(10, left) + 'px';
let top = rect.bottom + 8;
if (top + popHeight > window.innerHeight - 10) {
top = rect.top - popHeight - 8;
}
if (top < 10) top = 10;
let left = rect.left;
if (left + popWidth > window.innerWidth - 10) {
left = window.innerWidth - popWidth - 10;
}
if (left < 10) left = 10;
previewEl.style.top = top + 'px';
previewEl.style.left = left + 'px';
});
document.addEventListener('mouseout', (e) => {
@@ -875,6 +1182,26 @@
previewEl.style.display = 'none';
}
});
// Jump to active post in thread if onara active or hash present
const jumpToInitialActive = () => {
let target = document.querySelector('.chan-post-card.active-thread-post, .chan-post-card.onara-active');
if (!target && window.location.hash) {
target = document.querySelector(window.location.hash);
}
if (target) {
try {
target.scrollIntoView({ block: 'center', behavior: 'instant' });
} catch (_) {
target.scrollIntoView();
}
if (window.location.hash) {
highlightPost(target);
}
}
};
jumpToInitialActive();
setTimeout(jumpToInitialActive, 100);
})();
</script>