Files
f0ckm/views/chan/thread.html
T

1231 lines
39 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="/abyss/{{ board }}/{{ tid }}" class="chan-action-btn" title="Open in Abyss vertical video scroller">
<i class="fa-solid fa-dice-d6"></i> Abyss
</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' : '' }} {{ 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">
<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 timeago" data-iso="{{ p.iso_str }}" tooltip="{{ p.iso_str }}" 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.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>
<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 {{ 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
<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" 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, box-shadow 0.2s;
scroll-margin-top: 80px;
}
.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.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 {
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;
}
body.onara-modal-open:not(.onara-modal-closing) .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;
}
body.onara-modal-closing .chan-post-card.onara-active,
body.onara-modal-closing .chan-post-card.active-thread-post {
opacity: 1 !important;
border-color: rgba(255, 255, 255, 0.08) !important;
box-shadow: none !important;
transition: box-shadow 0.25s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.25s ease !important;
}
body.onara-modal-closing .chan-thumb-link.onara-active .chan-thumb-img {
box-shadow: none !important;
transition: box-shadow 0.25s ease !important;
}
.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;
}
/* Post Content Layout (Media + Comment) */
.chan-post-content {
display: flex;
gap: 16px;
align-items: flex-start;
}
@media (max-width: 640px) {
.chan-post-content {
flex-direction: column;
gap: 12px;
}
}
/* Media Wrapper */
.chan-media-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
width: 170px;
flex-shrink: 0;
}
.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: #0d0d0d;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.chan-thumb-link {
display: flex;
align-items: center;
justify-content: center;
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;
}
/* 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;
}
/* File Info (filename, dimensions, size) */
.chan-file-info {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
}
.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-file-download:hover {
color: #4ade80;
text-decoration: underline;
}
.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 */
.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-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;
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;
}
/* 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: #f87171;
text-decoration: underline;
font-family: monospace;
cursor: pointer;
transition: color 0.15s ease;
}
.chan-ref:hover,
.chan-post-comment .quotelink:hover {
color: #ff4d4d;
}
.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;
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: 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>
<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" 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');
if (postCard) {
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 (typeof window.showToast === 'function') {
window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!');
}
if (typeof window.openChanRehostModal === 'function') {
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');
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();
e.stopImmediatePropagation();
const itemId = editBtn.dataset.itemId;
if (itemId && typeof window.openChanRehostModal === 'function') {
window.openChanRehostModal({ itemId: Number(itemId), anchorEl: editBtn });
}
}
});
// 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;
const wasFaved = favBtn.classList.contains('favorited');
const isNowFav = !wasFaved;
// 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 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,
action: isNowFav ? 'add' : 'delete',
favorited: isNowFav
})
});
const favData = await favResp.json();
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 {
// 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');
}
}
})();
});
// 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);
};
// 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();
}
try {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} catch (_) {
el.scrollIntoView();
}
highlightPost(el);
}
}
});
// 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();
const popHeight = previewEl.offsetHeight || 150;
const popWidth = previewEl.offsetWidth || 380;
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) => {
const ref = e.target.closest('.chan-ref, .quotelink');
if (ref && previewEl) {
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);
}
}
};
// Clear active card highlight whenever onara modal closes
const clearOnaraActiveCards = () => {
if (!document.body.classList.contains('onara-modal-open')) {
document.querySelectorAll('.chan-post-card.active-thread-post, .chan-post-card.onara-active, .chan-thumb-link.onara-active').forEach(el => {
el.classList.remove('active-thread-post', 'onara-active');
});
}
};
const bodyObserver = new MutationObserver(() => {
clearOnaraActiveCards();
});
bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
jumpToInitialActive();
setTimeout(jumpToInitialActive, 100);
})();
</script>
@include(snippets/footer)