feat(chan): represent replies/backlinks and fix styling for reply cards with media
This commit is contained in:
+47
-5
@@ -48,13 +48,37 @@ export default (router, tpl) => {
|
|||||||
|
|
||||||
function formatComment(com) {
|
function formatComment(com) {
|
||||||
if (!com) return '';
|
if (!com) return '';
|
||||||
// Format quote lines
|
let formatted = com;
|
||||||
let formatted = com.replace(/(?:^|<br\s*\/?>)((?:>|>)((?!>|>)[^\n<]+))/g, '$1<span class="chan-quote">>$2</span>');
|
// 1. Normalize existing 4chan intra-thread quotelinks to standard format
|
||||||
// Format post cross-references >>123456
|
formatted = formatted.replace(/<a\s+[^>]*href="#p(\d+)"[^>]*>.*?<\/a>/gi, '___CHANREF_$1___');
|
||||||
formatted = formatted.replace(/(?:>|>)>(\d+)/g, '<a href="#p$1" class="chan-ref" data-post="$1">>>$1</a>');
|
// 2. Normalize cross-thread/cross-board quotelinks if any
|
||||||
|
formatted = formatted.replace(/<a\s+[^>]*href="(\/[^"]+)"[^>]*>(.*?)<\/a>/gi, '<a href="$1" class="chan-ref chan-cross-ref" target="_blank">$2</a>');
|
||||||
|
// 3. Format any unlinked quotes >>123456
|
||||||
|
formatted = formatted.replace(/(?<!>)>>(\d+)\b/g, '___CHANREF_$1___');
|
||||||
|
// 4. Also support plain >>123456 if plain text was supplied
|
||||||
|
formatted = formatted.replace(/(?<![>\w])>>(\d+)\b/g, '___CHANREF_$1___');
|
||||||
|
// 5. Restore CHANREF placeholders
|
||||||
|
formatted = formatted.replace(/___CHANREF_(\d+)___/g, '<a href="#p$1" class="chan-ref quotelink" data-post="$1">>>$1</a>');
|
||||||
|
// 6. Format quote lines (greentext)
|
||||||
|
formatted = formatted.replace(/(^|<br\s*\/?>)>(?!>)([^\n<]+)/g, '$1<span class="chan-quote">>$2</span>');
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractQuotes(com) {
|
||||||
|
if (!com) return [];
|
||||||
|
const quotes = new Set();
|
||||||
|
for (const m of com.matchAll(/href="#p(\d+)"/g)) {
|
||||||
|
quotes.add(Number(m[1]));
|
||||||
|
}
|
||||||
|
for (const m of com.matchAll(/(?<!>)>>(\d+)\b/g)) {
|
||||||
|
quotes.add(Number(m[1]));
|
||||||
|
}
|
||||||
|
for (const m of com.matchAll(/(?<![>\w])>>(\d+)\b/g)) {
|
||||||
|
quotes.add(Number(m[1]));
|
||||||
|
}
|
||||||
|
return Array.from(quotes);
|
||||||
|
}
|
||||||
|
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
// 0. GET /4 -> redirect to default board catalogue
|
// 0. GET /4 -> redirect to default board catalogue
|
||||||
// ───────────────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────────────
|
||||||
@@ -145,6 +169,21 @@ export default (router, tpl) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function formatPosts(posts, board, rehostMap, userFavSet, targetPostNo = null, lang = 'en') {
|
function formatPosts(posts, board, rehostMap, userFavSet, targetPostNo = null, lang = 'en') {
|
||||||
|
// Build reply mapping (targetPostNo -> array of replier post numbers)
|
||||||
|
const postNoSet = new Set(posts.map(p => p.no));
|
||||||
|
const replyMap = new Map();
|
||||||
|
|
||||||
|
for (const p of posts) {
|
||||||
|
if (!p.com) continue;
|
||||||
|
const quotes = extractQuotes(p.com);
|
||||||
|
for (const qNo of quotes) {
|
||||||
|
if (postNoSet.has(qNo) && qNo !== p.no) {
|
||||||
|
if (!replyMap.has(qNo)) replyMap.set(qNo, []);
|
||||||
|
replyMap.get(qNo).push(p.no);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return posts.map(p => {
|
return posts.map(p => {
|
||||||
const ext = (p.ext || '').toLowerCase();
|
const ext = (p.ext || '').toLowerCase();
|
||||||
const isVideo = ['.webm', '.mp4'].includes(ext);
|
const isVideo = ['.webm', '.mp4'].includes(ext);
|
||||||
@@ -154,6 +193,7 @@ export default (router, tpl) => {
|
|||||||
const localId = rehostInfo ? rehostInfo.id : null;
|
const localId = rehostInfo ? rehostInfo.id : null;
|
||||||
const effectiveTime = (localId && rehostInfo?.stamp) ? Number(rehostInfo.stamp) : p.time;
|
const effectiveTime = (localId && rehostInfo?.stamp) ? Number(rehostInfo.stamp) : p.time;
|
||||||
const isoStr = new Date(effectiveTime * 1000).toISOString();
|
const isoStr = new Date(effectiveTime * 1000).toISOString();
|
||||||
|
const postReplies = replyMap.get(p.no) || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
no: p.no,
|
no: p.no,
|
||||||
@@ -181,7 +221,9 @@ export default (router, tpl) => {
|
|||||||
local_id: localId,
|
local_id: localId,
|
||||||
rehosted: !!localId,
|
rehosted: !!localId,
|
||||||
user_has_favorited: localId ? userFavSet.has(Number(localId)) : false,
|
user_has_favorited: localId ? userFavSet.has(Number(localId)) : false,
|
||||||
is_onara_active: targetPostNo ? p.no === targetPostNo : false
|
is_onara_active: targetPostNo ? p.no === targetPostNo : false,
|
||||||
|
replies: postReplies,
|
||||||
|
replies_count: postReplies.length
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+473
-146
@@ -56,7 +56,7 @@
|
|||||||
<!-- Posts Stream -->
|
<!-- Posts Stream -->
|
||||||
<div class="chan-posts-stream" id="chan-posts-stream">
|
<div class="chan-posts-stream" id="chan-posts-stream">
|
||||||
@each(posts as p)
|
@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 -->
|
<!-- Post Header -->
|
||||||
<div class="chan-post-header">
|
<div class="chan-post-header">
|
||||||
@@ -74,6 +74,16 @@
|
|||||||
@if(p.sub && p.no !== tid)
|
@if(p.sub && p.no !== tid)
|
||||||
<span class="chan-reply-sub">{{ p.sub }}</span>
|
<span class="chan-reply-sub">{{ p.sub }}</span>
|
||||||
@endif
|
@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 }}">>>{{ rNo }}</a>
|
||||||
|
@endeach
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
@if(p.has_media)
|
@if(p.has_media)
|
||||||
<div class="chan-post-media-badge">
|
<div class="chan-post-media-badge">
|
||||||
<i class="fa-solid fa-paperclip"></i>
|
<i class="fa-solid fa-paperclip"></i>
|
||||||
@@ -87,7 +97,7 @@
|
|||||||
@if(p.has_media)
|
@if(p.has_media)
|
||||||
<div class="chan-media-wrapper">
|
<div class="chan-media-wrapper">
|
||||||
<div class="chan-thumb-box">
|
<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)
|
@if(p.thumb)
|
||||||
<img src="{{ p.thumb }}" alt="Media #{{ p.no }}" class="chan-thumb-img" loading="lazy" />
|
<img src="{{ p.thumb }}" alt="Media #{{ p.no }}" class="chan-thumb-img" loading="lazy" />
|
||||||
@else
|
@else
|
||||||
@@ -121,7 +131,7 @@
|
|||||||
<a href="/{{ p.local_id }}" class="chan-rehost-btn rehosted" target="_blank">
|
<a href="/{{ p.local_id }}" class="chan-rehost-btn rehosted" target="_blank">
|
||||||
<i class="fa-solid fa-check"></i> Rehosted #{{ p.local_id }}
|
<i class="fa-solid fa-check"></i> Rehosted #{{ p.local_id }}
|
||||||
</a>
|
</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>
|
<i class="fa-solid fa-pen"></i>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -293,7 +303,8 @@
|
|||||||
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
|
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 14px 18px;
|
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 {
|
.chan-post-card.is-rehosted {
|
||||||
border-color: rgba(74, 222, 128, 0.3);
|
border-color: rgba(74, 222, 128, 0.3);
|
||||||
@@ -303,9 +314,64 @@
|
|||||||
border-left: 4px solid #4ade80;
|
border-left: 4px solid #4ade80;
|
||||||
background: rgba(30, 30, 30, 0.95);
|
background: rgba(30, 30, 30, 0.95);
|
||||||
}
|
}
|
||||||
.chan-post-card:target {
|
.chan-post-card.is-reply {
|
||||||
border-color: #4ade80;
|
background: rgba(24, 24, 28, 0.8);
|
||||||
background: rgba(74, 222, 128, 0.05);
|
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 {
|
.chan-post-rehosted-pill {
|
||||||
@@ -389,85 +455,151 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chan-post-body {
|
/* Post Content Layout (Media + Comment) */
|
||||||
|
.chan-post-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 18px;
|
gap: 16px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.chan-post-body {
|
.chan-post-content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Media Box */
|
/* Media Wrapper */
|
||||||
.chan-media-box {
|
.chan-media-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 200px;
|
width: 170px;
|
||||||
flex-shrink: 0;
|
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;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 16 / 10;
|
aspect-ratio: 16 / 10;
|
||||||
background: #000;
|
background: #0d0d0d;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
.chan-media-link {
|
.chan-thumb-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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(0, 0, 0, 0.35);
|
width: 100%;
|
||||||
opacity: 0;
|
height: 100%;
|
||||||
transition: opacity 0.2s;
|
position: relative;
|
||||||
color: #fff;
|
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;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
font-size: 0.75rem;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.chan-media-filename {
|
.chan-filename-row {
|
||||||
white-space: nowrap;
|
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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
transition: color 0.15s ease;
|
||||||
}
|
}
|
||||||
.chan-media-filename a {
|
.chan-file-download:hover {
|
||||||
color: #aaa;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.chan-media-filename a:hover {
|
|
||||||
color: #4ade80;
|
color: #4ade80;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.chan-media-meta {
|
.chan-file-download i {
|
||||||
color: #777;
|
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;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rehost button */
|
/* Rehost button */
|
||||||
@@ -510,6 +642,18 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
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 {
|
.chan-card-fav-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -554,16 +698,66 @@
|
|||||||
.chan-post-comment .quote {
|
.chan-post-comment .quote {
|
||||||
color: #789922;
|
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-ref,
|
||||||
.chan-post-comment .quotelink {
|
.chan-post-comment .quotelink {
|
||||||
color: #d00;
|
color: #f87171;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: color 0.15s ease;
|
||||||
}
|
}
|
||||||
.chan-ref:hover,
|
.chan-ref:hover,
|
||||||
.chan-post-comment .quotelink:hover {
|
.chan-post-comment .quotelink:hover {
|
||||||
color: #ff3333;
|
color: #ff4d4d;
|
||||||
}
|
}
|
||||||
.chan-post-comment s,
|
.chan-post-comment s,
|
||||||
.chan-post-comment .spoiler {
|
.chan-post-comment .spoiler {
|
||||||
@@ -584,15 +778,41 @@
|
|||||||
.chan-quote-preview-popup {
|
.chan-quote-preview-popup {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 99999;
|
z-index: 99999;
|
||||||
max-width: 480px;
|
width: max-content;
|
||||||
background: #1e1e1e;
|
max-width: 520px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(22, 22, 24, 0.98);
|
||||||
border: 1px solid #4ade80;
|
border: 1px solid #4ade80;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
padding: 12px 14px;
|
padding: 12px 16px;
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
|
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.8), 0 0 20px rgba(74, 222, 128, 0.2);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
font-size: 0.85rem;
|
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>
|
</style>
|
||||||
|
|
||||||
@@ -672,7 +892,7 @@
|
|||||||
btn.outerHTML = '<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">' +
|
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">' +
|
'<a href="/' + data.item_id + '" class="chan-rehost-btn rehosted" target="_blank">' +
|
||||||
'<i class="fa-solid fa-check"></i> Rehosted #' + data.item_id + '</a>' +
|
'<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>';
|
'<i class="fa-solid fa-pen"></i></button></span>';
|
||||||
|
|
||||||
const postCard = document.getElementById('p' + postNo) || btn.closest('.chan-post-card');
|
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!');
|
window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!');
|
||||||
}
|
}
|
||||||
if (typeof window.openChanRehostModal === 'function') {
|
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 {
|
} else {
|
||||||
btn.classList.remove('loading');
|
btn.classList.remove('loading');
|
||||||
@@ -713,7 +935,7 @@
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
const itemId = editBtn.dataset.itemId;
|
const itemId = editBtn.dataset.itemId;
|
||||||
if (itemId && typeof window.openChanRehostModal === 'function') {
|
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;
|
let localId = favBtn.dataset.localId;
|
||||||
const mediaUrl = favBtn.dataset.url;
|
const mediaUrl = favBtn.dataset.url;
|
||||||
|
|
||||||
favBtn.classList.add('loading');
|
const wasFaved = favBtn.classList.contains('favorited');
|
||||||
const origHtml = favBtn.innerHTML;
|
const isNowFav = !wasFaved;
|
||||||
favBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
|
||||||
|
|
||||||
try {
|
// 1. Instantly apply client UI
|
||||||
// If not yet rehosted, auto-rehost first
|
favBtn.classList.toggle('favorited', isNowFav);
|
||||||
if (!localId && mediaUrl) {
|
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 csrfToken = window.f0ckSession?.csrf_token || '';
|
||||||
const rehostResp = await fetch('/api/v2/scroller/rehost', {
|
const favResp = await fetch('/api/v2/togglefav', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/json',
|
||||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: JSON.stringify({
|
||||||
url: mediaUrl,
|
postid: Number(localId),
|
||||||
original_filename: favBtn.dataset.filename || '',
|
chan_url: mediaUrl,
|
||||||
width: favBtn.dataset.width || '',
|
action: isNowFav ? 'add' : 'delete',
|
||||||
height: favBtn.dataset.height || ''
|
favorited: isNowFav
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const rehostData = await rehostResp.json();
|
const favData = await favResp.json();
|
||||||
if (rehostData.success && rehostData.item_id) {
|
|
||||||
localId = rehostData.item_id;
|
|
||||||
favBtn.dataset.localId = localId;
|
|
||||||
|
|
||||||
// Update rehost box
|
if (favData.success) {
|
||||||
const rehostBox = document.getElementById('rehost-box-' + postNo);
|
const finalIsFav = (favData.favorited !== undefined) ? !!favData.favorited : isNowFav;
|
||||||
if (rehostBox) {
|
favBtn.classList.toggle('favorited', finalIsFav);
|
||||||
const rehostBtn = rehostBox.querySelector('.chan-rehost-btn');
|
favBtn.innerHTML = '<i class="fa-' + (finalIsFav ? 'solid' : 'regular') + ' fa-heart"></i>';
|
||||||
if (rehostBtn) {
|
favBtn.title = finalIsFav ? 'Remove from favorites' : 'Favorite';
|
||||||
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'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} 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
|
// Flash highlight a post card
|
||||||
const csrfToken = window.f0ckSession?.csrf_token || '';
|
const highlightPost = (el) => {
|
||||||
const favResp = await fetch('/api/v2/togglefav', {
|
if (!el) return;
|
||||||
method: 'POST',
|
el.classList.remove('chan-flash-highlight');
|
||||||
headers: {
|
void el.offsetWidth;
|
||||||
'Content-Type': 'application/json',
|
el.classList.add('chan-flash-highlight');
|
||||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
setTimeout(() => {
|
||||||
},
|
el.classList.remove('chan-flash-highlight');
|
||||||
body: JSON.stringify({ postid: Number(localId), chan_url: mediaUrl })
|
}, 2000);
|
||||||
});
|
};
|
||||||
const favData = await favResp.json();
|
|
||||||
favBtn.classList.remove('loading');
|
|
||||||
|
|
||||||
if (favData.success) {
|
// Quote click navigation (smooth jump + flash highlight)
|
||||||
const isNowFav = !!favData.favorited;
|
document.addEventListener('click', (e) => {
|
||||||
favBtn.classList.toggle('favorited', isNowFav);
|
const ref = e.target.closest('.chan-ref, .quotelink');
|
||||||
favBtn.innerHTML = '<i class="fa-' + (isNowFav ? 'solid' : 'regular') + ' fa-heart"></i>';
|
if (!ref) return;
|
||||||
favBtn.title = isNowFav ? 'Remove from favorites' : 'Favorite';
|
|
||||||
if (typeof window.flashMessage === 'function') {
|
const targetPostNo = ref.dataset.post || ref.getAttribute('href')?.replace(/^#p?/, '');
|
||||||
window.flashMessage(isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES');
|
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 {
|
try {
|
||||||
favBtn.innerHTML = origHtml;
|
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
if (typeof window.flashMessage === 'function') {
|
} catch (_) {
|
||||||
window.flashMessage(favData.msg || 'Failed to update favorite', 3000, 'error');
|
el.scrollIntoView();
|
||||||
}
|
}
|
||||||
}
|
highlightPost(el);
|
||||||
} 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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -860,13 +1157,23 @@
|
|||||||
previewEl.style.display = 'block';
|
previewEl.style.display = 'block';
|
||||||
|
|
||||||
const rect = ref.getBoundingClientRect();
|
const rect = ref.getBoundingClientRect();
|
||||||
let top = rect.bottom + 8;
|
const popHeight = previewEl.offsetHeight || 150;
|
||||||
let left = rect.left;
|
const popWidth = previewEl.offsetWidth || 380;
|
||||||
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';
|
let top = rect.bottom + 8;
|
||||||
previewEl.style.left = Math.max(10, left) + 'px';
|
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) => {
|
document.addEventListener('mouseout', (e) => {
|
||||||
@@ -875,6 +1182,26 @@
|
|||||||
previewEl.style.display = 'none';
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user