init f0ckm

This commit is contained in:
2026-04-25 19:51:52 +02:00
commit b646107eb7
241 changed files with 70364 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
<div id="excluded-tags-overlay" style="display: none;">
<div id="excluded-tags-close">&times;</div>
<div class="search-container">
<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>
<!-- 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="mode-selector">
<a href="/mode/0" class="mode-btn @if(mode==0) active @endif">SFW</a>
<a href="/mode/1" class="mode-btn @if(mode==1) active @endif">NSFW</a>
@if(enable_nsfl)
<a href="/mode/4" class="mode-btn @if(mode==4) active @endif">NSFL</a>
@endif
@if(session.admin || session.is_moderator)
<a href="/mode/2" class="mode-btn @if(mode==2) active @endif">UNT</a>
@endif
<a href="/mode/3" class="mode-btn @if(mode==3) active @endif">ALL</a>
</div>
</div>
<!-- mimes -->
@if(show_mime_picker)
<div class="nav-mime-filter nav-mime-menu" id="nav-mime-menu">
<label class="nav-mime-item"><input type="checkbox" value="video" id="mime-video">{{ t('filter.video') }}</label>
<label class="nav-mime-item"><input type="checkbox" value="audio" id="mime-audio">{{ t('filter.audio') }}</label>
<label class="nav-mime-item"><input type="checkbox" value="image" id="mime-image">{{ t('filter.image') }}</label>
<label class="nav-mime-item"><input type="checkbox" value="flash" id="mime-flash">{{ t('filter.flash') }}</label>
</div>
@endif
@if(session && enable_xd_score)
<!-- xD score filter -->
<div class="nav-xd-filter">
<label class="nav-xd-label">{{ t('filter.min_xd_score') }}</label>
<div class="nav-xd-controls">
<input type="range" id="nav_min_xd_score" min="0" max="100" step="1" value="{{ session.min_xd_score || 0 }}" class="xd-slider">
<span id="nav_xd_val" class="xd-slider-val">{{ session.min_xd_score || 0 }}</span>
<span id="nav_xd_tier_label" class="xd-score-badge" style="display:none; font-size: 0.75em;"></span>
</div>
<button id="nav_xd_save_btn" class="mode-btn" style="margin-top: 6px; width: 100%;">{{ t('filter.apply_filter') }}</button>
<div id="nav_xd_status" style="font-size: 0.8em; color: #9f0; min-height: 1em; margin-top: 3px;"></div>
</div>
@endif
</div>
</div>

549
views/snippets/footer.html Normal file
View File

