bvcx
This commit is contained in:
@@ -926,6 +926,40 @@
|
||||
}
|
||||
}, { hideReason: true, confirmText: 'Apply Role', unsafeContent: true });
|
||||
}
|
||||
|
||||
async function adminSetGroups(btn) {
|
||||
var id = btn.dataset.id;
|
||||
var userName = btn.dataset.name;
|
||||
var currentGroups = (btn.dataset.groups || '').split(',').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
|
||||
var is4chan = currentGroups.indexOf('4chan') !== -1;
|
||||
|
||||
var hint = 'Manage groups for <strong>' + escHTML(userName) + '</strong>:<br><br>' +
|
||||
'<label style="display:flex;align-items:center;gap:10px;cursor:pointer;padding:8px 12px;background:rgba(255,255,255,0.05);border-radius:6px;border:1px solid rgba(255,255,255,0.1);">' +
|
||||
'<input type="checkbox" id="group-4chan-checkbox" value="4chan"' + (is4chan ? ' checked' : '') + ' style="width:18px;height:18px;accent-color:#4ade80;cursor:pointer;">' +
|
||||
'<span><strong style="color:#4ade80;">🍀 4chan</strong> — Grants access to the 4chan catalogue, thread viewer, and rehosting</span>' +
|
||||
'</label>';
|
||||
|
||||
ModAction.confirm('Manage Groups', hint, async () => {
|
||||
var chk = document.getElementById('group-4chan-checkbox') || document.querySelector('#mod-action-content input[type="checkbox"]');
|
||||
var newGroups = [];
|
||||
if (chk && chk.checked) newGroups.push('4chan');
|
||||
|
||||
var res = await fetch('/api/v2/admin/users/set-groups', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||
body: JSON.stringify({ user_id: id, groups: newGroups })
|
||||
});
|
||||
var data = await res.json();
|
||||
if (data.success) {
|
||||
showFlash(data.msg, 'success');
|
||||
btn.dataset.groups = newGroups.join(',');
|
||||
var labelEl = document.querySelector('.groups-label-' + id);
|
||||
if (labelEl) labelEl.textContent = newGroups.indexOf('4chan') !== -1 ? '4chan' : 'Groups';
|
||||
} else {
|
||||
throw new Error(data.msg || 'Failed to update groups');
|
||||
}
|
||||
}, { hideReason: true, confirmText: 'Save Groups', unsafeContent: true });
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" onclick="adminSetPassword(this)" class="btn-modern btn-pw" style="background: rgba(var(--accent-rgb, 0, 150, 255), 0.1); color: var(--accent, #0096ff); border: 1px solid rgba(var(--accent-rgb, 0, 150, 255), 0.2);">Set PW</button>
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" data-locked="{{ u.force_comment_display_mode }}" data-mode="{{ u.comment_display_mode }}" onclick="adminLockLayout(this)" class="btn-modern" style="background: rgba(255, 100, 0, 0.1); color: #ff6400; border: 1px solid rgba(255, 100, 0, 0.2);" title="{{ u.force_comment_display_mode ? 'Unlock Layout' : 'Lock Layout' }}"><i class="fa fa-{{ u.force_comment_display_mode ? 'lock-open' : 'lock' }}"></i> {{ u.force_comment_display_mode ? 'Unlock' : 'Lock' }}</button>
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" data-role="{{ u.admin ? 'admin' : (u.is_moderator ? 'mod' : 'user') }}" onclick="adminSetRole(this)" class="btn-modern btn-role" id="role-btn-{{ u.id }}" title="Change user role"><i class="fa fa-shield-halved"></i> <span class="role-label-{{ u.id }}">{{ u.admin ? 'Admin' : (u.is_moderator ? 'Mod' : 'User') }}</span></button>
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" data-groups="{{ (u.groups || []).join(',') }}" onclick="adminSetGroups(this)" class="btn-modern btn-groups" id="groups-btn-{{ u.id }}" style="background: rgba(74, 222, 128, 0.1); color: #4ade80; border: 1px solid rgba(74, 222, 128, 0.2);" title="Manage user groups"><i class="fa-solid fa-clover"></i> <span class="groups-label-{{ u.id }}">{{ (u.groups && u.groups.includes('4chan')) ? '4chan' : 'Groups' }}</span></button>
|
||||
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" onclick="adminDeleteUser(this)" class="btn-modern btn-delete" style="background: rgba(217, 83, 79, 0.1); color: #d9534f; border: 1px solid rgba(217, 83, 79, 0.2);">Delete</button>
|
||||
@elseif(u.login === 'deleted_user')
|
||||
<span style="font-size: 0.8rem; color: #666; font-style: italic; padding: 5px 10px;">Protected System Account</span>
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
@include(snippets/header)
|
||||
<div class="pagewrapper">
|
||||
<div id="main" class="chan-page chan-catalogue-page">
|
||||
<div class="chan-header-bar">
|
||||
<div class="chan-brand-title">
|
||||
<span class="chan-clover-icon"><i class="fa-solid fa-clover"></i></span>
|
||||
<h1>/{{ board }}/ <span class="chan-subtitle">Catalogue</span></h1>
|
||||
<span class="chan-thread-count-badge">{{ threads.length }} threads</span>
|
||||
</div>
|
||||
|
||||
<!-- Quick board selector -->
|
||||
<div class="chan-board-nav">
|
||||
@each(common_boards as b)
|
||||
<a href="/4/{{ b.id }}/catalogue" class="chan-board-pill {{ b.id === board ? 'active' : '' }}" title="{{ b.name }}">
|
||||
/{{ b.id }}/
|
||||
</a>
|
||||
@endeach
|
||||
<form class="chan-board-custom-form" onsubmit="event.preventDefault(); const b = document.getElementById('custom-board-input').value.trim().toLowerCase(); if(b) window.location.href='/4/' + b + '/catalogue';">
|
||||
<input type="text" id="custom-board-input" placeholder="board…" maxlength="12" class="chan-custom-board-input" />
|
||||
<button type="submit" class="chan-custom-board-btn" title="Go to board"><i class="fa-solid fa-arrow-right"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Controls: Filter & Sort -->
|
||||
<div class="chan-controls-bar">
|
||||
<div class="chan-search-wrap">
|
||||
<i class="fa-solid fa-magnifying-glass"></i>
|
||||
<input type="text" id="chan-catalog-search" class="chan-search-input" placeholder="Filter threads by subject, text, or post #…" autocomplete="off" />
|
||||
<button type="button" id="chan-search-clear" style="display:none;" title="Clear search"><i class="fa-solid fa-xmark"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="chan-sort-wrap">
|
||||
<label for="chan-sort-select"><i class="fa-solid fa-arrow-down-wide-short"></i> Sort:</label>
|
||||
<select id="chan-sort-select" class="chan-select">
|
||||
<option value="bump">Bump Order</option>
|
||||
<option value="replies">Most Replies</option>
|
||||
<option value="images">Most Images</option>
|
||||
<option value="date-new">Newest First</option>
|
||||
<option value="date-old">Oldest First</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Catalogue Thread Grid -->
|
||||
<div class="chan-catalogue-grid" id="chan-catalogue-grid">
|
||||
@each(threads as t)
|
||||
<div class="chan-thread-card" data-no="{{ t.no }}" data-replies="{{ t.replies }}" data-images="{{ t.images }}" data-time="{{ t.time }}" data-sticky="{{ t.sticky ? 1 : 0 }}">
|
||||
<a href="/4/{{ board }}/{{ t.no }}" class="chan-thread-thumb-link">
|
||||
@if(t.thumb)
|
||||
<img src="{{ t.thumb }}" alt="Thread #{{ t.no }}" loading="lazy" class="chan-thread-thumb" onerror="this.style.opacity='0.2';" />
|
||||
@else
|
||||
<div class="chan-thread-thumb-empty"><i class="fa-solid fa-file"></i></div>
|
||||
@endif
|
||||
|
||||
<div class="chan-card-badges">
|
||||
@if(t.sticky)
|
||||
<span class="chan-card-badge chan-badge-sticky" title="Sticky"><i class="fa-solid fa-thumbtack"></i></span>
|
||||
@endif
|
||||
@if(t.closed)
|
||||
<span class="chan-card-badge chan-badge-closed" title="Closed"><i class="fa-solid fa-lock"></i></span>
|
||||
@endif
|
||||
@if(t.local_id)
|
||||
<span class="chan-card-badge chan-badge-rehosted" title="OP Rehosted on f0ckm as Post #{{ t.local_id }}" style="background: rgba(16, 185, 129, 0.9); color: #fff; font-weight: 700;"><i class="fa-solid fa-check"></i> #{{ t.local_id }}</span>
|
||||
@endif
|
||||
<span class="chan-card-badge chan-badge-stats">
|
||||
<span title="Replies"><i class="fa-solid fa-comment"></i> {{ t.replies }}</span>
|
||||
<span title="Images"><i class="fa-solid fa-image"></i> {{ t.images }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="chan-thread-info">
|
||||
<a href="/4/{{ board }}/{{ t.no }}" class="chan-thread-title">
|
||||
@if(t.sub)
|
||||
<strong class="chan-subject">{{ t.sub }}</strong>
|
||||
@else
|
||||
<span class="chan-no-sub">No. {{ t.no }}</span>
|
||||
@endif
|
||||
</a>
|
||||
@if(t.com)
|
||||
<p class="chan-teaser">{{ t.com }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endeach
|
||||
</div>
|
||||
|
||||
<div id="chan-no-results" class="chan-empty-state" style="display: none;">
|
||||
<i class="fa-solid fa-search"></i>
|
||||
<p>No threads match your search filter.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chan-catalogue-page {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 15px 60px;
|
||||
}
|
||||
.chan-header-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
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: 18px 20px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.chan-brand-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chan-clover-icon {
|
||||
font-size: 1.8rem;
|
||||
color: #4ade80;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.chan-brand-title h1 {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
color: var(--text-main, #fff);
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.chan-subtitle {
|
||||
font-weight: 400;
|
||||
opacity: 0.6;
|
||||
font-size: 1.2rem;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.chan-thread-count-badge {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
background: rgba(74, 222, 128, 0.15);
|
||||
color: #4ade80;
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
|
||||
/* Board nav */
|
||||
.chan-board-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chan-board-pill {
|
||||
padding: 4px 12px;
|
||||
border-radius: 6px;
|
||||
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-board-pill:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.chan-board-pill.active {
|
||||
background: #4ade80;
|
||||
color: #111;
|
||||
border-color: #4ade80;
|
||||
font-weight: 700;
|
||||
}
|
||||
.chan-board-custom-form {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.chan-custom-board-input {
|
||||
width: 75px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
outline: none;
|
||||
}
|
||||
.chan-custom-board-input:focus {
|
||||
border-color: #4ade80;
|
||||
}
|
||||
.chan-custom-board-btn {
|
||||
background: rgba(74, 222, 128, 0.15);
|
||||
color: #4ade80;
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.8rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chan-custom-board-btn:hover {
|
||||
background: #4ade80;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.chan-controls-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding-top: 12px;
|
||||
}
|
||||
.chan-search-wrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.chan-search-wrap i.fa-magnifying-glass {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: #777;
|
||||
pointer-events: none;
|
||||
}
|
||||
.chan-search-input {
|
||||
width: 100%;
|
||||
padding: 8px 34px 8px 34px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chan-search-input:focus {
|
||||
border-color: #4ade80;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
#chan-search-clear {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
.chan-sort-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #aaa;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.chan-select {
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chan-select option {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Grid */
|
||||
.chan-catalogue-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.chan-thread-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;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.chan-thread-card:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: rgba(74, 222, 128, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.chan-thread-thumb-link {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 10;
|
||||
background: #0d0d0d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
}
|
||||
.chan-thread-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.chan-thread-card:hover .chan-thread-thumb {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
.chan-thread-thumb-empty {
|
||||
font-size: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
.chan-card-badges {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
}
|
||||
.chan-card-badge {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #eee;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.chan-badge-sticky {
|
||||
color: #4ade80;
|
||||
border-color: rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
.chan-badge-closed {
|
||||
color: #ff6b6b;
|
||||
border-color: rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
.chan-badge-stats {
|
||||
margin-left: auto;
|
||||
}
|
||||
.chan-thread-info {
|
||||
padding: 10px 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
}
|
||||
.chan-thread-title {
|
||||
text-decoration: none;
|
||||
color: #fff;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chan-subject {
|
||||
color: #4ade80;
|
||||
font-weight: 700;
|
||||
}
|
||||
.chan-no-sub {
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.chan-teaser {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
color: #888;
|
||||
line-height: 1.35;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.chan-empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #777;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.chan-empty-state i {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.4;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const searchInput = document.getElementById('chan-catalog-search');
|
||||
const searchClear = document.getElementById('chan-search-clear');
|
||||
const sortSelect = document.getElementById('chan-sort-select');
|
||||
const grid = document.getElementById('chan-catalogue-grid');
|
||||
const noResults = document.getElementById('chan-no-results');
|
||||
if (!grid) return;
|
||||
|
||||
const originalCards = Array.from(grid.querySelectorAll('.chan-thread-card'));
|
||||
|
||||
function filterAndSort() {
|
||||
const q = (searchInput?.value || '').trim().toLowerCase();
|
||||
const sort = sortSelect?.value || 'bump';
|
||||
|
||||
if (searchClear) {
|
||||
searchClear.style.display = q ? 'block' : 'none';
|
||||
}
|
||||
|
||||
let visibleCards = originalCards.filter(card => {
|
||||
if (!q) return true;
|
||||
const no = card.dataset.no || '';
|
||||
const text = card.textContent.toLowerCase();
|
||||
return no.includes(q) || text.includes(q);
|
||||
});
|
||||
|
||||
// Sort
|
||||
visibleCards.sort((a, b) => {
|
||||
const stickyA = parseInt(a.dataset.sticky || 0, 10);
|
||||
const stickyB = parseInt(b.dataset.sticky || 0, 10);
|
||||
if (stickyA !== stickyB) return stickyB - stickyA; // Stickies always first
|
||||
|
||||
if (sort === 'replies') {
|
||||
return parseInt(b.dataset.replies || 0, 10) - parseInt(a.dataset.replies || 0, 10);
|
||||
} else if (sort === 'images') {
|
||||
return parseInt(b.dataset.images || 0, 10) - parseInt(a.dataset.images || 0, 10);
|
||||
} else if (sort === 'date-new') {
|
||||
return parseInt(b.dataset.time || 0, 10) - parseInt(a.dataset.time || 0, 10);
|
||||
} else if (sort === 'date-old') {
|
||||
return parseInt(a.dataset.time || 0, 10) - parseInt(b.dataset.time || 0, 10);
|
||||
}
|
||||
// Bump order: preserve original order
|
||||
return originalCards.indexOf(a) - originalCards.indexOf(b);
|
||||
});
|
||||
|
||||
originalCards.forEach(c => c.style.display = 'none');
|
||||
visibleCards.forEach(c => {
|
||||
c.style.display = 'flex';
|
||||
grid.appendChild(c);
|
||||
});
|
||||
|
||||
if (noResults) {
|
||||
noResults.style.display = visibleCards.length === 0 ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', filterAndSort);
|
||||
}
|
||||
if (searchClear) {
|
||||
searchClear.addEventListener('click', () => {
|
||||
searchInput.value = '';
|
||||
filterAndSort();
|
||||
searchInput.focus();
|
||||
});
|
||||
}
|
||||
if (sortSelect) {
|
||||
sortSelect.addEventListener('change', filterAndSort);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
@include(snippets/footer)
|
||||
@@ -0,0 +1,855 @@
|
||||
@include(snippets/header)
|
||||
<div class="pagewrapper">
|
||||
<div id="main" class="chan-page chan-thread-page">
|
||||
|
||||
<!-- Top Header Navigation -->
|
||||
<div class="chan-header-bar chan-thread-header">
|
||||
<div class="chan-thread-nav-row">
|
||||
<a href="/4/{{ board }}/catalogue" class="chan-back-btn">
|
||||
<i class="fa-solid fa-arrow-left"></i> /{{ board }}/ Catalogue
|
||||
</a>
|
||||
<div class="chan-thread-actions">
|
||||
@if(first_media_no)
|
||||
<a href="/4/{{ board }}/{{ tid }}/{{ first_media_no }}" class="chan-action-btn primary" title="Open thread media in player view">
|
||||
<i class="fa-solid fa-play"></i> Player View
|
||||
</a>
|
||||
@endif
|
||||
<a href="/scroller#{{ board }}/{{ tid }}" class="chan-action-btn" title="Open in vertical video scroller">
|
||||
<i class="fa-solid fa-mobile-screen"></i> Scroller
|
||||
</a>
|
||||
<a href="https://boards.4chan.org/{{ board }}/thread/{{ tid }}" target="_blank" rel="noopener noreferrer" class="chan-action-btn" title="View original thread on 4chan">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i> 4chan.org
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chan-thread-title-row">
|
||||
<span class="chan-clover-icon"><i class="fa-solid fa-clover"></i></span>
|
||||
<div class="chan-thread-title-info">
|
||||
<h2>
|
||||
@if(op.sub)
|
||||
{{ op.sub }}
|
||||
@else
|
||||
Thread #{{ tid }}
|
||||
@endif
|
||||
</h2>
|
||||
<div class="chan-thread-meta">
|
||||
<span class="chan-meta-item"><i class="fa-solid fa-comments"></i> {{ posts.length }} posts</span>
|
||||
<span class="chan-meta-item"><i class="fa-solid fa-image"></i> {{ media_count }} media files</span>
|
||||
@if(rehosts_count > 0)
|
||||
<span class="chan-meta-item" style="color: #4ade80;"><i class="fa-solid fa-check"></i> {{ rehosts_count }} rehosted</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Toggle: All vs Media Only vs Rehosted -->
|
||||
<div class="chan-filter-toggle">
|
||||
<button type="button" class="chan-toggle-btn active" id="filter-all-btn">All Posts ({{ posts.length }})</button>
|
||||
<button type="button" class="chan-toggle-btn" id="filter-media-btn">Media Only ({{ media_count }})</button>
|
||||
@if(rehosts_count > 0)
|
||||
<button type="button" class="chan-toggle-btn" id="filter-rehosted-btn" style="color: #4ade80;"><i class="fa-solid fa-check"></i> Rehosted ({{ rehosts_count }})</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts Stream -->
|
||||
<div class="chan-posts-stream" id="chan-posts-stream">
|
||||
@each(posts as p)
|
||||
<article class="chan-post-card {{ p.no === tid ? 'is-op' : 'is-reply' }} {{ p.has_media ? 'has-media' : 'no-media' }} {{ p.local_id ? 'is-rehosted' : '' }}" id="p{{ p.no }}" data-no="{{ p.no }}" data-has-media="{{ p.has_media ? 1 : 0 }}" data-is-rehosted="{{ p.local_id ? 1 : 0 }}">
|
||||
|
||||
<!-- Post Header -->
|
||||
<div class="chan-post-header">
|
||||
<span class="chan-post-name">{{ p.name }}</span>
|
||||
@if(p.trip)
|
||||
<span class="chan-post-trip">{{ p.trip }}</span>
|
||||
@endif
|
||||
<time class="chan-post-time" title="{{ p.date_str }}">{{ p.timeago }}</time>
|
||||
<a href="#p{{ p.no }}" class="chan-post-no">No. {{ p.no }}</a>
|
||||
@if(p.local_id)
|
||||
<a href="/{{ p.local_id }}" target="_blank" class="chan-post-rehosted-pill" title="Already rehosted on f0ckm as Post #{{ p.local_id }}">
|
||||
<i class="fa-solid fa-check"></i> #{{ p.local_id }}
|
||||
</a>
|
||||
@endif
|
||||
@if(p.sub && p.no !== tid)
|
||||
<span class="chan-reply-sub">{{ p.sub }}</span>
|
||||
@endif
|
||||
@if(p.has_media)
|
||||
<div class="chan-post-media-badge">
|
||||
<i class="fa-solid fa-paperclip"></i>
|
||||
<span>{{ p.ext.toUpperCase().replace('.', '') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Post Content (Media + Comment) -->
|
||||
<div class="chan-post-content">
|
||||
@if(p.has_media)
|
||||
<div class="chan-media-wrapper">
|
||||
<div class="chan-thumb-box">
|
||||
<a href="/4/{{ board }}/{{ tid }}/{{ p.no }}" class="chan-thumb-link" title="Open in player view">
|
||||
@if(p.thumb)
|
||||
<img src="{{ p.thumb }}" alt="Media #{{ p.no }}" class="chan-thumb-img" loading="lazy" />
|
||||
@else
|
||||
<div class="chan-no-thumb"><i class="fa-solid fa-file"></i></div>
|
||||
@endif
|
||||
@if(p.is_video)
|
||||
<span class="chan-video-indicator"><i class="fa-solid fa-play"></i></span>
|
||||
@endif
|
||||
@if(p.local_id)
|
||||
<span class="chan-thumb-rehost-badge" title="Rehosted on f0ckm as Post #{{ p.local_id }}"><i class="fa-solid fa-check"></i> #{{ p.local_id }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="chan-file-info">
|
||||
<div class="chan-filename-row">
|
||||
<a href="{{ p.external_media_url }}" target="_blank" rel="noopener noreferrer" class="chan-file-download" title="Open full media in new tab">
|
||||
<i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
<span class="chan-file-title">{{ p.filename || ('File ' + p.tim + p.ext) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="chan-file-meta-row">
|
||||
@if(p.fsize)<span>{{ p.fsize }}</span>@endif
|
||||
@if(p.w && p.h)<span>{{ p.w }}×{{ p.h }}</span>@endif
|
||||
</div>
|
||||
|
||||
<!-- Rehost & Favorite actions -->
|
||||
<div class="chan-rehost-container" id="rehost-box-{{ p.no }}">
|
||||
@if(p.local_id)
|
||||
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;flex:1;">
|
||||
<a href="/{{ p.local_id }}" class="chan-rehost-btn rehosted" target="_blank">
|
||||
<i class="fa-solid fa-check"></i> Rehosted #{{ p.local_id }}
|
||||
</a>
|
||||
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ p.local_id }}" title="Edit rating, tags & comment" style="padding:4px 7px;border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;font-size:0.75rem;">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
</span>
|
||||
@else
|
||||
<button type="button" class="chan-rehost-btn" data-url="{{ p.external_media_url }}" data-board="{{ board }}" data-tid="{{ tid }}" data-post="{{ p.no }}" data-filename="{{ p.filename || '' }}" data-width="{{ p.w || '' }}" data-height="{{ p.h || '' }}">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i> Rehost
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="chan-card-fav-btn {{ p.user_has_favorited ? 'favorited' : '' }}" data-post="{{ p.no }}" data-local-id="{{ p.local_id || '' }}" data-url="{{ p.external_media_url }}" data-board="{{ board }}" data-tid="{{ tid }}" data-filename="{{ p.filename || '' }}" data-width="{{ p.w || '' }}" data-height="{{ p.h || '' }}" title="{{ p.user_has_favorited ? 'Remove from favorites' : 'Favorite' }}">
|
||||
<i class="fa-{{ p.user_has_favorited ? 'solid' : 'regular' }} fa-heart"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Post Comment -->
|
||||
@if(p.com_raw)
|
||||
<div class="chan-post-comment">
|
||||
{{ p.com_formatted }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</article>
|
||||
@endeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.chan-thread-page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 15px 80px;
|
||||
}
|
||||
.chan-thread-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 25px;
|
||||
background: var(--bg-card, rgba(25, 25, 25, 0.85));
|
||||
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.chan-thread-nav-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.chan-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-main, #fff);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.88rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chan-back-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.chan-thread-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chan-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-muted, #aaa);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chan-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
}
|
||||
.chan-action-btn.primary {
|
||||
background: rgba(74, 222, 128, 0.15);
|
||||
color: #4ade80;
|
||||
border-color: rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
.chan-action-btn.primary:hover {
|
||||
background: #4ade80;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.chan-thread-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding-top: 14px;
|
||||
}
|
||||
.chan-thread-title-info h2 {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 4px;
|
||||
color: #fff;
|
||||
}
|
||||
.chan-thread-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 0.82rem;
|
||||
color: #888;
|
||||
}
|
||||
.chan-meta-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Filter toggle */
|
||||
.chan-filter-toggle {
|
||||
display: inline-flex;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
gap: 3px;
|
||||
}
|
||||
.chan-toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chan-toggle-btn.active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Posts stream */
|
||||
.chan-posts-stream {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.chan-post-card {
|
||||
background: var(--bg-card, rgba(25, 25, 25, 0.85));
|
||||
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
}
|
||||
.chan-post-card.is-rehosted {
|
||||
border-color: rgba(74, 222, 128, 0.3);
|
||||
background: rgba(74, 222, 128, 0.03);
|
||||
}
|
||||
.chan-post-card.is-op {
|
||||
border-left: 4px solid #4ade80;
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
}
|
||||
.chan-post-card:target {
|
||||
border-color: #4ade80;
|
||||
background: rgba(74, 222, 128, 0.05);
|
||||
}
|
||||
|
||||
.chan-post-rehosted-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
color: #4ade80;
|
||||
border: 1px solid rgba(74, 222, 128, 0.4);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.chan-post-rehosted-pill:hover {
|
||||
background: #4ade80;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.chan-thumb-rehost-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
background: rgba(16, 185, 129, 0.92);
|
||||
color: #fff;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
backdrop-filter: blur(4px);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.chan-post-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.chan-post-name {
|
||||
font-weight: 700;
|
||||
color: #10b981;
|
||||
}
|
||||
.chan-post-trip {
|
||||
color: #888;
|
||||
font-weight: 600;
|
||||
}
|
||||
.chan-post-time {
|
||||
color: #777;
|
||||
}
|
||||
.chan-post-no {
|
||||
color: #aaa;
|
||||
text-decoration: none;
|
||||
font-family: monospace;
|
||||
}
|
||||
.chan-post-no:hover {
|
||||
color: #4ade80;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.chan-reply-sub {
|
||||
color: #fbbf24;
|
||||
font-weight: 600;
|
||||
}
|
||||
.chan-post-media-badge {
|
||||
margin-left: auto;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chan-post-body {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.chan-post-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Media Box */
|
||||
.chan-media-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chan-media-thumb-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 10;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.chan-media-link {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.chan-media-thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.chan-media-thumb-wrap:hover .chan-media-thumb {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.chan-media-play-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
color: #fff;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.chan-media-thumb-wrap:hover .chan-media-play-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
.chan-media-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.chan-media-filename {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.chan-media-filename a {
|
||||
color: #aaa;
|
||||
text-decoration: none;
|
||||
}
|
||||
.chan-media-filename a:hover {
|
||||
color: #4ade80;
|
||||
}
|
||||
.chan-media-meta {
|
||||
color: #777;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Rehost button */
|
||||
.chan-rehost-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
background: rgba(74, 222, 128, 0.15);
|
||||
color: #4ade80;
|
||||
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.chan-rehost-btn:hover {
|
||||
background: #4ade80;
|
||||
color: #111;
|
||||
}
|
||||
.chan-rehost-btn.rehosted {
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
color: #4ade80;
|
||||
cursor: default;
|
||||
}
|
||||
.chan-rehost-btn.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chan-rehost-container {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.chan-card-fav-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.chan-card-fav-btn:hover {
|
||||
background: rgba(255, 75, 75, 0.15);
|
||||
border-color: rgba(255, 75, 75, 0.4);
|
||||
color: #ff4b4b;
|
||||
}
|
||||
.chan-card-fav-btn.favorited {
|
||||
background: rgba(255, 75, 75, 0.2);
|
||||
border-color: rgba(255, 75, 75, 0.5);
|
||||
color: #ff4b4b;
|
||||
}
|
||||
.chan-card-fav-btn.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Comments & Greentext */
|
||||
.chan-post-comment {
|
||||
color: #ddd;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
flex: 1;
|
||||
}
|
||||
.chan-quote,
|
||||
.chan-post-comment .quote {
|
||||
color: #789922;
|
||||
}
|
||||
.chan-ref,
|
||||
.chan-post-comment .quotelink {
|
||||
color: #d00;
|
||||
text-decoration: underline;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chan-ref:hover,
|
||||
.chan-post-comment .quotelink:hover {
|
||||
color: #ff3333;
|
||||
}
|
||||
.chan-post-comment s,
|
||||
.chan-post-comment .spoiler {
|
||||
background: #333;
|
||||
color: #333;
|
||||
padding: 0 3px;
|
||||
border-radius: 2px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.chan-post-comment s:hover,
|
||||
.chan-post-comment .spoiler:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Quote preview popup */
|
||||
.chan-quote-preview-popup {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
max-width: 480px;
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #4ade80;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
|
||||
pointer-events: none;
|
||||
color: #eee;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
// Toggle filter: All vs Media Only
|
||||
const filterAllBtn = document.getElementById('filter-all-btn');
|
||||
const filterMediaBtn = document.getElementById('filter-media-btn');
|
||||
const filterRehostedBtn = document.getElementById('filter-rehosted-btn');
|
||||
const posts = document.querySelectorAll('.chan-post-card');
|
||||
|
||||
if (filterAllBtn && filterMediaBtn) {
|
||||
filterAllBtn.addEventListener('click', () => {
|
||||
filterAllBtn.classList.add('active');
|
||||
filterMediaBtn.classList.remove('active');
|
||||
if (filterRehostedBtn) filterRehostedBtn.classList.remove('active');
|
||||
posts.forEach(p => p.style.display = 'block');
|
||||
});
|
||||
|
||||
filterMediaBtn.addEventListener('click', () => {
|
||||
filterMediaBtn.classList.add('active');
|
||||
filterAllBtn.classList.remove('active');
|
||||
if (filterRehostedBtn) filterRehostedBtn.classList.remove('active');
|
||||
posts.forEach(p => {
|
||||
p.style.display = p.dataset.hasMedia === '1' ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
if (filterRehostedBtn) {
|
||||
filterRehostedBtn.addEventListener('click', () => {
|
||||
filterRehostedBtn.classList.add('active');
|
||||
filterAllBtn.classList.remove('active');
|
||||
filterMediaBtn.classList.remove('active');
|
||||
posts.forEach(p => {
|
||||
p.style.display = p.dataset.isRehosted === '1' ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Inline Rehosting
|
||||
document.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.chan-rehost-btn:not(.rehosted)');
|
||||
if (!btn) return;
|
||||
|
||||
e.preventDefault();
|
||||
const url = btn.dataset.url;
|
||||
const board = btn.dataset.board;
|
||||
const tid = btn.dataset.tid;
|
||||
const postNo = btn.dataset.post;
|
||||
const filename = btn.dataset.filename;
|
||||
if (!url) return;
|
||||
|
||||
btn.classList.add('loading');
|
||||
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Rehosting…';
|
||||
|
||||
try {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || '';
|
||||
const resp = await fetch('/api/v2/scroller/rehost', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
url,
|
||||
original_filename: filename || '',
|
||||
width: btn.dataset.width || '',
|
||||
height: btn.dataset.height || ''
|
||||
})
|
||||
});
|
||||
|
||||
const data = await resp.json();
|
||||
if (data.success && data.item_id) {
|
||||
btn.classList.remove('loading');
|
||||
btn.classList.add('rehosted');
|
||||
btn.outerHTML = '<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">' +
|
||||
'<a href="/' + data.item_id + '" class="chan-rehost-btn rehosted" target="_blank">' +
|
||||
'<i class="fa-solid fa-check"></i> Rehosted #' + data.item_id + '</a>' +
|
||||
'<button type="button" class="chan-rehost-edit-btn" data-item-id="' + data.item_id + '" title="Edit rating, tags & comment" style="padding:4px 7px;border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;font-size:0.75rem;">' +
|
||||
'<i class="fa-solid fa-pen"></i></button></span>';
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!');
|
||||
}
|
||||
if (typeof window.openChanRehostModal === 'function') {
|
||||
window.openChanRehostModal({ itemId: data.item_id, isPlayerView: false });
|
||||
}
|
||||
} else {
|
||||
btn.classList.remove('loading');
|
||||
btn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Retry';
|
||||
alert(data.msg || data.message || 'Rehosting failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CHAN] Rehost error:', err);
|
||||
btn.classList.remove('loading');
|
||||
btn.innerHTML = '<i class="fa-solid fa-cloud-arrow-up"></i> Error';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const editBtn = e.target.closest('.chan-rehost-edit-btn');
|
||||
if (editBtn) {
|
||||
e.preventDefault();
|
||||
const itemId = editBtn.dataset.itemId;
|
||||
if (itemId && typeof window.openChanRehostModal === 'function') {
|
||||
window.openChanRehostModal({ itemId: Number(itemId), isPlayerView: false });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Favorite from Thread Card (Auto-rehost if needed, then toggle favorite)
|
||||
document.addEventListener('click', async (e) => {
|
||||
const favBtn = e.target.closest('.chan-card-fav-btn');
|
||||
if (!favBtn || favBtn.classList.contains('loading')) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) {
|
||||
if (typeof window.flashMessage === 'function') window.flashMessage('Anonymous favoriting is disabled.', 3000, 'error');
|
||||
return;
|
||||
}
|
||||
if (!window.f0ckSession || !window.f0ckSession.user) {
|
||||
if (typeof window.flashMessage === 'function') window.flashMessage('Login to favorite posts', 3000, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const postNo = favBtn.dataset.post;
|
||||
let localId = favBtn.dataset.localId;
|
||||
const mediaUrl = favBtn.dataset.url;
|
||||
|
||||
favBtn.classList.add('loading');
|
||||
const origHtml = favBtn.innerHTML;
|
||||
favBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
||||
|
||||
try {
|
||||
// If not yet rehosted, auto-rehost first
|
||||
if (!localId && mediaUrl) {
|
||||
const csrfToken = window.f0ckSession?.csrf_token || '';
|
||||
const rehostResp = await fetch('/api/v2/scroller/rehost', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
url: mediaUrl,
|
||||
original_filename: favBtn.dataset.filename || '',
|
||||
width: favBtn.dataset.width || '',
|
||||
height: favBtn.dataset.height || ''
|
||||
})
|
||||
});
|
||||
const rehostData = await rehostResp.json();
|
||||
if (rehostData.success && rehostData.item_id) {
|
||||
localId = rehostData.item_id;
|
||||
favBtn.dataset.localId = localId;
|
||||
|
||||
// Update rehost box
|
||||
const rehostBox = document.getElementById('rehost-box-' + postNo);
|
||||
if (rehostBox) {
|
||||
const rehostBtn = rehostBox.querySelector('.chan-rehost-btn');
|
||||
if (rehostBtn) {
|
||||
rehostBtn.outerHTML = '<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;flex:1;">' +
|
||||
'<a href="/' + localId + '" class="chan-rehost-btn rehosted" target="_blank">' +
|
||||
'<i class="fa-solid fa-check"></i> Rehosted #' + localId + '</a>' +
|
||||
'<button type="button" class="chan-rehost-edit-btn" data-item-id="' + localId + '" title="Edit rating, tags & comment" style="padding:4px 7px;border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;font-size:0.75rem;">' +
|
||||
'<i class="fa-solid fa-pen"></i></button></span>';
|
||||
}
|
||||
}
|
||||
// Add thumb badge if missing
|
||||
const postCard = favBtn.closest('.chan-post');
|
||||
const thumbLink = postCard?.querySelector('.chan-thumb-link');
|
||||
if (thumbLink && !thumbLink.querySelector('.chan-thumb-rehost-badge')) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'chan-thumb-rehost-badge';
|
||||
badge.title = 'Rehosted on f0ckm as Post #' + localId;
|
||||
badge.innerHTML = '<i class="fa-solid fa-check"></i> #' + localId;
|
||||
thumbLink.appendChild(badge);
|
||||
}
|
||||
} else {
|
||||
throw new Error(rehostData.msg || 'Rehost failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Now toggle favorite
|
||||
const csrfToken = window.f0ckSession?.csrf_token || '';
|
||||
const favResp = await fetch('/api/v2/togglefav', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
|
||||
},
|
||||
body: JSON.stringify({ postid: Number(localId), chan_url: mediaUrl })
|
||||
});
|
||||
const favData = await favResp.json();
|
||||
favBtn.classList.remove('loading');
|
||||
|
||||
if (favData.success) {
|
||||
const isNowFav = !!favData.favorited;
|
||||
favBtn.classList.toggle('favorited', isNowFav);
|
||||
favBtn.innerHTML = '<i class="fa-' + (isNowFav ? 'solid' : 'regular') + ' fa-heart"></i>';
|
||||
favBtn.title = isNowFav ? 'Remove from favorites' : 'Favorite';
|
||||
if (typeof window.flashMessage === 'function') {
|
||||
window.flashMessage(isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES');
|
||||
}
|
||||
} else {
|
||||
favBtn.innerHTML = origHtml;
|
||||
if (typeof window.flashMessage === 'function') {
|
||||
window.flashMessage(favData.msg || 'Failed to update favorite', 3000, 'error');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CHAN-CARD-FAV] Error:', err);
|
||||
favBtn.classList.remove('loading');
|
||||
favBtn.innerHTML = origHtml;
|
||||
if (typeof window.flashMessage === 'function') {
|
||||
window.flashMessage('Failed to update favorite', 3000, 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Quote hover preview
|
||||
let previewEl = null;
|
||||
document.addEventListener('mouseover', (e) => {
|
||||
const ref = e.target.closest('.chan-ref, .quotelink');
|
||||
if (!ref) return;
|
||||
|
||||
const targetPostNo = ref.dataset.post || ref.getAttribute('href')?.replace(/^#p?/, '');
|
||||
if (!targetPostNo) return;
|
||||
const targetCard = document.getElementById('p' + targetPostNo);
|
||||
if (!targetCard) return;
|
||||
|
||||
if (!previewEl) {
|
||||
previewEl = document.createElement('div');
|
||||
previewEl.className = 'chan-quote-preview-popup';
|
||||
document.body.appendChild(previewEl);
|
||||
}
|
||||
|
||||
previewEl.innerHTML = targetCard.innerHTML;
|
||||
previewEl.style.display = 'block';
|
||||
|
||||
const rect = ref.getBoundingClientRect();
|
||||
let top = rect.bottom + 8;
|
||||
let left = rect.left;
|
||||
if (left + 480 > window.innerWidth) left = window.innerWidth - 490;
|
||||
if (top + 200 > window.innerHeight) top = rect.top - 210;
|
||||
|
||||
previewEl.style.top = Math.max(10, top) + 'px';
|
||||
previewEl.style.left = Math.max(10, left) + 'px';
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', (e) => {
|
||||
const ref = e.target.closest('.chan-ref, .quotelink');
|
||||
if (ref && previewEl) {
|
||||
previewEl.style.display = 'none';
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@include(snippets/footer)
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="item-main-content">
|
||||
|
||||
<div class="_204863">
|
||||
<div class="location">{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}</div>
|
||||
<div class="location">@if(item.is_chan)<a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/4</a><a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/{{ item.external_board }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}" class="location-link" title="Thread #{{ item.external_tid }} overview">/{{ item.external_tid }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}/{{ item.external_id }}" class="location-link" data-inherit-context="false" title="Post #{{ item.external_id }}">/{{ item.external_id }}</a>@if(item.local_id)<span class="location-sep" style="opacity: 0.6; margin: 0 4px;">→</span><a href="/{{ item.local_id }}" class="location-link is-rehosted" title="View rehosted post #{{ item.local_id }} on f0ckm" style="color: #4ade80; font-weight: 700;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>@endif@else{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}@endif</div>
|
||||
<div class="gapLeft"></div>
|
||||
</div>
|
||||
@if(enable_item_title)
|
||||
@@ -126,11 +126,21 @@
|
||||
<div class="user-infobox-body">
|
||||
<div class="user-infobox-actions">
|
||||
<div class="gapRight">
|
||||
@if(item.is_chan)
|
||||
@if(item.local_id)
|
||||
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
|
||||
<a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>
|
||||
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
|
||||
</span>
|
||||
@else
|
||||
<button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button>
|
||||
@endif
|
||||
@endif
|
||||
@if(session)
|
||||
@if(user_has_favorited)
|
||||
<i class="iconset fa-solid fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||
<i class="iconset fa-solid fa-heart" id="a_favo" data-item-id="{{ item.local_id || item.id }}" @if(item.is_chan) data-is-chan="true" data-local-id="{{ item.local_id || '' }}" data-chan-url="{{ item.external_media_url }}" data-chan-board="{{ item.external_board }}" data-chan-tid="{{ item.external_tid }}" data-chan-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" @endif title="Favorite"></i>
|
||||
@else
|
||||
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.local_id || item.id }}" @if(item.is_chan) data-is-chan="true" data-local-id="{{ item.local_id || '' }}" data-chan-url="{{ item.external_media_url }}" data-chan-board="{{ item.external_board }}" data-chan-tid="{{ item.external_tid }}" data-chan-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" @endif title="Favorite"></i>
|
||||
@endif
|
||||
<i class="iconset fa-solid fa-circle-info" id="a_info" data-item-id="{{ item.id }}" title="{{ t('info_modal.button_title') || 'Post & File Info' }}"></i>
|
||||
@if(enable_comments)
|
||||
@@ -144,7 +154,7 @@
|
||||
@if(enable_oc)
|
||||
<i class="iconset {{ item.is_oc ? 'fa-solid' : 'fa-regular' }} fa-star" id="a_oc" data-item-id="{{ item.id }}" data-is-oc="{{ item.is_oc }}" title="{{ item.is_oc ? 'Remove OC status' : 'Mark as OC' }}"></i>
|
||||
@endif
|
||||
@if(can_extract_meta)
|
||||
@if(typeof can_extract_meta !== 'undefined' && can_extract_meta)
|
||||
<i class="iconset fa-solid fa-magic" id="a_metadata" data-item-id="{{ item.id }}" @if(item.mime === 'video/youtube') data-src="https://www.youtube.com/watch?v={{ item.dest.replace('yt:', '') }}" @endif title="Extract Metadata"></i>
|
||||
@endif
|
||||
@if(item.mime === 'application/x-shockwave-flash' || item.mime === 'application/vnd.adobe.flash.movie')
|
||||
@@ -177,6 +187,14 @@
|
||||
@endif
|
||||
</span>
|
||||
@if(!user_alternative_infobox) — <span class="badge badge-dark"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>@if(item.expires_at) — <span class="badge badge-warning" style="background: rgba(255, 170, 0, 0.2); color: #ffaa00; border: 1px solid rgba(255, 170, 0, 0.4);" tooltip="Self-destructs {{ item.expires_in }}" flow="up"><i class="fa-solid fa-clock"></i> Expiring ({{ item.expires_in }})</span>@endif @endif
|
||||
@if(!user_alternative_infobox && item.is_chan) — <span class="badge badge-dark chan-post-badge" style="background: rgba(74, 222, 128, 0.15); border: 1px solid rgba(74, 222, 128, 0.3); display: inline-flex; align-items: center; gap: 6px;">
|
||||
<a href="/4/{{ item.external_board }}/{{ item.external_tid }}" style="color: #4ade80; text-decoration: none; font-weight: 600;" title="View thread overview"><i class="fa-solid fa-clover"></i> /{{ item.external_board }}/{{ item.external_tid }}</a>
|
||||
@if(item.local_id)
|
||||
<a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:0.75rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>
|
||||
@else
|
||||
<button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:0.75rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button>
|
||||
@endif
|
||||
</span>@endif
|
||||
|
||||
<span id="hall-badge-container">@if(halls_enabled && item.primaryHall)<span class="badge hall-badge-wrap">
|
||||
<a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<div class="item-main-content">
|
||||
|
||||
<div class="_204863">
|
||||
<div class="location">{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}</div>
|
||||
<div class="location">@if(item.is_chan)<a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/4</a><a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/{{ item.external_board }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}" class="location-link" title="Thread #{{ item.external_tid }} overview">/{{ item.external_tid }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}/{{ item.external_id }}" class="location-link" data-inherit-context="false" title="Post #{{ item.external_id }}">/{{ item.external_id }}</a>@if(item.local_id)<span class="location-sep" style="opacity: 0.6; margin: 0 4px;">→</span><a href="/{{ item.local_id }}" class="location-link is-rehosted" title="View rehosted post #{{ item.local_id }} on f0ckm" style="color: #4ade80; font-weight: 700;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>@endif@else{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}@endif</div>
|
||||
<div class="gapLeft"></div>
|
||||
</div>
|
||||
@if(enable_item_title)
|
||||
@@ -150,14 +150,29 @@
|
||||
@if(item.expires_at)
|
||||
— <span class="badge badge-warning" style="background: rgba(255, 170, 0, 0.2); color: #ffaa00; border: 1px solid rgba(255, 170, 0, 0.4);" tooltip="Self-destructs {{ item.expires_in }}" flow="up"><i class="fa-solid fa-clock"></i> Expiring ({{ item.expires_in }})</span>
|
||||
@endif
|
||||
@if(item.is_chan)
|
||||
<span class="badge badge-dark" style="background: rgba(74, 222, 128, 0.15); border: 1px solid rgba(74, 222, 128, 0.3);">
|
||||
<a href="/4/{{ item.external_board }}/{{ item.external_tid }}" style="color: #4ade80; text-decoration: none; font-weight: 600;" title="View thread overview"><i class="fa-solid fa-clover"></i> /{{ item.external_board }}/{{ item.external_tid }}</a>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="gapRight">
|
||||
@if(item.is_chan)
|
||||
@if(item.local_id)
|
||||
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
|
||||
<a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>
|
||||
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
|
||||
</span>
|
||||
@else
|
||||
<button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button>
|
||||
@endif
|
||||
@endif
|
||||
@if(session)
|
||||
@if(user_has_favorited)
|
||||
<i class="iconset fa-solid fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||
<i class="iconset fa-solid fa-heart" id="a_favo" data-item-id="{{ item.local_id || item.id }}" @if(item.is_chan) data-is-chan="true" data-local-id="{{ item.local_id || '' }}" data-chan-url="{{ item.external_media_url }}" data-chan-board="{{ item.external_board }}" data-chan-tid="{{ item.external_tid }}" data-chan-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" @endif title="Favorite"></i>
|
||||
@else
|
||||
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.id }}" title="Favorite"></i>
|
||||
<i class="iconset fa-regular fa-heart" id="a_favo" data-item-id="{{ item.local_id || item.id }}" @if(item.is_chan) data-is-chan="true" data-local-id="{{ item.local_id || '' }}" data-chan-url="{{ item.external_media_url }}" data-chan-board="{{ item.external_board }}" data-chan-tid="{{ item.external_tid }}" data-chan-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" @endif title="Favorite"></i>
|
||||
@endif
|
||||
@endif
|
||||
@if(session)
|
||||
@@ -170,7 +185,9 @@
|
||||
@if(enable_oc)
|
||||
<i class="iconset {{ item.is_oc ? 'fa-solid' : 'fa-regular' }} fa-star" id="a_oc" data-item-id="{{ item.id }}" data-is-oc="{{ item.is_oc }}" title="{{ item.is_oc ? 'Remove OC status' : 'Mark as OC' }}"></i>
|
||||
@endif
|
||||
@if(typeof can_extract_meta !== 'undefined' && can_extract_meta)
|
||||
<i class="iconset fa-solid fa-magic" id="a_metadata" data-item-id="{{ item.id }}" @if(item.mime === 'video/youtube') data-src="https://www.youtube.com/watch?v={{ item.dest.replace('yt:', '') }}" @endif title="Extract Metadata"></i>
|
||||
@endif
|
||||
@if(is_flash_item)
|
||||
<i class="iconset fa-solid fa-image" id="a_rethumb" data-item-id="{{ item.id }}" title="Re-upload Thumbnail"></i>
|
||||
@endif
|
||||
|
||||
@@ -159,6 +159,9 @@
|
||||
<button type="button" class="sidebar-tab" id="sidebar-tab-tag" data-tab="tag" title="Tag Feed" aria-label="Tag Feed" style="display: none;">
|
||||
<i class="fa-solid fa-hashtag"></i>
|
||||
</button>
|
||||
<button type="button" class="sidebar-tab" id="sidebar-tab-chan" data-tab="chan" title="4chan Thread" aria-label="4chan Thread" style="display: none;">
|
||||
<i class="fa-solid fa-clover"></i>
|
||||
</button>
|
||||
<button type="button" class="sidebar-tab" id="sidebar-tab-favs" data-tab="favs" title="Favorites" aria-label="Favorites" style="display: none;">
|
||||
<i class="fa-solid fa-heart"></i>
|
||||
</button>
|
||||
@@ -198,6 +201,24 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebar-chan-container" class="sidebar-recommendations-list sidebar-tab-content" data-tab-content="chan" style="display: none;">
|
||||
<div class="sidebar-tag-header">
|
||||
<div class="sidebar-tag-title">
|
||||
<i class="fa-solid fa-clover" style="color: #4ade80;"></i>
|
||||
<span class="sidebar-chan-name"></span>
|
||||
<span class="sidebar-chan-count"></span>
|
||||
</div>
|
||||
<a href="#" class="sidebar-chan-overview-btn" title="View thread overview" style="display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 6px; background: rgba(255,255,255,0.06); color: #aaa; text-decoration: none;">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-chan-items-container">
|
||||
<div class="sidebar-loading-state">
|
||||
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
<span>Loading thread media...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sidebar-favs-container" class="sidebar-recommendations-list sidebar-tab-content" data-tab-content="favs" style="display: none;">
|
||||
<div class="sidebar-tag-header">
|
||||
<div class="sidebar-tag-title">
|
||||
|
||||
@@ -94,9 +94,9 @@
|
||||
<div class="infobox-card-value font-mono" id="info-file-size">{{ item.size }}</div>
|
||||
</div>
|
||||
|
||||
<div class="infobox-card" id="info-card-dimensions" @if(!item_has_dimensions)style="display: none;"@endif>
|
||||
<div class="infobox-card" id="info-card-dimensions" @if(!item.width || !item.height)style="display: none;"@endif>
|
||||
<label class="infobox-card-label"><i class="fa-solid fa-ruler-combined"></i> Dimensions</label>
|
||||
<div class="infobox-card-value font-mono" id="info-file-dimensions">@if(item_has_dimensions){{ item.width }} × {{ item.height }} px@endif</div>
|
||||
<div class="infobox-card-value font-mono" id="info-file-dimensions">@if(item.width && item.height){{ item.width }} × {{ item.height }} px@endif</div>
|
||||
</div>
|
||||
|
||||
<div class="infobox-card" id="info-card-mime">
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
@elseif(item.mime.startsWith("video"))
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<video id="my-video" class="embed-responsive-item" width="640" height="360" src="{{ item.dest }}" preload="auto" data-size="{{ item.size }}" loop playsinline @if(!session || session.disable_autoplay !== true) autoplay @endif></video>
|
||||
<video id="my-video" class="embed-responsive-item" width="{{ item.width || 640 }}" height="{{ item.height || 360 }}" src="{{ item.dest }}" preload="auto" data-size="{{ item.size }}" loop playsinline @if(!session || session.disable_autoplay !== true) autoplay @endif></video>
|
||||
</div>
|
||||
@elseif(item.mime.startsWith("audio"))
|
||||
<div class="embed-responsive embed-responsive-16by9" style="background: #000;">
|
||||
@@ -90,7 +90,7 @@
|
||||
@elseif(item.mime.startsWith("image"))
|
||||
<div class="embed-responsive embed-responsive-16by9">
|
||||
<div class="embed-responsive-image" id="image-scroll">
|
||||
<a href="{{ item.dest }}" id="elfe" target="_blank"><img id="f0ck-image" class="img-fluid" src="{{ item.dest }}" loading="lazy" decoding="async" /></a>
|
||||
<a href="{{ item.dest }}" id="elfe" target="_blank"><img id="f0ck-image" class="img-fluid" src="{{ item.dest }}" loading="lazy" decoding="async" @if(item.width)width="{{ item.width }}"@endif @if(item.height)height="{{ item.height }}"@endif /></a>
|
||||
</div>
|
||||
<button class="image-modal-btn" title="Open in modal"><i class="fa-solid fa-plus"></i></button>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
</div>
|
||||
@endif
|
||||
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tags"></i></a>
|
||||
@if(session.can_chan)
|
||||
<a href="/4/wsg/catalogue" id="nav-chan-link" title="4chan Viewer & Rehost" style="color: #4ade80;"><i class="fa-solid fa-clover"></i></a>
|
||||
@endif
|
||||
@if(abyss_enabled)
|
||||
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dice-d6"></i></a>
|
||||
@endif
|
||||
|
||||
Reference in New Issue
Block a user