Ajout d'un système de gestion des utilisateurs en ligne dans la shoutbox pour les modérateurs Twitch. Implémentation d'un endpoint de heartbeat pour suivre la présence des modérateurs et mise à jour de l'interface utilisateur pour afficher la liste des utilisateurs en ligne. Amélioration de l'expérience utilisateur avec un bouton pour ouvrir la shoutbox en popout.
This commit is contained in:
@@ -16,6 +16,7 @@ webapp.config["BOT_STATUS"] = {
|
||||
"twitch_is_live": False,
|
||||
"twitch_viewer_count": 0,
|
||||
"twitch_chat_messages": [], # Derniers messages du chat (max 100)
|
||||
"shoutbox_heartbeats": {}, # {"username": datetime} — présence des modos
|
||||
}
|
||||
|
||||
login_manager = LoginManager()
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<!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">
|
||||
<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 overflow-y-auto font-mono text-xs p-2" id="shoutboxDisplay" style="scrollbar-width: thin;">
|
||||
<div class="text-gray-500 text-center py-4" id="shoutboxPlaceholder">Aucun message</div>
|
||||
</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">></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 audioCtx = null;
|
||||
|
||||
document.addEventListener('visibilitychange', function() { tabVisible = !document.hidden; });
|
||||
|
||||
function beep() {
|
||||
try {
|
||||
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
var osc = audioCtx.createOscillator();
|
||||
var gain = audioCtx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
osc.frequency.value = 660;
|
||||
osc.type = 'sine';
|
||||
gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
|
||||
osc.start(audioCtx.currentTime);
|
||||
osc.stop(audioCtx.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];
|
||||
}
|
||||
|
||||
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 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';
|
||||
var time = '<span class="text-gray-500">[' + fmtTime(item.created_at) + ']</span> ';
|
||||
|
||||
if (item.type === 'message') {
|
||||
var c = userColor(item.author);
|
||||
line.innerHTML = time + '<span style="color:' + c + '" class="font-semibold"><' + esc(item.author) + '></span> <span class="text-gray-200">' + esc(item.text) + '</span>';
|
||||
} 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) beep();
|
||||
while (display.children.length > 200) display.removeChild(display.firstChild);
|
||||
if (autoScroll) display.scrollTop = display.scrollHeight;
|
||||
}
|
||||
|
||||
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';
|
||||
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 (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;
|
||||
});
|
||||
|
||||
setInterval(poll, 3000);
|
||||
poll();
|
||||
setInterval(heartbeat, 10000);
|
||||
heartbeat();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -297,10 +297,24 @@
|
||||
<span>Shoutbox Modos</span>
|
||||
<span class="text-xs text-gray-400" id="shoutboxCount"></span>
|
||||
</div>
|
||||
<button onclick="clearShoutbox()" class="text-xs text-red-500 hover:text-red-600" title="Effacer">Effacer</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button onclick="openShoutboxPopout()" class="text-xs text-indigo-400 hover:text-indigo-300 flex items-center gap-1" title="Ouvrir en popup">
|
||||
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
||||
Popout
|
||||
</button>
|
||||
<button onclick="clearShoutbox()" class="text-xs text-red-500 hover:text-red-600" title="Effacer">Effacer</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto font-mono text-xs p-2 bg-gray-950 dark:bg-gray-950 bg-opacity-95" id="shoutboxDisplay" style="scrollbar-width: thin;">
|
||||
<div class="text-gray-500 text-center py-4" id="shoutboxPlaceholder">Aucun message</div>
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<div class="flex-1 overflow-y-auto font-mono text-xs p-2 bg-gray-950 dark:bg-gray-950 bg-opacity-95" id="shoutboxDisplay" style="scrollbar-width: thin;">
|
||||
<div class="text-gray-500 text-center py-4" id="shoutboxPlaceholder">Aucun message</div>
|
||||
</div>
|
||||
<div class="w-28 border-l border-gray-700 bg-gray-900 p-2 overflow-y-auto" style="scrollbar-width: thin;">
|
||||
<div class="text-xs text-gray-400 font-semibold mb-1.5">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-200 dark:border-gray-700 p-2 bg-gray-50 dark:bg-gray-800">
|
||||
<form onsubmit="sendShoutboxMessage(event)" class="flex gap-1.5">
|
||||
@@ -974,6 +988,33 @@ function addShoutboxItem(item) {
|
||||
if (shoutboxAutoScroll) display.scrollTop = display.scrollHeight;
|
||||
}
|
||||
|
||||
function updateOnlineList(users) {
|
||||
var list = document.getElementById('shoutboxOnlineList');
|
||||
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';
|
||||
el.innerHTML = '<span class="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0"></span>' + escapeHtml(u);
|
||||
list.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function shoutboxHeartbeat() {
|
||||
fetch('{{ url_for("shoutbox_heartbeat") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}'
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) { updateOnlineList(data.online_users); })
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
function fetchShoutbox() {
|
||||
var url = '{{ url_for("shoutbox_messages") }}';
|
||||
if (shoutboxLastTimestamp) url += '?since=' + encodeURIComponent(shoutboxLastTimestamp);
|
||||
@@ -985,12 +1026,17 @@ function fetchShoutbox() {
|
||||
data.items.forEach(addShoutboxItem);
|
||||
}
|
||||
if (data.timestamp) shoutboxLastTimestamp = data.timestamp;
|
||||
if (data.online_users) updateOnlineList(data.online_users);
|
||||
var cnt = document.getElementById('shoutboxCount');
|
||||
if (cnt) cnt.textContent = '(' + shoutboxKnownIds.size + ')';
|
||||
})
|
||||
.catch(function(e) { console.error('Shoutbox error:', e); });
|
||||
}
|
||||
|
||||
function openShoutboxPopout() {
|
||||
window.open('{{ url_for("shoutbox_popout") }}', 'shoutbox_popout', 'width=520,height=420,resizable=yes,scrollbars=no,menubar=no,toolbar=no,location=no,status=no');
|
||||
}
|
||||
|
||||
function sendShoutboxMessage(event) {
|
||||
event.preventDefault();
|
||||
var input = document.getElementById('shoutboxInput');
|
||||
@@ -1040,5 +1086,7 @@ document.getElementById('shoutboxDisplay').addEventListener('scroll', function()
|
||||
|
||||
setInterval(fetchShoutbox, 3000);
|
||||
fetchShoutbox();
|
||||
setInterval(shoutboxHeartbeat, 10000);
|
||||
shoutboxHeartbeat();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -557,9 +557,24 @@ def shoutbox_messages():
|
||||
return jsonify({
|
||||
"items": items,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"online_users": _get_online_users(),
|
||||
})
|
||||
|
||||
|
||||
def _get_online_users():
|
||||
heartbeats = webapp.config["BOT_STATUS"].get("shoutbox_heartbeats", {})
|
||||
cutoff = datetime.now() - timedelta(seconds=15)
|
||||
return sorted(u for u, t in heartbeats.items() if t > cutoff)
|
||||
|
||||
|
||||
@webapp.route("/twitch-moderation/shoutbox/heartbeat", methods=['POST'])
|
||||
@require_page("twitch_moderation")
|
||||
def shoutbox_heartbeat():
|
||||
hb = webapp.config["BOT_STATUS"].setdefault("shoutbox_heartbeats", {})
|
||||
hb[current_user.username] = datetime.now()
|
||||
return jsonify({"online_users": _get_online_users()})
|
||||
|
||||
|
||||
@webapp.route("/twitch-moderation/shoutbox/clear")
|
||||
@require_page("twitch_moderation")
|
||||
def shoutbox_clear():
|
||||
@@ -568,3 +583,9 @@ def shoutbox_clear():
|
||||
ModShoutboxMessage.query.delete()
|
||||
db.session.commit()
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@webapp.route("/twitch-moderation/shoutbox/popout")
|
||||
@require_page("twitch_moderation")
|
||||
def shoutbox_popout():
|
||||
return render_template("shoutbox-popout.html")
|
||||
|
||||
Reference in New Issue
Block a user