@@ -0,0 +1,549 @@
@if(session && (session.admin || session.is_moderator))
<div id="mod-action-modal" class="modal-overlay" style="display:none;">
<div class="modal-content">
<h3 id="mod-action-title">{{ t('mod.confirm_action') }}</h3>
<div id="mod-action-content"></div>
<textarea id="mod-reason" class="mod-reason" placeholder="{{ t('mod.reason_placeholder') }}"></textarea>
<div id="mod-action-error" class="error-msg"></div>
<div class="modal-actions">
<button id="mod-action-confirm" class="btn-danger">{{ t('mod.confirm') }}</button>
<button id="mod-action-cancel" class="btn-secondary">{{ t('common.cancel') }}</button>
</div>
</div>
</div>
@endif
@if(session)
<div id="halls-modal" class="modal-overlay" style="display:none;">
<div class="modal-content">
<h3>{{ t('hall.modal_title') }}</h3>
@if(session.admin || session.is_moderator)
<div class="form-group" style="margin-bottom: 8px;">
<label for="hall-select" style="display: block; margin-bottom: 5px; font-size:0.83em; color:#888;">{{ t('hall.site_hall_label') }}</label>
<select id="hall-select" class="form-control" style="width: 100%; background: #333; color: #fff; border: 1px solid #444; padding: 5px;">
<option value="">{{ t('hall.choose_hall') }}</option>
</select>
</div>
<div id="halls-modal-error" class="error-msg" style="display:none; color: #ff5555; margin-bottom: 8px;"></div>
<div class="modal-actions" style="display: flex; gap: 10px; justify-content: flex-end; margin-bottom:14px;">
<button id="halls-modal-confirm" class="btn-primary" style="padding: 6px 14px;">{{ t('hall.add_btn') }}</button>
<button id="halls-modal-remove" class="btn-danger" style="padding: 6px 14px; display:none;">{{ t('hall.remove_btn') }}</button>
</div>
<hr style="border-color:rgba(255,255,255,0.1);margin:0 0 12px 0;">
@endif
<div id="my-halls-section">
<label style="display: block; margin-bottom: 5px; font-size:0.83em; color:#888;">{{ t('hall.my_hall_label') }}</label>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<select id="my-hall-select" class="form-control" style="flex:1;min-width:120px;background: #333; color: #fff; border: 1px solid #444; padding: 5px;">
<option value="">{{ t('hall.my_hall_placeholder') }}</option>
</select>
<button id="my-halls-modal-confirm" class="btn-primary" style="padding: 6px 14px; white-space:nowrap;">{{ t('common.add') }}</button>
<button id="my-halls-modal-remove" class="btn-secondary" style="padding: 6px 14px; white-space:nowrap; display:none;">{{ t('common.remove') }}</button>
</div>
<div style="margin-top:6px;display:flex;gap:6px;align-items:center;">
<span style="font-size:0.78em;color:#888;">{{ t('hall.create_new') }}</span>
<input type="text" id="my-hall-new-name" placeholder="{{ t('hall.new_name_placeholder') }}" style="flex:1;background:#2a2a2a;border:1px solid rgba(255,255,255,0.12);color:#fff;padding:3px 8px;border-radius:3px;font-family:var(--font);font-size:0.85em;height:26px;box-sizing:border-box;">
</div>
<div id="my-halls-modal-error" class="error-msg" style="display:none; color: #ff5555; margin-top: 6px; font-size:0.83em;"></div>
</div>
<div class="modal-actions" style="display: flex; gap: 10px; justify-content: flex-end; margin-top:14px;">
<button id="halls-modal-cancel" class="btn-secondary" style="padding: 6px 14px;">{{ t('common.cancel') }}</button>
</div>
</div>
</div>
@endif
@if(show_content_warning)
<div id="content-warning-modal" class="modal-overlay" style="display:none;">
<div class="modal-content content-warning-content">
<h3>{{ t('content_warning.title') }}</h3>
<p>{{ t('content_warning.text') }}</p>
<div class="modal-actions">
<button id="cw-accept" class="btn-danger">{{ t('content_warning.proceed') }}</button>
<button id="cw-decline" class="btn-secondary">{{ t('content_warning.decline') }}</button>
</div>
</div>
</div>
@endif
@if(!private_society || session)
<div id="report-modal" class="modal-overlay" style="display:none;">
<div class="modal-content" style="max-height: 90vh; overflow-y: auto;">
<h3>{{ t('report.title') }}</h3>
<input type="hidden" id="report-item-id">
<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>
<div id="report-error" class="error-msg"></div>
<div class="modal-actions">
<button id="report-submit" class="btn-danger">{{ t('report.submit') }}</button>
<button id="report-cancel" class="btn-secondary">{{ t('common.cancel') }}</button>
</div>
</div>
</div>
<div id="warning-modal" class="modal-overlay" style="display:none; z-index: 10000;">
<div class="modal-content content-warning-content">
<h3 style="color: var(--danger);">{{ t('account_warning.title') }}</h3>
<p>{{ t('account_warning.text') }}</p>
<blockquote id="warning-reason" style="border-left: 4px solid var(--danger); padding-left: 10px; margin: 10px 0; font-style: italic;"></blockquote>
<input type="hidden" id="warning-id">
<div class="modal-actions">
<button id="warning-acknowledge" class="btn-danger">{{ t('account_warning.acknowledge') }}</button>
</div>
</div>
</div>
<div id="image-modal" class="modal-overlay image-modal-overlay">
<div class="image-modal-container">
<img id="image-modal-img" src="" alt="Expanded View">
</div>
</div>
@endif
@if(session)
@include(snippets/metadata-modal)
@endif
@if(!private_society || session)
<div class="global-sidebar-right">
<div class="sidebar-activity">
<div id="sidebar-activity-container" class="sidebar-comments-list">
<div class="loading">{{ t('sidebar.loading_activity') }}</div>
</div>
</div>
<div class="global-sidebar-right-footer">
<a href="/ranking">{{ t('footer.ranking') }}</a>
<a href="/rules">{{ t('footer.rules') }}</a>
<a href="/about">{{ t('footer.about') }}</a>
<div id="help-button" style="color: var(--accent); font-weight: bold; opacity: 0.7;" title="Keyboard Shortcuts">?</div>
</div>
</div>
@endif
@if(!private_society || session)
<script src="/s/js/sanitizer.js?v={{ ts }}"></script>
<script async src="/s/js/theme.js?v={{ ts }}"></script>
@endif
@if(private_society && !session)
<script>
window.f0ckSession = { logged_in: false, 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 };
(() => {
const loginModal = document.getElementById('login-modal');
const registerModal = document.getElementById('register-modal');
const close = (m) => { if (m) m.style.display = 'none'; };
const open = (m) => { if (m) m.style.display = 'flex'; };
// View switching for login/forgot/reset within the login modal
const switchModalView = (view) => {
if (!loginModal) return;
['login', 'forgot', 'reset'].forEach(v => {
const el = document.getElementById('modal-' + v + '-view');
if (el) el.style.display = (v === view) ? 'block' : 'none';
});
};
if (loginModal) {
const loginClose = document.getElementById('login-modal-close');
if (loginClose) loginClose.addEventListener('click', () => close(loginModal));
loginModal.addEventListener('click', (e) => { if (e.target === loginModal) close(loginModal); });
// Forgot Password link
const modalForgotBtn = document.getElementById('modal-forgot-btn');
if (modalForgotBtn) {
modalForgotBtn.addEventListener('click', (e) => { e.preventDefault(); switchModalView('forgot'); });
}
const forgotToLogin = document.getElementById('forgot-to-login');
if (forgotToLogin) {
forgotToLogin.addEventListener('click', (e) => { e.preventDefault(); switchModalView('login'); });
}
// Check for reset token or login flag in URL
const urlParams = new URLSearchParams(window.location.search);
const resetToken = urlParams.get('token');
if (resetToken) {
const tokenInput = document.getElementById('reset-token');
if (tokenInput) {
tokenInput.value = resetToken;
switchModalView('reset');
loginModal.style.display = 'flex';
const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]token=[^&]+/, '').replace(/[?&]$/, '') + window.location.hash;
window.history.replaceState({}, '', cleanUrl);
}
} else if (urlParams.get('login') === '1') {
switchModalView('login');
loginModal.style.display = 'flex';
const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]login=1/, '').replace(/[?&]$/, '') + window.location.hash;
window.history.replaceState({}, '', cleanUrl);
}
// Login form AJAX
const loginForm = loginModal.querySelector('.login-form');
if (loginForm && loginForm.id !== 'forgot-password-form' && loginForm.id !== 'reset-password-form') {
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(loginForm);
if (formData.get('password') && formData.get('password').length < 20) {
let errDiv = loginForm.querySelector('.flash-error');
if (!errDiv) { errDiv = document.createElement('div'); errDiv.className = 'flash-error'; loginForm.insertBefore(errDiv, loginForm.firstChild); }
errDiv.textContent = 'Invalid username or password.';
return;
}
try {
const res = await fetch('/login', {
method: 'POST',
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(new FormData(loginForm))
});
if (res.redirected) { window.location.href = res.url; return; }
const json = await res.json();
if (json && json.success === false) {
let errDiv = loginForm.querySelector('.flash-error');
if (!errDiv) { errDiv = document.createElement('div'); errDiv.className = 'flash-error'; loginForm.insertBefore(errDiv, loginForm.firstChild); }
errDiv.textContent = json.msg;
}
} catch (err) { console.error('Login error:', err); }
});
}
// Forgot Password form AJAX
const forgotForm = document.getElementById('forgot-password-form');
if (forgotForm) {
forgotForm.addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('forgot-email').value;
const status = document.getElementById('forgot-status');
const btn = forgotForm.querySelector('button');
btn.disabled = true; btn.textContent = 'Sending...';
if (status) { status.textContent = ''; status.className = ''; }
try {
const res = await fetch('/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },
body: new URLSearchParams({ email })
});
const data = await res.json();
if (data.success) {
if (status) { status.textContent = data.msg || 'Success! Check your email.'; status.className = 'flash-success'; }
forgotForm.reset();
} else {
if (status) { status.textContent = data.msg || 'Error sending link.'; status.className = 'flash-error'; }
}
} catch (err) {
if (status) { status.textContent = 'Network error.'; status.className = 'flash-error'; }
} finally { btn.disabled = false; btn.textContent = 'Send Reset Link'; }
});
}
// Reset Password form AJAX
const resetForm = document.getElementById('reset-password-form');
if (resetForm) {
const resetToLogin = document.getElementById('reset-to-login');
resetForm.addEventListener('submit', async (e) => {
e.preventDefault();
const token = document.getElementById('reset-token').value;
const password = document.getElementById('reset-password').value;
const password_confirm = document.getElementById('reset-password-confirm').value;
const status = document.getElementById('reset-status');
const btn = resetForm.querySelector('button');
if (password !== password_confirm) {
if (status) { status.textContent = 'Passwords do not match.'; status.className = 'flash-error'; }
return;
}
if (password.length < 20) {
if (status) { status.textContent = 'Password is too short (minimum 20 characters).'; status.className = 'flash-error'; }
return;
}
btn.disabled = true; btn.textContent = 'Updating...';
if (status) { status.textContent = ''; status.className = ''; }
try {
const res = await fetch('/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' },
body: new URLSearchParams({ token, password, password_confirm })
});
const data = await res.json();
if (data.success) {
if (status) { status.textContent = data.msg || 'Password updated successfully!'; status.className = 'flash-success'; }
resetForm.reset();
btn.style.display = 'none';
if (resetToLogin) resetToLogin.style.display = 'inline-block';
} else {
if (status) { status.textContent = data.msg || 'Error resetting password.'; status.className = 'flash-error'; }
}
} catch (err) {
if (status) { status.textContent = 'Network error.'; status.className = 'flash-error'; }
} finally { btn.disabled = false; btn.textContent = 'Update Password'; }
});
if (resetToLogin) {
resetToLogin.addEventListener('click', (e) => {
e.preventDefault();
switchModalView('login');
resetToLogin.style.display = 'none';
resetForm.querySelector('button').style.display = 'inline-block';
});
}
}
}
if (registerModal) {
const regClose = document.getElementById('register-modal-close');
if (regClose) regClose.addEventListener('click', () => close(registerModal));
registerModal.addEventListener('click', (e) => { if (e.target === registerModal) close(registerModal); });
}
// ESC key closes modals
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { close(loginModal); close(registerModal); }
});
// Auto-open login modal if URL has #login hash
if (window.location.hash === '#login' && loginModal) {
switchModalView('login');
loginModal.style.display = 'flex';
}
const toRegister = document.getElementById('login-to-register');
if (toRegister) toRegister.addEventListener('click', (e) => { e.preventDefault(); close(loginModal); open(registerModal); });
const toLogin = document.getElementById('register-to-login');
if (toLogin) toLogin.addEventListener('click', (e) => { e.preventDefault(); close(registerModal); open(loginModal); });
// Registration form AJAX
const regForm = document.getElementById('modal-register-form');
if (regForm) {
regForm.addEventListener('submit', async (e) => {
e.preventDefault();
const status = document.getElementById('register-status');
const formData = new FormData(regForm);
const btn = regForm.querySelector('button[type="submit"]');
const password = formData.get('password');
const password_confirm = formData.get('password_confirm');
if (password && password.length < 20) {
if (status) {
status.textContent = 'Password is too short (minimum 20 characters).';
status.className = 'flash-error';
}
return;
}
if (password !== password_confirm) {
if (status) {
status.textContent = 'Passwords do not match.';
status.className = 'flash-error';
}
return;
}
btn.disabled = true; btn.textContent = 'Creating...';
try {
const resp = await fetch('/register', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' },
body: new URLSearchParams(new FormData(regForm)),
credentials: 'include'
});
const json = await resp.json();
if (status) { status.textContent = json.msg || ''; status.className = json.success ? 'flash-success' : 'flash-error'; }
if (json.success) setTimeout(() => { close(registerModal); open(loginModal); }, 2000);
} catch (err) {
if (status) { status.textContent = 'Network error.'; status.className = 'flash-error'; }
} finally { btn.disabled = false; btn.textContent = 'Create Account'; }
});
}
})();
</script>
@else
<script src="/s/js/v0ck.js?v={{ ts }}"></script>
<script src="/s/js/danmaku.js?v={{ ts }}"></script>
@if(enable_swf && typeof item !== "undefined" && item.mime === 'application/x-shockwave-flash')
{{-- Ruffle: self-hosted. Place ruffle.js + wasm bundle in public/s/ruffle/ --}}
<script src="/s/ruffle/ruffle.js"></script>
@elseif(enable_swf && typeof item !== "undefined" && item.mime === 'application/vnd.adobe.flash.movie')
<script src="/s/ruffle/ruffle.js"></script>
@endif
<script>
window.f0ckSession = {
strict_mode: @if(session && session.strict_mode) true @else false @endif,
logged_in: @if(session) 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 }}",
private_society: @if(private_society) true @else false @endif,
show_content_warning: @if(show_content_warning) true @else false @endif,
disable_autoplay: @if(session && session.disable_autoplay) true @else false @endif,
disable_swiping: @if(session && session.disable_swiping) true @else false @endif,
show_background: @if(session)@if(session.show_background !== false) true @else false @endif@else @if(show_background_cfg) true @else false @endif@endif,
nsfl_tag_id: {{ nsfl_tag_id }},
user: @if(session) "{{ session.user }}" @else null @endif,
display_name: @if(session && session.display_name) "{!! session.display_name !!}" @else null @endif,
is_admin: @if(session && session.admin) true @else false @endif,
is_moderator: @if(session && session.is_moderator) true @else false @endif,
enable_swf: @if(enable_swf) true @else false @endif,
enable_danmaku: @if(enable_danmaku) true @else false @endif,
enable_global_chat: @if(enable_global_chat) true @else false @endif,
ruffle_volume: @if(session && session.ruffle_volume !== undefined && session.ruffle_volume !== null) {{ session.ruffle_volume }} @else 0.5 @endif,
ruffle_background: @if(session && session.ruffle_background !== false) true @else false @endif,
quote_emojis: @if(session && session.quote_emojis !== false) true @else false @endif,
embed_youtube_in_comments: @if(session && session.embed_youtube_in_comments !== false) true @else false @endif,
avatar: @if(session && session.avatar) {{ session.avatar }} @else null @endif,
avatar_file: @if(session && session.avatar_file) "{{ session.avatar_file }}" @else null @endif
};
window.f0ckI18n = {
write_comment: "{{ t('comments.write_comment') }}",
post: "{{ t('comments.post') }}",
cancel: "{{ t('comments.cancel') }}",
sidebar_view: "{{ t('sidebar.view') }}",
sidebar_read_more: "{{ t('sidebar.read_more') }}",
sidebar_see_less: "{{ t('sidebar.see_less') }}",
select_file: "{{ t('upload_btn.select_file') }}",
enter_url: "{{ t('upload_btn.enter_url') }}",
tags_required: "{{ t('upload_btn.tags_required') }}",
select_rating: "{{ t('upload_btn.select_rating') }}",
embed_youtube: "{{ t('upload_btn.embed_youtube') }}",
upload_from_url: "{{ t('upload_btn.upload_from_url') }}",
upload: "{{ t('upload_btn.upload') }}",
uploading: "{{ t('upload.uploading') }}",
processing: "{{ t('toast.processing') }}",
upload_await_approval: "{{ t('upload.pending_approval_patient') }}",
// timeago
timeago_just_now: "{{ t('timeago.just_now') }}",
timeago_year: "{{ t('timeago.year') }}",
timeago_years: "{{ t('timeago.years') }}",
timeago_month: "{{ t('timeago.month') }}",
timeago_months: "{{ t('timeago.months') }}",
timeago_day: "{{ t('timeago.day') }}",
timeago_days: "{{ t('timeago.days') }}",
timeago_hour: "{{ t('timeago.hour') }}",
timeago_hours: "{{ t('timeago.hours') }}",
timeago_minute: "{{ t('timeago.minute') }}",
timeago_minutes: "{{ t('timeago.minutes') }}",
timeago_second: "{{ t('timeago.second') }}",
timeago_seconds: "{{ t('timeago.seconds') }}",
timeago_ago: "{{ t('timeago.ago') }}",
lang: "{{ lang }}",
// search overlay
search_placeholder: "{{ t('search_overlay.placeholder') }}",
search_strict_mode: "{{ t('search_overlay.strict_mode') }}",
// toasts
report_success: "{{ t('toast.report_success') }}",
report_error: "{{ t('toast.report_error') }}",
network_error: "{{ t('toast.network_error') }}",
network_error_short: "{{ t('toast.network_error_short') }}",
reason_required: "{{ t('toast.reason_required') }}",
reason_optional: "{{ t('toast.reason_optional') }}",
reason_required_label: "{{ t('toast.reason_required_label') }}",
confirm_yes: "{{ t('toast.yes') }}",
confirm_no: "{{ t('toast.no') }}",
confirm_btn: "{{ t('toast.confirm') }}",
cancel_btn: "{{ t('toast.cancel') }}",
add_tag: "{{ t('toast.add_tag') }}",
item_pinned: "{{ t('toast.item_pinned') }}",
item_unpinned: "{{ t('toast.item_unpinned') }}",
error_adding_tag: "{{ t('toast.error_adding_tag') }}",
error_saving: "{{ t('toast.error_saving') }}",
zomg_on: "{{ t('toast.zomg_on') }}",
zomg_off: "{{ t('toast.zomg_off') }}",
copied: "{{ t('toast.copied') }}",
// actions
fav_added: "{{ t('toast.fav_added') }}",
fav_removed: "{{ t('toast.fav_removed') }}",
subscribed_thread: "{{ t('toast.subscribed_thread') }}",
unsubscribed_thread: "{{ t('toast.unsubscribed_thread') }}",
oc_marked: "{{ t('toast.oc_marked') }}",
oc_removed: "{{ t('toast.oc_removed') }}",
no_tags_excluded: "{{ t('toast.no_tags_excluded') }}",
tag_delete_title: "{{ t('toast.tag_delete_title') }}",
tag_delete_confirm: "{{ t('toast.tag_delete_confirm') }}",
// halls
hall_added: "{{ t('toast.hall_added') }}",
hall_removed: "{{ t('toast.hall_removed') }}",
hall_my_placeholder: "{{ t('hall.my_hall_placeholder') }}",
hall_no_halls: "{{ t('hall.no_halls') }}",
hall_select_a_hall: "{{ t('hall.select_a_hall') }}",
hall_choose_or_create: "{{ t('hall.choose_or_create') }}",
hall_creating: "{{ t('hall.creating') }}",
hall_adding: "{{ t('hall.adding') }}",
hall_removing: "{{ t('hall.removing') }}",
hall_saving: "{{ t('hall.saving') }}",
hall_deleting: "{{ t('hall.deleting') }}",
hall_saved: "{{ t('hall.saved') }}",
hall_created: "{{ t('hall.created') }}",
hall_add_btn: "{{ t('hall.add_btn') }}",
hall_remove_btn: "{{ t('hall.remove_btn') }}",
hall_name_empty: "{{ t('hall.enter_name_error') }}",
hall_enter_name_error: "{{ t('hall.enter_name_error') }}",
hall_slug_empty_error: "{{ t('hall.slug_empty_error') }}",
hall_image_uploaded: "{{ t('hall.image_uploaded') }}",
hall_image_removed: "{{ t('hall.image_removed') }}",
hall_click_upload_hint: "{{ t('hall.click_upload_hint') }}",
// notifications
notif_upload_approved: "{{ t('notifications.upload_approved_short') }}",
notif_upload_pending: "{{ t('notifications.upload_pending_short') }}",
notif_new_report: "{{ t('notifications.new_report_short') }}",
notif_upload_denied: "{{ t('notifications.upload_denied_short') }}",
notif_upload_deleted: "{{ t('notifications.upload_deleted_short') }}",
notif_click_reason: "{{ t('notifications.click_reason') }}",
notif_no_reason: "{{ t('notifications.no_reason') }}",
notif_reason_label: "{{ t('notifications.reason_label') }}",
notif_replied: "{{ t('notifications.replied_short') }}",
notif_subscribed: "{{ t('notifications.subscribed_short') }}",
notif_mentioned: "{{ t('notifications.mentioned_short') }}",
notif_commented: "{{ t('notifications.commented') }}",
notif_system: "{{ t('notifications.system') }}",
notif_admin: "{{ t('notifications.admin') }}",
notif_moderation: "{{ t('notifications.moderation') }}",
no_notifications: "{{ t('nav.no_notifications') }}",
// meme creator
meme: {
text_layer: "{{ t('meme.text_layer') }}",
enter_text: "{{ t('meme.enter_text') }}",
size_label: "{{ t('meme.size_label') }}",
upload_btn: "{{ t('meme.upload_btn') }}"
},
// global chat
chat_title: "{{ t('chat.title') }}",
chat_placeholder: "{{ t('chat.placeholder') }}",
chat_minimize: "{{ t('chat.minimize') }}",
chat_expand: "{{ t('chat.expand') }}",
chat_slow_down: "{{ t('chat.slow_down') }}",
chat_error_send: "{{ t('chat.error_send') }}",
chat_network_error: "{{ t('chat.network_error') }}"
};
</script>
<script src="/s/js/f0ckm.js?v={{ ts }}"></script>
<script src="/s/js/sidebar-activity.js?v={{ ts }}"></script>
<script src="/s/js/flash_yank.js?v={{ ts }}"></script>
@if(show_koepfe && !(session && session.hide_koepfe))
<script>window.f0ckKoepfe = {{ koepfe_json }};</script>
<script src="/s/js/koepfe.js?v={{ ts }}"></script>
@endif
@if(session)
<script src="/s/js/upload.js?v={{ ts }}"></script>
<script src="/s/js/tag_autocomplete.js?v={{ ts }}"></script>
<script src="/s/js/mention_autocomplete.js?v={{ ts }}"></script>
<script src="/s/js/user.js?v={{ ts }}"></script>
@endif
@if(enable_global_chat && session)
<script src="/s/js/globalchat.js?v={{ ts }}"></script>
@endif
@if(session && private_messages)
<script src="/s/js/wordlist.js?v={{ ts }}"></script>
<script src="/s/js/messages.js?v={{ ts }}"></script>
@endif
@if(session && (session.admin || session.is_moderator))
<script src="/s/js/admin.js?v={{ ts }}"></script>
@endif
@endif
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then((registration) => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, (err) => {
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
</body>
</html>

127
views/snippets/header.html Normal file
View File

@@ -0,0 +1,127 @@
<!doctype html>
<html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined'){{ theme }}@endif" res="@if(typeof fullscreen !== 'undefined'){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif">
<head>
@if(typeof page_meta !== 'undefined' && page_meta.title)<title>{{ domain }} - {{ page_meta.title }}</title>@elseif(typeof item !== 'undefined')<title>{{ domain }} - {{ item.id }}</title>@else<title>{{ domain }}</title>@endif
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#0096ff">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="{{ domain }}">
<style>
html { background-color: #000; color: #fff; }
@if(session && session.font)
{{ "@" }}font-face {
font-family: 'CustomUserFont';
src: url('/s/fonts/{{ session.font }}');
}
@endif
:root {
@if(session && session.font)
--font: 'CustomUserFont', monospace !important;
@endif
}
@if(session && session.font)
*:not(canvas):not(.fa-solid):not(.fa-regular):not(.fa-brands):not(.fa-light):not(.fa-thin):not(.fa-duotone):not([class*=" fa-"]):not([class^="fa-"]) {
font-family: 'CustomUserFont', monospace !important;
}
/* Explicitly protect FA pseudo-elements */
.fa-solid::before, .fa-regular::before, .fa-brands::before,
.fas::before, .far::before, .fab::before, .fa::before {
font-family: "Font Awesome 6 Free", "Font Awesome 6 Brands" !important;
}
@endif
</style>
<link rel="preload" href="/s/vcr.ttf" as="font" type="font/ttf" crossorigin>
<link rel="stylesheet" href="/s/fa/all.min.css">
<link rel="icon" @if(custom_favicon && custom_favicon.length > 0)href="{{ custom_favicon }}"@else type="image/gif" href="/s/img/favicon.gif"@endif />
@if(!private_society || session)
@if(development || (private_society && !session))
<link rel="stylesheet" href="/s/css/f0ckm.css?v={{ ts }}">
@endif
@if(development)
<link rel="stylesheet" href="/s/css/v0ck.css?v={{ ts }}">
@else
<link rel="stylesheet" href="/s/css/bundle.css?v={{ ts }}">
@endif
<link rel="stylesheet" href="/s/css/upload.css?v={{ ts }}">
@endif
<script>window.f0ckThemes = {{ themes_json }}; window.f0ckDefaultTheme = "{{ default_theme }}"; window.f0ckDomain = "{{ domain }}"; window.f0ckGitHash = "{{ git_hash }}"; window.f0ckAllowedImages = {{ allowed_comment_images_json }}; window.f0ckEmbedYoutubeInComments = {{ embed_youtube_in_comments ? 'true' : 'false' }}; window.f0ckEnableYoutubeUpload = {{ enable_youtube_upload ? 'true' : 'false' }}; window.f0ckBrandImages = {{ custom_brand_images_json }};</script>
@if(!private_society || session)
<script src="/s/js/marked.min.js" defer></script>
<script src="/s/js/comments.js?v={{ ts }}" defer></script>
@endif
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@if(typeof item !== 'undefined')
<link rel="canonical" href="https://{{ domain }}/{{ item.id }}" />
<meta property="og:site_name" content="{{ domain }}" />
<meta property="og:title" content="{{ item.id }}" />
<meta property="og:url" content="https://{{ domain }}/{{ item.id }}" />
<meta property="og:image" content="https://{{ domain }}{{ item.og_thumbnail }}" />
<meta name="description" content="{{ site_description }}" />
<meta property="og:description" content="{{ site_description }}" />
<meta property="og:type" content="website" />
<meta property="twitter:card" content="summary" />
<meta property="twitter:title" content="{{ item.id }}" />
<meta property="twitter:image" content="https://{{ domain }}{{ item.og_thumbnail }}" />
<meta property="twitter:url" content="https://{{ domain }}/{{ item.id }}" />
@else
<meta property="og:site_name" content="{{ domain }}" />
<meta property="og:title" content="@if(typeof page_meta !== 'undefined' && page_meta.title){{ page_meta.title }} - {{ domain }}@else{{ domain }}@endif" />
<meta property="og:url" content="@if(typeof page_meta !== 'undefined' && page_meta.url){{ page_meta.url }}@elsehttps://{{ domain }}@endif" />
<meta property="og:image" content="@if(typeof page_meta !== 'undefined' && page_meta.image){{ page_meta.image }}@else@if(custom_favicon && custom_favicon.length > 0){{ custom_favicon }}@elsehttps://{{ domain }}/s/img/favicon.gif@endif@endif" />
<meta name="description" content="@if(typeof page_meta !== 'undefined' && page_meta.description){{ page_meta.description }}@else{{ site_description }}@endif" />
<meta property="og:description" content="@if(typeof page_meta !== 'undefined' && page_meta.description){{ page_meta.description }}@else{{ site_description }}@endif" />
<meta property="og:type" content="website" />
@endif
</head>
<body class="@if(session)@if(session.use_new_layout)layout-modern@else layout-legacy @endif@else @if(default_layout === 'legacy')layout-legacy@else layout-modern@endif @endif @if(private_society && !session)private-gate-active@endif">
<script>if(localStorage.getItem('sidebarRightHidden')==='true')document.body.classList.add('sidebar-right-hidden');</script>
@if(!private_society || session)
<canvas class="hidden-xs" id="bg"></canvas>
@endif
@include(snippets/navbar)
@if(session)
@include(snippets/excluded-tags-modal)
@endif
@if(session && session.force_password_change)
<style>
#main, footer, .global-sidebar-right, .navbar, #bg {
display: none !important;
}
body {
background: #000 !important;
overflow: hidden !important;
}
</style>
<div id="force-password-modal" class="modal-overlay" style="display: flex; align-items: center; justify-content: center; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10000; background: #000;">
<div class="modal-content" style="background: #1a1a1a; padding: 40px; border-radius: 12px; width: 100%; max-width: 500px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); border: 1px solid rgba(0, 150, 255, 0.3);">
<h2 style="margin-top: 0; color: #fff; font-weight: 800; letter-spacing: -0.5px;">{{ t('auth.password_change_required') }}</h2>
<p style="color: #ccc; line-height: 1.6;">{{ t('auth.password_change_desc') }}</p>
<form id="force-password-form" style="margin-top: 30px;">
<div class="form-group" style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 8px; font-size: 0.8rem; color: #888; text-transform: uppercase;">{{ t('auth.new_password_label') }}</label>
<input type="password" id="force_new_password" required minlength="20" placeholder="{{ t('auth.min_chars_placeholder') }}"
style="width: 100%; padding: 12px 15px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: #fff; outline: none; transition: border-color 0.2s;">
</div>
<div class="form-group" style="margin-bottom: 25px;">
<label style="display: block; margin-bottom: 8px; font-size: 0.8rem; color: #888; text-transform: uppercase;">{{ t('auth.confirm_password_label') }}</label>
<input type="password" id="force_new_password_confirm" required minlength="20" placeholder="{{ t('auth.confirm_placeholder') }}"
style="width: 100%; padding: 12px 15px; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: #fff; outline: none; transition: border-color 0.2s;">
</div>
<div id="force-password-status" style="margin-bottom: 15px; font-size: 0.85rem; display: none; padding: 10px; border-radius: 4px;"></div>
<button type="submit" style="width: 100%; padding: 15px; background: var(--accent, #0096ff); color: #000; border: 0; border-radius: 6px; font-weight: 800; cursor: pointer; text-transform: uppercase; letter-spacing: 1px;">{{ t('auth.update_password_btn') }}</button>
</form>
<div style="margin-top: 20px; text-align: center;">
<a href="/logout" style="color: #666; font-size: 0.8rem; text-decoration: none;">{{ t('auth.or_logout') }}</a>
</div>
</div>
</div>
@endif

View File

@@ -0,0 +1,30 @@
@if(item.mime === 'video/youtube')
<div class="embed-responsive embed-responsive-16by9">
<iframe id="yt-embed" class="embed-responsive-item" width="640" height="360" src="https://www.youtube.com/embed/{!! item.dest.replace('yt:', '') !!}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</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="metadata" data-size="{{ item.size }}" loop playsinline></video>
</div>
@elseif(item.mime.startsWith("audio"))
<div class="embed-responsive embed-responsive-16by9" style="background: url('@if(item.coverart)//w0bm.com{{ item.coverart }}@else/s/img/200.gif@endif') no-repeat center / contain black;">
<audio id="my-video" class="embed-responsive-item" autoplay loop src="{{ item.dest }}" data-setup="{}" data-size="{{ item.size }}" poster="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" type="{{ item.mime }}"></audio>
<img id="f0ck-audio-cover" src="@if(item.coverart){{ item.coverart }}@else/s/img/200.gif@endif" style="display: none;">
</div>
@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>
</div>
</div>
@elseif(item.mime === 'application/x-shockwave-flash' && enable_swf)
<div class="embed-responsive embed-responsive-16by9">
<div id="ruffle-container" class="embed-responsive-item" data-swf="{{ item.dest }}"></div>
</div>
@elseif(item.mime === 'application/vnd.adobe.flash.movie' && enable_swf)
<div class="embed-responsive embed-responsive-16by9">
<div id="ruffle-container" class="embed-responsive-item" data-swf="{{ item.dest }}"></div>
</div>
@else
<h1>404 - Not f0cked</h1>
@endif

View File

@@ -0,0 +1,16 @@
@each(items as item)
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" 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')) }}" style="background-image: url('/t/{{ item.id }}.webp')">
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
@endif
@if(item.is_oc)
<span class="oc-indicator anim">OC</span>
@endif
@if(enable_xd_score && item.xd_score > 0)
<span class="thumb-xd-indicator xd-tier-{{ item.xd_score >= 60 ? 5 : (item.xd_score >= 30 ? 4 : (item.xd_score >= 15 ? 3 : (item.xd_score >= 5 ? 2 : 1))) }}" title="xD Score: {{ item.xd_score }}">xD</span>
@endif
</div>
<p></p>
</a>
@endeach

View File

@@ -0,0 +1,26 @@
<div id="metadata-modal" class="modal-overlay" style="display: none;">
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3><i class="fa-solid fa-magic"></i> {{ t('metadata_modal.title') }}</h3>
</div>
<div class="modal-body" style="padding: 20px 0;">
<div id="metadata-loading" style="text-align: center; padding: 20px;">
<i class="fa fa-spinner fa-spin fa-2x"></i>
<p>{{ t('metadata_modal.loading') }}</p>
</div>
<div id="metadata-results" style="display: none;">
<p style="font-size: 0.9em; color: var(--accent); margin-bottom: 12px; font-weight: 600; text-transform: uppercase;">{{ t('metadata_modal.found') }}</p>
<div class="meta-suggestions-list" id="metadata-suggestion-list">
<!-- Pills injected here -->
</div>
<div id="metadata-no-results" style="display: none; text-align: center; color: #888; padding: 10px;">
{{ t('metadata_modal.no_results') }}
</div>
</div>
<div id="metadata-error" style="display: none; color: #e74c3c; padding: 10px; text-align: center;"></div>
</div>
<div class="modal-actions" style="display: flex; justify-content: flex-end; gap: 10px;">
<button class="btn-secondary" id="metadata-modal-cancel">{{ t('common.close') }}</button>
</div>
</div>
</div>

369
views/snippets/navbar.html Normal file
View File

@@ -0,0 +1,369 @@
@if(session)
<!-- logged in -->
<nav class="navbar navbar-expand-lg">
<!-- Brand: always visible, flush left -->
<a class="navbar-brand" href="/">
@if(custom_brand_image)
<img id="navbar-logo" src="{{ custom_brand_image }}" alt="{{ domain }}" style="max-height: 40px; vertical-align: middle; max-width: 180px; width: initial;">
@else
<span class="f0ck">{{ domain }}</span>
@endif
</a>
<!-- Nav links: desktop always-on, mobile hidden until toggled -->
<div class="nav-collapse" id="navbarContent">
<div class="nav-links">
<a id="nav-upload-link" style="cursor:pointer;"><i class="fa-solid fa-upload"></i> {{ t('nav.upload') }}</a>
@if(meme_creator)
<a href="/meme">{{ t('nav.meme') }}</a>
@endif
<div class="nav-user-dropdown nav-halls-dropdown">
<a href="/halls" class="nav-halls-btn" title="{{ t('nav.halls') }}">
<i class="fa-solid fa-building-columns"></i>
</a>
</div>
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tag"></i></a>
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dungeon"></i></a>
<a href="#" id="nav-search-btn" title="{{ t('nav.search') }}"><i class="fa-solid fa-magnifying-glass"></i></a>
@if(!/^\/\d$/.test(url.pathname))
<a href="/random" id="nav-random" title="{{ t('nav.random') }}"><i class="fa-solid fa-shuffle"></i></a>
@endif
</div>
</div>
<!-- Right controls: always visible, flush right -->
<div class="nav-right-group">
<!-- Avatar dropdown -->
<div class="nav-user-dropdown">
<button class="nav-user-btn nav-avatar-btn" id="nav-user-toggle">
@if(session.avatar_file)
<img class="nav-avatar-img" src="/a/{{ session.avatar_file }}" alt="{{ session.user }}" onerror="this.src='/a/default.png'">
@elseif(session.avatar && session.avatar > 0)
<img class="nav-avatar-img" src="/t/{{ session.avatar }}.webp" alt="{{ session.user }}" onerror="this.src='/a/default.png'">
@else
<img class="nav-avatar-img" src="/a/default.png" alt="{{ session.user }}">
@endif
<span class="nav-avatar-name" id="nav-display-name" @if(session.username_color)style="color: {{ session.username_color }}"@endif>{!! session.display_name || session.user !!}</span>
<span class="nav-avatar-caret"></span>
</button>
<div class="nav-user-menu" id="nav-user-menu">
<a href="/user/{!! session.user.toLowerCase() !!}">{{ t('nav.profile') }}</a>
<a href="/user/{!! session.user.toLowerCase() !!}/halls">{{ t('nav.my_halls') }}</a>
<a href="/user/{{ session.user.toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
@if(session.admin)
<a href="/admin">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">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>
</div>
</div>
<!-- Notification bell -->
<div id="nav-notifications" class="nav-item-rel">
<a href="#" id="nav-notif-btn" title="Notifications">
<i class="fa-solid fa-bell"></i>
<span class="notif-count" style="display:none">0</span>
</a>
<div id="notif-dropdown" class="notif-dropdown">
<div class="notif-header">
<span>{{ t('nav.notifications') }}</span>
<button id="mark-all-read">{{ t('nav.mark_all_read') }}</button>
</div>
<div class="notif-list">
<div class="notif-empty">{{ t('nav.no_notifications') }}</div>
</div>
<div class="notif-footer">
<a href="/notifications" class="view-all-notifs">{{ t('nav.view_all_notifications') }}</a>
</div>
<div class="submanage">
<a href="/subscriptions">{{ t('nav.manage_subscriptions') }}</a>
</div>
</div>
</div>
<!-- Quick Favs -->
<a href="/user/{{ session.user.toLowerCase() }}/favs" title="Favorites" class="desktop-only"><i class="fa-solid fa-heart"></i></a>
<!-- DM -->
@if(private_messages)
<div id="nav-dm" class="nav-item-rel">
<a href="/messages" id="nav-dm-btn" title="Direct Messages">
<i class="fa-solid fa-envelope"></i>
<span id="dm-badge" class="notif-count-dm" style="display:none">0</span>
</a>
</div>
@endif
<!-- Quick Settings -->
<a href="/settings" title="Settings" class="desktop-only"><i class="fa-solid fa-gear"></i></a>
<!-- Filter -->
<a href="#" id="nav-filter-btn" title="Excluded Tags"><i class="fa-solid fa-filter"></i></a>
<!-- Logout -->
<a href="/logout" title="Logout" class="desktop-only"><i class="fa-solid fa-right-from-bracket"></i></a>
<!-- Hamburger: mobile only -->
<button class="navbar-toggler" type="button" id="nav-toggler"
onclick="document.getElementById('navbarContent').classList.toggle('show'); this.classList.toggle('is-open')">
<span></span><span></span><span></span>
</button>
</div><!-- /.nav-right-group -->
<!-- MOTD: below the nav row -->
<div class="motd-container" id="motd-container" @if(typeof motd==='undefined' || motd==='' ) style="display:none" @endif>
<span class="motd-content" id="motd-display"></span>
<div id="motd-data" style="display:none">@if(typeof motd !== 'undefined'){!! motd !!}@endif</div>
<div id="user-pref-show-motd" style="display:none">@if(session.show_motd !== false)true@else false @endif</div>
</div>
</nav>
@else
<!-- not logged in -->
@if(!private_society)
<nav class="navbar navbar-expand-lg">
<a class="navbar-brand" href="/">
@if(custom_brand_image)
<img id="navbar-logo" src="{{ custom_brand_image }}" alt="{{ domain }}" style="max-height: 40px; vertical-align: middle; max-width: 180px; width: auto;">
@else
<span class="f0ck">{{ domain }}</span>
@endif
</a>
<div class="nav-collapse" id="navbarContent">
<div class="nav-links">
<a href="/tags" title="Tags"><i class="fa-solid fa-tag"></i></a>
<div class="">
<a href="/halls" class="nav-halls-btn" title="Halls">
<i class="fa-solid fa-building-columns"></i>
</a>
<div class="nav-user-menu">
<a href="/halls" style="font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.1); margin-bottom: 5px;"><i class="fa-solid fa-building-columns"></i> Overview</a>
@if(typeof halls !== 'undefined' && halls.length > 0)
@each(halls as h)
<a href="/h/{{ h.slug }}">{{ h.name }}</a>
@endeach
@else
<a href="/h/legendary">Legendary</a>
@endif
</div>
</div>
<a href="/tags" title="Tags"><i class="fa-solid fa-tag"></i></a>
<a href="/abyss" title="Abyss"><i class="fa-solid fa-dungeon"></i></a>
@if(!/^\/\d$/.test(url.pathname))
<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>
</div>
</div>
<div class="nav-right-group">
<div class="nav-user-dropdown">
<button class="nav-user-btn" id="nav-visitor-toggle"><i class="fa-solid fa-user-secret"></i> {{ t('nav.guest') }} <span class="nav-avatar-caret"></span></button>
<div class="nav-user-menu" id="nav-visitor-menu">
<a href="#" id="nav-login-btn">{{ t('nav.login') }}</a>
<a href="#" id="nav-register-btn">{{ t('nav.register') }}</a>
<div class="nav-user-divider"></div>
</div>
</div>
<button class="navbar-toggler" type="button" id="nav-toggler"
onclick="document.getElementById('navbarContent').classList.toggle('show'); this.classList.toggle('is-open')">
<span></span><span></span><span></span>
</button>
</div>
<div class="motd-container" id="motd-container" @if(typeof motd==='undefined' || motd==='' ) style="display:none" @endif>
<span class="motd-content" id="motd-display"></span>
<div id="motd-data" style="display:none">@if(typeof motd !== 'undefined'){!! motd !!}@endif</div>
</div>
</nav>
@endif
@endif
<div id="login-modal" style="display: none;">
<div class="login-modal-content">
<button id="login-modal-close">&times;</button>
<!-- Login View -->
<div id="modal-login-view">
<form class="login-form" method="post" action="/login" novalidate>
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('auth.login_title') }}</h2>
<input type="text" name="username" placeholder="{{ t('auth.username_or_email') }}" autocomplete="off" required />
<input type="password" name="password" placeholder="{{ t('auth.password_placeholder_min') }}" autocomplete="off" required minlength="20" />
<p style="text-align: left; font-size: 0.9em; margin: 0;"><input type="checkbox" id="kmsi-modal" name="kmsi" />
<label for="kmsi-modal">{{ t('auth.stay_signed_in') }}</label>
</p>
<button type="submit">{{ t('auth.login_title') }}</button>
@if(smtp_enabled)
<div style="text-align: center; margin-top: 10px;">
<a href="#" id="modal-forgot-btn" style="font-size: 0.85em; color: var(--accent); text-decoration: underline;">{{ t('auth.forgot_password') }}</a>
</div>
@endif
@if(registration_open || private_society)
<p style="text-align: center; font-size: 0.9em; margin-top: 15px; color: #888;">
{{ t('auth.no_account') }} <a href="#" id="login-to-register" style="color: var(--accent); text-decoration: underline;">{{ t('auth.register_now') }}</a>
</p>
@endif
</form>
</div>
@if(smtp_enabled)
<!-- Forgot Password View -->
<div id="modal-forgot-view" style="display: none;">
<form class="login-form" id="forgot-password-form" novalidate>
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('auth.forgot_title') }}</h2>
<p style="text-align: center; margin-bottom: 20px; font-size: 0.9em; color: #ccc;">{{ t('auth.forgot_desc') }}</p>
<input type="email" id="forgot-email" placeholder="{{ t('auth.email_address') }}" required />
<div id="forgot-status" style="margin-bottom: 15px; text-align: center; font-size: 0.9em;"></div>
<button type="submit">{{ t('auth.send_reset') }}</button>
<div style="text-align: center; margin-top: 20px;">
<a href="#" id="forgot-to-login" style="font-size: 0.9em; color: var(--accent); text-decoration: underline;">{{ t('auth.back_to_login') }}</a>
</div>
</form>
</div>
@endif
<!-- Reset Password View -->
<div id="modal-reset-view" style="display: none;">
<form class="login-form" id="reset-password-form" novalidate>
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('auth.reset_title') }}</h2>
<p style="text-align: center; margin-bottom: 20px; font-size: 0.9em; color: #ccc;">{{ t('auth.reset_desc') }}</p>
<input type="hidden" id="reset-token" />
<input type="password" id="reset-password" placeholder="{{ t('auth.new_password_min') }}" required minlength="20" autocomplete="new-password" />
<input type="password" id="reset-password-confirm" placeholder="{{ t('auth.confirm_new_password') }}" required minlength="20" autocomplete="new-password" />
<div id="reset-status" style="margin-bottom: 15px; text-align: center; font-size: 0.9em;"></div>
<button type="submit">{{ t('auth.update_password') }}</button>
<div style="text-align: center; margin-top: 20px;">
<a href="#" id="reset-to-login" style="font-size: 0.9em; color: var(--accent); text-decoration: underline; display: none;">{{ t('auth.back_to_login') }}</a>
</div>
</form>
</div>
</div>
</div>
@if(!private_society || session)
<!-- Shortcuts Modal -->
<div id="shortcuts-modal" style="display: none;">
<div class="login-modal-content">
<button id="shortcuts-modal-close">&times;</button>
<div class="shortcuts-content">
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('shortcuts.title') }}</h2>
<div class="shortcut-list">
<div class="shortcut-item"><span><kbd>k</kbd></span><span>{{ t('shortcuts.search') }}</span></div>
<div class="shortcut-item"><span><kbd>m</kbd></span><span>{{ t('shortcuts.main_page') }}</span></div>
<div class="shortcut-item"><span><kbd>r</kbd></span><span>{{ t('shortcuts.random') }}</span></div>
<div class="shortcut-item"><span><kbd>z</kbd></span><span>{{ t('shortcuts.shuffle') }}</span></div>
<div class="shortcut-item"><span><kbd>u</kbd></span><span>{{ t('shortcuts.quick_upload') }}</span></div>
<div class="shortcut-item"><span><kbd>c</kbd></span><span>{{ t('shortcuts.toggle_comments') }}</span></div>
<div class="shortcut-item"><span><kbd>h</kbd></span><span>{{ t('shortcuts.toggle_sidebar') }}</span></div>
<div class="shortcut-item"><span><kbd>Ctrl</kbd> + <kbd>&#46;</kbd></span><span>{{ t('shortcuts.focus_comment') }}</span></div>
<div class="shortcut-item"><span><kbd>Ctrl</kbd> + <kbd>Enter</kbd></span><span>{{ t('shortcuts.send_comment') }}</span></div>
<div class="shortcut-item"><span><kbd>s</kbd></span><span>{{ t('shortcuts.flash_yank') }}</span></div>
@if(session)
<div class="shortcut-item"><span><kbd>e</kbd></span><span>{{ t('shortcuts.tag_exclude') }}</span></div>
<div class="shortcut-item"><span><kbd>i</kbd></span><span>{{ t('shortcuts.tag_input') }}</span></div>
@endif
<div class="shortcut-item"><span><kbd>l</kbd></span><span>{{ t('shortcuts.toggle_bg') }}</span></div>
<div class="shortcut-item"><span><kbd id="shortcut-theme">t</kbd></span><span>{{ t('shortcuts.cycle_themes') }}</span></div>
@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>
@endif
<div class="shortcut-item"><span><kbd>Space</kbd></span><span>{{ t('shortcuts.play_pause') }}</span></div>
<div class="shortcut-item"><span><kbd>Arrows</kbd></span><span>{{ t('shortcuts.next_prev') }}</span></div>
<div class="shortcut-item"><span><kbd>Scroll</kbd></span><span>{{ t('shortcuts.scroll_nav') }}</span></div>
<div class="shortcut-item"><span><kbd>?</kbd></span><span>{{ t('shortcuts.show_help') }}</span></div>
</div>
</div>
</div>
</div>
<div id="flash-container"></div>
@endif
<!-- Register Modal -->
<div id="register-modal" style="display: none;">
<div class="login-modal-content">
<button id="register-modal-close">&times;</button>
<form class="login-form" id="modal-register-form" method="post" action="/register" novalidate>
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('auth.register_title') }}</h2>
<div id="register-status" style="margin-bottom: 15px; text-align: center; font-size: 0.9em;"></div>
<input type="text" name="username" placeholder="{{ t('auth.username_placeholder') }}" autocomplete="off" required />
<input type="password" name="password" placeholder="{{ t('auth.password_placeholder') }}" autocomplete="off" required minlength="20"
title="Must be at least 20 characters long." />
<input type="password" name="password_confirm" placeholder="{{ t('auth.confirm_password') }}" autocomplete="off" required minlength="20" />
@if(registration_open)
<input type="email" name="email" placeholder="{{ t('auth.email_placeholder') }}" autocomplete="off" required />
@else
<input type="text" name="token" placeholder="{{ t('auth.invite_token') }}" autocomplete="off" required />
@endif
<input type="text" name="email_confirm_field" style="display: none !important;" tabindex="-1" autocomplete="off" />
<p style="text-align: left; font-size: 0.9em; margin: 0; color: #fff;">
<input type="checkbox" id="tos-modal" name="tos" required />
<label for="tos-modal">@if(private_society){{ t('auth.tos_private') }}@else{{ t('auth.tos_public') }} <a href="/terms" target="_blank" style="color: var(--accent); text-decoration: underline;">{{ t('auth.tos_terms') }}</a>, <a href="/rules" target="_blank" style="color: var(--accent); text-decoration: underline;">{{ t('auth.tos_rules') }}</a> {{ t('auth.tos_age') }}@endif</label>
</p>
<button type="submit">{{ t('auth.create_account') }}</button>
<div style="text-align: center; margin-top: 20px;">
<a href="#" id="register-to-login" style="font-size: 0.9em; color: var(--accent); text-decoration: underline;">{{ t('auth.back_to_login') }}</a>
</div>
</form>
</div>
</div>
<!-- Global Drag & Drop Feature for Logged-in Users -->
@if(session)
<!-- Global Drop Overlay -->
<div id="drop-overlay">
<div class="overlay-content">
<i class="fa-solid fa-cloud-arrow-up" style="font-size: 5rem;"></i>
<h2>{{ t('upload.drop_anywhere') }}</h2>
</div>
</div>
<!-- Upload Drag Modal -->
<div id="upload-drag-modal">
<div class="modal-content">
<button class="modal-close" id="drag-modal-close" title="Cancel Upload">&times;</button>
<div class="modal-body">
<div class="upload-container" style="padding: 0; animation: none; opacity: 1;">
<h2>{{ t('upload.title') }}</h2>
<div class="upload-limit-info">
@if(session.uploads_remaining === undefined)
<span class="limit-unlimited"><i class="fa-solid fa-infinity"></i> {{ t('upload.unlimited') }}</span>
@elseif(session.uploads_remaining === 0)
<span class="limit-exhausted"><i class="fa-solid fa-triangle-exclamation"></i> {{ t('upload.limit_reached') }} (0/{{ upload_limit }})</span>
@else
<span class="limit-remaining"><i class="fa-solid fa-upload"></i> {{ session.uploads_remaining }}/{{ upload_limit }} {{ t('upload.remaining') }}</span>
@endif
</div>
@include(snippets/upload-form)
</div>
</div>
</div>
</div>
<script src="/s/js/upload.js?v={{ ts }}"></script>
<script src="/s/js/f0ck_upload_init.js?v={{ ts }}"></script>
@endif

