This commit is contained in:
2026-09-13 04:05:59 +02:00
parent 90860b9279
commit 340c825019
48 changed files with 2727 additions and 532 deletions
+265 -17
View File
@@ -87,6 +87,99 @@
.btn-role { background: rgba(180, 100, 255, 0.15); color: #c084fc; border: 1px solid rgba(180, 100, 255, 0.3); }
.btn-role:hover { background: rgba(180, 100, 255, 0.25); opacity: 1; }
.legacy-filter-btn {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 0 14px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
font-size: 0.85rem;
color: #aaa;
white-space: nowrap;
user-select: none;
transition: all 0.2s ease;
height: 42px;
box-sizing: border-box;
}
.legacy-filter-btn:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
color: #fff;
}
.legacy-filter-btn.active {
background: rgba(var(--accent-rgb, 0, 150, 255), 0.15);
border-color: var(--accent);
color: var(--accent);
}
.legacy-filter-btn input {
cursor: pointer;
accent-color: var(--accent);
width: 15px;
height: 15px;
margin: 0;
}
.admin-select-filter {
padding: 0 14px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: #ccc;
font-size: 0.85rem;
outline: none;
cursor: pointer;
transition: all 0.2s ease;
height: 42px;
box-sizing: border-box;
}
.admin-select-filter:hover {
background: rgba(255, 255, 255, 0.08);
border-color: rgba(255, 255, 255, 0.2);
color: #fff;
}
.admin-select-filter:focus {
border-color: var(--accent);
}
.admin-select-filter.active {
background: rgba(var(--accent-rgb, 0, 150, 255), 0.15);
border-color: var(--accent);
color: var(--accent);
font-weight: 600;
}
.admin-select-filter option {
background: #1a1a1a;
color: #fff;
}
.admin-select-filter:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.btn-filter-reset {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #aaa;
padding: 0 14px;
height: 42px;
border-radius: 8px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
transition: all 0.2s;
box-sizing: border-box;
white-space: nowrap;
}
.btn-filter-reset:hover {
background: rgba(255, 60, 60, 0.15);
border-color: rgba(255, 60, 60, 0.3);
color: #ff6b6b;
}
/* Create User Modal */
#create-user-modal {
display: none;
@@ -236,28 +329,60 @@
</div>
<div style="padding: 0 15px;">
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 30px; gap: 20px; flex-wrap: wrap;">
<div style="display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 25px; gap: 20px; flex-wrap: wrap;">
<div>
<h2 style="margin: 0; font-weight: 800; letter-spacing: -0.5px;">User Management</h2>
<p style="color: #888; margin: 5px 0 0 0;">Administration hub for <span id="total-count">{!! total !!}</span> registered members.</p>
<p style="color: #888; margin: 5px 0 0 0;">Administration hub for <span id="total-count">{!! total !!}</span> <span id="total-label">{{ totalLabel }}</span>.</p>
</div>
<div style="display: flex; gap: 10px; align-items: center; flex-grow: 1; max-width: 500px;">
<div style="flex-grow: 1; position: relative;">
<input type="text" id="user-search" placeholder='Search by name or email… use "exact" for exact match'
style="width: 100%; padding: 12px 20px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; transition: border-color 0.2s;"
value="{{ q }}">
<div id="search-spinner" style="position: absolute; right: 15px; top: 12px; display: none;">
<svg class="spinner" width="20" height="20" viewBox="0 0 50 50" style="animation: rotate 2s linear infinite;">
<circle cx="25" cy="25" r="20" fill="none" stroke="var(--accent)" stroke-width="5" stroke-dasharray="90,150" stroke-dashoffset="0" style="stroke-linecap: round;"></circle>
</svg>
</div>
</div>
<button class="btn-modern btn-create" onclick="openCreateUserModal()" style="padding: 10px 16px; font-size: 0.85rem; white-space: nowrap;" title="Create new user">
<div>
<button class="btn-modern btn-create" onclick="openCreateUserModal()" style="padding: 11px 18px; font-size: 0.85rem; white-space: nowrap; height: 42px;" title="Create new user">
<i class="fa fa-user-plus"></i> Create User
</button>
</div>
</div>
<!-- Filter & Search Toolbar -->
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 25px; flex-wrap: wrap;">
<div style="flex-grow: 1; min-width: 240px; position: relative;">
<input type="text" id="user-search" placeholder='Search by name or email… use "exact" for exact match'
style="width: 100%; height: 42px; padding: 0 20px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; outline: none; transition: border-color 0.2s; box-sizing: border-box;"
value="{{ q }}">
<div id="search-spinner" style="position: absolute; right: 15px; top: 11px; display: none;">
<svg class="spinner" width="20" height="20" viewBox="0 0 50 50" style="animation: rotate 2s linear infinite;">
<circle cx="25" cy="25" r="20" fill="none" stroke="var(--accent)" stroke-width="5" stroke-dasharray="90,150" stroke-dashoffset="0" style="stroke-linecap: round;"></circle>
</svg>
</div>
</div>
<!-- Status Filter -->
<select id="status-filter" class="admin-select-filter {{ status ? 'active' : '' }}" {{ onlyLegacy ? 'disabled' : '' }} title="Filter by account status">
<option value="">All Statuses</option>
<option value="banned" {{ status === 'banned' ? 'selected' : '' }}>⛔ Banned</option>
<option value="active" {{ status === 'active' ? 'selected' : '' }}>🟢 Active</option>
<option value="pending" {{ status === 'pending' ? 'selected' : '' }}>⏳ Pending</option>
</select>
<!-- Role Filter -->
<select id="role-filter" class="admin-select-filter {{ role ? 'active' : '' }}" {{ onlyLegacy ? 'disabled' : '' }} title="Filter by user role">
<option value="">All Roles</option>
<option value="staff" {{ role === 'staff' ? 'selected' : '' }}>🛡️ Staff (All)</option>
<option value="admin" {{ role === 'admin' ? 'selected' : '' }}>⚡ Admins</option>
<option value="mod" {{ role === 'mod' ? 'selected' : '' }}>🗡️ Moderators</option>
<option value="user" {{ role === 'user' ? 'selected' : '' }}>👤 Regular Users</option>
</select>
<!-- Legacy Only Toggle -->
<label id="legacy-filter-label" class="legacy-filter-btn {{ onlyLegacy ? 'active' : '' }}" title="Toggle to only show legacy ghost users">
<input type="checkbox" id="legacy-filter" {{ onlyLegacy ? 'checked' : '' }}>
<span><i class="fa fa-ghost" style="opacity: 0.8; margin-right: 4px;"></i>Only legacy</span>
</label>
<!-- Reset Filters Button -->
<button id="reset-filters-btn" class="btn-filter-reset" style="display: {{ (q || status || role || onlyLegacy) ? 'inline-flex' : 'none' }};" title="Reset all filters and search">
<i class="fa fa-rotate-left"></i> Reset
</button>
</div>
<div class="upload-form">
<table class="admin-users-table responsive-table">
<thead>
@@ -276,7 +401,7 @@
</table>
<div id="loading-trigger" style="height: 20px; margin-top: 10px;"></div>
<div id="no-users-msg" style="display: {{ users.length === 0 ? 'block' : 'none' }}; padding: 40px; text-align: center; color: #666;">
No users matched your search.
{{ emptyMsg }}
</div>
</div>
</div>
@@ -502,28 +627,81 @@
var hasMore = {!! hasMore ? 'true' : 'false' !!};
var isLoading = false;
var searchQuery = '{{ q }}';
var onlyLegacy = {!! onlyLegacy ? 'true' : 'false' !!};
var currentStatus = '{{ status }}';
var currentRole = '{{ role }}';
var searchInput = document.getElementById('user-search');
var statusFilter = document.getElementById('status-filter');
var roleFilter = document.getElementById('role-filter');
var legacyFilter = document.getElementById('legacy-filter');
var legacyFilterLabel = document.getElementById('legacy-filter-label');
var resetFiltersBtn = document.getElementById('reset-filters-btn');
var totalLabel = document.getElementById('total-label');
var tableBody = document.getElementById('user-table-body');
var loadingTrigger = document.getElementById('loading-trigger');
var noUsersMsg = document.getElementById('no-users-msg');
var spinner = document.getElementById('search-spinner');
var countSpan = document.getElementById('total-count');
function updateUrlAndControls() {
var urlParams = new URLSearchParams();
if (searchQuery) urlParams.set('q', searchQuery);
if (onlyLegacy) {
urlParams.set('legacy', '1');
} else {
if (currentStatus) urlParams.set('status', currentStatus);
if (currentRole) urlParams.set('role', currentRole);
}
var newUrl = window.location.pathname + (urlParams.toString() ? ('?' + urlParams.toString()) : '');
window.history.replaceState({}, '', newUrl);
// Update reset button
var hasFilter = !!(searchQuery || onlyLegacy || currentStatus || currentRole);
if (resetFiltersBtn) resetFiltersBtn.style.display = hasFilter ? 'inline-flex' : 'none';
// Update control states
if (statusFilter) {
statusFilter.disabled = onlyLegacy;
statusFilter.classList.toggle('active', !!currentStatus && !onlyLegacy);
}
if (roleFilter) {
roleFilter.disabled = onlyLegacy;
roleFilter.classList.toggle('active', !!currentRole && !onlyLegacy);
}
if (legacyFilterLabel) {
legacyFilterLabel.classList.toggle('active', onlyLegacy);
}
}
async function fetchUsers(page, q, append) {
if (isLoading) return;
isLoading = true;
if (!append) spinner.style.display = 'block';
try {
var url = '/admin/users?page=' + page + '&q=' + encodeURIComponent(q);
var params = new URLSearchParams();
params.set('page', page);
if (q) params.set('q', q);
if (onlyLegacy) {
params.set('legacy', '1');
} else {
if (currentStatus) params.set('status', currentStatus);
if (currentRole) params.set('role', currentRole);
}
var url = '/admin/users?' + params.toString();
var res = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
// Update state from headers
var total = res.headers.get('X-Total-Count');
var hasMoreHeader = res.headers.get('X-Has-More');
var totalLabelHeader = res.headers.get('X-Total-Label');
var emptyMsgHeader = res.headers.get('X-Empty-Msg');
if (total !== null) countSpan.textContent = total;
if (totalLabelHeader && totalLabel) totalLabel.textContent = totalLabelHeader;
if (emptyMsgHeader && noUsersMsg) noUsersMsg.textContent = emptyMsgHeader;
if (hasMoreHeader !== null) hasMore = (hasMoreHeader === 'true');
var html = await res.text();
@@ -548,12 +726,74 @@
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
searchQuery = searchInput.value;
searchQuery = searchInput.value.trim();
currentPage = 1;
updateUrlAndControls();
fetchUsers(1, searchQuery, false);
}, 300);
});
// Status Filter Handling
if (statusFilter) {
statusFilter.addEventListener('change', function() {
currentStatus = this.value;
if (currentStatus && onlyLegacy) {
onlyLegacy = false;
if (legacyFilter) legacyFilter.checked = false;
}
updateUrlAndControls();
currentPage = 1;
fetchUsers(1, searchQuery, false);
});
}
// Role Filter Handling
if (roleFilter) {
roleFilter.addEventListener('change', function() {
currentRole = this.value;
if (currentRole && onlyLegacy) {
onlyLegacy = false;
if (legacyFilter) legacyFilter.checked = false;
}
updateUrlAndControls();
currentPage = 1;
fetchUsers(1, searchQuery, false);
});
}
// Legacy Only Filter Handling
if (legacyFilter) {
legacyFilter.addEventListener('change', function() {
onlyLegacy = this.checked;
if (onlyLegacy) {
currentStatus = '';
currentRole = '';
if (statusFilter) statusFilter.value = '';
if (roleFilter) roleFilter.value = '';
}
updateUrlAndControls();
currentPage = 1;
fetchUsers(1, searchQuery, false);
});
}
// Reset All Filters
if (resetFiltersBtn) {
resetFiltersBtn.addEventListener('click', function() {
searchQuery = '';
searchInput.value = '';
onlyLegacy = false;
currentStatus = '';
currentRole = '';
if (legacyFilter) legacyFilter.checked = false;
if (statusFilter) statusFilter.value = '';
if (roleFilter) roleFilter.value = '';
updateUrlAndControls();
currentPage = 1;
fetchUsers(1, '', false);
});
}
// Infinite Scroll
var observer = new IntersectionObserver(function(entries) {
if (entries[0].isIntersecting && hasMore && !isLoading) {
@@ -624,6 +864,14 @@
if (data.success) {
closeCreateUserModal();
showFlash(data.msg, 'success');
// If we were filtering by legacy or banned, reset filter so admin can see newly created user
if (onlyLegacy || currentStatus === 'banned') {
onlyLegacy = false;
currentStatus = '';
if (legacyFilter) legacyFilter.checked = false;
if (statusFilter) statusFilter.value = '';
updateUrlAndControls();
}
// Refresh the table from page 1
currentPage = 1;
fetchUsers(1, searchQuery, false);
+66 -9
View File
@@ -1,13 +1,70 @@
@include(snippets/header)
<div id="main" style="display: flex; justify-content: center; align-items: center; min-height: 80vh;">
<div class="banned-container" style="background: rgba(0,0,0,0.85); padding: 40px; margin: 25px; border: 2px solid var(--accent); text-align: center; position: relative; z-index: 10002;">
<h1 style="color: var(--accent); margin-bottom: 20px;">YOU ARE BANNED!</h1>
<video style="max-width: 100%;" src="{{ ban_video }}" autoplay loop controls></video>
<p style="font-size: 1.2em; margin-bottom: 20px;">Reason: <strong>{{ reason }}</strong></p>
<p style="font-size: 1.1em; color: rgba(255,255,255,0.7);">Ban expires: <strong>{{ expires }}</strong></p>
<div style="margin-top: 30px;">
<a href="/logout" class="btn btn-outline-danger" style="padding: 10px 20px; border: 1px solid var(--accent); color: var(--accent); text-decoration: none; border-radius: 4px;">Leave</a>
</div>
<div class="pagewrapper">
<div id="main" style="padding: 40px 15px; box-sizing: border-box;">
<div style="min-height: 70vh; display: flex; align-items: center; justify-content: center;">
<!-- BANNED VIEW -->
<div id="banned-view" class="banned-container" style="display: {{ isBanned ? 'block' : 'none' }}; background: rgba(18, 18, 20, 0.95); padding: 40px 30px; margin: 0 auto; text-align: center; max-width: 520px; width: 100%; box-sizing: border-box;">
<h1 style="color: #ff4757; margin-bottom: 20px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase;">YOU ARE BANNED!</h1>
<img src="/s/img/banned_angry.png" alt="Banned" style="width: 170px; height: auto; margin: 0 auto 25px auto; display: block;" />
<div style="background: rgba(0,0,0,0.45); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 14px 18px; margin-bottom: 25px; text-align: left;">
<p style="margin: 0 0 8px 0; font-size: 0.95em; color: #ddd;">
<span style="color: #888; font-size: 0.82em; text-transform: uppercase; display: block; margin-bottom: 2px;">Reason</span>
<strong id="ban-reason-display" style="color: #ff6b6b; word-break: break-word;">{{ reason }}</strong>
</p>
<p style="margin: 0; font-size: 0.95em; color: #ddd;">
<span style="color: #888; font-size: 0.82em; text-transform: uppercase; display: block; margin-bottom: 2px;">Ban expires</span>
<strong id="ban-expires-display" style="color: #bbb;">{{ expires }}</strong>
</p>
</div>
<div>
<a href="/logout" class="btn btn-outline-danger" style="padding: 10px 24px; border: 1px solid #e74c3c; color: #e74c3c; text-decoration: none; border-radius: 6px; font-weight: 600;">Leave</a>
</div>
</div>
<!-- UNBANNED / GOOD STANDING VIEW -->
<div id="unbanned-view" class="unbanned-container" style="display: {{ isBanned ? 'none' : 'block' }}; background: rgba(18, 18, 20, 0.95); padding: 40px 30px; margin: 0 auto; border: 2px solid #2ecc71; text-align: center; border-radius: 12px; max-width: 520px; width: 100%; box-shadow: 0 10px 50px rgba(46, 204, 113, 0.25); box-sizing: border-box;">
<div style="width: 80px; height: 80px; border-radius: 50%; background: rgba(46, 204, 113, 0.15); display: flex; align-items: center; justify-content: center; margin: 0 auto 20px auto; border: 2px solid rgba(46, 204, 113, 0.4);">
<i class="fa-solid fa-shield-halved" style="font-size: 38px; color: #2ecc71;"></i>
</div>
<h1 style="color: #2ecc71; margin-bottom: 10px; font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase; font-size: 1.7em;">YOU ARE NOT BANNED</h1>
<p style="font-size: 1em; color: #aaa; margin-bottom: 22px; line-height: 1.5;">
Your account, connection, and device are in good standing. There are no active bans on this browser.
</p>
<div style="background: rgba(0,0,0,0.4); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 14px 18px; margin-bottom: 25px; text-align: left;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="color: #888; font-size: 0.88em; text-transform: uppercase;">Status</span>
<span style="color: #2ecc71; font-weight: 700; font-size: 0.9em;"><i class="fa-solid fa-circle" style="font-size: 0.6em; vertical-align: middle; margin-right: 4px;"></i> Clear / Active</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="color: #888; font-size: 0.88em; text-transform: uppercase;">IP Address</span>
<code style="color: #ddd; font-family: monospace; font-size: 0.9em;">{{ clientIp || '127.0.0.1' }}</code>
</div>
</div>
<div>
<a href="/" class="btn" style="background: #2ecc71; border: 1px solid #27ae60; color: #fff; padding: 10px 28px; border-radius: 6px; font-size: 1em; font-weight: 600; text-decoration: none; display: inline-block;">Return to Home</a>
</div>
</div>
</div>
</div>
</div>
<script>
(async function() {
try {
const raw = localStorage.getItem('f0ck_anon_tombstone');
const tombstone = raw ? JSON.parse(raw) : null;
if (tombstone && tombstone.banned) {
const bView = document.getElementById('banned-view');
const uView = document.getElementById('unbanned-view');
if (bView) bView.style.display = 'block';
if (uView) uView.style.display = 'none';
const rEl = document.getElementById('ban-reason-display');
const eEl = document.getElementById('ban-expires-display');
if (rEl && tombstone.reason) rEl.textContent = tombstone.reason;
if (eEl && tombstone.expires) eEl.textContent = new Date(tombstone.expires).toLocaleString();
}
} catch (e) {}
})();
</script>
@include(snippets/footer)
+1 -1
View File
@@ -3,7 +3,7 @@
@include(snippets/page-title)
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
@each(items as item)
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }} {{ item.is_onara_active ? 'onara-active' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(session || !guest_anonymize){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }} {{ item.is_onara_active ? 'onara-active' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
+11 -9
View File
@@ -93,10 +93,10 @@
<div class="blahlol">
@if(user_alternative_infobox)
<div class="user-infobox-block" data-banner-user="@if(session || !guest_anonymize){{ item.username || '' }}@endif" style="--author-accent: @if(session || !guest_anonymize)@if(item.author_color){{ item.author_color }}@else var(--accent) @endif@else var(--accent) @endif; --author-border: @if(session || !guest_anonymize)@if(item.author_color){{ item.author_color }}@else var(--accent) @endif@else var(--accent) @endif;@if(session || !guest_anonymize)@if(item.author_banner_file && user_banner_enabled) --author-banner: url('/a/{{ item.author_banner_file }}'); --author-banner-position: {{ item.author_banner_position === 'center' ? 'center top' : (item.author_banner_position || 'center top') }}; --author-banner-size: {{ item.author_banner_size || 'cover' }};@endif@endif">
<div class="user-infobox-block" data-banner-user="@if(!is_anonymized){{ item.username || '' }}@endif" style="--author-accent: @if(!is_anonymized)@if(item.author_color){{ item.author_color }}@else var(--accent) @endif@else var(--accent) @endif; --author-border: @if(!is_anonymized)@if(item.author_color){{ item.author_color }}@else var(--accent) @endif@else var(--accent) @endif;@if(!is_anonymized)@if(item.author_banner_file && user_banner_enabled) --author-banner: url('/a/{{ item.author_banner_file }}'); --author-banner-position: {{ item.author_banner_position === 'center' ? 'center top' : (item.author_banner_position || 'center top') }}; --author-banner-size: {{ item.author_banner_size || 'cover' }};@endif@endif">
<div class="user-infobox-avatar">
@if(session || !guest_anonymize)
@if(!is_anonymized)
<a href="/user/{{ (item.username || '').toLowerCase() }}">
@if(item.author_avatar_file)
<img src="/a/{{ item.author_avatar_file }}" />
@@ -115,8 +115,8 @@
<div class="user-infobox-info">
<div class="user-infobox-header">
<div class="user-infobox-username-container">
@if(session || !guest_anonymize)
<a id="a_username" data-username="{{ item.username || '' }}" data-author-id="{{ item.author_id || '' }}" href="/user/{{ (item.username || '').toLowerCase() }}" tooltip="ID: {{ item.author_id }}" class="user-infobox-username">{!! item.author_display_name || item.username !!}</a>
@if(!is_anonymized)
<a id="a_username" data-username="{{ item.username || '' }}" @if(session && !session.is_anon) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && !session.is_anon) tooltip="ID: {{ item.author_id }}" @endif class="user-infobox-username">{!! item.author_display_name || item.username !!}</a>
@else
<span class="user-infobox-username">anonymous</span>
@endif
@@ -125,7 +125,7 @@
</div>
<div class="user-infobox-body">
<div class="user-infobox-description">
@if(session || !guest_anonymize){!! item.author_description || '' !!}@endif
@if(!is_anonymized){!! item.author_description || '' !!}@endif
</div>
<div class="user-infobox-actions">
@if(session)
@@ -145,7 +145,7 @@
<span class="badge badge-dark">
<a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}" @if(user_alternative_infobox)style="display:none"@endif>{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a>
@if(!user_alternative_infobox) — [@if(session || !guest_anonymize)<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>@else<span>anonymous</span>@endif] @endif
@if(!user_alternative_infobox) — [@if(!is_anonymized)<a id="a_username" data-username="{{ item.username || '' }}" @if(session && !session.is_anon) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && !session.is_anon && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>@else<span>anonymous</span>@endif] @endif
@if(!user_alternative_infobox)
<span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
@endif
@@ -205,8 +205,8 @@
<span class="rating-label">{!! tag.tag !!}</span>
</span>
@else
<span @if(session && (tag.display_name || tag.user)) tooltip="{!! tag.display_name || tag.user !!}" @endif class="badge {{ tag.badge }}">
<a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(is_mod_or_admin)&nbsp;<a class="removetag" href="#"><i class="fa-solid fa-xmark"></i></a>@endif
<span @if(!is_anonymized && session && !session.is_anon && (tag.display_name || tag.user)) tooltip="{!! tag.display_name || tag.user !!}" @endif class="badge {{ tag.badge }} tag-badge @if(tag.is_excluded) tag-is-excluded @endif" data-tag-id="{{ tag.id }}" data-tag="{{ tag.tag }}" data-tag-normalized="{{ tag.normalized }}">
<a href="/tag/{{ tag.normalized }}" class="tag-name">{!! tag.tag !!}</a>@if(tag.can_exclude)<button type="button" class="tag-exclude-btn" title="@if(tag.is_excluded){{ t('filter.unexclude_tag') || 'Excluded (click to unexclude)' }}@else{{ t('filter.exclude_tag') || 'Exclude tag' }}@endif" aria-label="Exclude tag"><i class="fa-solid @if(tag.is_excluded) fa-circle-check @else fa-ban @endif"></i></button>@endif@if(is_mod_or_admin)&nbsp;<a class="removetag" href="#"><i class="fa-solid fa-xmark"></i></a>@endif
</span>
@endif
@endeach
@@ -227,7 +227,7 @@
@each(item.favorites as fav)
@if(fav.hide_fav_badge && (!session || session.user !== fav.user))
<a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a>
@elseif(!session && guest_anonymize)
@elseif(is_anonymized)
<a class="ghost-fav" tooltip="anonymous" flow="up" style="cursor: default;"><img src="/a/default.png" style="height: 32px; width: 32px;" loading="lazy" /></a>
@else
<a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a>
@@ -252,6 +252,8 @@
@if(item.is_comments_locked) data-is-locked="true" @endif>
@if(item.is_comments_locked && !is_mod_or_admin)
<div class="lock-notice">🔒 Comments are disabled on this thread.</div>
@elseif(session && session.is_anon && !anon_permissions.comment)
<div class="lock-notice">🔒 Anonymous comments are disabled.</div>
@else
<div class="comment-input main-input">
<textarea disabled></textarea>
+6 -4
View File
@@ -21,6 +21,8 @@
@if(item.is_comments_locked) data-is-locked="true" @endif>
@if(item.is_comments_locked && !is_mod_or_admin)
<div class="lock-notice">🔒 Comments are disabled on this thread.</div>
@elseif(session && session.is_anon && !anon_permissions.comment)
<div class="lock-notice">🔒 Anonymous comments are disabled.</div>
@else
<div class="comment-input main-input">
<textarea disabled></textarea>
@@ -51,8 +53,8 @@
<span class="rating-label">{!! tag.tag !!}</span>
</span>
@else
<span @if(session && (tag.display_name || tag.user)) tooltip="{!! tag.display_name || tag.user !!}" @endif class="badge {{ tag.badge }}">
<a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(is_mod_or_admin)&nbsp;<a class="removetag" href="#"><i class="fa-solid fa-xmark"></i></a>@endif
<span @if(!is_anonymized && session && !session.is_anon && (tag.display_name || tag.user)) tooltip="{!! tag.display_name || tag.user !!}" @endif class="badge {{ tag.badge }} tag-badge @if(tag.is_excluded) tag-is-excluded @endif" data-tag-id="{{ tag.id }}" data-tag="{{ tag.tag }}" data-tag-normalized="{{ tag.normalized }}">
<a href="/tag/{{ tag.normalized }}" class="tag-name">{!! tag.tag !!}</a>@if(tag.can_exclude)<button type="button" class="tag-exclude-btn" title="@if(tag.is_excluded){{ t('filter.unexclude_tag') || 'Excluded (click to unexclude)' }}@else{{ t('filter.exclude_tag') || 'Exclude tag' }}@endif" aria-label="Exclude tag"><i class="fa-solid @if(tag.is_excluded) fa-circle-check @else fa-ban @endif"></i></button>@endif@if(is_mod_or_admin)&nbsp;<a class="removetag" href="#"><i class="fa-solid fa-xmark"></i></a>@endif
</span>
@endif
@endeach
@@ -138,7 +140,7 @@
</div>
<div class="blahlol">
<span class="badge badge-dark">
<a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}">{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a> — [@if(session || !guest_anonymize)<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ item_username_lower }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>@else<span>anonymous</span>@endif] <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
<a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}">{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a> — [@if(!is_anonymized)<a id="a_username" data-username="{{ item.username || '' }}" @if(session && !session.is_anon) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ item_username_lower }}" @if(session && !session.is_anon && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>@else<span>anonymous</span>@endif] <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
</span>
<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
@@ -185,7 +187,7 @@
@each(item.favorites as fav)
@if(fav.hide_fav_badge && (!session || session.user !== fav.user))
<a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a>
@elseif(!session && guest_anonymize)
@elseif(is_anonymized)
<a class="ghost-fav" tooltip="anonymous" flow="up" style="cursor: default;"><img src="/a/default.png" style="height: 32px; width: 32px;" loading="lazy" /></a>
@else
<a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a>
+43 -4
View File
@@ -942,6 +942,19 @@
window.scrollerLoggedIn = @if(typeof session !== 'undefined' && session)true@else false@endif;
window.scrollerIsMod = @if(typeof session !== 'undefined' && session && (session.admin || session.is_moderator))true@else false@endif;
window.scrollerCsrf = "@if(typeof session !== 'undefined' && session){{ session.csrf_token || '' }}@else@endif";
window.f0ckSession = {
logged_in: window.scrollerLoggedIn,
user: @if(typeof session !== 'undefined' && session && session.user)"{{ session.user }}"@else null@endif,
is_anon: @if(typeof session !== 'undefined' && session && session.is_anon)true@else false@endif,
anon_permissions: {{ anon_permissions_json || '{}' }},
csrf_token: window.scrollerCsrf,
guest_anonymize: @if(guest_anonymize) true @else false @endif,
anon_anonymize: @if(anon_anonymize) true @else false @endif,
is_anonymized: @if(is_anonymized) true @else false @endif,
excluded_tags: @if(typeof session !== 'undefined' && session && session.excluded_tags) {{ JSON.stringify(session.excluded_tags) }} @else [] @endif,
development: @if(development) true @else false @endif
};
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
window.scrollerEnableNsfl = {{ enable_nsfl ? 'true' : 'false' }};
window.scrollerEnableSwf = {{ enable_swf ? 'true' : 'false' }};
window.scrollerRuffleVolume = @if(typeof session !== 'undefined' && session && session.ruffle_volume !== undefined && session.ruffle_volume !== null){{ session.ruffle_volume }}@else 0.5@endif;
@@ -1110,27 +1123,52 @@
<button class="filter-reset-btn" id="filter-reset-btn">{{ t('scroller.reset_all') }}</button>
</div>
<div class="filter-scroll-area">
@js
const _isScrollerAnon = !(typeof session !== 'undefined' && session && session.user && !session.is_anon);
const _scrollerAllowedModes = (typeof anon_permissions !== 'undefined' && anon_permissions && anon_permissions.allowed_modes) || ['sfw', 'nsfw', 'untagged', 'all', 'nsfl'];
const _scrollerShowSfw = !_isScrollerAnon || _scrollerAllowedModes.includes('sfw');
const _scrollerShowNsfw = (typeof session !== 'undefined' && session && !session.is_anon) || (_isScrollerAnon && _scrollerAllowedModes.includes('nsfw'));
const _scrollerShowNsfl = enable_nsfl && ((typeof session !== 'undefined' && session && !session.is_anon) || (_isScrollerAnon && _scrollerAllowedModes.includes('nsfl')));
const _scrollerShowAll = (typeof session !== 'undefined' && session && !session.is_anon) || (_isScrollerAnon && _scrollerAllowedModes.includes('all'));
const _scrollerShowUntagged = (typeof session !== 'undefined' && session && (session.admin || session.is_moderator)) || (_isScrollerAnon && _scrollerAllowedModes.includes('untagged'));
const _scrollerActiveModes = [_scrollerShowSfw ? 'sfw' : null, _scrollerShowNsfw ? 'nsfw' : null, _scrollerShowNsfl ? 'nsfl' : null, _scrollerShowUntagged ? 'untagged' : null].filter(Boolean);
const _scrollerIsSingleMode = _isScrollerAnon && _scrollerActiveModes.length === 1 && !_scrollerShowAll;
const _scrollerSingleMode = _scrollerIsSingleMode ? _scrollerActiveModes[0] : null;
const _scrollerAllowedMimes = (typeof anon_permissions !== 'undefined' && anon_permissions && anon_permissions.allowed_mimes) || ['image', 'video', 'audio', 'flash', 'pdf'];
const _scrollerIsSingleMime = _isScrollerAnon && _scrollerAllowedMimes.length === 1;
const _scrollerSingleMime = _scrollerIsSingleMime ? _scrollerAllowedMimes[0] : null;
@endjs
<div class="filter-section">
<div class="filter-section-label">{{ t('scroller.rating') }}</div>
<div class="pill-group" id="mode-pills">
@if(_scrollerIsSingleMode)
<button class="filter-pill active locked" data-mode="{{ _scrollerSingleMode === 'sfw' ? 0 : (_scrollerSingleMode === 'nsfw' ? 1 : (_scrollerSingleMode === 'nsfl' ? 4 : 2)) }}" style="cursor: not-allowed; pointer-events: none;" title="Locked by site permissions"><i class="fa-solid fa-lock" style="margin-right: 4px;"></i>{{ _scrollerSingleMode.toUpperCase() }}</button>
@else
@if(_scrollerShowSfw)
<button class="filter-pill active" data-mode="0"><i class="fa-solid fa-shield-halved"></i>SFW</button>
@if(session)
@endif
@if(_scrollerShowNsfw)
<button class="filter-pill" data-mode="1"><i class="fa-solid fa-fire"></i>NSFW</button>
@endif
@if(session && enable_nsfl)
@if(_scrollerShowNsfl)
<button class="filter-pill" data-mode="4"><i class="fa-solid fa-skull"></i>NSFL</button>
@endif
@if(session)
@if(_scrollerShowAll)
<button class="filter-pill" data-mode="3">{{ t('scroller.all') }}</button>
@endif
@if(session && (session.admin || session.is_moderator))
@if(_scrollerShowUntagged)
<button class="filter-pill" data-mode="2">{{ t('scroller.untagged') }}</button>
@endif
@endif
</div>
</div>
<div class="filter-section">
<div class="filter-section-label">{{ t('scroller.media_type') }}</div>
<div class="pill-group" id="mime-pills">
@if(_scrollerIsSingleMime)
<button class="filter-pill active locked" data-mime="{{ _scrollerSingleMime }}" style="cursor: not-allowed; pointer-events: none;" title="Locked by site permissions"><i class="fa-solid fa-lock" style="margin-right: 4px;"></i>{{ t('scroller.' + _scrollerSingleMime) || _scrollerSingleMime }}</button>
@else
<button class="filter-pill active" data-mime=""><i class="fa-solid fa-layer-group"></i>{{ t('scroller.all') }}</button>
@if(scroller_mime_cats.includes('video'))
<button class="filter-pill" data-mime="video"><i class="fa-solid fa-film"></i>{{ t('scroller.video') }}</button>
@@ -1141,6 +1179,7 @@
@if(scroller_mime_cats.includes('audio'))
<button class="filter-pill" data-mime="audio"><i class="fa-solid fa-music"></i>{{ t('scroller.audio') }}</button>
@endif
@endif
</div>
</div>
<div class="filter-section">
+52 -17
View File
@@ -1,40 +1,75 @@
<div id="excluded-tags-overlay" style="display: none;">
<div id="excluded-tags-close">&times;</div>
<div class="search-container">
@js
const _isAnonUser = !(typeof session !== 'undefined' && session && session.user && !session.is_anon);
const _canExcludeTags = !_isAnonUser || (typeof anon_permissions !== 'undefined' && anon_permissions && anon_permissions.exclude_tags !== false && anon_permissions.filter !== false);
const _allowedModes = (typeof anon_permissions !== 'undefined' && anon_permissions && anon_permissions.allowed_modes) || ['sfw', 'nsfw', 'untagged', 'all', 'nsfl'];
const _showSfw = !_isAnonUser || _allowedModes.includes('sfw');
const _showNsfw = !_isAnonUser || _allowedModes.includes('nsfw');
const _showNsfl = enable_nsfl && (!_isAnonUser || _allowedModes.includes('nsfl'));
const _showUntagged = (typeof session !== 'undefined' && session && (session.admin || session.is_moderator)) || (_isAnonUser && _allowedModes.includes('untagged'));
const _showAll = !_isAnonUser || _allowedModes.includes('all');
const _activeRatingList = [_showSfw ? 'sfw' : null, _showNsfw ? 'nsfw' : null, _showNsfl ? 'nsfl' : null, _showUntagged ? 'untagged' : null].filter(Boolean);
const _isSingleRating = _isAnonUser && _activeRatingList.length === 1 && !_showAll;
const _singleRating = _isSingleRating ? _activeRatingList[0] : null;
const _allowedMimes = (typeof anon_permissions !== 'undefined' && anon_permissions && anon_permissions.allowed_mimes) || ['image', 'video', 'audio', 'flash', 'pdf'];
const _isSingleMime = _isAnonUser && _allowedMimes.length === 1;
const _singleMime = _isSingleMime ? _allowedMimes[0] : null;
const _hasMimeCats = typeof scroller_mime_cats !== 'undefined' && Array.isArray(scroller_mime_cats);
const _showVideo = _hasMimeCats && scroller_mime_cats.includes('video') && (!_isAnonUser || _allowedMimes.includes('video'));
const _showAudio = _hasMimeCats && scroller_mime_cats.includes('audio') && (!_isAnonUser || _allowedMimes.includes('audio'));
const _showImage = _hasMimeCats && scroller_mime_cats.includes('image') && (!_isAnonUser || _allowedMimes.includes('image'));
const _showFlash = enable_swf && (!_isAnonUser || _allowedMimes.includes('flash'));
@endjs
@if(_canExcludeTags)
<div class="excluded-tags-section-header">
<span class="excluded-tags-section-title"><i class="fa-solid fa-ban"></i> {{ t('filter.excluded_tags') || 'Excluded Tags' }}</span>
<span id="nav_excluded_tags_count" class="badge badge-danger excluded-tags-count-badge" style="display: none;">0</span>
</div>
<div id="nav_excluded_tags_list"></div>
<div class="nav-exclude">
<input type="text" id="nav_exclude_tag_input" class="input" placeholder="{{ t('filter.tag_placeholder') }}" autocomplete="off" enterkeyhint="enter">
<div id="nav_exclude_suggestions" class="tag-suggestions"></div>
</div>
@endif
<!-- modes -->
<div class="mode-filter">
<button id="nav-shuffle-btn" class="shuffle-btn" title="Random Mode — shuffle all items"><span class="shuffle-icon">( - _ - )</span> {{ t('filter.random_mode') }}</button>
<div class="rating-selector" id="rating-selector">
<button class="rating-toggle-btn" data-rating="sfw" id="rating-btn-sfw" title="SFW">SFW</button>
<button class="rating-toggle-btn" data-rating="nsfw" id="rating-btn-nsfw" title="NSFW">NSFW</button>
@if(enable_nsfl)
<button class="rating-toggle-btn" data-rating="nsfl" id="rating-btn-nsfl" title="NSFL">NSFL</button>
<button id="nav-shuffle-btn" class="shuffle-btn" title="Random Mode — shuffle all items"@if(_isSingleRating) disabled style="cursor: not-allowed; opacity: 0.6;" title="Locked by site permissions"@endif><span class="shuffle-icon">( - _ - )</span> {{ t('filter.random_mode') }}</button>
<div class="rating-selector @if(_isSingleRating) locked @endif" id="rating-selector" @if(_isSingleRating) data-locked-rating="{{ _singleRating }}" @endif>
@if(_showSfw)
<button class="rating-toggle-btn @if(_isSingleRating && _singleRating === 'sfw') active locked @endif" data-rating="sfw" id="rating-btn-sfw" title="SFW"@if(_isSingleRating) disabled style="cursor: not-allowed; opacity: 0.85;"@endif>SFW@if(_isSingleRating && _singleRating === 'sfw')<i class="fa-solid fa-lock" style="margin-left: 5px; font-size: 0.75em; opacity: 0.7;"></i>@endif</button>
@endif
@if(session && session.admin || session && session.is_moderator)
<button class="rating-toggle-btn" data-rating="untagged" id="rating-btn-untagged" title="Untagged">UNT</button>
@if(_showNsfw)
<button class="rating-toggle-btn @if(_isSingleRating && _singleRating === 'nsfw') active locked @endif" data-rating="nsfw" id="rating-btn-nsfw" title="NSFW"@if(_isSingleRating) disabled style="cursor: not-allowed; opacity: 0.85;"@endif>NSFW@if(_isSingleRating && _singleRating === 'nsfw')<i class="fa-solid fa-lock" style="margin-left: 5px; font-size: 0.75em; opacity: 0.7;"></i>@endif</button>
@endif
@if(_showNsfl)
<button class="rating-toggle-btn @if(_isSingleRating && _singleRating === 'nsfl') active locked @endif" data-rating="nsfl" id="rating-btn-nsfl" title="NSFL"@if(_isSingleRating) disabled style="cursor: not-allowed; opacity: 0.85;"@endif>NSFL@if(_isSingleRating && _singleRating === 'nsfl')<i class="fa-solid fa-lock" style="margin-left: 5px; font-size: 0.75em; opacity: 0.7;"></i>@endif</button>
@endif
@if(_showUntagged)
<button class="rating-toggle-btn @if(_isSingleRating && _singleRating === 'untagged') active locked @endif" data-rating="untagged" id="rating-btn-untagged" title="Untagged"@if(_isSingleRating) disabled style="cursor: not-allowed; opacity: 0.85;"@endif>UNT@if(_isSingleRating && _singleRating === 'untagged')<i class="fa-solid fa-lock" style="margin-left: 5px; font-size: 0.75em; opacity: 0.7;"></i>@endif</button>
@endif
@if(_showAll && !_isSingleRating)
<button class="rating-toggle-btn rating-toggle-all" id="rating-btn-all" title="Show All Ratings">ALL</button>
@endif
</div>
</div>
<!-- mimes -->
@if(show_mime_picker)
<div class="nav-mime-filter nav-mime-menu" id="nav-mime-menu">
@if(scroller_mime_cats && scroller_mime_cats.includes('video'))
<label class="nav-mime-item"><input type="checkbox" value="video" id="mime-video">{{ t('filter.video') }}</label>
<div class="nav-mime-filter nav-mime-menu" id="nav-mime-menu" @if(_isSingleMime) data-locked-mime="{{ _singleMime }}" @endif>
@if(_showVideo)
<label class="nav-mime-item @if(_isSingleMime) locked @endif" @if(_isSingleMime) style="cursor: not-allowed; opacity: 0.85;" title="Locked by site permissions" @endif><input type="checkbox" value="video" id="mime-video" @if(_isSingleMime) checked disabled @endif>{{ t('filter.video') }}@if(_isSingleMime)<i class="fa-solid fa-lock" style="margin-left: 6px; font-size: 0.8em; opacity: 0.7;"></i>@endif</label>
@endif
@if(scroller_mime_cats && scroller_mime_cats.includes('audio'))
<label class="nav-mime-item"><input type="checkbox" value="audio" id="mime-audio">{{ t('filter.audio') }}</label>
@if(_showAudio)
<label class="nav-mime-item @if(_isSingleMime) locked @endif" @if(_isSingleMime) style="cursor: not-allowed; opacity: 0.85;" title="Locked by site permissions" @endif><input type="checkbox" value="audio" id="mime-audio" @if(_isSingleMime) checked disabled @endif>{{ t('filter.audio') }}@if(_isSingleMime)<i class="fa-solid fa-lock" style="margin-left: 6px; font-size: 0.8em; opacity: 0.7;"></i>@endif</label>
@endif
@if(scroller_mime_cats && scroller_mime_cats.includes('image'))
<label class="nav-mime-item"><input type="checkbox" value="image" id="mime-image">{{ t('filter.image') }}</label>
@if(_showImage)
<label class="nav-mime-item @if(_isSingleMime) locked @endif" @if(_isSingleMime) style="cursor: not-allowed; opacity: 0.85;" title="Locked by site permissions" @endif><input type="checkbox" value="image" id="mime-image" @if(_isSingleMime) checked disabled @endif>{{ t('filter.image') }}@if(_isSingleMime)<i class="fa-solid fa-lock" style="margin-left: 6px; font-size: 0.8em; opacity: 0.7;"></i>@endif</label>
@endif
@if(enable_swf)
<label class="nav-mime-item"><input type="checkbox" value="flash" id="mime-flash">{{ t('filter.flash') }}</label>
@if(_showFlash)
<label class="nav-mime-item @if(_isSingleMime) locked @endif" @if(_isSingleMime) style="cursor: not-allowed; opacity: 0.85;" title="Locked by site permissions" @endif><input type="checkbox" value="flash" id="mime-flash" @if(_isSingleMime) checked disabled @endif>{{ t('filter.flash') }}@if(_isSingleMime)<i class="fa-solid fa-lock" style="margin-left: 6px; font-size: 0.8em; opacity: 0.7;"></i>@endif</label>
@endif
</div>
@endif
+11 -2
View File
@@ -178,7 +178,7 @@
@endif
@if(private_society && !session)
<script>
window.f0ckSession = { logged_in: false, onara: @if(onara) true @else false @endif, enable_item_slugs: @if(enable_item_slugs) true @else false @endif, enable_anonymous_access: @if(enable_anonymous_access) true @else false @endif, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, guest_anonymize: @if(guest_anonymize) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif, public_untagged: @if(public_untagged) true @else false @endif, public_nsfw: @if(public_nsfw) true @else false @endif };
window.f0ckSession = { logged_in: false, is_anon: false, excluded_tags: [], onara: @if(onara) true @else false @endif, enable_item_slugs: @if(enable_item_slugs) true @else false @endif, enable_anonymous_access: @if(enable_anonymous_access) true @else false @endif, anon_permissions: {{ anon_permissions_json || '{}' }}, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, guest_anonymize: @if(guest_anonymize) true @else false @endif, anon_anonymize: @if(anon_anonymize) true @else false @endif, is_anonymized: @if(is_anonymized) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif, public_untagged: @if(public_untagged) true @else false @endif, public_nsfw: @if(public_nsfw) true @else false @endif };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
window.f0ckDefaultThumbSize = "{{ default_thumb_size }}";
(() => {
@@ -422,10 +422,14 @@
public_nsfw: @if(public_nsfw) true @else false @endif,
enable_item_slugs: @if(enable_item_slugs) true @else false @endif,
enable_anonymous_access: @if(enable_anonymous_access) true @else false @endif,
anon_permissions: {{ anon_permissions_json || '{}' }},
strict_mode: @if(session && session.strict_mode) true @else false @endif,
logged_in: @if(session) true @else false @endif,
excluded_tags: @if(session && session.excluded_tags) {{ JSON.stringify(session.excluded_tags) }} @else [] @endif,
hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif,
guest_anonymize: @if(guest_anonymize) true @else false @endif,
anon_anonymize: @if(anon_anonymize) true @else false @endif,
is_anonymized: @if(is_anonymized) true @else false @endif,
use_new_layout: @if(session)@if(session.use_new_layout) true @else false @endif@else @if(default_layout === 'legacy')false @else true @endif@endif,
csrf_token: "{{ csrf_token }}",
default_theme: "{{ default_theme }}",
@@ -477,7 +481,7 @@
allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif,
enable_comment_polls: @if(enable_comment_polls) true @else false @endif,
hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif,
mode: {{ mode !== undefined ? mode : 0 }}
mode: {{ typeof mode !== 'undefined' ? mode : 0 }}
};
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
window.f0ckDefaultThumbSize = "{{ default_thumb_size }}";
@@ -557,6 +561,11 @@
zomg_on: "{{ t('toast.zomg_on') }}",
zomg_off: "{{ t('toast.zomg_off') }}",
copied: "{{ t('toast.copied') }}",
exclude_tag: "{{ t('filter.exclude_tag') || 'Exclude tag' }}",
unexclude_tag: "{{ t('filter.unexclude_tag') || 'Excluded (click to unexclude)' }}",
no_tags_excluded: "{{ t('filter.no_tags_excluded') || 'No tags excluded yet' }}",
tag_excluded_msg: "{{ t('filter.tag_excluded_msg') || 'Tag {tag} added to excluded tags' }}",
tag_unexcluded_msg: "{{ t('filter.tag_unexcluded_msg') || 'Tag {tag} removed from excluded tags' }}",
// actions
fav_added: "{{ t('toast.fav_added') }}",
fav_removed: "{{ t('toast.fav_removed') }}",
+1 -1
View File
@@ -104,7 +104,7 @@
<canvas class="hidden-xs" id="bg"></canvas>
@endif
@include(snippets/navbar)
@if(session)
@if(session || (enable_anonymous_access && anon_permissions.filter))
@include(snippets/excluded-tags-modal)
@endif
+1 -1
View File
@@ -1,5 +1,5 @@
@each(items as item)
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(session || !guest_anonymize){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
+9 -1
View File
@@ -14,7 +14,7 @@
<!-- Nav links: desktop always-on, mobile hidden until toggled -->
<div class="nav-collapse" id="navbarContent">
<div class="nav-links">
@if(!session.is_anon)
@if(!session.is_anon || (enable_anonymous_access && anon_permissions.upload))
<a id="nav-upload-link" style="cursor:pointer;"><i class="fa-solid fa-angle-up"></i> {{ t('nav.upload') }}</a>
@endif
@if(meme_creator)
@@ -118,6 +118,7 @@
<a href="/settings" title="Settings" class="desktop-only"><i class="fa-solid fa-gear"></i></a>
<!-- Filter -->
@if(!session.is_anon || (enable_anonymous_access && anon_permissions.filter))
<a href="#" id="nav-filter-btn" title="Filter" style="position: relative; display: inline-flex; align-items: center; justify-content: center; z-index: 1000;">
<i class="fa-solid fa-filter"></i>
@if(session.mode == 0)
@@ -134,6 +135,7 @@
<span id="nav-filter-badge" class="filter-badge filter-badge-sfw">SFW</span>
@endif
</a>
@endif
<!-- Logout -->
<a href="/logout" title="Logout" class="desktop-only"><i class="fa-solid fa-right-from-bracket"></i></a>
@@ -267,6 +269,12 @@
<a href="/random" id="nav-random" title="Random"><i class="fa-solid fa-shuffle"></i></a>
@endif
<a href="#" id="nav-search-btn" title="Search"><i class="fa-solid fa-magnifying-glass"></i></a>
@if(enable_anonymous_access && anon_permissions.filter)
<a href="#" id="nav-filter-btn" title="Filter" style="position: relative; display: inline-flex; align-items: center; justify-content: center; z-index: 1000;">
<i class="fa-solid fa-filter"></i>
<span id="nav-filter-badge" class="filter-badge filter-badge-sfw">SFW</span>
</a>
@endif
</div>
</div>