updating from dev

This commit is contained in:
2026-05-04 04:24:18 +02:00
parent 46afca976d
commit 2f1e42343b
76 changed files with 5554 additions and 2527 deletions

View File

@@ -22,7 +22,10 @@
<li><a href="/admin/motd">MOTD Manager</a></li>
<li><a href="/admin/about">About Page</a></li>
<li><a href="/admin/rules">Rules Page</a></li>
<li><a href="/admin/chat">Global Chat Manager</a></li>
</ul>
<hr style="margin: 20px 0; border: 0; border-top: 1px solid rgba(255,255,255,0.1);">
<hr style="margin: 20px 0; border: 0; border-top: 1px solid rgba(255,255,255,0.1);">
@@ -55,7 +58,7 @@
<label style="display: block; font-weight: bold; color: var(--accent);">Minimum Tags</label>
<p style="margin: 2px 0 0 0; font-size: 0.8em; color: #aaa;">Minimum number of tags required per upload.</p>
</div>
<input type="number" id="min_tags_input" value="{{ min_tags }}" min="1" max="20" style="width: 60px; background: #333; border: 1px solid #444; color: #fff; padding: 5px; border-radius: 4px; text-align: center;" onchange="saveAdminSettings()">
<input type="number" id="min_tags_input" value="{{ min_tags }}" min="0" max="20" style="width: 60px; background: #333; border: 1px solid #444; color: #fff; padding: 5px; border-radius: 4px; text-align: center;" onchange="saveAdminSettings()">
</div>
<div class="settings-item" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;">
@@ -90,6 +93,7 @@
</style>
<script>
async function saveAdminSettings() {
const status = document.getElementById('settings-status');
const approvalToggle = document.getElementById('manual_approval_toggle');
@@ -170,6 +174,7 @@
btn.textContent = 'Regenerate All';
}
}
</script>
</div>

223
views/admin/chat.html Normal file
View File

@@ -0,0 +1,223 @@
@include(snippets/header)
<div class="pagewrapper">
<div id="main">
<div class="container">
<h1>ADMINBEREICH</h1>
<h5>Global Chat Management</h5>
<a href="/admin" style="font-size: 0.8em; color: var(--accent); text-decoration: none;"><i class="fa-solid fa-arrow-left"></i> Back to Dashboard</a>
<hr>
<div class="chat-cheatsheet" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; margin-bottom: 20px;">
<label style="display: block; font-weight: bold; color: var(--accent); margin-bottom: 10px;">Global Chat Cheat Sheet</label>
<p style="font-size: 0.85em; color: #ccc; margin-bottom: 10px;">Admin commands for the global chat widget.</p>
<table style="width: 100%; font-size: 0.85em; border-collapse: collapse;">
<tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">
<th style="text-align: left; padding: 8px 0; color: #fff;">Command</th>
<th style="text-align: left; padding: 8px 0; color: #fff;">Description</th>
</tr>
<tr>
<td style="padding: 8px 0;"><code style="color: var(--accent);">/clear</code></td>
<td style="padding: 8px 0;">Deletes all messages and restarts history.</td>
</tr>
<tr>
<td style="padding: 8px 0;"><code style="color: var(--accent);">/setbackground &lt;url&gt; [opts]</code></td>
<td style="padding: 8px 0;">Sets a background image. Opts: <code style="color: #aaa;">center / cover no-repeat</code> etc.</td>
</tr>
<tr>
<td style="padding: 8px 0;"><code style="color: var(--accent);">/clearbg</code></td>
<td style="padding: 8px 0;">Removes the chat background.</td>
</tr>
<tr>
<td style="padding: 8px 0;"><code style="color: var(--accent);">/settopic &lt;text&gt;</code></td>
<td style="padding: 8px 0;">Sets the pinned topic at the top of the chat.</td>
</tr>
<tr>
<td style="padding: 8px 0;"><code style="color: var(--accent);">/cleartopic</code></td>
<td style="padding: 8px 0;">Removes the pinned topic.</td>
</tr>
</table>
</div>
<div class="chat-bg-tool" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; margin-bottom: 20px;">
<label style="display: block; font-weight: bold; color: var(--accent); margin-bottom: 10px;">Chat Background Builder</label>
<p style="font-size: 0.85em; color: #ccc; margin-bottom: 10px;">Craft the perfect background command or apply it instantly.</p>
<div style="display: flex; flex-direction: column; gap: 10px;">
<input type="text" id="bg_url" placeholder="Image URL (must be from allowed host)" style="width: 100%; background: #333; border: 1px solid #444; color: #fff; padding: 8px; border-radius: 4px; outline: none;">
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<select id="bg_pos" style="background: #333; border: 1px solid #444; color: #fff; padding: 8px; border-radius: 4px; cursor: pointer; outline: none;">
<option value="center">Position: Center</option>
<option value="top">Position: Top</option>
<option value="bottom">Position: Bottom</option>
<option value="left">Position: Left</option>
<option value="right">Position: Right</option>
<option value="top left">Position: Top Left</option>
<option value="top right">Position: Top Right</option>
<option value="bottom left">Position: Bottom Left</option>
<option value="bottom right">Position: Bottom Right</option>
</select>
<div style="display: flex; gap: 5px; flex-grow: 1; min-width: 150px;">
<select id="bg_size_presets" onchange="document.getElementById('bg_size').value = this.value; updateBgCommand();" style="background: #333; border: 1px solid #444; color: #fff; padding: 8px; border-radius: 4px 0 0 4px; cursor: pointer; outline: none; flex-shrink: 0;">
<option value="cover">Size: Cover</option>
<option value="contain">Size: Contain</option>
<option value="auto">Size: Auto</option>
<option value="100% 100%">Size: Stretch</option>
<option value="">Custom...</option>
</select>
<input type="text" id="bg_size" value="cover" placeholder="e.g. 50% or 200px" style="background: #333; border: 1px solid #444; border-left: 0; color: #fff; padding: 8px; border-radius: 0 4px 4px 0; outline: none; flex-grow: 1;" oninput="updateBgCommand()">
</div>
<select id="bg_repeat" style="background: #333; border: 1px solid #444; color: #fff; padding: 8px; border-radius: 4px; cursor: pointer; outline: none;">
<option value="no-repeat">Repeat: No</option>
<option value="repeat">Repeat: Yes</option>
<option value="repeat-x">Repeat: X only</option>
<option value="repeat-y">Repeat: Y only</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 10px; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 4px; font-family: monospace; font-size: 0.9em;">
<span style="color: #888; white-space: nowrap;">Command:</span>
<code id="bg_command_output" style="color: var(--accent); white-space: nowrap; overflow-x: auto; flex-grow: 1;">/setbackground center / cover no-repeat</code>
<button onclick="copyBgCommand()" style="background: #444; border: 0; color: #fff; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 0.8em; font-weight: bold; transition: background 0.2s;" onmouseover="this.style.background='#555'" onmouseout="this.style.background='#444'">Copy</button>
</div>
<div style="display: flex; gap: 10px;">
<button onclick="applyBgFromAdmin()" id="apply_bg_btn" style="flex-grow: 1; background: var(--accent); border: 0; color: #000; padding: 12px; border-radius: 4px; cursor: pointer; font-weight: bold; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.9'" onmouseout="this.style.opacity='1'">Apply Instantly</button>
<button onclick="clearBgFromAdmin()" style="background: #d9534f; border: 0; color: #fff; padding: 12px 20px; border-radius: 4px; cursor: pointer; font-weight: bold; transition: opacity 0.2s;" onmouseover="this.style.opacity='0.9'" onmouseout="this.style.opacity='1'">Clear Background</button>
</div>
</div>
<span id="chat-status" style="display: block; margin-top: 10px; font-size: 0.8em; font-weight: bold; text-align: right;"></span>
</div>
</div>
<script>
// Chat Background Builder Logic
window.builder_url = "";
window.builder_pos = "";
window.builder_size = "";
window.builder_repeat = "";
window.builder_opts = "";
window.builder_cmd = "";
function updateBgCommand() {
var builder_uEl = document.getElementById('bg_url');
var builder_pEl = document.getElementById('bg_pos');
var builder_sEl = document.getElementById('bg_size');
var builder_rEl = document.getElementById('bg_repeat');
var builder_oEl = document.getElementById('bg_command_output');
if (!builder_uEl || !builder_pEl || !builder_sEl || !builder_rEl || !builder_oEl) return { url: '', opts: '' };
window.builder_url = builder_uEl.value.trim();
window.builder_pos = builder_pEl.value;
window.builder_size = builder_sEl.value;
window.builder_repeat = builder_rEl.value;
window.builder_opts = window.builder_pos + " / " + window.builder_size + " " + window.builder_repeat;
window.builder_cmd = "/setbackground " + window.builder_url + " " + window.builder_opts;
builder_oEl.textContent = window.builder_cmd;
return { url: window.builder_url, opts: window.builder_opts };
}
if (document.getElementById('bg_url')) {
document.getElementById('bg_url').addEventListener('input', updateBgCommand);
document.getElementById('bg_pos').addEventListener('change', updateBgCommand);
document.getElementById('bg_size').addEventListener('change', updateBgCommand);
document.getElementById('bg_repeat').addEventListener('change', updateBgCommand);
}
function copyBgCommand() {
const outputElem = document.getElementById('bg_command_output');
if (!outputElem) return;
const cmd = outputElem.textContent;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(cmd);
const status = document.getElementById('chat-status');
if (status) {
status.textContent = 'Command copied to clipboard!';
status.style.color = 'var(--accent)';
setTimeout(() => { status.textContent = ''; }, 2000);
}
} else {
alert('Clipboard access denied or not supported.');
}
}
async function applyBgFromAdmin() {
const { url, opts } = updateBgCommand();
if (!url) return alert('Please enter an image URL first.');
const btn = document.getElementById('apply_bg_btn');
const status = document.getElementById('chat-status');
if (btn) {
btn.disabled = true;
btn.textContent = 'Applying...';
}
try {
const res = await fetch('/api/chat/background', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': '{{ csrf_token }}'
},
body: new URLSearchParams({ url, opts }).toString()
});
const data = await res.json();
if (data.success) {
if (status) {
status.textContent = 'Background applied successfully!';
status.style.color = '#28a745';
}
if (btn) btn.textContent = 'Applied!';
setTimeout(() => {
if (btn) {
btn.disabled = false;
btn.textContent = 'Apply Instantly';
}
if (status) status.textContent = '';
}, 3000);
} else {
throw new Error(data.msg || 'Failed to apply background');
}
} catch (err) {
alert(err.message);
if (btn) {
btn.disabled = false;
btn.textContent = 'Apply Instantly';
}
}
}
async function clearBgFromAdmin() {
if (!confirm('Are you sure you want to clear the chat background?')) return;
const status = document.getElementById('chat-status');
try {
const res = await fetch('/api/chat/background', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': '{{ csrf_token }}'
},
body: new URLSearchParams({}).toString()
});
const data = await res.json();
if (data.success) {
if (status) {
status.textContent = 'Background cleared!';
status.style.color = '#28a745';
}
const urlInput = document.getElementById('bg_url');
if (urlInput) urlInput.value = '';
updateBgCommand();
setTimeout(() => { if (status) status.textContent = ''; }, 3000);
}
} catch (err) { alert(err.message); }
}
</script>
</div>
</div>
@include(snippets/footer)