View File

@@ -0,0 +1,125 @@
@each(notifications as n)
@if(n.type === 'approve')
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<img src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};}" />
</div>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
<div class="notif-msg">{{ t('notifications.upload_approved').replace('{id}', n.item_id) }}</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@elseif(n.type === 'admin_pending')
<a href="/mod/approve" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<img src="/mod/pending/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};" />
</div>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.admin') }}</strong></div>
<div class="notif-msg">{{ t('notifications.upload_pending').replace('{id}', n.item_id) }}</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@elseif(n.type === 'report')
<a href="/mod/reports" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<img src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};}" />
</div>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.moderation') }}</strong></div>
<div class="notif-msg">{{ t('notifications.new_report').replace('{id}', n.reference_id) }}</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@elseif(n.type === 'deny')
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<img src="/mod/deleted/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
</div>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
<div class="notif-msg">
<strong>{{ t('notifications.upload_denied').replace('{id}', n.item_id) }}</strong>
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 4px;">Reason: {{ n.reason }}</div>
</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@elseif(n.type === 'item_deleted')
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<img src="/mod/deleted/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
</div>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.moderation') }}</strong></div>
<div class="notif-msg">
<strong>{{ t('notifications.upload_deleted').replace('{id}', n.item_id) }}</strong>
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 4px;">Reason: {{ n.reason }}</div>
</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@elseif(n.type === 'upload_comment')
<a href="/{{ n.item_id }}#c{{ n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<img src="/t/{{ n.item_id }}.webp" alt="thumbnail" onerror="this.style.display='none'">
</div>
<div class="notif-content">
<div class="notif-info"><strong>{{ t('notifications.new_comments') }}</strong></div>
<div class="notif-msg">{{ t('notifications.on_your_upload').replace('{id}', n.item_id) }}</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@elseif(n.type === 'upload_success')
<a href="/{{ n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
<div class="notif-thumb">
<img src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
</div>
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
<div class="notif-msg">{{ t('notifications.upload_success') }}</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@elseif(n.type === 'upload_error')
<a href="{{ n.item_id ? '/' + n.item_id : '#' }}" class="notif-item {{ n.is_read ? '' : 'unread' }} {{ n.item_id ? 'notif-with-thumb' : '' }}" data-id="{{ n.id }}">
@if(n.item_id)
<div class="notif-thumb">
<img src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
</div>
@endif
<div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
<div class="notif-msg">
<strong>{{ t('notifications.upload_error') }}</strong>
@if(n.data && n.data.msg)
<div style="font-size: 0.85em; color: #ffb8b8; margin-top: 4px;">Reason: {{ n.data.msg }}</div>
@endif
@if(n.data && n.data.url)
<div style="font-size: 0.75em; opacity: 0.6; margin-top: 2px; word-break: break-all;">URL: {{ n.data.url }}</div>
@endif
</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@else
<a href="/{{ n.item_id }}#c{{ n.comment_id || n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}">
@if(n.item_id)
<div class="notif-thumb">
<img src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
</div>
@endif
<div class="notif-content">
<div class="notif-user"><strong @if(n.username_color) style="color: {{ n.username_color }}" @endif>{!! n.from_user !!}</strong></div>
<div class="notif-msg">
@if(n.type === 'comment_reply') {{ t('notifications.replied').replace('{id}', n.item_id) }}
@elseif(n.type === 'subscription') {{ t('notifications.subscribed').replace('{id}', n.item_id) }}
@elseif(n.type === 'mention') {{ t('notifications.mentioned').replace('{id}', n.item_id) }}
@endif
</div>
<div class="notif-time">{{ new Date(n.created_at).toLocaleString() }}</div>
</div>
</a>
@endif
@endeach

