aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md76
-rw-r--r--config.php73
-rw-r--r--index.php363
3 files changed, 512 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b3a233b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,76 @@
+# 🚀 Explorateur Web TeamSpeak 6 (WebQuery)
+
+Un visualiseur de serveur TeamSpeak léger, moderne et 100% AJAX, conçu spécifiquement pour la nouvelle API (WebQuery) de TeamSpeak 6.
+
+Il permet d'afficher en temps réel les salons et les joueurs sur votre site web. Aucune base de données n'est requise. Le script utilise un système de cache pour protéger votre serveur TeamSpeak contre le spam.
+
+---
+
+## ✨ Fonctionnalités
+* **Actualisation Fluide :** La page se met à jour en temps réel sans jamais clignoter.
+* **Respect de votre Serveur :** Affiche les salons et sous-canaux exactement dans le même ordre que sur votre TeamSpeak.
+* **Système de Rôles :** Affiche les icônes de vos groupes (Admins, VIP, etc.) à côté des pseudos.
+* **Zéro Risque :** Fonctionne avec une clé API en lecture seule. Aucun risque de piratage de votre serveur vocal.
+* **Outil de Diagnostic :** Un script `debug.php` inclus pour vous aider si la connexion échoue.
+
+---
+
+## 🛠️ Prérequis
+Pour faire fonctionner ce script, vous avez besoin de :
+1. **Un hébergement web** (ou un serveur local type Unraid/Docker) supportant **PHP 7.4 ou supérieur**.
+2. Un serveur **TeamSpeak 6** actif.
+3. Le port **WebQuery** de votre TeamSpeak accessible (par défaut le port `10080`).
+
+---
+
+## 📦 Guide d'Installation pour Débutants
+
+### Étape 1 : Télécharger les fichiers
+1. En haut de cette page GitHub, cliquez sur le bouton vert **"Code"**, puis sur **"Download ZIP"**.
+2. Décompressez le fichier `.zip` sur votre ordinateur.
+3. Envoyez tous ces fichiers sur votre hébergement web (via votre logiciel FTP comme FileZilla, ou le gestionnaire de fichiers de votre hébergeur).
+
+### Étape 2 : Créer votre fichier de configuration
+1. Allez dans le dossier où vous avez envoyé les fichiers.
+2. Cherchez le fichier nommé `config.example.php`.
+3. **Renommez-le** simplement en `config.php`.
+4. Ouvrez ce nouveau fichier `config.php` avec un éditeur de texte (comme Notepad++, Bloc-notes ou VS Code). Vous y entrerez vos informations à l'étape suivante.
+
+---
+
+## 🔑 Obtenir votre Clé API TeamSpeak 6
+
+Pour que le site web puisse lire la liste des joueurs, il lui faut une "Clé API". Pour des raisons de sécurité, nous allons créer une clé qui a **uniquement le droit de lire**, et aucun droit de modifier votre serveur.
+
+**Voici comment la créer pas à pas :**
+1. Ouvrez le terminal de votre ordinateur (ou utilisez un logiciel comme **PuTTY**).
+2. Connectez-vous à l'IP de votre serveur TeamSpeak en mode **SSH** sur le port ServerQuery (port par défaut : **10022**).
+ *Exemple de commande :* `ssh serveradmin@VOTRE_IP -p 10022`
+3. Une console s'ouvre et vous demande votre mot de passe `serveradmin`. Tapez-le (il ne s'affiche pas à l'écran, c'est normal) et appuyez sur *Entrée*.
+4. Tapez les commandes suivantes, en appuyant sur la touche *Entrée* après chaque ligne :
+
+ *Pour sélectionner votre serveur vocal principal :*
+ `use 1`
+
+ *Pour générer une clé en lecture seule permanente :*
+ `apikeyadd scope=read lifetime=0`
+
+5. Le serveur va vous répondre avec une ligne contenant `apikey=VOTRE_CLE_ICI`. Copiez cette longue suite de caractères.
+6. Tapez `quit` pour fermer la console.
+
+### Finalisation
+Retournez dans votre fichier `config.php` ouvert précédemment, assurez-vous que le port API est bien configuré sur **10080**, et collez votre clé à cet endroit :
+`'api_key' => 'VOTRE_CLE_ICI',`
+Enregistrez le fichier. C'est prêt, votre site fonctionne ! 🎉
+
+---
+
+## 🛡️ Conseil de Sécurité
+Si vous utilisez un hébergement avancé ou un VPS (avec Nginx), il est fortement recommandé de bloquer l'accès direct aux fichiers système pour empêcher les curieux de lire votre configuration.
+
+Ajoutez ceci dans la configuration de votre site :
+```nginx
+location ~* /(config\.php|cache\.json|debug\.php)$ {
+ deny all;
+ return 403;
+} \ No newline at end of file
diff --git a/config.php b/config.php
new file mode 100644
index 0000000..6a40406
--- /dev/null
+++ b/config.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Configuration de l'Explorateur TeamSpeak
+ * Renommez ce fichier en 'config.php' et remplissez vos informations.
+ */
+
+return [
+ // Paramètres de connexion au serveur
+ 'server' => [
+ 'host' => '127.0.0.1', // IP du serveur (ex: 192.168.1.50)
+ 'port' => '10080', // Port HTTP WebQuery (TS6)
+ 'api_key' => 'VOTRE_CLE_API_ICI',
+ 'server_id' => '1',
+ 'ts_domain' => 'ts.votre-domaine.com', // Utilisé pour le bouton de connexion
+ 'cache_time' => 30, // Actualisation en secondes (anti-spam)
+ 'cache_file' => __DIR__ . '/cache.json'
+ ],
+
+ // Design et Textes principaux
+ 'ui' => [
+ 'titre' => 'Mon Serveur TeamSpeak',
+ 'sous_titre' => 'Bienvenue sur notre espace de discussion.',
+ 'couleur_fond' => '#070a10', // Couleur de fond principale
+ 'couleur_panneau' => '#131823', // Couleur des blocs de canaux
+ 'couleur_accent' => '#3b82f6', // Couleur des boutons et de la barre de temps
+ 'couleur_texte' => '#e2e8f0',
+ 'banniere_url' => '', // Laissez vide ('') pour ne pas afficher d'en-tête
+ 'activer_etoiles' => false, // Passez à true pour activer une animation spatiale en arrière-plan
+ ],
+
+ // Textes et liens de l'interface
+ 'boutons' => [
+ 'rejoindre' => 'Se connecter au serveur',
+ 'telecharger_texte' => 'Le logiciel n\'est pas installé sur votre PC ?',
+ 'telecharger_lien' => 'Télécharger TeamSpeak',
+ 'telecharger_url' => 'https://www.teamspeak.com/fr/downloads/' // Lien officiel au cas où il change
+ ],
+
+ // Liens utiles (Ajoutez ou retirez autant de lignes que vous le souhaitez)
+ 'liens_externes' => [
+ 'Notre Site Web' => 'https://www.votre-site.com',
+ 'Discord / Forum'=> 'https://discord.gg/votre-lien'
+ ],
+
+ // Pied de page
+ 'footer' => [
+ 'texte' => 'Hébergé par',
+ 'auteur' => 'VotrePseudo',
+ 'lien_auteur' => 'https://github.com/votre-pseudo'
+ ],
+
+ // Personnalisation des canaux (Nom exact du canal => nom du fichier image)
+ 'bannieres_canaux' => [
+ // 'Accueil' => 'accueil_fond.png',
+ // 'Général' => 'general_fond.png',
+ ],
+
+ 'icones_canaux' => [
+ // 'Accueil' => 'accueil_icone.png',
+ // 'Général' => 'general_icone.png',
+ ],
+
+ 'images_generiques' => [
+ 'fond' => '', // Image appliquée par défaut aux canaux inconnus
+ 'icone' => '' // Icône appliquée par défaut aux canaux inconnus
+ ],
+
+ // Groupes de serveur (Rôles) : 'ID_DU_GROUPE' => 'Image'
+ 'icones_roles' => [
+ // '6' => 'admin.png',
+ // '9' => 'vip.png',
+ ]
+]; \ No newline at end of file
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..60d3239
--- /dev/null
+++ b/index.php
@@ -0,0 +1,363 @@
+<?php
+/**
+ * Explorateur Web - TeamSpeak 6
+ */
+
+$config_file = __DIR__ . '/config.php';
+if (!file_exists($config_file)) {
+ die("Erreur : Le fichier 'config.php' est introuvable. Veuillez le créer à partir de 'config.example.php'.");
+}
+$config = require $config_file;
+
+// Utilitaires
+function e($string) { return htmlspecialchars($string ?? '', ENT_QUOTES, 'UTF-8'); }
+function safeUrl($filename) { return str_replace('%2F', '/', rawurlencode($filename)); }
+
+function genererEtoiles($nombre, $opacite) {
+ $ombres = [];
+ for ($i = 0; $i < $nombre; $i++) {
+ $x = rand(0, 2000); $y = rand(0, 2000);
+ $ombres[] = "{$x}px {$y}px rgba(255, 255, 255, {$opacite})";
+ }
+ return implode(", ", $ombres);
+}
+
+// Récupération API (Strictement en LECTURE SEULE - scope=read)
+function fetch_teamspeak_data($cfg) {
+ $cache_file = $cfg['cache_file'];
+ if (file_exists($cache_file) && (time() - filemtime($cache_file)) < $cfg['cache_time']) {
+ $cached_data = file_get_contents($cache_file);
+ if ($cached_data) return json_decode($cached_data, true);
+ }
+
+ $options = ['http' => ['header' => "x-api-key: {$cfg['api_key']}\r\n", 'method' => 'GET', 'timeout' => 2, 'ignore_errors' => true]];
+ $context = stream_context_create($options);
+
+ $base_url = "http://{$cfg['host']}:{$cfg['port']}/{$cfg['server_id']}";
+ $channels_res = @file_get_contents("$base_url/channellist", false, $context);
+ $clients_res = @file_get_contents("$base_url/clientlist?-groups", false, $context);
+
+ if ($channels_res !== false && $clients_res !== false) {
+ $data = [
+ 'channels' => json_decode($channels_res, true)['body'] ?? [],
+ 'clients' => json_decode($clients_res, true)['body'] ?? []
+ ];
+ file_put_contents($cache_file, json_encode($data));
+ return $data;
+ }
+ return ['channels' => [], 'clients' => []];
+}
+
+// Tri hiérarchique
+function sort_ts_channels(array $channels, $pid = 0) {
+ $ordered = []; $by_order = [];
+ foreach ($channels as $c) { if (($c['pid'] ?? 0) == $pid) $by_order[$c['channel_order'] ?? 0] = $c; }
+ $current_order = 0; $max_iterations = count($channels) + 1; $i = 0;
+ while (isset($by_order[$current_order]) && $i < $max_iterations) {
+ $channel = $by_order[$current_order]; $ordered[] = $channel;
+ $sub_channels = sort_ts_channels($channels, $channel['cid']);
+ foreach ($sub_channels as $sc) $ordered[] = $sc;
+ $current_order = $channel['cid']; $i++;
+ }
+ return $ordered;
+}
+
+// Traitement des données
+$start_time = microtime(true);
+$ts_data = fetch_teamspeak_data($config['server']);
+$api_ping = round((microtime(true) - $start_time) * 1000); // Calcule le temps de réponse de l'API locale
+
+$raw_channels = $ts_data['channels'];
+$raw_clients = $ts_data['clients'];
+$is_online = !empty($raw_channels);
+
+$channels = sort_ts_channels($raw_channels, 0);
+
+$clients_by_channel = [];
+$total_real_clients = 0;
+
+if ($is_online) {
+ foreach ($raw_clients as $client) {
+ if (($client['client_type'] ?? 0) == 1) continue; // On ignore les bots/ServerQuery
+ $clients_by_channel[$client['cid']][] = $client;
+ $total_real_clients++;
+ }
+}
+
+// Rendu du fragment AJAX
+ob_start();
+if ($is_online): ?>
+
+ <div class="server-stats">
+ <div class="stat-item">
+ <span class="stat-label">En Ligne</span>
+ <span class="stat-value"><?php echo $total_real_clients; ?></span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">Salons</span>
+ <span class="stat-value"><?php echo count($channels); ?></span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">Latence API</span>
+ <span class="stat-value"><?php echo $api_ping; ?> ms</span>
+ </div>
+ </div>
+
+ <div class="channel-list">
+ <?php foreach ($channels as $channel):
+ $c_name = $channel['channel_name'];
+ if (strpos($c_name, '[spacer') !== false) continue;
+
+ $cid = $channel['cid'];
+ $channel_clients = $clients_by_channel[$cid] ?? [];
+
+ $bg_img = $config['bannieres_canaux'][$c_name] ?? ($config['images_generiques']['fond'] ?? '');
+ $icon_img = $config['icones_canaux'][$c_name] ?? ($config['images_generiques']['icone'] ?? '');
+
+ $bg_style = "";
+ if (!empty($bg_img)) {
+ $bg_style = "background-image: linear-gradient(90deg, rgba(19, 24, 35, 0.95) 0%, rgba(19, 24, 35, 0.7) 40%, rgba(19, 24, 35, 0.9) 100%), url('" . safeUrl($bg_img) . "');";
+ }
+ ?>
+ <article class="channel">
+ <header class="channel-header" style="<?php echo $bg_style; ?>">
+ <div class="channel-title-group">
+ <?php if (!empty($icon_img)): ?>
+ <img src="<?php echo safeUrl($icon_img); ?>" class="channel-icon" alt="" aria-hidden="true">
+ <?php endif; ?>
+ <h2><?php echo e($c_name); ?></h2>
+ </div>
+ <span class="channel-meta">
+ <?php echo count($channel_clients); ?> / <?php echo ($channel['channel_maxclients'] == -1) ? '∞' : e($channel['channel_maxclients']); ?>
+ </span>
+ </header>
+
+ <?php if (!empty($channel_clients)): ?>
+ <ul class="client-list">
+ <?php foreach ($channel_clients as $client): ?>
+ <li class="client">
+ <?php
+ $role_icon = '<span class="client-no-role-icon"></span>';
+ if (isset($client['client_servergroups'])) {
+ $groups = explode(',', $client['client_servergroups']);
+ foreach ($groups as $g_id) {
+ if (isset($config['icones_roles'][$g_id])) {
+ $role_icon = '<img src="' . safeUrl($config['icones_roles'][$g_id]) . '" class="role-icon" alt="Rôle">';
+ break;
+ }
+ }
+ }
+ echo $role_icon;
+ ?>
+ <span><?php echo e($client['client_nickname']); ?></span>
+ </li>
+ <?php endforeach; ?>
+ </ul>
+ <?php endif; ?>
+ </article>
+ <?php endforeach; ?>
+ </div>
+<?php else: ?>
+ <div class="empty-server">Impossible d'établir le contact avec le serveur central.</div>
+<?php endif;
+$html_fragment = ob_get_clean();
+
+// Gestion de la réponse AJAX
+if (isset($_GET['ajax']) && $_GET['ajax'] === '1') {
+ header('Content-Type: text/html; charset=utf-8');
+ echo $html_fragment;
+ exit;
+}
+?>
+<!DOCTYPE html>
+<html lang="fr">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="<?php echo e($config['ui']['sous_titre']); ?>">
+ <title><?php echo e($config['ui']['titre']); ?></title>
+ <link rel="icon" type="image/png" href="logo.png">
+ <style>
+ :root {
+ --bg-color: <?php echo e($config['ui']['couleur_fond']); ?>;
+ --panel-bg: <?php echo e($config['ui']['couleur_panneau']); ?>;
+ --accent: <?php echo e($config['ui']['couleur_accent']); ?>;
+ --text-main: <?php echo e($config['ui']['couleur_texte']); ?>;
+ --text-muted: #64748b;
+ --channel-bg: #1e2433;
+ }
+
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background-color: var(--bg-color); color: var(--text-main); margin: 0; padding: 2rem; display: flex; justify-content: center; min-height: 100vh; box-sizing: border-box; }
+
+ <?php if ($config['ui']['activer_etoiles'] ?? false): ?>
+ #space-bg { position: fixed; inset: 0; z-index: -1; background-color: var(--bg-color); overflow: hidden;}
+ .star-layer { position: absolute; background: transparent; }
+ .star-layer::after { content: ""; position: absolute; top: 2000px; background: inherit; box-shadow: inherit; }
+ .stars-s { width: 1px; height: 1px; box-shadow: <?php echo genererEtoiles(150, 0.4); ?>; animation: animStar 250s linear infinite; }
+ .stars-m { width: 2px; height: 2px; box-shadow: <?php echo genererEtoiles(50, 0.6); ?>; animation: animStar 180s linear infinite; }
+ .stars-l { width: 2px; height: 2px; box-shadow: <?php echo genererEtoiles(15, 0.8); ?>; animation: animStar 100s linear infinite; }
+ @keyframes animStar { from { transform: translateY(0); } to { transform: translateY(-2000px); } }
+ <?php endif; ?>
+
+ .app-container { background: var(--panel-bg); border-radius: 12px; width: 100%; max-width: 650px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7); border: 1px solid rgba(255, 255, 255, 0.04); position: relative; overflow: hidden; height: fit-content; }
+
+ .banner { width: 100%; height: auto; display: block; border-bottom: 2px solid var(--accent); opacity: 0.95; }
+ .content-wrapper { padding: 2rem; }
+
+ /* Barre de progression fluide */
+ .progress-bar { position: absolute; top: 0; left: 0; height: 2px; background-color: var(--accent); width: 100%; z-index: 10; }
+
+ .header { text-align: center; margin-bottom: 2rem; }
+ .header h1 { margin: 0 0 0.25rem 0; font-size: 1.5rem; color: #f8fafc; }
+ .header p { color: var(--text-muted); margin: 0; font-size: 0.9rem; }
+
+ .status-badge { display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; margin-top: 15px; letter-spacing: 1px; }
+ .status-online { color: #a7f3d0; background-color: rgba(6, 78, 59, 0.4); border: 1px solid rgba(16, 185, 129, 0.2); }
+ .status-offline { color: #fca5a5; background-color: rgba(127, 29, 29, 0.4); border: 1px solid rgba(239, 68, 68, 0.2); }
+ .pulse-dot { width: 6px; height: 6px; background-color: #10b981; border-radius: 50%; margin-right: 8px; animation: pulse 2s infinite; }
+ .status-offline .pulse-dot { background-color: #ef4444; animation: none; }
+ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } }
+
+ /* Stats du serveur adaptées à la lecture seule */
+ .server-stats { display: flex; justify-content: space-around; background: rgba(0, 0, 0, 0.2); padding: 12px; border-radius: 8px; margin-bottom: 1.5rem; border: 1px solid rgba(255, 255, 255, 0.03); }
+ .stat-item { display: flex; flex-direction: column; align-items: center; }
+ .stat-label { font-size: 0.65rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
+ .stat-value { font-size: 1rem; font-weight: 700; color: var(--text-main); font-variant-numeric: tabular-nums; }
+
+ .btn-primary { display: block; padding: 12px; background-color: var(--accent); color: #000; text-align: center; text-decoration: none; border-radius: 6px; font-weight: 800; text-transform: uppercase; font-size: 0.85rem; transition: filter 0.2s; margin-bottom: 0.5rem; }
+ .btn-primary:hover { filter: brightness(1.1); }
+
+ .help-text { text-align: center; font-size: 0.8rem; color: var(--text-muted); margin: 0.5rem 0 2rem 0; }
+ .help-text a { color: var(--text-main); text-decoration: none; border-bottom: 1px solid rgba(255,255,255,0.2); transition: border-color 0.2s; }
+ .help-text a:hover { border-color: var(--accent); color: var(--accent); }
+
+ .channel-list { display: flex; flex-direction: column; gap: 6px; }
+ .channel { border-radius: 12px; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.03); background-color: var(--channel-bg); }
+ .channel-header { padding: 8px 14px; display: flex; justify-content: space-between; align-items: center; color: #f1f5f9; background-size: cover; background-position: center; background-repeat: no-repeat; }
+
+ .channel-title-group { display: flex; align-items: center; margin: 0; font-size: 0.9rem; font-weight: 600; }
+ .channel-title-group h2 { font-size: 1rem; margin: 0; font-weight: 600; }
+ .channel-icon { width: 20px; height: 20px; margin-right: 10px; border-radius: 4px; object-fit: contain; }
+ .channel-meta { font-size: 0.8rem; color: rgba(255,255,255,0.7); }
+
+ .client-list { list-style: none; padding: 4px 0 8px 0; margin: 0; }
+ .client { padding: 4px 15px 4px 38px; font-size: 0.85rem; color: #cbd5e1; display: flex; align-items: center; font-weight: 500; }
+ .role-icon { width: 16px; height: 16px; object-fit: contain; margin-right: 10px; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.5)); }
+ .client-no-role-icon { display: inline-block; width: 8px; height: 8px; border: 2px solid #475569; border-radius: 50%; margin-right: 10px; }
+
+ .empty-server { text-align: center; color: var(--text-muted); padding: 3rem 0; font-size: 0.9rem; }
+
+ .footer-links { text-align: center; padding-top: 1.5rem; margin-top: 2rem; border-top: 1px solid rgba(255, 255, 255, 0.05); }
+ .footer-links a { color: var(--text-muted); text-decoration: none; font-size: 0.85rem; margin: 0 10px; transition: color 0.2s; }
+ .footer-links a:hover { color: var(--text-main); }
+
+ .credits { text-align: center; margin-top: 1rem; font-size: 0.75rem; color: #475569; }
+ .credits a { color: var(--text-muted); font-weight: 600; text-decoration: none; transition: color 0.2s; }
+ .credits a:hover { color: var(--accent); }
+
+ #ajax-container { transition: opacity 0.3s ease-in-out; }
+ </style>
+</head>
+<body>
+
+<?php if ($config['ui']['activer_etoiles'] ?? false): ?>
+<div id="space-bg" aria-hidden="true">
+ <div class="star-layer stars-s"></div>
+ <div class="star-layer stars-m"></div>
+ <div class="star-layer stars-l"></div>
+</div>
+<?php endif; ?>
+
+<main class="app-container">
+ <?php if ($is_online): ?><div class="progress-bar" id="refresh-bar"></div><?php endif; ?>
+
+ <?php if (!empty($config['ui']['banniere_url'])): ?>
+ <img src="<?php echo safeUrl($config['ui']['banniere_url']); ?>" alt="Bannière d'en-tête" class="banner">
+ <?php endif; ?>
+
+ <div class="content-wrapper">
+ <header class="header">
+ <h1><?php echo e($config['ui']['titre']); ?></h1>
+ <p><?php echo e($config['ui']['sous_titre']); ?></p>
+ <?php if ($is_online): ?>
+ <div class="status-badge status-online"><span class="pulse-dot"></span> TRANSMISSION ACTIVE</div>
+ <?php else: ?>
+ <div class="status-badge status-offline"><span class="pulse-dot"></span> SIGNAL PERDU</div>
+ <?php endif; ?>
+ </header>
+
+ <a href="ts3server://<?php echo e($config['server']['ts_domain']); ?>" class="btn-primary"><?php echo e($config['boutons']['rejoindre']); ?></a>
+ <p class="help-text"><?php echo e($config['boutons']['telecharger_texte']); ?> <a href="<?php echo e($config['boutons']['telecharger_url']); ?>" target="_blank" rel="noopener"><?php echo e($config['boutons']['telecharger_lien']); ?></a></p>
+
+ <section id="ajax-container" aria-live="polite">
+ <?php echo $html_fragment; ?>
+ </section>
+
+ <?php if (!empty($config['liens_externes'])): ?>
+ <nav class="footer-links">
+ <?php foreach ($config['liens_externes'] as $nom => $url): ?>
+ <a href="<?php echo e($url); ?>" target="_blank" rel="noopener"><?php echo e($nom); ?></a>
+ <?php endforeach; ?>
+ </nav>
+ <?php endif; ?>
+
+ <footer class="credits">
+ <?php echo e($config['footer']['texte']); ?> <a href="<?php echo e($config['footer']['lien_auteur']); ?>" target="_blank" rel="noopener"><?php echo e($config['footer']['auteur']); ?></a> — Scan radar : <span id="timer-text"><?php echo $config['server']['cache_time']; ?></span>s
+ </footer>
+ </div>
+</main>
+
+<script>
+ <?php if ($is_online): ?>
+ document.addEventListener("DOMContentLoaded", () => {
+ let timeLeft = <?php echo (int)$config['server']['cache_time']; ?>;
+ const totalTime = <?php echo (int)$config['server']['cache_time']; ?>;
+
+ const progressBar = document.getElementById('refresh-bar');
+ const timerText = document.getElementById('timer-text');
+ const container = document.getElementById('ajax-container');
+
+ // Animation fluide de la barre de temps
+ function resetProgressBar() {
+ progressBar.style.transition = 'none';
+ progressBar.style.width = '100%';
+ void progressBar.offsetWidth;
+ progressBar.style.transition = `width ${totalTime}s linear`;
+ progressBar.style.width = '0%';
+ }
+
+ resetProgressBar();
+
+ setInterval(() => {
+ timeLeft--;
+ timerText.innerText = timeLeft;
+
+ if (timeLeft <= 0) {
+ container.style.opacity = '0.5';
+ fetch(window.location.href.split('?')[0] + '?ajax=1', { cache: 'no-store', headers: {'X-Requested-With': 'XMLHttpRequest'} })
+ .then(r => {
+ if (!r.ok) throw new Error("HTTP error " + r.status);
+ return r.text();
+ })
+ .then(html => {
+ container.innerHTML = html;
+ container.style.opacity = '1';
+
+ timeLeft = totalTime;
+ timerText.innerText = timeLeft;
+
+ resetProgressBar();
+ })
+ .catch(err => {
+ console.error("Erreur de rafraîchissement :", err);
+ container.style.opacity = '1';
+ timeLeft = totalTime;
+ resetProgressBar();
+ });
+ }
+ }, 1000);
+ });
+ <?php endif; ?>
+</script>
+</body>
+</html> \ No newline at end of file
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage