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

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>