ghost rider

This commit is contained in:
2026-09-12 03:11:29 +02:00
parent 155395237b
commit b55a17dc85
25 changed files with 1012 additions and 204 deletions
+21 -4
View File
@@ -10,8 +10,8 @@
<div class="gapLeft"></div>
</div>
@if(enable_item_title)
<div class="item_title">
{!! item.title || '' !!}
<div class="item_title item-title">
<span class="item_title_text" @if(can_manage_item) style="display:none;" @endif>{!! item.title || '' !!}</span>
@if(can_manage_item)
<div class="info-title-edit-wrap">
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
@@ -93,9 +93,10 @@
<div class="blahlol">
@if(user_alternative_infobox)
<div class="user-infobox-block" data-banner-user="{{ item.username || '' }}" style="--author-accent: @if(item.author_color){{ item.author_color }}@else var(--accent) @endif; --author-border: @if(item.author_color){{ item.author_color }}@else var(--accent) @endif;@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">
<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-avatar">
@if(session || !guest_anonymize)
<a href="/user/{{ (item.username || '').toLowerCase() }}">
@if(item.author_avatar_file)
<img src="/a/{{ item.author_avatar_file }}" />
@@ -105,6 +106,11 @@
<img src="/a/default.png" style="border-color: #444;" />
@endif
</a>
@else
<span>
<img src="/a/default.png" style="border-color: #444;" />
</span>
@endif
</div>
<div class="user-infobox-info">
<div class="user-infobox-header">
@@ -119,7 +125,7 @@
</div>
<div class="user-infobox-body">
<div class="user-infobox-description">
{!! item.author_description || '' !!}
@if(session || !guest_anonymize){!! item.author_description || '' !!}@endif
</div>
<div class="user-infobox-actions">
@if(session)
@@ -177,10 +183,12 @@
@endif
@if(is_mod_or_admin)
<i class="iconset fa-solid fa-thumbtack{{ item.is_pinned ? ' active' : '' }}" id="a_pin" data-pinned="{{ item.is_pinned }}" title="{{ item.is_pinned ? 'Unpin from main' : 'Pin to main' }}"></i>
<i class="iconset fa-solid fa-ban{{ item.visibility === 3 ? ' active' : '' }}" id="a_unavailable" data-item-id="{{ item.id }}" data-visibility="{{ item.visibility || 0 }}" title="{{ item.visibility === 3 ? 'Make Available (Public)' : 'Make Unavailable (451)' }}" @if(item.visibility === 3) style="color: var(--danger, #ff4444);" @endif></i>
<i class="iconset fa-solid fa-xmark" id="a_delete" title="Delete"></i>
@endif
@else
<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>
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
@endif
</div>
<span class="badge badge-dark" id="tags" data-can-manage="{{ can_manage_item ? 'true' : 'false' }}">
@@ -295,6 +303,15 @@
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to you and site moderators.</span>
</div>
</label>
@if(is_mod_or_admin)
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
<input type="radio" name="visibility" value="3" style="margin-top: 3px;" />
<div>
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-ban" style="color: var(--danger, #ff4444);"></i> Unavailable (451)</strong>
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to administrators. Serves HTTP 451 on direct media for all other users.</span>
</div>
</label>
@endif
</div>
</form>
</div>
+13 -2
View File
@@ -74,8 +74,8 @@
<div class="gapLeft"></div>
</div>
@if(enable_item_title)
<div class="item_title">
{!! item.title || '' !!}
<div class="item_title item-title">
<span class="item_title_text" @if(can_manage_item) style="display:none;" @endif>{!! item.title || '' !!}</span>
@if(can_manage_item)
<div class="info-title-edit-wrap">
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
@@ -173,10 +173,12 @@
@endif
@if(is_mod_or_admin)
<i class="iconset fa-solid fa-thumbtack{{ item.is_pinned ? ' active' : '' }}" id="a_pin" data-pinned="{{ item.is_pinned }}" title="{{ item.is_pinned ? 'Unpin from main' : 'Pin to main' }}"></i>
<i class="iconset fa-solid fa-ban{{ item.visibility === 3 ? ' active' : '' }}" id="a_unavailable" data-item-id="{{ item.id }}" data-visibility="{{ item.visibility || 0 }}" title="{{ item.visibility === 3 ? 'Make Available (Public)' : 'Make Unavailable (451)' }}" @if(item.visibility === 3) style="color: var(--danger, #ff4444);" @endif></i>
<i class="iconset fa-solid fa-xmark" id="a_delete" title="Delete"></i>
@endif
@else
<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>
<i class="iconset fa-solid fa-triangle-exclamation report-item-btn" data-item-id="{{ item.id }}" title="Report this post"></i>
@endif
</div>
<span id="favs" @if(!item.favorites.length) hidden@endif style="margin-top: 5px;">
@@ -235,6 +237,15 @@
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to you and site moderators.</span>
</div>
</label>
@if(is_mod_or_admin)
<label class="visibility-option" style="display: flex; align-items: flex-start; gap: 12px; padding: 12px; border: 1px solid var(--nav-border-color, #444); border-radius: 6px; cursor: pointer; background: var(--bg-secondary, rgba(255,255,255,0.03));">
<input type="radio" name="visibility" value="3" style="margin-top: 3px;" />
<div>
<strong style="display: block; color: var(--text-color, #fff);"><i class="fa-solid fa-ban" style="color: var(--danger, #ff4444);"></i> Unavailable (451)</strong>
<span style="font-size: 0.85em; color: var(--text-muted, #aaa);">Only visible to administrators. Serves HTTP 451 on direct media for all other users.</span>
</div>
</label>
@endif
</div>
</form>
</div>
+42 -3
View File
@@ -136,10 +136,17 @@ window.loadReports = async function(page = 1) {
'<button class="btn btn-sm btn-danger" onclick="window.resolveReport(' + r.id + ', &quot;rejected&quot;)">Reject</button>';
}
let reporterHtml = '';
if (r.reporter_name) {
reporterHtml = '<a href="/user/' + r.reporter_name + '" style="color: var(--accent); font-weight: bold;">' + r.reporter_name + '</a>' + (r.reporter_ip ? ' <span style="font-size: 0.75rem; color: #888;">(' + r.reporter_ip + ')</span>' : '');
} else {
reporterHtml = '<span style="color: #aaa; font-style: italic;">Guest' + (r.reporter_ip ? ' (' + r.reporter_ip + ')' : '') + '</span>';
}
const tr = document.createElement('tr');
tr.innerHTML =
'<td data-label="ID">' + r.id + '</td>' +
'<td data-label="Reporter"><a href="/user/' + r.reporter_name + '" style="color: var(--accent); font-weight: bold;">' + r.reporter_name + '</a></td>' +
'<td data-label="Reporter">' + reporterHtml + '</td>' +
'<td data-label="Target">' + targetHtml + '</td>' +
'<td data-label="Reason"><span style="opacity: 0.8;">' + r.reason + '</span></td>' +
'<td data-label="Date"><span style="font-size: 0.85rem; color: #888;">' + new Date(r.created_at).toLocaleString() + '</span></td>' +
@@ -255,9 +262,11 @@ window.expandItem = function(e, id) {
}
let buttonsHtml = '';
// Delete Video button only for direct Video reports
// Delete Item and Make Unavailable buttons for direct Item reports
if (isItem && !isComment) {
buttonsHtml += '<button class="btn btn-danger" onclick="window.adminDeleteItem(' + r.resolved_item_id + ')">Delete Item</button>';
const isUnav = r.resolved_item_visibility === 3;
buttonsHtml += ' <button class="btn ' + (isUnav ? 'btn-success' : 'btn-warning') + '" onclick="window.modToggleUnavailable(' + r.resolved_item_id + ', ' + (r.resolved_item_visibility || 0) + ')">' + (isUnav ? 'Make Available' : 'Make Unavailable (451)') + '</button>';
}
if (isComment) {
buttonsHtml += '<button class="btn btn-danger" onclick="window.adminDeleteComment(' + r.comment_id + ')">Delete Comment</button>';
@@ -289,7 +298,7 @@ window.expandItem = function(e, id) {
'<div style="font-weight: bold; opacity: 1; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 1.5px; margin-bottom: 5px;">Moderation Action:</div>' +
'<div style="display: flex; gap: 12px; flex-wrap: wrap; justify-content: center;">' +
buttonsHtml +
'<button class="btn btn-secondary" onclick="window.modWarnUser(' + r.reporter_id + ')">Warn Reporter (' + r.reporter_name + ')</button>' +
(r.reporter_id && r.reporter_name ? '<button class="btn btn-secondary" onclick="window.modWarnUser(' + r.reporter_id + ')">Warn Reporter (' + r.reporter_name + ')</button>' : (r.reporter_ip ? '<span style="color: #888; font-size: 0.85rem;">Reporter: Guest (' + r.reporter_ip + ')</span>' : '')) +
'</div>' +
'</div>' +
'</div>' +
@@ -335,6 +344,36 @@ window.adminDeleteItem = function(id) {
});
};
window.modToggleUnavailable = function(id, currentVis) {
const willBeUnavailable = currentVis !== 3;
const targetVis = willBeUnavailable ? 3 : 0;
const actionText = willBeUnavailable ? 'Make Unavailable (serves HTTP 451 to non-logged in visitors)' : 'Make Available (Public)';
window.ModAction.confirm('Item Visibility', actionText + ' for item #' + id + '?', async () => {
const params = new URLSearchParams();
params.append('postid', id);
params.append('id', id);
params.append('visibility', targetVis);
const res = await fetch('/api/v2/item/visibility', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': window.f0ckSession?.csrf_token
},
body: params
});
const data = await res.json();
if (data.success) {
if (window.showFlash) window.showFlash(willBeUnavailable ? 'Item marked unavailable (451)' : 'Item restored to public', 'success');
const item = window.currentReports.find(x => x.resolved_item_id === id);
if (item) item.resolved_item_visibility = targetVis;
window.loadReports(window.currentPage);
} else {
throw new Error(data.msg || 'Failed to update visibility');
}
});
};
window.modWarnUser = function(userId) {
window.ModAction.confirm('Warn User ID ' + userId, '', async (reason) => {
const params = new URLSearchParams();
+5
View File
@@ -74,6 +74,9 @@
<input type="hidden" id="report-comment-id">
<input type="hidden" id="report-user-id">
<textarea id="report-reason" class="mod-reason" placeholder="{{ t('report.placeholder') }}"></textarea>
@if(recaptcha_enabled && !session)
<div id="modal-report-recaptcha" data-sitekey="{{ recaptcha_site_key }}" style="margin: 10px 0; display: flex; justify-content: center;"></div>
@endif
<div id="report-error" class="error-msg"></div>
<div class="modal-actions">
<button id="report-submit" class="btn-danger">{{ t('report.submit') }}</button>
@@ -496,6 +499,8 @@
reason_required: "{{ t('toast.reason_required') }}",
reason_optional: "{{ t('toast.reason_optional') }}",
reason_required_label: "{{ t('toast.reason_required_label') }}",
captcha_required: "{{ t('toast.captcha_required') }}",
captcha_loading: "{{ t('toast.captcha_loading') }}",
confirm_yes: "{{ t('toast.yes') }}",
confirm_no: "{{ t('toast.no') }}",
confirm_btn: "{{ t('toast.confirm') }}",
+266 -71
View File
@@ -58,20 +58,6 @@
<a href="/user/{!! session.user.toLowerCase() !!}/halls">{{ t('nav.my_halls') }}</a>
@endif
<a href="/user/{{ session.user.toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
@if(session.admin)
<a href="/admin">{{ t('nav.admin') }}
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="notification-dot" title="{{ session.pending_count }} Pending" onclick="event.preventDefault(); window.location.href='/admin/approve';"></span>
@endif
</a>
@endif
@if(session.admin || session.is_moderator)
<a href="/mod">{{ t('nav.mod') }}
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="notification-dot" title="{{ session.pending_count }} Pending" onclick="event.preventDefault(); window.location.href='/mod/approve';"></span>
@endif
</a>
@endif
<a href="/settings" class="mobile-only">{{ t('nav.settings') }}</a>
<div class="nav-user-divider"></div>
<a href="/logout" class="mobile-only">{{ t('nav.logout') }}</a>
@@ -156,63 +142,70 @@
<div id="user-pref-show-motd" style="display:none">@if(session.show_motd !== false)true@else false @endif</div>
</div>
@if(session.admin || session._is_impersonating)
<!-- Admin Bar -->
<div class="admin-bar" id="admin-bar">
@if(session._is_impersonating)
<!-- Impersonation Banner -->
<div class="admin-bar-impersonate-notice">
<span class="admin-bar-imp-icon"><i class="fa-solid fa-user-secret"></i></span>
<span class="admin-bar-imp-text">Viewing as <strong>{!! session.user !!}</strong></span>
<span class="admin-bar-imp-hint">— all actions are performed as this user</span>
<button class="admin-bar-exit-btn" id="admin-bar-exit-impersonate" title="Exit impersonation and return to your admin session">
<i class="fa-solid fa-door-open"></i> Exit
</button>
</nav>
@if(session.admin || session._is_impersonating)
<!-- Admin Bar (Bottom, hotkey-activated) -->
<div class="admin-bar" id="admin-bar">
@if(session._is_impersonating)
<!-- Impersonation Banner -->
<div class="admin-bar-impersonate-notice">
<span class="admin-bar-imp-icon"><i class="fa-solid fa-user-secret"></i></span>
<span class="admin-bar-imp-text">Viewing as <strong>{!! session.user !!}</strong></span>
<span class="admin-bar-imp-hint">— all actions are performed as this user</span>
<button class="admin-bar-exit-btn" id="admin-bar-exit-impersonate" title="Exit impersonation and return to your admin session">
<i class="fa-solid fa-door-open"></i> Exit
</button>
<button class="admin-bar-close-btn" id="admin-bar-close-imp" title="Close admin bar (^ or ~)" aria-label="Close admin bar" style="margin-left: 8px;">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
@else
<!-- Normal Admin Bar -->
<div class="admin-bar-inner">
<div class="admin-bar-brand">
<i class="fa-solid fa-shield-halved"></i>
<span>Admin</span>
</div>
@else
<!-- Normal Admin Bar -->
<div class="admin-bar-inner">
<div class="admin-bar-brand">
<i class="fa-solid fa-shield-halved"></i>
<span>Admin</span>
</div>
<div class="admin-bar-links">
<a href="/admin" class="admin-bar-link" title="Admin panel">
<i class="fa-solid fa-cog"></i> Admin
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="admin-bar-badge">{{ session.pending_count }}</span>
@endif
</a>
<a href="/admin/approve" class="admin-bar-link" title="Pending approvals">
<i class="fa-solid fa-clock"></i> Pending
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="admin-bar-badge">{{ session.pending_count }}</span>
@endif
</a>
<a href="/mod" class="admin-bar-link" title="Moderator panel">
<i class="fa-solid fa-gavel"></i> Mod
</a>
<a href="/admin/audit" class="admin-bar-link" title="Audit log">
<i class="fa-solid fa-scroll"></i> Audit
</a>
</div>
<div class="admin-bar-links">
<a href="/admin" class="admin-bar-link" title="Admin panel">
<i class="fa-solid fa-cog"></i> Admin
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="admin-bar-badge">{{ session.pending_count }}</span>
@endif
</a>
<a href="/admin/approve" class="admin-bar-link" title="Pending approvals">
<i class="fa-solid fa-clock"></i> Pending
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="admin-bar-badge">{{ session.pending_count }}</span>
@endif
</a>
<a href="/mod" class="admin-bar-link" title="Moderator panel">
<i class="fa-solid fa-gavel"></i> Mod
</a>
<a href="/admin/audit" class="admin-bar-link" title="Audit log">
<i class="fa-solid fa-scroll"></i> Audit
</a>
</div>
<!-- Preview As role toggle (right-aligned) -->
<div class="admin-bar-preview" id="admin-bar-preview-wrap" style="margin-left: auto;">
<span class="admin-bar-preview-label"><i class="fa-solid fa-eye" style="opacity:0.6;"></i> Preview:</span>
<div class="admin-bar-preview-pills" id="admin-bar-preview-pills">
<button class="admin-bar-preview-pill active" data-role="admin" title="View as yourself (admin)">Admin</button>
<button class="admin-bar-preview-pill" data-role="mod" title="Simulate moderator view">Mod</button>
<button class="admin-bar-preview-pill" data-role="user" title="Simulate regular user view">User</button>
</div>
<!-- Preview As role toggle (right-aligned) -->
<div class="admin-bar-preview" id="admin-bar-preview-wrap" style="margin-left: auto;">
<span class="admin-bar-preview-label"><i class="fa-solid fa-eye" style="opacity:0.6;"></i> Preview:</span>
<div class="admin-bar-preview-pills" id="admin-bar-preview-pills">
<button class="admin-bar-preview-pill active" data-role="admin" title="View as yourself (admin)">Admin</button>
<button class="admin-bar-preview-pill" data-role="mod" title="Simulate moderator view">Mod</button>
<button class="admin-bar-preview-pill" data-role="user" title="Simulate regular user view">User</button>
</div>
</div>
@endif
<button class="admin-bar-close-btn" id="admin-bar-close" title="Close admin bar (^ or ~)" aria-label="Close admin bar">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
@endif
</nav>
</div>
@endif
<!-- Floating exit-preview pill (visible only during role preview) -->
@if(session.admin)
@@ -380,6 +373,7 @@
@if(session && session.admin)
<div class="shortcut-item"><span><kbd>x</kbd></span><span>{{ t('shortcuts.delete') }}</span></div>
<div class="shortcut-item"><span><kbd>p</kbd></span><span>{{ t('shortcuts.toggle_rating') }}</span></div>
<div class="shortcut-item"><span><kbd>^</kbd> / <kbd>~</kbd></span><span>Toggle Admin Bar</span></div>
@endif
<div class="shortcut-item"><span><kbd>Space</kbd></span><span>{{ t('shortcuts.play_pause') }}</span></div>
<div class="shortcut-item"><span><kbd></kbd> <kbd></kbd></span><span>{{ t('shortcuts.next_prev') }}</span></div>
@@ -483,6 +477,7 @@
window.onRecaptchaModalReady = function() {
_rcLoaded = true;
renderModalRecaptcha();
if (typeof window.renderReportRecaptcha === 'function') window.renderReportRecaptcha();
};
document.addEventListener('DOMContentLoaded', function() {
@@ -506,23 +501,111 @@
<style>
/* ── Admin Bar ──────────────────────────────────────────────────────────────── */
.admin-bar {
width: 100%;
position: fixed;
bottom: 0;
left: var(--admin-bar-left, 0px);
right: var(--admin-bar-right, 300px);
width: auto;
font-family: var(--font, system-ui, sans-serif);
font-size: 0.76rem;
z-index: 900;
box-shadow: 0 2px 8px rgba(0,0,0,0.35);
border-bottom: 1px solid rgba(255, 180, 0, 0.18);
z-index: 100080 !important;
box-shadow: 0 -3px 14px rgba(0, 0, 0, 0.5);
border-top: 1px solid rgba(255, 190, 0, 0.28);
border-bottom: none;
transform: translateY(100%);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.22s ease,
left 0.3s ease-in-out,
right 0.3s ease-in-out;
}
.admin-bar.open {
transform: translateY(0);
opacity: 1;
visibility: visible;
pointer-events: auto;
}
body.onara-modal-open .admin-bar {
z-index: 100080 !important;
filter: none !important;
}
body.onara-modal-open .admin-bar.open {
opacity: 1 !important;
visibility: visible !important;
pointer-events: auto !important;
}
/* Sidebar boundaries via CSS */
body.sidebar-right-hidden .admin-bar {
right: 0 !important;
}
@media (max-width: 999px) {
.admin-bar {
right: min(280px, 80vw);
}
body.sidebar-right-hidden .admin-bar {
right: 0 !important;
}
}
body:not(:has(.global-sidebar-right, .item-sidebar-right, .index-sidebar-right)) .admin-bar {
right: 0 !important;
}
@media (min-width: 1000px) {
body.layout-modern:not(.sidebar-left-hidden):not(.comments-hidden):has(.item-sidebar-left) .admin-bar {
left: min(350px, 30vw);
}
body.layout-modern.sidebar-left-hidden .admin-bar,
body.layout-modern.comments-hidden .admin-bar,
body:has(.item-layout-container.sidebar-hidden) .admin-bar {
left: 0 !important;
}
}
/* Lift bottom pagination smoothly if admin bar is open */
body.admin-bar-open .pagination-container-fluid {
bottom: 32px;
}
.pagination-container-fluid {
transition: right 0.3s ease-in-out, bottom 0.3s ease-in-out !important;
}
.admin-bar-close-btn {
background: transparent;
border: none;
color: rgba(255, 210, 80, 0.45);
cursor: pointer;
padding: 3px 7px;
font-size: 0.8rem;
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 6px;
transition: color 0.15s, background 0.15s;
flex-shrink: 0;
line-height: 1;
}
.admin-bar-close-btn:hover {
color: #ffd844;
background: rgba(255, 200, 0, 0.15);
}
/* Normal admin bar */
.admin-bar-inner {
background: linear-gradient(90deg, #1a1200 0%, #1e1600 40%, #141000 100%);
border-top: 1px solid rgba(255, 190, 0, 0.22);
display: flex;
align-items: center;
gap: 6px;
padding: 0 14px;
height: 30px;
height: 32px;
overflow: hidden;
}
@@ -647,7 +730,7 @@
border: 1px solid rgba(255, 200, 0, 0.3);
border-radius: 4px;
box-shadow: 0 6px 20px rgba(0,0,0,0.5);
z-index: 9999;
z-index: 100090 !important;
display: none;
overflow: hidden;
min-width: 180px;
@@ -846,6 +929,7 @@ body.preview-as-user .admin-pin-btn,
body.preview-as-user .delete-btn,
body.preview-as-user .poll-delete-btn,
body.preview-as-user [data-is-admin] .admin-only { display: none !important; }
body.preview-as-user .item_title_text { display: inline !important; }
/* Hide admin-only actions in mod preview (keep delete — mods can delete) */
body.preview-as-mod #a_rethumb,
@@ -866,6 +950,117 @@ body.preview-as-mod .btn-infobox-action { display: none !important; }
(function() {
'use strict';
/* ── Admin bar: Sidebar Width Awareness & Hotkey Toggle ────────────────── */
const adminBar = document.getElementById('admin-bar');
const closeBtn = document.getElementById('admin-bar-close');
const closeImpBtn = document.getElementById('admin-bar-close-imp');
function syncAdminBarSidebars() {
if (!adminBar) return;
// Right sidebar detection
const rightSb = document.querySelector('.global-sidebar-right, .item-sidebar-right, .index-sidebar-right');
const isRightHidden = !rightSb ||
document.body.classList.contains('sidebar-right-hidden') ||
window.getComputedStyle(rightSb).display === 'none' ||
window.getComputedStyle(rightSb).visibility === 'hidden';
let rw = 0;
if (!isRightHidden && rightSb) {
rw = rightSb.offsetWidth || (window.innerWidth <= 999 ? Math.min(280, Math.round(window.innerWidth * 0.8)) : 300);
}
// Left sidebar detection (modern layout item page)
const leftSb = document.querySelector('.item-sidebar-left');
const isLeftHidden = !leftSb ||
document.body.classList.contains('sidebar-left-hidden') ||
document.body.classList.contains('comments-hidden') ||
window.innerWidth < 1000 ||
window.getComputedStyle(leftSb).display === 'none' ||
window.getComputedStyle(leftSb).visibility === 'hidden' ||
!!document.querySelector('.item-layout-container.sidebar-hidden');
let lw = 0;
if (!isLeftHidden && leftSb) {
lw = leftSb.offsetWidth || Math.min(350, Math.round(window.innerWidth * 0.3));
}
adminBar.style.setProperty('--admin-bar-right', rw + 'px');
adminBar.style.setProperty('--admin-bar-left', lw + 'px');
adminBar.style.right = rw + 'px';
adminBar.style.left = lw + 'px';
}
function openAdminBar() {
if (!adminBar) return;
syncAdminBarSidebars();
adminBar.classList.add('open');
document.body.classList.add('admin-bar-open');
}
function closeAdminBar() {
if (!adminBar) return;
adminBar.classList.remove('open');
document.body.classList.remove('admin-bar-open');
}
function toggleAdminBar() {
if (!adminBar) return;
if (adminBar.classList.contains('open')) {
closeAdminBar();
} else {
openAdminBar();
}
}
if (closeBtn) closeBtn.addEventListener('click', closeAdminBar);
if (closeImpBtn) closeImpBtn.addEventListener('click', closeAdminBar);
window.addEventListener('keydown', (e) => {
// Never trigger if focus is inside an input, textarea, select or contenteditable
const target = e.target;
const tag = (target && target.tagName) ? target.tagName.toUpperCase() : '';
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (target && target.isContentEditable)) {
return;
}
// Hotkey: '^' or '~' (also match Backquote key without modifier keys)
const isHotkey = (
e.key === '^' ||
e.key === '~' ||
(e.code === 'Backquote' && !e.ctrlKey && !e.metaKey && !e.altKey)
);
if (isHotkey) {
e.preventDefault();
toggleAdminBar();
return;
}
// Escape closes admin bar if open
if (e.key === 'Escape' && adminBar && adminBar.classList.contains('open')) {
closeAdminBar();
}
});
// Watch body class changes for sidebar toggles
if (window.MutationObserver) {
const bodyObserver = new MutationObserver(() => {
syncAdminBarSidebars();
});
bodyObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] });
}
window.addEventListener('resize', syncAdminBarSidebars, { passive: true });
document.addEventListener('f0ck:contentLoaded', syncAdminBarSidebars);
document.addEventListener('transitionend', (e) => {
if (e.target && (e.target.classList.contains('global-sidebar-right') ||
e.target.classList.contains('item-sidebar-right') ||
e.target.classList.contains('item-sidebar-left'))) {
syncAdminBarSidebars();
}
});
/* ── Admin bar: View-As (impersonation) ────────────────────────────────── */
const input = document.getElementById('admin-viewas-input');
const suggestBox = document.getElementById('admin-viewas-suggestions');