View File

@@ -39,7 +39,7 @@
<script>
(() => {
var i18n = window.f0ckI18n || {};
console.log('[MEME_ADMIN] Initializing');
window.f0ckDebug('[MEME_ADMIN] Initializing');
const esc = (s) => (s || '').toString().replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
const loadMemes = async () => {
try {
@@ -65,7 +65,7 @@
const addMeme = async (e) => {
if (e) e.preventDefault();
console.log('[MEME_ADMIN] addMeme triggered');
window.f0ckDebug('[MEME_ADMIN] addMeme triggered');
const template_id = document.getElementById('meme-id').value;
const name = document.getElementById('meme-name').value;
@@ -145,7 +145,7 @@
const btnAddMeme = document.getElementById('add-meme');
if (btnAddMeme) {
console.log('[MEME_ADMIN] Registering click listener');
window.f0ckDebug('[MEME_ADMIN] Registering click listener');
btnAddMeme.addEventListener('click', addMeme);
} else {
console.error('[MEME_ADMIN] Add button not found!');

View File

@@ -27,10 +27,10 @@
<script>
const loadTokens = async () => {
try {
console.log('Loading tokens...');
window.f0ckDebug('Loading tokens...');
const res = await fetch('/api/v2/admin/tokens');
const data = await res.json();
console.log('Tokens data:', data);
window.f0ckDebug('Tokens data:', data);
if (data.success) {
const tbody = document.getElementById('token-list');
if (!tbody) return;
@@ -57,14 +57,14 @@
};
const generateToken = async () => {
console.log('Generating...');
window.f0ckDebug('Generating...');
try {
const res = await fetch('/api/v2/admin/tokens/create', {
method: 'POST',
headers: { 'X-CSRF-Token': window.f0ckSession?.csrf_token }
});
const data = await res.json();
console.log('Gen result:', data);
window.f0ckDebug('Gen result:', data);
if (data.success) {
loadTokens();
} else {

View File

@@ -313,6 +313,57 @@
}, { hideReason: false, allowEmpty: true, confirmText: 'Set Nick', placeholder: currentDisplay || 'e.g. F.O.O' });
}
async function adminLockLayout(btn) {
var id = btn.dataset.id;
var userName = btn.dataset.name;
var isLocked = btn.dataset.locked === '1';
var currentMode = btn.dataset.mode || '0';
if (isLocked) {
ModAction.confirm('Unlock Layout', 'Unlock comment layout for <strong>' + escHTML(userName) + '</strong>? They will be able to change it again.', async () => {
var res = await fetch('/api/v2/admin/users/lock-layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: id, lock: false })
});
var data = await res.json();
if (data.success) {
showFlash('Layout unlocked for ' + escHTML(userName), 'success');
btn.dataset.locked = '0';
btn.innerHTML = '<i class="fa fa-lock"></i> Lock';
btn.title = 'Lock Layout';
} else {
throw new Error(data.msg || 'Failed to unlock layout');
}
}, { hideReason: true });
} else {
var hint = 'Select comment display mode to force for <strong>' + escHTML(userName) + '</strong>:<br><br>' +
'<select id="force-mode-select" class="input" style="width: 100%; padding: 8px;">' +
'<option value="0" ' + (currentMode == '0' ? 'selected' : '') + '>Tree</option>' +
'<option value="1" ' + (currentMode == '1' ? 'selected' : '') + '>Linear</option>' +
'</select>';
ModAction.confirm('Lock Layout', hint, async () => {
var mode = document.getElementById('force-mode-select').value;
var res = await fetch('/api/v2/admin/users/lock-layout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: id, lock: true, mode: mode })
});
var data = await res.json();
if (data.success) {
showFlash('Layout locked to ' + (mode == '0' ? 'Tree' : 'Linear') + ' for ' + escHTML(userName), 'success');
btn.dataset.locked = '1';
btn.dataset.mode = mode;
btn.innerHTML = '<i class="fa fa-lock-open"></i> Unlock';
btn.title = 'Unlock Layout';
} else {
throw new Error(data.msg || 'Failed to lock layout');
}
}, { hideReason: true, confirmText: 'Lock & Apply' });
}
}
var currentPage = {!! page !!};
var hasMore = {!! hasMore ? 'true' : 'false' !!};
var isLoading = false;

View File

@@ -83,6 +83,7 @@
@if(u.id && u.login !== 'deleted_user')
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" data-display="{!! u.display_name || '' !!}" onclick="adminSetDisplayName(this)" class="btn-modern btn-nick" style="background: rgba(100, 200, 100, 0.1); color: #64c864; border: 1px solid rgba(100, 200, 100, 0.2);" title="Set stylized display name">✏ Nick</button>
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" onclick="adminSetPassword(this)" class="btn-modern btn-pw" style="background: rgba(var(--accent-rgb, 0, 150, 255), 0.1); color: var(--accent, #0096ff); border: 1px solid rgba(var(--accent-rgb, 0, 150, 255), 0.2);">Set PW</button>
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" data-locked="{{ u.force_comment_display_mode }}" data-mode="{{ u.comment_display_mode }}" onclick="adminLockLayout(this)" class="btn-modern" style="background: rgba(255, 100, 0, 0.1); color: #ff6400; border: 1px solid rgba(255, 100, 0, 0.2);" title="{{ u.force_comment_display_mode ? 'Unlock Layout' : 'Lock Layout' }}"><i class="fa fa-{{ u.force_comment_display_mode ? 'lock-open' : 'lock' }}"></i> {{ u.force_comment_display_mode ? 'Unlock' : 'Lock' }}</button>
<button data-id="{{ u.id }}" data-name="{!! u.user !!}" data-username="{{ u.login }}" onclick="adminDeleteUser(this)" class="btn-modern btn-delete" style="background: rgba(217, 83, 79, 0.1); color: #d9534f; border: 1px solid rgba(217, 83, 79, 0.2);">Delete</button>
@elseif(u.login === 'deleted_user')
<span style="font-size: 0.8rem; color: #666; font-style: italic; padding: 5px 10px;">Protected System Account</span>

View File

@@ -65,7 +65,7 @@
<div class="blahlol">
@if(user_alternative_infobox)
<div class="user-infobox-block" style="--author-accent: @if(item.author_color){{ item.author_color }}@else var(--acent) @endif; --author-border: @if(item.author_color){{ item.author_color }}@else var(--accent) @endif;">
<div class="user-infobox-block" style="--author-accent: @if(item.author_color){{ item.author_color }}@else var(--accent) @endif; --author-border: @if(item.author_color){{ item.author_color }}@else var(--accent) @endif;">
<div class="user-infobox-avatar">
<a href="/user/{{ (item.username || '').toLowerCase() }}">

View File

@@ -13,11 +13,19 @@
@each(pending as post)
<div class="approval-card">
<div class="approval-card-media">
@if(post.mime.startsWith('video'))
@if(post.mime === 'video/youtube')
<div class="embed-responsive embed-responsive-16by9" style="width: 100%;">
<iframe class="embed-responsive-item" src="https://www.youtube.com/embed/{!! post.dest.replace('yt:', '') !!}" frameborder="0" allowfullscreen></iframe>
</div>
@elseif(post.mime.startsWith('video'))
<video controls loop muted preload="metadata">
<source src="/mod/pending/b/{!! post.dest !!}" type="{!! post.mime !!}">
Your browser does not support the video tag.
</video>
@elseif(post.mime === 'application/pdf')
<div class="embed-responsive embed-responsive-16by9" style="width: 100%;">
<iframe class="embed-responsive-item" src="/mod/pending/b/{!! post.dest !!}#toolbar=0" loading="lazy" style="border: none;"></iframe>
</div>
@else
<img src="/mod/pending/t/{!! post.id !!}.webp" alt="Preview">
@endif
@@ -34,8 +42,8 @@
@endeach
</div>
<div class="approval-card-actions" style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<a href="/mod/approve/?id={!! post.id !!}" class="badge badge-success btn-approve-async" style="margin: 0; text-align: center;">Approve</a>
<a href="/mod/deny/?id={!! post.id !!}" class="badge badge-danger btn-deny-async" style="margin: 0; text-align: center;">Deny / Delete</a>
<button data-id="{!! post.id !!}" class="badge badge-success btn-approve-async" style="margin: 0; text-align: center; border: none; cursor: pointer;">Approve</button>
<button data-id="{!! post.id !!}" class="badge badge-danger btn-deny-async" style="margin: 0; text-align: center; border: none; cursor: pointer;">Deny / Delete</button>
<a href="/api/v2/tags/{!! post.id !!}/toggle" class="badge btn-rating-toggle-async" style="grid-column: span 2; background: #444; color: #ccc; margin: 0; text-align: center;">Rating</a>
</div>
</div>
@@ -56,11 +64,19 @@
@each(trash as post)
<div class="approval-card">
<div class="approval-card-media">
@if(post.mime.startsWith('video'))
@if(post.mime === 'video/youtube')
<div class="embed-responsive embed-responsive-16by9" style="width: 100%;">
<iframe class="embed-responsive-item" src="https://www.youtube.com/embed/{!! post.dest.replace('yt:', '') !!}" frameborder="0" allowfullscreen></iframe>
</div>
@elseif(post.mime.startsWith('video'))
<video controls loop muted preload="metadata">
<source src="/mod/deleted/b/{!! post.dest !!}" type="{!! post.mime !!}">
Your browser does not support the video tag.
</video>
@elseif(post.mime === 'application/pdf')
<div class="embed-responsive embed-responsive-16by9" style="width: 100%;">
<iframe class="embed-responsive-item" src="/mod/deleted/b/{!! post.dest !!}#toolbar=0" loading="lazy" style="border: none;"></iframe>
</div>
@else
<img src="/mod/deleted/t/{!! post.id !!}.webp" style="filter: grayscale(50%);" alt="Preview">
@endif
@@ -80,9 +96,9 @@
@endeach
</div>
<div class="approval-card-actions" style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<a href="/mod/approve/?id={!! post.id !!}" class="badge badge-warning btn-approve-async" style="margin: 0; text-align: center;">Restore</a>
<button data-id="{!! post.id !!}" class="badge badge-warning btn-approve-async" style="margin: 0; text-align: center; border: none; cursor: pointer;">Restore</button>
@if(session.admin)
<a href="/mod/deny/?id={!! post.id !!}" class="badge badge-danger btn-deny-async" style="margin: 0; text-align: center;">Purge</a>
<button data-id="{!! post.id !!}" class="badge badge-danger btn-deny-async" style="margin: 0; text-align: center; border: none; cursor: pointer;">Purge</button>
@else
<span></span>
@endif
@@ -195,14 +211,18 @@
document.querySelectorAll('.btn-deny-async').forEach(btn => {
btn.addEventListener('click', e => {
e.preventDefault();
const url = btn.getAttribute('href');
const itemId = btn.getAttribute('data-id');
const card = btn.closest('.approval-card');
showModal('{!! t('mod.confirm_action') !!}', '{!! t('mod.confirm_action') !!}?', async (reason) => {
const res = await fetch(url + (url.indexOf('?') > -1 ? '&' : '?') + 'reason=' + encodeURIComponent(reason), {
const res = await fetch('/mod/deny', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
'Accept': 'application/json',
'X-CSRF-Token': window.f0ckSession?.csrf_token || ''
},
body: JSON.stringify({ id: itemId, reason })
});
const data = await res.json();
if (res.ok && data.success) {
@@ -221,15 +241,19 @@
document.querySelectorAll('.btn-approve-async').forEach(btn => {
btn.addEventListener('click', async e => {
e.preventDefault();
const url = btn.getAttribute('href');
const card = btn.closest('.approval-card'); // Updated selector
const itemId = btn.getAttribute('data-id');
const card = btn.closest('.approval-card');
try {
const res = await fetch(url, {
const res = await fetch('/mod/approve', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
'Accept': 'application/json',
'X-CSRF-Token': window.f0ckSession?.csrf_token || ''
},
body: JSON.stringify({ id: itemId })
});
const data = await res.json();
if (data.success) {
@@ -303,7 +327,12 @@
if (btnPurgeTrash) {
btnPurgeTrash.addEventListener('click', () => {
showModal('Purge Trash', 'Permanently delete ALL items in the trash? This cannot be undone.', async () => {
const res = await fetch('/mod/purge-trash-all', { method: 'POST' });
const res = await fetch('/mod/purge-trash-all', {
method: 'POST',
headers: {
'X-CSRF-Token': window.f0ckSession?.csrf_token || ''
}
});
const data = await res.json();
if (data.success) {
location.reload();

View File

@@ -166,7 +166,12 @@ window.expandItem = function(e, id) {
const mime = r.resolved_item_mime || '';
const src = '/b/' + r.resolved_item_dest;
const baseStyle = 'max-height: 250px; border: 1px solid #333; border-radius: 4px;';
if (mime.startsWith('image/')) {
if (mime === 'video/youtube') {
const ytId = r.resolved_item_dest.replace('yt:', '');
previewHtml = '<div><iframe width="444" height="250" src="https://www.youtube.com/embed/' + ytId + '" frameborder="0" allowfullscreen style="' + baseStyle + '"></iframe></div>';
} else if (mime === 'application/pdf') {
previewHtml = '<div><iframe src="' + src + '#toolbar=0" style="' + baseStyle + ' width: 444px; height: 250px;" frameborder="0" allowfullscreen></iframe></div>';
} else if (mime.startsWith('image/')) {
previewHtml = '<div><img src="' + src + '" style="' + baseStyle + ' background: #000;"></div>';
} else if (mime.startsWith('audio/')) {
previewHtml = '<div><audio src="' + src + '" controls style="' + baseStyle + '"></audio></div>';

View File

@@ -35,10 +35,10 @@
// Load first page for this tab
try {
const res = await fetch(`/ajax/notifications?page=1&tab=${tabName}`);
const res = await fetch('/ajax/notifications?page=1&tab=' + tabName);
const data = await res.json();
if (data.success) {
container.innerHTML = data.html || `<div class="notif-empty">${window.f0ckI18n?.no_notifications || 'No new notifications'}</div>`;
container.innerHTML = data.html || '<div class="notif-empty">' + (window.f0ckI18n?.no_notifications || 'No new notifications') + '</div>';
const footbar = document.getElementById('footbar');
if (footbar) {
footbar.style.display = data.hasMore ? '' : 'none';

View File

@@ -593,11 +593,18 @@
.scroller-blur.revealed, .scroller-blur:hover { filter: none; }
.comment-meta { display: flex; align-items: center; gap: 10px; margin-top: 4px; }
.comment-time { font-size: .67rem; color: rgba(255,255,255,.38); }
.comment-reply-btn {
.comment-reply-btn, .comment-quote-btn {
background: none; border: none; color: rgba(255,255,255,.45); font-size: .67rem;
font-weight: 700; cursor: pointer; padding: 0; text-transform: none;
}
.comment-reply-btn:hover { color: var(--accent, #fff); }
.comment-reply-btn:hover, .comment-quote-btn:hover { color: var(--accent, #fff); }
.comment-context-link { color: var(--accent); text-decoration: none; font-family: 'VCR', monospace; }
.comment-context-link:hover { text-decoration: underline; }
@keyframes comment-highlight {
0% { background: rgba(255,255,255,.15); border-color: var(--accent); }
100% { background: rgba(255,255,255,.03); border-color: rgba(255,255,255,.05); }
}
.highlight-comment { animation: comment-highlight 2.5s cubic-bezier(0.2, 0, 0, 1); }
#reply-indicator {
display: flex; align-items: center; gap: 8px;
padding: 6px 12px; background: rgba(255,255,255,.05);

View File

@@ -1,301 +1,365 @@
@include(snippets/header)
<div class="pagewrapper">
<div id="main">
<div class="settings">
<h1>{{ t('settings.title') }}</h1>
<h2>{{ t('settings.avatar') }}</h2>
<div class="avatar-settings-wrapper">
<div class="avatar-preview-container">
<div class="avatar-preview-label">{{ t('settings.current_avatar') }}</div>
@if(avatar_file)
<a href="/user/{!! session.user !!}" target="_blank">
<img id="avatar-preview" class="avatar-preview-img" src="/a/{{ avatar_file }}">
</a>
@elseif(session.avatar && session.avatar > 0)
<a href="/user/{!! session.user !!}" target="_blank">
<img id="avatar-preview" class="avatar-preview-img" src="/t/{{ session.avatar }}.webp">
</a>
@else
<div id="avatar-preview" class="avatar-preview-img avatar-placeholder">?</div>
@endif
</div>
<div class="avatar-upload-section">
<h4>{{ t('settings.upload_custom_avatar') }}</h4>
<p class="avatar-hint">{{ t('settings.avatar_hint') }}</p>
<div class="avatar-upload-wrapper">
<input type="file" id="avatar-file-input" accept="image/gif,image/jpeg,image/png,image/webp" hidden>
<button type="button" id="avatar-choose-btn" class="button">{{ t('settings.choose_file') }}</button>
<span id="avatar-filename" class="avatar-filename">{{ t('settings.no_file_selected') }}</span>
</div>
<div class="avatar-progress-wrapper" id="avatar-progress-wrapper" style="display: none;">
<div class="avatar-progress-bar">
<div class="avatar-progress-fill" id="avatar-progress-fill"></div>
</div>
<span class="avatar-progress-text" id="avatar-progress-text">0%</span>
</div>
<div class="avatar-upload-actions">
<button type="button" id="avatar-upload-btn" class="button" disabled>{{ t('settings.upload_btn') }}</button>
<div id="main">
<div class="settings">
<h1>{{ t('settings.title') }}</h1>
<h2>{{ t('settings.avatar') }}</h2>
<div class="avatar-settings-wrapper">
<div class="avatar-preview-container">
<div class="avatar-preview-label">{{ t('settings.current_avatar') }}</div>
@if(avatar_file)
<button type="button" id="avatar-remove-btn" class="button button-danger">{{ t('settings.remove_custom') }}</button>
<a href="/user/{!! session.user !!}" target="_blank">
<img id="avatar-preview" class="avatar-preview-img" src="/a/{{ avatar_file }}">
</a>
@elseif(session.avatar && session.avatar > 0)
<a href="/user/{!! session.user !!}" target="_blank">
<img id="avatar-preview" class="avatar-preview-img" src="/t/{{ session.avatar }}.webp">
</a>
@else
<div id="avatar-preview" class="avatar-preview-img avatar-placeholder">?</div>
@endif
</div>
<div id="avatar-upload-status" class="avatar-status"></div>
<div class="avatar-upload-section">
<h4>{{ t('settings.upload_custom_avatar') }}</h4>
<p class="avatar-hint">{{ t('settings.avatar_hint') }}</p>
<div class="avatar-upload-wrapper">
<input type="file" id="avatar-file-input" accept="image/gif,image/jpeg,image/png,image/webp" hidden>
<button type="button" id="avatar-choose-btn" class="button">{{ t('settings.choose_file') }}</button>
<span id="avatar-filename" class="avatar-filename">{{ t('settings.no_file_selected') }}</span>
</div>
<div class="avatar-progress-wrapper" id="avatar-progress-wrapper" style="display: none;">
<div class="avatar-progress-bar">
<div class="avatar-progress-fill" id="avatar-progress-fill"></div>
</div>
<span class="avatar-progress-text" id="avatar-progress-text">0%</span>
</div>
<div class="avatar-upload-actions">
<button type="button" id="avatar-upload-btn" class="button" disabled>{{ t('settings.upload_btn') }}</button>
@if(avatar_file)
<button type="button" id="avatar-remove-btn" class="button button-danger">{{ t('settings.remove_custom') }}</button>
@endif
</div>
<div id="avatar-upload-status" class="avatar-status"></div>
</div>
</div>
</div>
@if(enable_profile_description)
<div class="profile-settings-wrapper">
@if(enable_profile_description)
<div class="profile-settings-wrapper">
<div class="setting-item">
<label for="profile_description">{{ t('settings.custom_description') }}</label>
<textarea id="profile_description" class="input" placeholder="{{ t('settings.description_placeholder') }}" maxlength="255">{!! session.description || '' !!}</textarea>
<textarea id="profile_description" class="input" placeholder="{{ t('settings.description_placeholder') }}"
maxlength="255">{!! session.description || '' !!}</textarea>
<div class="profile-settings-actions">
<button type="button" id="btn-save-description" class="button">{{ t('settings.save_description') }}</button>
<button type="button" id="btn-clear-description" class="button button-danger">{{ t('settings.clear') }}</button>
</div>
<div id="description-status" class="avatar-status"></div>
</div>
</div>
@endif
<h2>{{ t('settings.preferences') }}</h2>
<div class="preferences-settings-wrapper">
<fieldset style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.ui_section') }}</legend>
<div class="setting-item" style="margin-bottom: 15px;">
<label for="show_motd_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="show_motd_toggle" @if(session.show_motd !==false) checked @endif>
<span>{{ t('settings.show_motd') }}</span>
</label>
</div>
@endif
<div class="setting-item" style="margin-top: 20px;">
<label for="username_color_picker" style="display: block; margin-bottom: 5px;">{{ t('settings.username_color') }}</label>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<input type="color" id="username_color_picker" value="{{ session.username_color || '#ffffff' }}"
style="width: 50px; height: 30px; padding: 0; border: 1px solid var(--nav-border-color); cursor: pointer; background: none;">
<input type="text" id="username_color_hex" value="{{ session.username_color || '#ffffff' }}" maxlength="7"
placeholder="#ffffff" class="input"
style="width: 90px; font-family: monospace; font-size: 0.9em; padding: 4px 8px; height: 30px;">
<button type="button" id="btn-save-username-color" class="button"
style="padding: 5px 10px; font-size: 0.85em;">{{ t('settings.save_color') }}</button>
<button type="button" id="btn-reset-username-color" class="button button-secondary"
style="padding: 5px 10px; font-size: 0.85em;">{{ t('settings.reset') }}</button>
</div>
<div class="setting-item">
<label for="use_new_layout_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="use_new_layout_toggle" @if(session.use_new_layout === true) checked @endif>
<span>{{ t('settings.modern_layout') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.modern_layout_hint') }}</small>
</div>
@if(!session.use_new_layout)
<div class="setting-item" style="margin-top: 15px;">
<label for="alternative_infobox_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="alternative_infobox_toggle" @if(session.use_alternative_infobox === true) checked @endif>
<span>{{ t('settings.alternative_infobox') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.alternative_infobox_hint') }}</small>
</div>
@endif
<div class="setting-item" style="margin-top: 15px;">
<label for="disable_autoplay_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="disable_autoplay_toggle" @if(session.disable_autoplay === true) checked @endif>
<span>{{ t('settings.disable_autoplay') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_autoplay_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="disable_swiping_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="disable_swiping_toggle" @if(session.disable_swiping === true) checked @endif>
<span>{{ t('settings.disable_swiping') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_swiping_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="show_background_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="show_background_toggle" @if(session.show_background !== false) checked @endif>
<span>{{ t('settings.enable_bg_blur') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.enable_bg_blur_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="quote_emojis_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="quote_emojis_toggle" @if(session.quote_emojis !== false) checked @endif>
<span>{{ t('settings.render_emojis') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.render_emojis_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="embed_youtube_in_comments_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="embed_youtube_in_comments_toggle" @if(session.embed_youtube_in_comments !== false) checked @endif>
<span>{{ t('settings.embed_yt') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.embed_yt_hint') }}</small>
</div>
@if(show_koepfe)
<div class="setting-item" style="margin-top: 15px;">
<label for="hide_koepfe_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="hide_koepfe_toggle" @if(session.hide_koepfe === true) checked @endif>
<span>{{ t('settings.hide_koepfe') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_koepfe_hint') }}</small>
</div>
@endif
@if(allow_language_change)
<div class="setting-item" style="margin-top: 15px;">
<label for="language_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.language') }}</label>
<select id="language_select" class="input" style="padding: 6px 10px; max-width: 220px;">
<option value="" @if(!session.language) selected @endif>{{ t('settings.language_default') }}</option>
<option value="en" @if(session.language === 'en') selected @endif>{{ t('settings.language_en') }}</option>
<option value="de" @if(session.language === 'de') selected @endif>{{ t('settings.language_de') }}</option>
<option value="nl" @if(session.language === 'nl') selected @endif>{{ t('settings.language_nl') }}</option>
<option value="zange" @if(session.language === 'zange') selected @endif>{{ t('settings.language_zange') }}</option>
</select>
<br><small class="text-muted">{{ t('settings.language_hint') }}</small>
</div>
@endif
<div class="setting-item" style="margin-top: 15px;">
<label for="wheel_nav_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="wheel_nav_toggle">
<span>{{ t('settings.scroll_nav') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.scroll_nav_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 20px;">
<label for="username_color_picker" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.username_color') }}</label>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<input type="color" id="username_color_picker" value="{{ session.username_color || '#ffffff' }}" style="width: 50px; height: 30px; padding: 0; border: 1px solid var(--nav-border-color); cursor: pointer; background: none;">
<input type="text" id="username_color_hex" value="{{ session.username_color || '#ffffff' }}" maxlength="7" placeholder="#ffffff" class="input" style="width: 90px; font-family: monospace; font-size: 0.9em; padding: 4px 8px; height: 30px;">
<button type="button" id="btn-save-username-color" class="button" style="padding: 5px 10px; font-size: 0.85em;">{{ t('settings.save_color') }}</button>
<button type="button" id="btn-reset-username-color" class="button button-secondary" style="padding: 5px 10px; font-size: 0.85em;">{{ t('settings.reset') }}</button>
<small class="text-muted">{{ t('settings.username_color_hint') }}</small>
</div>
<h2>{{ t('settings.preferences') }}</h2>
<div class="preferences-settings-wrapper">
<fieldset
style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.ui_section') }}</legend>
<div class="setting-item" style="margin-bottom: 15px;">
<label for="show_motd_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="show_motd_toggle" @if(session.show_motd !==false) checked @endif>
<span>{{ t('settings.show_motd') }}</span>
</label>
</div>
<small class="text-muted">{{ t('settings.username_color_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 20px;">
<label for="website_font_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.website_font') }}</label>
<div style="display: flex; align-items: center; gap: 10px;">
<select id="website_font_select" class="input" style="flex: 1; max-width: 300px;">
<option value="">{{ t('settings.font_default') }}</option>
@each(fonts as font)
<option value="{{ font.file }}" @if(session.font === font.file) selected @endif>{{ font.name }}</option>
@endeach
</select>
<div class="setting-item">
<label for="use_new_layout_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="use_new_layout_toggle" @if(session.use_new_layout===true) checked @endif>
<span>{{ t('settings.modern_layout') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.modern_layout_hint') }}</small>
</div>
</div>
<div class="setting-item" style="margin-top: 20px;">
<label for="website_theme_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.theme') }}</label>
<div style="display: flex; align-items: center; gap: 10px;">
<select id="website_theme_select" class="input" style="flex: 1; max-width: 300px;">
@each(themes as t)
<option value="{{ t }}" @if(theme === t) selected @endif>{{ t }}</option>
@endeach
</select>
@if(!session.use_new_layout)
<div class="setting-item" style="margin-top: 15px;">
<label for="alternative_infobox_toggle"
style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="alternative_infobox_toggle" @if(session.use_alternative_infobox===true) checked @endif>
<span>{{ t('settings.alternative_infobox') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.alternative_infobox_hint') }}</small>
</div>
</div>
</fieldset>
@if(enable_swf)
<fieldset style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.flash_section') }}</legend>
<div class="setting-item" style="margin-bottom: 15px;">
<label for="ruffle_volume_input" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.flash_volume') }}</label>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<input type="range" id="ruffle_volume_input" min="0" max="1" step="0.01" value="{{ session.ruffle_volume !== undefined && session.ruffle_volume !== null ? session.ruffle_volume : 0.5 }}" class="xd-slider" style="flex: 1; min-width: 140px;">
<span id="ruffle_volume_val" class="xd-slider-val">{{ session.ruffle_volume !== undefined && session.ruffle_volume !== null ? Math.round(session.ruffle_volume * 100) : 50 }}%</span>
</div>
</div>
<div class="setting-item">
<label for="ruffle_background_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="ruffle_background_toggle" @if(session.ruffle_background !== false) checked @endif>
<span>{{ t('settings.flash_bg') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.flash_bg_hint') }}</small>
</div>
<div style="margin-top: 15px;">
<button type="button" id="btn-save-ruffle-settings" class="button" style="padding: 5px 15px;">{{ t('settings.save_flash') }}</button>
<div id="ruffle-settings-status" class="avatar-status"></div>
</div>
</fieldset>
@endif
</div>
<h2>{{ t('settings.account') }}</h2>
<div class="account-settings-wrapper" style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
<table class="table account-info-table" style="margin-bottom: 30px; border-collapse: separate; border-spacing: 0 5px;">
<tbody>
<tr>
<td style="width: 150px; font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.user_id') }}</td>
<td style="border: none;">{{ session.id }}</td>
</tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.username') }}</td>
<td style="border: none;">{!! session.user !!}</td>
</tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.display_name') }}</td>
<td style="border: none; display: flex; align-items: center; gap: 10px;">
<input type="text" id="display_name_input" class="input" placeholder="{{ t('settings.display_name_placeholder') }}" value="{!! session.display_name || '' !!}" maxlength="32" style="max-width: 200px; height: 30px; font-size: 0.9em;">
<button type="button" id="btn-update-display-name" class="button" style="padding: 2px 10px; font-size: 0.8em; height: 30px;">{{ t('settings.save') }}</button>
</td>
</tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.email') }}</td>
<td id="display-email" style="border: none;">{{ email || t('settings.email_not_set') }}</td>
</tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.joined') }}</td>
<td style="border: none;">@if(joined){{ new Date(joined).toLocaleDateString() }}@else {{ t('settings.joined_unknown') }} @endif</td>
</tr>
</tbody>
</table>
<div id="display-name-status" class="avatar-status" style="margin-bottom: 20px;"></div>
<div class="account-actions-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px;">
<div class="password-change-section" style="display: flex; flex-direction: column; gap: 15px;">
<h4 style="margin-top: 0; color: var(--accent);">{{ t('settings.change_password') }}</h4>
<form id="password-change-form" style="display: flex; flex-direction: column; gap: 10px;">
<input type="password" id="current_password" class="input" placeholder="{{ t('settings.current_password') }}" required autocomplete="current-password">
<input type="password" id="new_password" class="input" placeholder="{{ t('settings.new_password') }}" required minlength="20" autocomplete="new-password">
<input type="password" id="new_password_confirm" class="input" placeholder="{{ t('settings.confirm_new_password') }}" required minlength="20" autocomplete="new-password">
<button type="submit" id="btn-update-password" class="button">{{ t('settings.update_password') }}</button>
</form>
<div id="password-status" class="avatar-status"></div>
</div>
<div class="email-update-section" style="display: flex; flex-direction: column; gap: 15px;">
<h4 style="margin-top: 0; color: var(--accent);">{{ t('settings.update_email') }}</h4>
<form id="email-update-form" style="display: flex; flex-direction: column; gap: 10px;">
<input type="email" id="email_input" class="input" placeholder="{{ t('settings.new_email') }}" value="{{ email }}" required>
<button type="submit" id="btn-update-email" class="button">{{ t('settings.update_email_btn') }}</button>
</form>
@if(smtp_enabled)
{!! t('settings.email_warning_smtp') !!}
@else
{!! t('settings.email_info_no_smtp') !!}
@endif
<div id="email-status" class="avatar-status"></div>
<div class="setting-item" style="margin-bottom: 15px;">
<label for="wheel_nav_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="wheel_nav_toggle">
<span>{{ t('settings.scroll_nav') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.scroll_nav_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="disable_autoplay_toggle"
style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="disable_autoplay_toggle" @if(session.disable_autoplay===true) checked @endif>
<span>{{ t('settings.disable_autoplay') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_autoplay_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="disable_swiping_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="disable_swiping_toggle" @if(session.disable_swiping===true) checked @endif>
<span>{{ t('settings.disable_swiping') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.disable_swiping_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="show_background_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="show_background_toggle" @if(session.show_background !==false) checked @endif>
<span>{{ t('settings.enable_bg_blur') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.enable_bg_blur_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="quote_emojis_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="quote_emojis_toggle" @if(session.quote_emojis !==false) checked @endif>
<span>{{ t('settings.render_emojis') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.render_emojis_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label for="embed_youtube_in_comments_toggle"
style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="embed_youtube_in_comments_toggle" @if(session.embed_youtube_in_comments !==false) checked @endif>
<span>{{ t('settings.embed_yt') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.embed_yt_hint') }}</small>
</div>
@if(show_koepfe)
<div class="setting-item" style="margin-top: 15px;">
<label for="hide_koepfe_toggle" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="hide_koepfe_toggle" @if(session.hide_koepfe===true) checked @endif>
<span>{{ t('settings.hide_koepfe') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.hide_koepfe_hint') }}</small>
</div>
@endif
@if(allow_language_change)
<div class="setting-item" style="margin-top: 15px;">
<label for="language_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.language') }}</label>
<select id="language_select" class="input" style="padding: 6px 10px; max-width: 220px;">
<option value="" @if(!session.language) selected @endif>{{ t('settings.language_default') }}</option>
<option value="en" @if(session.language==='en' ) selected @endif>{{ t('settings.language_en') }}</option>
<option value="de" @if(session.language==='de' ) selected @endif>{{ t('settings.language_de') }}</option>
<option value="nl" @if(session.language==='nl' ) selected @endif>{{ t('settings.language_nl') }}</option>
<option value="zange" @if(session.language==='zange' ) selected @endif>{{ t('settings.language_zange') }}
</option>
</select>
<br><small class="text-muted">{{ t('settings.language_hint') }}</small>
</div>
@endif
<div class="setting-item" style="margin-top: 15px;">
<label for="comment_display_mode_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.comment_display_mode') }}</label>
<select id="comment_display_mode_select" class="input" style="padding: 6px 10px; max-width: 220px;" @if(session.force_comment_display_mode) disabled @endif>
<option value="0" @if(session.comment_display_mode==0) selected @endif>{{ t('settings.comment_display_tree') }}</option>
<option value="1" @if(session.comment_display_mode==1) selected @endif>{{ t('settings.comment_display_linear') }}</option>
</select>
<br><small class="text-muted">
@if(session.force_comment_display_mode)
<strong>{{ t('settings.forced_mode_notice') || 'This setting is managed by an administrator.' }}</strong>
@else
{{ t('settings.comment_display_mode_hint') }}
@endif
</small>
</div>
</fieldset>
<fieldset
style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.notifications_section') }}</legend>
<div class="setting-item">
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="chk-receive-system-notifications" @if(session.receive_system_notifications !==false) checked @endif>
<span>{{ t('settings.receive_system_notifications') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.receive_system_notifications_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="chk-receive-user-notifications" @if(session.receive_user_notifications !==false) checked @endif>
<span>{{ t('settings.receive_user_notifications') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.receive_user_notifications_hint') }}</small>
</div>
<div class="setting-item" style="margin-top: 15px;">
<label class="checkbox-container" style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="chk-do-not-disturb" @if(session.do_not_disturb===true) checked @endif>
<span>{{ t('settings.do_not_disturb') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.do_not_disturb_hint') }}</small>
</div>
</fieldset>
<fieldset
style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.appearance_section') }}</legend>
<div class="setting-item" style="margin-top: 20px;">
<label for="website_font_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.website_font') }}</label>
<div style="display: flex; align-items: center; gap: 10px;">
<select id="website_font_select" class="input" style="flex: 1; max-width: 300px;">
<option value="">{{ t('settings.font_default') }}</option>
@each(fonts as font)
<option value="{{ font.file }}" @if(session.font===font.file) selected @endif>{{ font.name }}</option>
@endeach
</select>
</div>
</div>
<div class="setting-item" style="margin-top: 20px;">
<label for="website_theme_select" style="display: block; margin-bottom: 5px; font-weight: bold;">{{ t('settings.theme') }}</label>
<div style="display: flex; align-items: center; gap: 10px;">
<select id="website_theme_select" class="input" style="flex: 1; max-width: 300px;">
@each(themes as t)
<option value="{{ t }}" @if(theme===t) selected @endif>{{ t }}</option>
@endeach
</select>
</div>
</div>
</fieldset>
@if(enable_swf)
<fieldset
style="border: 1px solid var(--nav-border-color); padding: 10px; border-radius: 4px; margin-bottom: 15px;">
<legend style="width: auto; padding: 0 5px; font-size: 1.1em; font-weight: bold;">{{ t('settings.flash_section') }}</legend>
<div class="setting-item">
<label for="ruffle_background_toggle"
style="cursor: pointer; display: flex; align-items: center; gap: 10px;">
<input type="checkbox" id="ruffle_background_toggle" @if(session.ruffle_background !==false) checked @endif>
<span>{{ t('settings.flash_bg') }}</span>
</label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.flash_bg_hint') }}</small>
</div>
<div id="ruffle-settings-status" class="avatar-status" style="margin-top: 10px;"></div>
</fieldset>
@endif
</div>
<h2>{{ t('settings.account') }}</h2>
<div class="account-settings-wrapper"
style="background: rgba(0,0,0,0.1); padding: 20px; border-radius: 4px; border: 1px solid var(--nav-border-color); margin-bottom: 30px;">
<table class="table account-info-table"
style="margin-bottom: 30px; border-collapse: separate; border-spacing: 0 5px;">
<tbody>
<tr>
<td style="width: 150px; font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.user_id') }}</td>
<td style="border: none;">{{ session.id }}</td>
</tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.username') }}</td>
<td style="border: none;">{!! session.user !!}</td>
</tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.display_name') }}
</td>
<td style="border: none; display: flex; align-items: center; gap: 10px;">
<input type="text" id="display_name_input" class="input"
placeholder="{{ t('settings.display_name_placeholder') }}" value="{!! session.display_name || '' !!}"
maxlength="32" style="max-width: 200px; height: 30px; font-size: 0.9em;">
<button type="button" id="btn-update-display-name" class="button"
style="padding: 2px 10px; font-size: 0.8em; height: 30px;">{{ t('settings.save') }}</button>
</td>
</tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.email') }}</td>
<td id="display-email" style="border: none;">{{ email || t('settings.email_not_set') }}</td>
</tr>
<tr>
<td style="font-weight: bold; color: var(--text-muted); border: none;">{{ t('settings.joined') }}</td>
<td style="border: none;">@if(joined){{ new Date(joined).toLocaleDateString() }}@else {{ t('settings.joined_unknown') }} @endif</td>
</tr>
</tbody>
</table>
<div id="display-name-status" class="avatar-status" style="margin-bottom: 20px;"></div>
<div class="account-actions-grid"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px;">
<div class="password-change-section" style="display: flex; flex-direction: column; gap: 15px;">
<h4 style="margin-top: 0; color: var(--accent);">{{ t('settings.change_password') }}</h4>
<form id="password-change-form" style="display: flex; flex-direction: column; gap: 10px;">
<input type="password" id="current_password" class="input"
placeholder="{{ t('settings.current_password') }}" required autocomplete="current-password">
<input type="password" id="new_password" class="input" placeholder="{{ t('settings.new_password') }}"
required minlength="20" autocomplete="new-password">
<input type="password" id="new_password_confirm" class="input"
placeholder="{{ t('settings.confirm_new_password') }}" required minlength="20"
autocomplete="new-password">
<button type="submit" id="btn-update-password" class="button">{{ t('settings.update_password') }}</button>
</form>
<div id="password-status" class="avatar-status"></div>
</div>
<div class="email-update-section" style="display: flex; flex-direction: column; gap: 15px;">
<h4 style="margin-top: 0; color: var(--accent);">{{ t('settings.update_email') }}</h4>
<form id="email-update-form" style="display: flex; flex-direction: column; gap: 10px;">
<input type="email" id="email_input" class="input" placeholder="{{ t('settings.new_email') }}"
value="{{ email }}" required>
<button type="submit" id="btn-update-email" class="button">{{ t('settings.update_email_btn') }}</button>
</form>
@if(smtp_enabled)
{!! t('settings.email_warning_smtp') !!}
@else
{!! t('settings.email_info_no_smtp') !!}
@endif
<div id="email-status" class="avatar-status"></div>
</div>
</div>
</div>
@if(matrix_enabled)
<h2>{{ t('settings.linked_accounts') }}</h2>
<div class="linked-accounts-wrapper">
<p>{{ t('settings.matrix_link_desc') }}</p>
<div id="linked-accounts-container" style="margin-bottom: 1rem;">
<strong>{{ t('settings.active_links') }}</strong>
<div id="linked-accounts-list" style="margin-top: 0.5rem; display: flex; flex-direction: column; gap: 5px;">
<span class="text-muted">{{ t('settings.loading') }}</span>
</div>
</div>
<div class="link-action-area">
<h4 style="margin-top: 20px;">{{ t('settings.add_new_link') }}</h4>
<p style="margin-bottom: 15px; color: var(--text-muted); font-size: 0.9em;">
{!! t('settings.matrix_instructions') !!}
</p>
<div id="token-display-area" style="display: none; margin-bottom: 15px;">
<div class="alert alert-info">
<strong>{{ t('settings.your_token') }}</strong> <code id="generated-token-code"
style="font-size: 1.2em; user-select: all; margin-left: 10px;"></code>
<br><small>{{ t('settings.one_time_use') }}</small>
</div>
</div>
<button id="btn-gen-link-token" class="button button-primary">{{ t('settings.generate_token') }}</button>
</div>
</div>
@endif
<script src="/s/js/settings.js?v=@mtime(/public/s/js/settings.js)"></script>
</div>
@if(matrix_enabled)
<h2>{{ t('settings.linked_accounts') }}</h2>
<div class="linked-accounts-wrapper">
<p>{{ t('settings.matrix_link_desc') }}</p>
<div id="linked-accounts-container" style="margin-bottom: 1rem;">
<strong>{{ t('settings.active_links') }}</strong>
<div id="linked-accounts-list" style="margin-top: 0.5rem; display: flex; flex-direction: column; gap: 5px;">
<span class="text-muted">{{ t('settings.loading') }}</span>
</div>
</div>
<div class="link-action-area">
<h4 style="margin-top: 20px;">{{ t('settings.add_new_link') }}</h4>
<p style="margin-bottom: 15px; color: var(--text-muted); font-size: 0.9em;">
{!! t('settings.matrix_instructions') !!}
</p>
<div id="token-display-area" style="display: none; margin-bottom: 15px;">
<div class="alert alert-info">
<strong>{{ t('settings.your_token') }}</strong> <code id="generated-token-code" style="font-size: 1.2em; user-select: all; margin-left: 10px;"></code>
<br><small>{{ t('settings.one_time_use') }}</small>
</div>
</div>
<button id="btn-gen-link-token" class="button button-primary">{{ t('settings.generate_token') }}</button>
</div>
</div>
@endif
<script src="/s/js/settings.js?v=@mtime(/public/s/js/settings.js)"></script>
</div>
</div>
</div>
@include(snippets/footer)

View File

@@ -124,7 +124,8 @@
@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 };
window.f0ckSession = { logged_in: false, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, development: @if(development) true @else false @endif };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
(() => {
const loginModal = document.getElementById('login-modal');
const registerModal = document.getElementById('register-modal');
@@ -379,13 +380,20 @@
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,
enable_xd_score: @if(enable_xd_score) 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
avatar_file: @if(session && session.avatar_file) "{{ session.avatar_file }}" @else null @endif,
receive_system_notifications: @if(session)@if(session.receive_system_notifications !== false) true @else false @endif@else true @endif,
receive_user_notifications: @if(session)@if(session.receive_user_notifications !== false) true @else false @endif@else true @endif,
do_not_disturb: @if(session && session.do_not_disturb) true @else false @endif,
comment_display_mode: {{ comment_display_mode }},
development: @if(development) true @else false @endif
};
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
window.f0ckI18n = {
write_comment: "{{ t('comments.write_comment') }}",
post: "{{ t('comments.post') }}",
@@ -409,6 +417,8 @@
timeago_years: "{{ t('timeago.years') }}",
timeago_month: "{{ t('timeago.month') }}",
timeago_months: "{{ t('timeago.months') }}",
timeago_week: "{{ t('timeago.week') }}",
timeago_weeks: "{{ t('timeago.weeks') }}",
timeago_day: "{{ t('timeago.day') }}",
timeago_days: "{{ t('timeago.days') }}",
timeago_hour: "{{ t('timeago.hour') }}",
@@ -539,9 +549,9 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').then((registration) => {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
window.f0ckDebug('ServiceWorker registration successful with scope: ', registration.scope);
}, (err) => {
console.log('ServiceWorker registration failed: ', err);
window.f0ckDebug('ServiceWorker registration failed: ', err);
});
});
}

View File

@@ -25,6 +25,10 @@
<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/pdf' && enable_pdf)
<div class="embed-responsive embed-responsive-16by9" style="background: #fff;border: 1px solid #444; overflow: hidden;">
<iframe class="embed-responsive-item" src="{{ item.dest }}#toolbar=0" sandbox="allow-scripts allow-same-origin" loading="lazy" style="border: none; overscroll-behavior: contain !important;"></iframe>
</div>
@else
<h1>404 - Not f0cked</h1>
@endif

View File

@@ -1,5 +1,5 @@
@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')">
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! 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')) }}" data-bg="/t/{{ item.id }}.webp">
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>

View File

@@ -14,18 +14,18 @@
<!-- 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>
<a id="nav-upload-link" style="cursor:pointer;"><i class="fa-solid fa-angle-up"></i> {{ t('nav.upload') }}</a>
@if(meme_creator)
<a href="/meme">{{ t('nav.meme') }}</a>
<a id="nav-meme-link" href="/meme"><i class="fa-regular fa-image"></i> {{ t('nav.meme') }}</a>
@endif
@if(halls_enabled)
<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>
<i class="fa-solid fa-layer-group"></i>
</a>
</div>
@endif
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tag"></i></a>
<a href="/tags" title="{{ t('nav.tags') }}"><i class="fa-solid fa-tags"></i></a>
@if(abyss_enabled)
<a href="/abyss" title="{{ t('nav.abyss') }}"><i class="fa-solid fa-dice-d6"></i></a>
@endif
@@ -59,14 +59,14 @@
@endif
<a href="/user/{{ session.user.toLowerCase() }}/favs" class="mobile-only">{{ t('nav.favs') }}</a>
@if(session.admin)
<a href="/admin">Admin
<a href="/admin">{{ t('nav.admin') }}
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="notification-dot" title="{{ session.pending_count }} Pending" onclick="event.preventDefault(); window.location.href='/admin/approve';"></span>
@endif
</a>
@endif
@if(session.admin || session.is_moderator)
<a href="/mod">mod
<a href="/mod">{{ t('nav.mod') }}
@if(typeof session.pending_count !== 'undefined' && session.pending_count > 0)
<span class="notification-dot" title="{{ session.pending_count }} Pending" onclick="event.preventDefault(); window.location.href='/mod/approve';"></span>
@endif
@@ -121,7 +121,7 @@
<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>
<a href="#" id="nav-filter-btn" title="Filter"><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>
@@ -157,11 +157,11 @@
<div class="nav-collapse" id="navbarContent">
<div class="nav-links">
<a href="/tags" title="Tags"><i class="fa-solid fa-tag"></i></a>
<a href="/tags" title="Tags"><i class="fa-solid fa-tags"></i></a>
@if(halls_enabled)
<div class="">
<a href="/halls" class="nav-halls-btn" title="Halls">
<i class="fa-solid fa-building-columns"></i>
<i class="fa-solid fa-layer-group"></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>
@@ -357,7 +357,7 @@
<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-title">{{ t('upload.title') }}</div>
<div class="upload-limit-info">
@if(session.uploads_remaining === undefined)

View File

@@ -1,4 +1,4 @@
<form id="upload-form" class="upload-form" enctype="multipart/form-data" data-mimes='{!! mimes_json !!}' data-max-bytes="{{ max_file_size_bytes }}">
<form id="upload-form" class="upload-form" enctype="multipart/form-data" data-mimes='{!! mimes_json !!}' data-max-bytes="{{ max_file_size_bytes }}" data-min-tags="{{ min_tags }}">
<div class="form-section">
@if(web_url_upload)
<div class="upload-mode-tabs">
@@ -18,7 +18,6 @@
<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>
@@ -77,14 +76,21 @@
</div>
<div class="form-section">
<label class="oc-option">
<div 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>
<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>
<label>
{{ t('upload.tags') }}
@if(min_tags > 0)
<span class="required">*</span> <span class="tag-count">(0/{{ min_tags }} {{ t('upload.tags_minimum') }})</span>
@else
<span style="opacity: 0.5; font-weight: normal;">{{ t('upload.comment_optional') }}</span>
@endif
</label>
<div class="tag-input-container">
<div class="sync-spinner">
<span class="spinner-icon"></span>

View File

@@ -3,7 +3,7 @@
<div id="main">
<link rel="stylesheet" href="/s/css/upload.css">
<div class="upload-container" style="opacity: 0;" data-mimes='{!! mimes_json !!}'>
<h2>{{ t('upload_page.title') }}</h2>
<div class="upload-title">{{ t('upload.title') }}</div>
<div class="upload-limit-info">
@if(uploads_remaining === null)