init f0ckm
This commit is contained in:
47
views/snippets/excluded-tags-modal.html
Normal file
47
views/snippets/excluded-tags-modal.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<div id="excluded-tags-overlay" style="display: none;">
|
||||
<div id="excluded-tags-close">×</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
549
views/snippets/footer.html
Normal 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
127
views/snippets/header.html
Normal 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
|
||||
30
views/snippets/item-media.html
Normal file
30
views/snippets/item-media.html
Normal 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
|
||||
16
views/snippets/items-grid.html
Normal file
16
views/snippets/items-grid.html
Normal 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
|
||||
26
views/snippets/metadata-modal.html
Normal file
26
views/snippets/metadata-modal.html
Normal 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
369
views/snippets/navbar.html
Normal 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">×</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">×</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>.</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">×</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">×</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
|
||||
125
views/snippets/notifications-list.html
Normal file
125
views/snippets/notifications-list.html
Normal 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
|
||||
22
views/snippets/page-title.html
Normal file
22
views/snippets/page-title.html
Normal 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
|
||||
17
views/snippets/pagination.html
Normal file
17
views/snippets/pagination.html
Normal 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">«</a>
|
||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.prev }}{{ link.suffix }}" class="page-item-2 btn prev@if(!pagination.prev) disabled@endif">‹</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">›</a>
|
||||
<a href="{{ link.main }}{{ link.path }}{{ pagination.end }}{{ link.suffix }}" class="page-item-4 btn start@if(!pagination.next) disabled@endif">»</a>
|
||||
</nav>
|
||||
@endif
|
||||
17
views/snippets/subscriptions-grid.html
Normal file
17
views/snippets/subscriptions-grid.html
Normal 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
|
||||
129
views/snippets/upload-form.html
Normal file
129
views/snippets/upload-form.html
Normal 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>
|
||||
Reference in New Issue
Block a user