View File

@@ -0,0 +1,22 @@
@if(tmp.user)
<h2>user: {{ tmp.user }} @if(tmp.mime)({{ tmp.mime }}s)@else(all)@endif @if(tmp.view_mode === 'favs')favs @endif @if(tmp.view_mode === 'uploads')uploads @endif @if(total !== undefined)- {{ total }} items@endif</h2>
@endif
@if(tmp.tag)
<h2>tag: {{ tmp.tag }} @if(tmp.mime)({{ tmp.mime }}s)@else(all)@endif @if(total !== undefined)- {{ total }} items@endif</h2>
@endif
@if(tmp.hall)
@if(typeof tmp.hall === 'object')
<h2>hall: {!! tmp.hall.name !!} @if(tmp.mime)({{ tmp.mime }}s)@else(all)@endif @if(total !== undefined)- {{ total }} items@endif</h2>
@if(tmp.hall.description)<p class="page-description" style="color: #aaa; margin-top: 5px; font-size: 0.9em;">{!! tmp.hall.description !!}</p>@endif
@else
<h2>hall: {{ tmp.hall }} @if(tmp.mime)({{ tmp.mime }}s)@else(all)@endif @if(total !== undefined)- {{ total }} items@endif</h2>
@endif
@endif
@if(tmp.userHall)
@if(typeof tmp.userHall === 'object')
<h2>hall: {!! tmp.userHall.name !!} @if(tmp.mime)({{ tmp.mime }}s)@else(all)@endif @if(total !== undefined)- {{ total }} items@endif</h2>
@if(tmp.userHall.description)<p class="page-description" style="color: #aaa; margin-top: 5px; font-size: 0.9em;">{!! tmp.userHall.description !!}</p>@endif
@else
<h2>hall: {{ tmp.userHall }} @if(tmp.mime)({{ tmp.mime }}s)@else(all)@endif @if(total !== undefined)- {{ total }} items@endif</h2>
@endif
@endif

View File

@@ -0,0 +1,17 @@
@if(typeof pagination !== "undefined" && typeof link !== "undefined")
<nav class="pagination">
<a href="{{ link.main }}{{ link.path }}{{ pagination.start }}{{ link.suffix }}" class="page-item-1 btn start@if(!pagination.prev) disabled@endif">&laquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}{{ link.suffix }}" class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">&lsaquo;</a>
@if(pagination.cheat)
@each(pagination.cheat as i)
@if(i == pagination.page)
<span class="btn disabled">{{ i }}</span>
@else
<a href="{{ link.main }}{{ link.path }}{{ i }}{{ link.suffix }}" class="pagination-int-item btn">{{ i }}</a>
@endif
@endeach
@endif
<a href="{{ link.main }}{{ link.path }}{{ pagination.next }}{{ link.suffix }}" class="page-item-3 btn next@if(!pagination.next) disabled@endif">&rsaquo;</a>
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}{{ link.suffix }}" class="page-item-4 btn start@if(!pagination.next) disabled@endif">&raquo;</a>
</nav>
@endif

View File

@@ -0,0 +1,17 @@
@each(items as item)
<div class="sub-card {{ item.is_pinned ? 'anim-boxshadow is-pinned' : '' }}" id="sub-{{ item.id }}">
<a href="/{{ item.id }}" class="sub-link">
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
@endif
</div>
<img src="{{ item.thumb }}" loading="lazy" />
<div class="sub-info">
<span class="sub-id">#{{ item.id }}</span>
<span class="sub-user">{{ t('subscriptions.by_user').replace('{user}', item.user) }}</span>
</div>
</a>
<button class="unsub-btn" data-id="{{ item.id }}">{{ t('subscriptions.unsubscribe') }}</button>
</div>
@endeach

