Files
Mow910 bb92c3dd92 Enhance shoutbox functionality and UI in popout template
- Added buttons to adjust font size and toggle sound notifications in the shoutbox.
- Updated sound notification logic to improve user experience when new messages arrive.
- Refactored font size handling to persist user preferences using local storage.
- Improved layout for better responsiveness and usability across devices.
2026-03-25 22:30:58 +01:00

297 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="fr" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shoutbox Modos</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #030712; }
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #374151; border-radius: 2px; }
</style>
</head>
<body class="bg-gray-950 text-gray-100 flex flex-col h-screen">
<div class="flex items-center justify-between px-3 py-1.5 bg-gray-900 border-b border-gray-800 flex-shrink-0">
<div class="flex items-center gap-2">
<svg class="w-4 h-4 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a2 2 0 01-2-2v-6a2 2 0 012-2h8z"></path></svg>
<span class="text-sm font-semibold text-gray-200">Shoutbox Modos</span>
<span class="text-xs text-gray-500" id="shoutboxCount"></span>
</div>
<div class="flex items-center gap-3 flex-wrap justify-end">
<div class="flex items-center gap-1.5">
<button type="button" onclick="shoutboxFontSize(-1)" class="text-gray-400 hover:text-white text-xs leading-none px-1" title="Réduire la police">A-</button>
<span class="text-gray-500 text-xs tabular-nums min-w-[1.25rem] text-center" id="shoutboxFontLabel">14</span>
<button type="button" onclick="shoutboxFontSize(1)" class="text-gray-400 hover:text-white text-xs leading-none px-1" title="Agrandir la police">A+</button>
</div>
<button type="button" onclick="toggleShoutboxSound()" id="shoutboxSoundBtn" class="text-xs flex items-center gap-1 text-green-400 hover:text-green-300" title="Son activé">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072M17.95 6.05a8 8 0 010 11.9M11 5L6 9H2v6h4l5 4V5z"></path></svg>
<span id="shoutboxSoundLabel">Son</span>
</button>
<div id="onlineIndicator" class="flex items-center gap-1.5 text-xs text-gray-400">
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
<span id="onlineCountText">0 en ligne</span>
</div>
</div>
</div>
<div class="flex flex-1 overflow-hidden">
<div class="flex-1 relative">
<div class="absolute inset-0 overflow-y-auto font-mono p-2" id="shoutboxDisplay" style="scrollbar-width: thin;">
<div class="text-gray-500 text-center py-4" id="shoutboxPlaceholder">Aucun message</div>
</div>
<button id="newMsgBtn" onclick="scrollToBottom()" class="hidden absolute bottom-2 left-1/2 -translate-x-1/2 z-10 px-3 py-1 rounded-full bg-indigo-600/90 hover:bg-indigo-500 text-white text-xs font-medium shadow-lg animate-pulse flex items-center gap-1.5 backdrop-blur-sm">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path></svg>
<span id="newMsgText">Nouveaux messages</span>
</button>
</div>
<div class="w-24 border-l border-gray-800 bg-gray-900/50 p-2 overflow-y-auto flex-shrink-0" style="scrollbar-width: thin;">
<div class="text-xs text-gray-500 font-semibold mb-1">En ligne</div>
<div id="shoutboxOnlineList" class="space-y-1">
<div class="text-xs text-gray-600 italic">---</div>
</div>
</div>
</div>
<div class="border-t border-gray-800 p-2 bg-gray-900 flex-shrink-0">
<form onsubmit="sendShoutboxMessage(event)" class="flex gap-1.5">
<span class="text-xs text-indigo-400 font-mono flex items-center">&gt;</span>
<input type="text" id="shoutboxInput" placeholder="Message..." maxlength="500" autocomplete="off"
class="flex-1 px-2 py-1 rounded border border-gray-700 bg-gray-800 text-gray-100 text-xs font-mono focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 focus:outline-none">
<button type="submit" class="px-3 py-1 bg-indigo-600 hover:bg-indigo-700 text-white rounded text-xs font-medium transition-colors">Envoyer</button>
</form>
</div>
<script>
var knownIds = new Set();
var lastTimestamp = '';
var autoScroll = true;
var tabVisible = true;
var shoutboxAudioCtx = null;
var unreadCount = 0;
var shoutboxSoundEnabled = localStorage.getItem('shoutbox_sound') !== 'off';
var shoutboxFontSizePx = parseInt(localStorage.getItem('shoutbox_fontsize'), 10);
if (isNaN(shoutboxFontSizePx)) shoutboxFontSizePx = 14;
document.addEventListener('visibilitychange', function() { tabVisible = !document.hidden; });
function shoutboxApplyFontSize() {
var display = document.getElementById('shoutboxDisplay');
if (display) display.style.fontSize = shoutboxFontSizePx + 'px';
var label = document.getElementById('shoutboxFontLabel');
if (label) label.textContent = shoutboxFontSizePx;
localStorage.setItem('shoutbox_fontsize', String(shoutboxFontSizePx));
}
function shoutboxFontSize(delta) {
shoutboxFontSizePx = Math.max(8, Math.min(20, shoutboxFontSizePx + delta));
shoutboxApplyFontSize();
}
function shoutboxUpdateSoundUI() {
var btn = document.getElementById('shoutboxSoundBtn');
if (!btn) return;
btn.className = 'text-xs flex items-center gap-1 ' + (shoutboxSoundEnabled ? 'text-green-400 hover:text-green-300' : 'text-red-400 hover:text-red-300');
btn.title = shoutboxSoundEnabled ? 'Son activé (nouveaux messages si fenêtre en arrière-plan)' : 'Son désactivé';
var lab = document.getElementById('shoutboxSoundLabel');
if (lab) lab.textContent = shoutboxSoundEnabled ? 'Son' : 'Muet';
}
function toggleShoutboxSound() {
shoutboxSoundEnabled = !shoutboxSoundEnabled;
localStorage.setItem('shoutbox_sound', shoutboxSoundEnabled ? 'on' : 'off');
shoutboxUpdateSoundUI();
}
function shoutboxBeep() {
if (!shoutboxSoundEnabled) return;
try {
if (!shoutboxAudioCtx) shoutboxAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
var osc = shoutboxAudioCtx.createOscillator();
var gain = shoutboxAudioCtx.createGain();
osc.connect(gain);
gain.connect(shoutboxAudioCtx.destination);
osc.frequency.value = 660;
osc.type = 'sine';
gain.gain.setValueAtTime(0.15, shoutboxAudioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, shoutboxAudioCtx.currentTime + 0.3);
osc.start(shoutboxAudioCtx.currentTime);
osc.stop(shoutboxAudioCtx.currentTime + 0.3);
} catch(e) {}
}
var COLORS = ['#6366f1','#8b5cf6','#ec4899','#14b8a6','#f59e0b','#3b82f6','#10b981','#ef4444','#06b6d4','#84cc16'];
var colorMap = {};
function userColor(name) {
if (!colorMap[name]) {
var h = 0;
for (var i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h);
colorMap[name] = COLORS[Math.abs(h) % COLORS.length];
}
return colorMap[name];
}
var currentUser = '{{ current_user.username }}';
function esc(t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
function fmtTime(iso) {
var d = new Date(iso);
return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0');
}
function mentionUser(username) {
var input = document.getElementById('shoutboxInput');
var val = input.value;
var mention = '@' + username + ' ';
if (val && !val.endsWith(' ')) mention = ' ' + mention;
input.value = val + mention;
input.focus();
}
function renderText(text) {
var escaped = esc(text);
var mentioned = currentUser && text.toLowerCase().indexOf('@' + currentUser.toLowerCase()) >= 0;
escaped = escaped.replace(/@(\w+)/g, '<span class="text-indigo-400 font-semibold">@$1</span>');
return { html: escaped, mentioned: mentioned };
}
function addItem(item) {
if (knownIds.has(item.id)) return;
knownIds.add(item.id);
var display = document.getElementById('shoutboxDisplay');
var ph = document.getElementById('shoutboxPlaceholder');
if (ph) ph.remove();
var line = document.createElement('div');
line.className = 'py-0.5 leading-relaxed rounded px-1';
var time = '<span class="text-gray-500">[' + fmtTime(item.created_at) + ']</span> ';
if (item.type === 'message') {
var c = userColor(item.author);
var rendered = renderText(item.text);
line.innerHTML = time + '<span style="color:' + c + '" class="font-semibold cursor-pointer hover:underline" onclick="mentionUser(\'' + esc(item.author).replace(/'/g, "\\'") + '\')">&lt;' + esc(item.author) + '&gt;</span> <span class="text-gray-200">' + rendered.html + '</span>';
if (rendered.mentioned) {
line.className += ' bg-red-900/40 font-bold';
}
} else {
var au = (item.action || '').toUpperCase();
var sc = 'text-red-400';
if (['timeout','clean','permit'].indexOf(item.action) >= 0) sc = 'text-orange-400';
if (['subon','suboff','emoteon','emoteoff','follon','folloff'].indexOf(item.action) >= 0) sc = 'text-blue-400';
if (item.action === 'unban') sc = 'text-green-400';
var txt = '*** ' + au;
if (item.moderator) txt += ' par ' + item.moderator;
if (item.target) txt += ' \u2192 ' + item.target;
if (item.details && item.details !== '-') txt += ' (' + item.details + ')';
txt += ' ***';
line.innerHTML = time + '<span class="' + sc + ' font-semibold">' + esc(txt) + '</span>';
}
display.appendChild(line);
if (!tabVisible) shoutboxBeep();
while (display.children.length > 200) display.removeChild(display.firstChild);
if (autoScroll) {
display.scrollTop = display.scrollHeight;
} else {
unreadCount++;
var btn = document.getElementById('newMsgBtn');
var txt = document.getElementById('newMsgText');
if (btn && txt) {
txt.textContent = unreadCount + ' nouveau' + (unreadCount > 1 ? 'x' : '') + ' message' + (unreadCount > 1 ? 's' : '');
btn.classList.remove('hidden');
}
}
}
function scrollToBottom() {
var d = document.getElementById('shoutboxDisplay');
d.scrollTop = d.scrollHeight;
autoScroll = true;
unreadCount = 0;
var btn = document.getElementById('newMsgBtn');
if (btn) btn.classList.add('hidden');
}
function updateOnline(users) {
var list = document.getElementById('shoutboxOnlineList');
var countEl = document.getElementById('onlineCountText');
if (countEl) countEl.textContent = (users ? users.length : 0) + ' en ligne';
if (!list) return;
if (!users || users.length === 0) { list.innerHTML = '<div class="text-xs text-gray-600 italic">---</div>'; return; }
list.innerHTML = '';
users.forEach(function(u) {
var el = document.createElement('div');
el.className = 'flex items-center gap-1.5 text-xs text-gray-300 cursor-pointer hover:text-white';
el.onclick = function() { mentionUser(u); };
el.innerHTML = '<span class="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0"></span>' + esc(u);
list.appendChild(el);
});
}
function poll() {
var url = '{{ url_for("shoutbox_messages") }}';
if (lastTimestamp) url += '?since=' + encodeURIComponent(lastTimestamp);
fetch(url)
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.items && data.items.length > 0) {
data.items.forEach(addItem);
if (autoScroll) {
setTimeout(function() {
var d = document.getElementById('shoutboxDisplay');
d.scrollTop = d.scrollHeight;
}, 0);
}
}
if (data.timestamp) lastTimestamp = data.timestamp;
if (data.online_users) updateOnline(data.online_users);
var cnt = document.getElementById('shoutboxCount');
if (cnt) cnt.textContent = '(' + knownIds.size + ')';
})
.catch(function(e) { console.error('Poll error:', e); });
}
function heartbeat() {
fetch('{{ url_for("shoutbox_heartbeat") }}', { method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}' })
.then(function(r) { return r.json(); })
.then(function(d) { updateOnline(d.online_users); })
.catch(function() {});
}
function sendShoutboxMessage(event) {
event.preventDefault();
var input = document.getElementById('shoutboxInput');
var msg = input.value.trim();
if (!msg) return;
var btn = event.target.querySelector('button[type="submit"]');
btn.disabled = true;
fetch('{{ url_for("shoutbox_send") }}', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({message: msg}) })
.then(function(r) { return r.json(); })
.then(function(d) { if (d.success) { input.value = ''; poll(); } })
.catch(function() {})
.finally(function() { btn.disabled = false; });
}
document.getElementById('shoutboxDisplay').addEventListener('scroll', function() {
autoScroll = this.scrollHeight - this.scrollTop <= this.clientHeight + 30;
if (autoScroll && unreadCount > 0) {
unreadCount = 0;
var btn = document.getElementById('newMsgBtn');
if (btn) btn.classList.add('hidden');
}
});
shoutboxApplyFontSize();
shoutboxUpdateSoundUI();
setInterval(poll, 3000);
poll();
setInterval(heartbeat, 10000);
heartbeat();
</script>
</body>
</html>