Add stream uptime formatting to Twitch moderation and enhance template layout
- Introduced a new helper function `_format_stream_uptime` to calculate and format the stream uptime based on the start time. - Updated the Twitch moderation route to include the formatted stream uptime in the rendered template. - Enhanced the Twitch moderation template layout for better responsiveness and visual clarity, including adjustments to the live status display and overall structure.
This commit is contained in:
@@ -75,62 +75,60 @@
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
#editModal.hidden, #addCommandModal.hidden { display: none; }
|
||||
@keyframes tmOfflineGlow {
|
||||
0%, 100% { opacity: 0.55; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
.tm-offline-icon { animation: tmOfflineGlow 3s ease-in-out infinite; }
|
||||
</style>
|
||||
|
||||
<div id="notificationStack"></div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">Modération Twitch</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Commandes et logs de modération pour {{ twitch_channel }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Info bricks -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg {{ 'bg-red-100 dark:bg-red-900/30' if is_live else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-5 h-5 {{ 'text-red-600 dark:text-red-400' if is_live else 'text-gray-500' }}" fill="currentColor" viewBox="0 0 20 20"><circle cx="10" cy="10" r="8"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-sm">Live</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ 'En ligne' if is_live else 'Hors ligne' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if is_live %}
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-bold text-purple-600 dark:text-purple-400">{{ viewer_count }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">viewers</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 sm:gap-3 lg:flex-row lg:items-center lg:justify-between lg:gap-4">
|
||||
<div class="min-w-0 flex-shrink">
|
||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white leading-tight">Modération Twitch</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mt-0.5">Commandes et logs de modération pour {{ twitch_channel }}</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg {{ 'bg-green-100 dark:bg-green-900/30' if link_filter_enabled else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-5 h-5 {{ 'text-green-600 dark:text-green-400' if link_filter_enabled else 'text-gray-500' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-sm">Filtre de liens</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ 'Actif' if link_filter_enabled else 'Inactif' }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5 sm:gap-2 lg:justify-end lg:max-w-[min(100%,40rem)] lg:flex-shrink-0">
|
||||
<!-- Diffusion : live / hors ligne (compact, aligné au titre) -->
|
||||
<div class="flex flex-1 min-w-[128px] max-w-full sm:flex-initial sm:max-w-[11.5rem] items-center gap-1.5 rounded-md border shadow-sm px-2 py-1.5 {{ 'border-violet-200/80 dark:border-violet-500/25 bg-gradient-to-br from-violet-50/90 to-white dark:from-violet-950/40 dark:to-gray-900/90' if is_live else 'border-indigo-200/60 dark:border-indigo-400/20 bg-gradient-to-br from-slate-100/90 to-indigo-50/50 dark:from-slate-800/80 dark:to-indigo-950/40' }}">
|
||||
<div class="p-1 rounded shrink-0 {{ 'bg-violet-500/15 dark:bg-violet-500/20' if is_live else 'bg-indigo-500/10 dark:bg-indigo-400/15' }}">
|
||||
{% if is_live %}
|
||||
<svg class="w-3 h-3 text-violet-600 dark:text-violet-400" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z"></path></svg>
|
||||
{% else %}
|
||||
<svg class="w-3 h-3 text-indigo-500 dark:text-indigo-300 tm-offline-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ url_for('link_filter') }}" class="text-xs text-purple-600 dark:text-purple-400 hover:underline">Configurer</a>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-[11px] leading-tight tracking-tight">{{ 'En direct' if is_live else 'Hors ligne' }}</h3>
|
||||
{% if is_live %}
|
||||
<p class="text-[10px] text-gray-600 dark:text-gray-300 mt-0.5 line-clamp-1 leading-tight" title="{{ stream_title or '' }}">{{ stream_title or 'Sans titre' }}</p>
|
||||
{% else %}
|
||||
<p class="text-[10px] text-indigo-600/80 dark:text-indigo-300/80 mt-0.5 leading-tight">Repos — à bientôt sur Twitch</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" rel="noopener noreferrer" class="text-[10px] text-purple-600 dark:text-purple-400 hover:underline shrink-0" title="Ouvrir la chaîne Twitch">Chaîne</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg {{ 'bg-orange-100 dark:bg-orange-900/30' if banned_words else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-5 h-5 {{ 'text-orange-600 dark:text-orange-400' if banned_words else 'text-gray-500' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-sm">Mots interdits</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ banned_words|length }} mot{{ 's' if banned_words|length > 1 else '' }}</p>
|
||||
</div>
|
||||
<div class="flex flex-1 min-w-[128px] max-w-full sm:flex-initial sm:max-w-[10rem] items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 py-1.5 shadow-sm">
|
||||
<div class="p-1 rounded shrink-0 {{ 'bg-green-100 dark:bg-green-900/30' if link_filter_enabled else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-3 h-3 {{ 'text-green-600 dark:text-green-400' if link_filter_enabled else 'text-gray-500' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path></svg>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-[11px] leading-tight">Filtre liens</h3>
|
||||
<p class="text-[10px] text-gray-500 dark:text-gray-400">{{ 'Actif' if link_filter_enabled else 'Inactif' }}</p>
|
||||
</div>
|
||||
<a href="{{ url_for('link_filter') }}" class="text-[10px] text-purple-600 dark:text-purple-400 hover:underline shrink-0">Réglages</a>
|
||||
</div>
|
||||
<div class="flex flex-1 min-w-[128px] max-w-full sm:flex-initial sm:max-w-[10rem] items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 py-1.5 shadow-sm">
|
||||
<div class="p-1 rounded shrink-0 {{ 'bg-orange-100 dark:bg-orange-900/30' if banned_words else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-3 h-3 {{ 'text-orange-600 dark:text-orange-400' if banned_words else 'text-gray-500' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path></svg>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-[11px] leading-tight">Mots interdits</h3>
|
||||
<p class="text-[10px] text-gray-500 dark:text-gray-400">{{ banned_words|length }} actif{{ 's' if banned_words|length != 1 else '' }}</p>
|
||||
</div>
|
||||
<a href="#section-mots-interdits" class="text-[10px] text-purple-600 dark:text-purple-400 hover:underline shrink-0">Liste</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -146,27 +144,37 @@
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
{% if is_live %}
|
||||
<span class="flex h-2 w-2 relative" id="liveIndicator">
|
||||
<span class="flex h-2 w-2 relative shrink-0" id="liveIndicator">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-red-500/10 dark:bg-red-500/15 px-2 py-0.5 border border-red-500/25 dark:border-red-400/20 shrink-0" id="liveStatusPill">
|
||||
<span class="text-xs font-semibold text-red-700 dark:text-red-300 tracking-wide" id="liveStatusLabel">EN DIRECT</span>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-gray-400"></span>
|
||||
<span class="inline-flex items-center gap-1.5 rounded-full bg-indigo-500/10 dark:bg-indigo-400/10 px-2 py-0.5 border border-indigo-400/20 dark:border-indigo-400/25 shrink-0" id="liveStatusPill">
|
||||
<svg class="w-3.5 h-3.5 text-indigo-500 dark:text-indigo-300 tm-offline-icon shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg>
|
||||
<span class="text-xs font-medium text-indigo-800 dark:text-indigo-200" id="liveStatusLabel">Pas en direct</span>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white" id="liveStatusLabel">{{ 'EN DIRECT' if is_live else 'HORS LIGNE' }}</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400" id="playerViewerCount">• {{ viewer_count }} viewers</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 truncate" id="playerViewerCount">• {{ viewer_count }} viewers</span>
|
||||
</div>
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
|
||||
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
||||
Ouvrir
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-1 p-3 flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ 'Live en cours' if is_live else 'En attente du prochain live' }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">Lecture Twitch intégrée</div>
|
||||
<div class="flex-1 p-3 min-h-[3.5rem] flex items-center">
|
||||
<div class="min-w-0 text-left w-full">
|
||||
{% if is_live %}
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white line-clamp-2" title="{{ stream_title or '' }}">{{ stream_title or 'Sans titre' }}</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">{{ game_name or 'Catégorie non renseignée' }}{% if stream_uptime %}<span class="text-gray-400 dark:text-gray-500"> · {{ stream_uptime }}</span>{% endif %}</p>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">En attente du prochain live</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Lecteur intégré — son activé automatiquement quand la chaîne est en ligne</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -426,7 +434,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Section mots interdits -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div id="section-mots-interdits" class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden scroll-mt-4">
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Mots interdits ({{ banned_words|length }})</h3>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,31 @@ from database import db
|
||||
from database.models import Commande, TwitchModerationLog, TwitchLinkFilter, TwitchBannedWord, ModShoutboxMessage
|
||||
from flask_login import current_user
|
||||
from database.helpers import ConfigurationHelper
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
def _format_stream_uptime(started_at_iso):
|
||||
"""Durée depuis le début du live (texte court pour le panneau)."""
|
||||
if not started_at_iso:
|
||||
return None
|
||||
try:
|
||||
s = str(started_at_iso).replace("Z", "+00:00")
|
||||
started = datetime.fromisoformat(s)
|
||||
if started.tzinfo is None:
|
||||
started = started.replace(tzinfo=timezone.utc)
|
||||
now = datetime.now(timezone.utc)
|
||||
sec = int((now - started).total_seconds())
|
||||
if sec < 0:
|
||||
return None
|
||||
h, sec = divmod(sec, 3600)
|
||||
m, sec = divmod(sec, 60)
|
||||
if h > 0:
|
||||
return f"{h}h {m}min"
|
||||
if m > 0:
|
||||
return f"{m} min"
|
||||
return "< 1 min"
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
import asyncio
|
||||
|
||||
MODERATION_COMMANDS = [
|
||||
@@ -139,7 +163,8 @@ def twitch_moderation():
|
||||
stream_title = bot_status.get("twitch_stream_title", "")
|
||||
game_name = bot_status.get("twitch_game_name", "")
|
||||
started_at = bot_status.get("twitch_started_at")
|
||||
|
||||
stream_uptime = _format_stream_uptime(started_at) if is_live else None
|
||||
|
||||
return render_template(
|
||||
"twitch-moderation.html",
|
||||
commands=MODERATION_COMMANDS,
|
||||
@@ -155,6 +180,7 @@ def twitch_moderation():
|
||||
stream_title=stream_title,
|
||||
game_name=game_name,
|
||||
started_at=started_at,
|
||||
stream_uptime=stream_uptime,
|
||||
)
|
||||
|
||||
@webapp.route("/twitch-moderation/logs/clear")
|
||||
|
||||
Reference in New Issue
Block a user