View File

@@ -0,0 +1,129 @@
<form id="upload-form" class="upload-form" enctype="multipart/form-data" data-mimes='{!! mimes_json !!}' data-max-bytes="{{ max_file_size_bytes }}">
<div class="form-section">
@if(web_url_upload)
<div class="upload-mode-tabs">
<button type="button" class="upload-mode-tab active" data-mode="file">
<i class="fa-solid fa-upload"></i>
{{ t('upload.file_tab') }}
</button>
<button type="button" class="upload-mode-tab" data-mode="url">
<i class="fa-solid fa-link"></i>
@if(enable_youtube_upload){{ t('upload.url_tab_yt') }}@else {{ t('upload.url_tab') }} @endif
</button>
</div>
@endif
<!-- File input area -->
<div class="upload-mode-content" id="mode-file">
<div class="drop-zone" id="upload-form-drop-zone">
<input type="file" class="file-input" name="file" accept="{{ allowed_mimes }}">
<div class="drop-zone-prompt">
<i class="fa-solid fa-cloud-arrow-up" style="font-size: 4rem; opacity: 0.7; margin-bottom: 1rem;"></i>
<p style="font-size: 1.1rem; font-weight: 500;">{{ t('upload.drop_here') }}</p>
<p style="font-size: 0.9rem; opacity: 0.6;">(max {{ max_file_size }})@if(session.admin) <span style="color: var(--accent);">{{ t('upload.admin_boost') }}</span>@endif</p>
</div>
<!-- Preview Container -->
<div class="file-preview" style="display: none;">
<!-- Video will be injected here via JS -->
<div class="file-meta-row">
<div class="file-info">
<span class="file-name"></span>
<span class="file-size"></span>
</div>
<button type="button" class="btn-remove" title="{{ t('upload.remove_file') }}"></button>
</div>
</div>
</div>
</div>
@if(web_url_upload)
<!-- URL input area -->
<div class="upload-mode-content" id="mode-url" style="display: none;">
<div class="url-input-container">
<input type="url" id="url-upload-input" name="url" placeholder="@if(enable_youtube_upload){{ t('upload.url_placeholder_yt') }}@else {{ t('upload.url_placeholder') }}@endif" autocomplete="off">
</div>
<div class="url-type-badge" id="url-type-badge" style="display: none;"></div>
</div>
@endif
<!-- Custom Thumbnail for Flash -->
<div class="form-section" id="custom-thumbnail-section" style="display: none; margin-top: 1rem; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 1rem;">
<label>{{ t('upload.custom_thumbnail') }}</label>
<span style="opacity: 0.5; font-weight: normal;">{{ t('upload.custom_thumbnail_hint') }}</span>
<div class="custom-thumbnail-input">
<input type="file" name="thumbnail" id="upload-thumbnail-input" accept="image/jpeg,image/png,image/webp" style="font-size: 0.9rem;">
</div>
</div>
</div>
<div class="form-section">
<label>{{ t('upload.rating') }} <span class="required">*</span></label>
<div class="rating-options">
<label class="rating-option">
<input type="radio" name="rating" value="sfw" required>
<span class="rating-label sfw">SFW</span>
</label>
<label class="rating-option">
<input type="radio" name="rating" value="nsfw">
<span class="rating-label nsfw">NSFW</span>
</label>
@if(enable_nsfl)
<label class="rating-option">
<input type="radio" name="rating" value="nsfl">
<span class="rating-label nsfl">NSFL</span>
</label>
@endif
</div>
</div>
<div class="form-section">
<label class="oc-option">
<input type="checkbox" name="is_oc" id="upload-oc-checkbox">
<span class="oc-label">{{ t('upload.original_content') }}</span>
</label>
</div>
<div class="form-section">
<label>{{ t('upload.tags') }} <span class="required">*</span> <span class="tag-count">(0/{{ min_tags }} {{ t('upload.tags_minimum') }})</span></label>
<div class="tag-input-container">
<div class="sync-spinner">
<span class="spinner-icon"></span>
<span>{{ t('upload.extracting_title') }}</span>
</div>
<div class="tags-list"></div>
<input type="text" class="tag-input" placeholder="{{ t('upload.tag_placeholder') }}" autocomplete="off" enterkeyhint="enter">
<div class="tag-suggestions"></div>
</div>
<input type="hidden" name="tags" class="tags-hidden">
<div class="meta-suggestions-container" style="display: none;">
<div class="suggestions-header">
<i class="fa-solid fa-wand-magic-sparkles"></i> {{ t('upload.suggestions_header') }}
</div>
<div class="meta-suggestions-list"></div>
</div>
</div>
<div class="form-section">
<label>{{ t('upload.comment') }} <span style="opacity: 0.5; font-weight: normal;">{{ t('upload.comment_optional') }}</span></label>
<div class="upload-comment-input comment-input">
<textarea class="upload-comment" placeholder="{{ t('upload.comment_placeholder') }}" maxlength="2000"></textarea>
<div class="input-actions"></div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn-upload" disabled>
<span class="btn-text">{{ t('upload.select_file') }}</span>
<span class="btn-loading" style="display: none;">{{ t('upload.uploading') }}</span>
</button>
</div>
<div class="upload-progress" style="display: none;">
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<span class="progress-text">0%</span>
</div>
<div class="upload-status"></div>
</form>