ajoute la visio de classe : Jitsi auto-hébergé, jetons du portail, prof seul modérateur
Jitsi (docker-jitsi-meet, version figée) derrière Nginx sur meet.syllabuslab.fr, accès par jeton JWT signé par le portail, modération du micro, mains levées synchronisées avec l'application. Pare-feu : UDP 10000 et pont interne br-visio (accords d'Adam). Jicofo n'authentifie plus lui-même : il donnait le rôle de propriétaire aux élèves. Capacité ramenée à 20 VM. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dd4f9c3c55
commit
ec4eae3744
36 changed files with 1406 additions and 18 deletions
|
|
@ -7,6 +7,9 @@
|
|||
@section('contenu')
|
||||
<div class="mx-auto max-w-5xl px-5 py-14 lg:px-10">
|
||||
<p class="font-mono text-sm font-semibold">{{ $seance->lab->titre }}</p>
|
||||
@if ($seance->visioOuverte())
|
||||
<p class="mt-3"><a href="{{ route('seances.visio.eleve', $seance) }}" class="inline-flex h-11 items-center rounded-full bg-[#0b57d0] px-5 font-bold text-white">Rejoindre la visio de la classe</a></p>
|
||||
@endif
|
||||
@if ($labSession->etat === $etats::EN_ATTENTE)
|
||||
<div class="py-20 text-center">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight">En attente que le prof t'admette…</h1>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
<p class="font-mono text-sm font-semibold">{{ $seance->classe->nom }} · {{ $seance->lab->titre }}</p>
|
||||
<h1 class="mt-1 text-4xl font-extrabold tracking-tight">Salle d'attente</h1>
|
||||
<p class="mt-2 font-semibold">La page se rafraîchit toutes les 10 secondes.</p>
|
||||
<p class="mt-4"><a href="{{ route('seances.visio.prof', $seance) }}" class="inline-flex h-11 items-center rounded-full bg-[#0b57d0] px-5 font-bold text-white">{{ $seance->visioOuverte() ? 'Retourner à la visio' : 'Visio de la classe' }}</a></p>
|
||||
|
||||
@if ($seance->labSessions->isEmpty())
|
||||
<p class="mt-8 font-semibold">Personne n'a encore rejoint la séance.</p>
|
||||
|
|
|
|||
100
portail/resources/views/seances/visio-eleve.blade.php
Normal file
100
portail/resources/views/seances/visio-eleve.blade.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
@extends('layouts.vitrine')
|
||||
|
||||
@section('titre', 'Visio · '.$seance->classe->nom)
|
||||
|
||||
{{-- Étape 15 : reprend design/resources/views/visio.blade.php (vue élève) ;
|
||||
l'image factice est remplacée par la vraie visio (Jitsi, IFrame API). --}}
|
||||
@section('contenu')
|
||||
<main style="background:#f8f9fa">
|
||||
<section style="padding:20px 20px 72px"><div style="max-width:1100px;margin:0 auto;background:#fff;border:1px solid #e3e5e8;border-radius:20px;overflow:hidden">
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid #e3e5e8"><span style="font-size:14px;font-weight:700;color:#0b1220">{{ $seance->classe->nom }} · {{ $seance->lab->titre }}</span><span data-statut-e style="display:inline-flex;align-items:center;gap:8px;height:32px;padding:0 14px;border-radius:999px;font-size:14px;font-weight:700;background:#eef0f3;color:#3c4043">{{ $jeton ? 'En sourdine' : 'En attente' }}</span></div>
|
||||
@if ($jeton)
|
||||
<div style="padding:20px"><div id="v-scene" style="position:relative;aspect-ratio:16/9;border-radius:16px;overflow:hidden;background:#202124"></div></div>
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:12px;padding:4px 20px 24px">
|
||||
<button type="button" data-micro-e aria-label="Micro" style="width:52px;height:52px;border-radius:50%;display:grid;place-items:center;border:1px solid #dadce0;background:#fff;cursor:pointer"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v3"/></svg></button>
|
||||
<button type="button" data-cam-e aria-label="Caméra" style="width:52px;height:52px;border-radius:50%;display:grid;place-items:center;border:1px solid #dadce0;background:#fff;cursor:pointer"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 7l-7 5 7 5V7z"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg></button>
|
||||
<button type="button" data-main-e aria-pressed="false" class="border border-[#e3e5e8] rounded-2xl bg-[#fce8e6] px-5 py-4 text-left text-lg font-extrabold aria-pressed:bg-[#fef7e0]" style="display:inline-flex;align-items:center;gap:10px;cursor:pointer"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 11V6a2 2 0 0 0-4 0v5"/><path d="M14 10V4a2 2 0 0 0-4 0v6"/><path d="M10 10.5V6a2 2 0 0 0-4 0v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg><span>Lever la main</span></button>
|
||||
<a href="{{ route('seances.eleve.attente', $seance) }}" data-quitter style="display:inline-flex;align-items:center;gap:8px;font-size:14px;font-weight:700;cursor:pointer;border-radius:999px;height:52px;padding:0 22px;background:#d93025;color:#fff;text-decoration:none"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>Quitter</a>
|
||||
</div>
|
||||
<p data-aide-e style="margin:0;padding:0 20px 24px;text-align:center;font-size:14px;font-weight:600;color:#5f6368">Tu es en sourdine. Lève la main pour demander la parole.</p>
|
||||
@else
|
||||
<p style="margin:0;padding:48px 20px;text-align:center;font-size:18px;font-weight:700;color:#3c4043">Ton prof n’a pas encore ouvert la visio. Garde cette page ouverte : tu entreras dès qu’il l’ouvrira.</p>
|
||||
@endif
|
||||
</div></section>
|
||||
</main>
|
||||
|
||||
@if ($jeton)
|
||||
<script nonce="{{ Vite::cspNonce() }}" src="https://{{ config('visio.domaine') }}/external_api.js"></script>
|
||||
@endif
|
||||
<script nonce="{{ Vite::cspNonce() }}">
|
||||
(function () {
|
||||
var URL_ETAT = @json(route('seances.visio.eleve.etat', $seance));
|
||||
var URL_MAIN = @json(route('seances.visio.eleve.main', $seance));
|
||||
var URL_PARTICIPANT = @json(route('seances.visio.eleve.participant', $seance));
|
||||
var CSRF = @json(csrf_token());
|
||||
var OUVERTE = @json((bool) $jeton);
|
||||
var $ = function (s) { return document.querySelector(s); };
|
||||
var poster = function (url, donnees) {
|
||||
return fetch(url, { method: 'POST', headers: { 'X-CSRF-TOKEN': CSRF, 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(donnees || {}) });
|
||||
};
|
||||
var api = null, moi = null, mainLevee = false, chat = null;
|
||||
var BOUTONS = ['microphone', 'camera', 'raisehand', 'tileview', 'fullscreen', 'hangup'];
|
||||
|
||||
function statut(texte, couleur, fond, aide) {
|
||||
var s = $('[data-statut-e]'); s.textContent = texte; s.style.color = couleur; s.style.background = fond;
|
||||
var a = $('[data-aide-e]'); if (a && aide) a.textContent = aide;
|
||||
}
|
||||
|
||||
@if ($jeton)
|
||||
api = new JitsiMeetExternalAPI(@json(config('visio.domaine')), {
|
||||
roomName: @json($seance->visio_salle),
|
||||
jwt: @json($jeton),
|
||||
parentNode: $('#v-scene'),
|
||||
width: '100%', height: '100%',
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true, startWithVideoMuted: true, readOnlyName: true,
|
||||
prejoinConfig: { enabled: false }, disableDeepLinking: true,
|
||||
toolbarButtons: BOUTONS.concat(@json($seance->visio_chat) ? ['chat'] : [])
|
||||
}
|
||||
});
|
||||
api.addListener('videoConferenceJoined', function (e) {
|
||||
moi = e.id;
|
||||
poster(URL_PARTICIPANT, { participant_id: e.id });
|
||||
});
|
||||
// La main levée passe toujours par Jitsi (bouton de la page ou de Jitsi) ;
|
||||
// l'application suit l'événement : une seule source, pas de désaccord.
|
||||
api.addListener('raiseHandUpdated', function (e) {
|
||||
if (e.id !== moi) return;
|
||||
mainLevee = !!e.handRaised;
|
||||
$('[data-main-e]').setAttribute('aria-pressed', String(mainLevee));
|
||||
$('[data-main-e] span').textContent = mainLevee ? 'Baisser la main' : 'Lever la main';
|
||||
poster(URL_MAIN, { levee: mainLevee });
|
||||
});
|
||||
api.addListener('audioMuteStatusChanged', function (e) {
|
||||
if (e.muted) statut('En sourdine', '#3c4043', '#eef0f3', 'Tu es en sourdine. Lève la main pour demander la parole.');
|
||||
else statut('Tu as la parole', '#0d652d', '#e6f4ea', 'Ton prof t’a donné la parole. Coupe ton micro quand tu as fini.');
|
||||
});
|
||||
api.addListener('readyToClose', function () { location.href = $('[data-quitter]').href; });
|
||||
$('[data-main-e]').addEventListener('click', function () { api.executeCommand('toggleRaiseHand'); });
|
||||
$('[data-micro-e]').addEventListener('click', function () { api.executeCommand('toggleAudio'); });
|
||||
$('[data-cam-e]').addEventListener('click', function () { api.executeCommand('toggleVideo'); });
|
||||
$('[data-quitter]').addEventListener('click', function () { try { api.executeCommand('hangup'); } catch (e) {} });
|
||||
@endif
|
||||
|
||||
function rafraichir() {
|
||||
fetch(URL_ETAT, { headers: { 'Accept': 'application/json' } }).then(function (r) { return r.json(); }).then(function (e) {
|
||||
if (e.ouverte !== OUVERTE) { location.reload(); return; }
|
||||
if (!api) return;
|
||||
// Le prof a donné la parole : l'application a baissé la main, Jitsi suit.
|
||||
if (mainLevee && !e.main_levee) api.executeCommand('toggleRaiseHand');
|
||||
if (chat !== e.chat) {
|
||||
chat = e.chat;
|
||||
api.executeCommand('overwriteConfig', { toolbarButtons: BOUTONS.concat(chat ? ['chat'] : []) });
|
||||
}
|
||||
}).catch(function () {});
|
||||
}
|
||||
rafraichir();
|
||||
setInterval(rafraichir, 2000);
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
138
portail/resources/views/seances/visio-prof.blade.php
Normal file
138
portail/resources/views/seances/visio-prof.blade.php
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
@extends('layouts.vitrine')
|
||||
|
||||
@section('titre', 'Visio · '.$seance->classe->nom)
|
||||
|
||||
{{-- Étape 15 : reprend design/resources/views/visio.blade.php (vue prof) ; la
|
||||
grille factice est remplacée par la vraie visio (Jitsi, IFrame API). --}}
|
||||
@section('contenu')
|
||||
<main style="background:#f8f9fa">
|
||||
<section style="padding:40px 20px 8px"><div style="max-width:1280px;margin:0 auto">
|
||||
<p style="margin:0;display:inline-flex;align-items:center;gap:8px;border:1px solid #b8c1cf;border-radius:999px;background:#fff;padding:4px 12px;font-size:14px;font-weight:700;color:#1d3fa8">Visio de classe</p>
|
||||
<h1 style="margin:14px 0 0;font-size:clamp(30px,4vw,44px);line-height:1.1;font-weight:800;letter-spacing:-0.02em;color:#0b1220">{{ $seance->classe->nom }} · {{ $seance->lab->titre }}</h1>
|
||||
<p style="margin:12px 0 0;max-width:720px;font-size:18px;line-height:1.6;font-weight:500;color:#3c4043">Tous les élèves arrivent en sourdine. Ils lèvent la main, vous donnez la parole, un à la fois.</p>
|
||||
</div></section>
|
||||
|
||||
<section style="padding:20px 20px 72px"><div class="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]" style="max-width:1280px;margin:0 auto">
|
||||
<div style="background:#fff;border:1px solid #e3e5e8;border-radius:20px;overflow:hidden;min-width:0">
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;padding:16px 20px;border-bottom:1px solid #e3e5e8">
|
||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
@if ($jeton)
|
||||
<span style="display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border-radius:999px;background:#fce8e6;color:#b3261e;font-size:13px;font-weight:800"><span style="width:8px;height:8px;border-radius:50%;background:#d93025"></span>En direct · <span id="v-chrono" style="font-family:'IBM Plex Mono',monospace">00:00:00</span></span>
|
||||
@else
|
||||
<span style="display:inline-flex;align-items:center;height:28px;padding:0 10px;border-radius:999px;background:#eef0f3;color:#3c4043;font-size:13px;font-weight:800">Visio fermée</span>
|
||||
@endif
|
||||
<span style="font-size:14px;font-weight:700;color:#0b1220">{{ $seance->classe->nom }}</span><span id="v-nb" style="font-size:14px;font-weight:600;color:#5f6368"></span>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
@if ($jeton)
|
||||
<button type="button" id="v-mute-all" style="display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 18px;border-radius:999px;font-size:14px;font-weight:700;cursor:pointer;border:1px solid #dadce0;background:#fff;color:#0b1220"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v3M3 3l18 18"/></svg>Couper tous les micros</button>
|
||||
<form id="v-terminer" action="{{ route('seances.visio.fermer', $seance) }}" method="post" style="margin:0">@csrf<button type="submit" style="display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 18px;border-radius:999px;font-size:14px;font-weight:700;cursor:pointer;border:0;background:#d93025;color:#fff">Terminer la visio</button></form>
|
||||
@else
|
||||
<form action="{{ route('seances.visio.ouvrir', $seance) }}" method="post" style="margin:0">@csrf<button type="submit" style="display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 18px;border-radius:999px;font-size:14px;font-weight:700;cursor:pointer;border:0;background:#0b57d0;color:#fff">Ouvrir la visio</button></form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if ($jeton)
|
||||
<div id="v-scene" style="height:min(70vh,620px);background:#202124"></div>
|
||||
@else
|
||||
<p style="margin:0;padding:40px 20px;font-size:16px;font-weight:600;color:#3c4043">La visio n’est pas ouverte. Les élèves qui la rejoignent attendent dans la salle d’attente ; ouvrez-la pour les faire entrer.</p>
|
||||
@endif
|
||||
</div>
|
||||
<div style="display:grid;gap:20px;align-content:start;min-width:0">
|
||||
<section style="background:#fff;border:1px solid #e3e5e8;border-radius:20px" aria-labelledby="t-mains"><div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e3e5e8"><h2 id="t-mains" style="margin:0;font-size:16px;font-weight:800;color:#0b1220">Mains levées</h2><span id="v-nb-mains" style="min-width:28px;height:28px;padding:0 8px;border-radius:999px;background:#fef7e0;color:#7a4b00;display:inline-grid;place-items:center;font-size:13px;font-weight:800">0</span></div><ol id="v-mains" style="margin:0;padding:8px 12px;list-style:none"></ol><p style="margin:0;padding:0 20px 14px;font-size:13px;font-weight:600;color:#5f6368">Premier arrivé en haut.</p></section>
|
||||
<section style="background:#fff;border:1px solid #e3e5e8;border-radius:20px" aria-labelledby="t-attente"><div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:16px 20px;border-bottom:1px solid #e3e5e8"><h2 id="t-attente" style="margin:0;font-size:16px;font-weight:800;color:#0b1220">Salle d’attente</h2>@unless ($jeton)<form action="{{ route('seances.visio.ouvrir', $seance) }}" method="post" style="margin:0">@csrf<button type="submit" id="v-tout-admettre" style="border:0;background:none;color:#0b57d0;font-size:14px;font-weight:700;cursor:pointer">Tout admettre</button></form>@endunless</div><ul id="v-attente" style="margin:0;padding:8px 12px;list-style:none"></ul></section>
|
||||
<section style="background:#fff;border:1px solid #e3e5e8;border-radius:20px" aria-labelledby="t-chat"><div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:16px 20px;border-bottom:1px solid #e3e5e8"><h2 id="t-chat" style="margin:0;font-size:16px;font-weight:800;color:#0b1220">Chat des élèves</h2><button type="button" id="v-chat-switch" role="switch" aria-checked="{{ $seance->visio_chat ? 'true' : 'false' }}" aria-label="Activer le chat des élèves" @disabled(! $jeton) style="position:relative;width:44px;height:26px;border-radius:999px;border:0;cursor:pointer;transition:background .2s;background:{{ $seance->visio_chat ? '#0b57d0' : '#c4c7c5' }}"><span style="position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.3);transition:transform .2s;transform:translateX({{ $seance->visio_chat ? '18px' : '0' }})"></span></button></div><div id="v-chat" style="padding:16px 20px;font-size:14px;font-weight:600;color:#5f6368">{{ $seance->visio_chat ? 'Le chat est ouvert aux élèves.' : 'Le chat est désactivé.' }}</div></section>
|
||||
</div>
|
||||
</div></section>
|
||||
</main>
|
||||
|
||||
@if ($jeton)
|
||||
<script nonce="{{ Vite::cspNonce() }}" src="https://{{ config('visio.domaine') }}/external_api.js"></script>
|
||||
@endif
|
||||
<script nonce="{{ Vite::cspNonce() }}">
|
||||
(function () {
|
||||
var URL_ETAT = @json(route('seances.visio.etat', $seance));
|
||||
var URL_CHAT = @json(route('seances.visio.chat', $seance));
|
||||
var URL_PAROLE = @json(route('seances.visio.parole', [$seance, '__ELEVE__']));
|
||||
var CSRF = @json(csrf_token());
|
||||
var $ = function (s) { return document.querySelector(s); };
|
||||
var poster = function (url, donnees) {
|
||||
return fetch(url, { method: 'POST', headers: { 'X-CSRF-TOKEN': CSRF, 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(donnees || {}) });
|
||||
};
|
||||
var esc = function (t) { var d = document.createElement('span'); d.textContent = t; return d.innerHTML; };
|
||||
var api = null;
|
||||
var BOUTONS = ['microphone', 'camera', 'desktop', 'raisehand', 'participants-pane', 'tileview', 'settings', 'fullscreen', 'hangup'];
|
||||
|
||||
@if ($jeton)
|
||||
api = new JitsiMeetExternalAPI(@json(config('visio.domaine')), {
|
||||
roomName: @json($seance->visio_salle),
|
||||
jwt: @json($jeton),
|
||||
parentNode: $('#v-scene'),
|
||||
width: '100%', height: '100%',
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true, startWithVideoMuted: true, readOnlyName: true,
|
||||
prejoinConfig: { enabled: false }, disableDeepLinking: true,
|
||||
toolbarButtons: BOUTONS.concat(@json($seance->visio_chat) ? ['chat'] : [])
|
||||
}
|
||||
});
|
||||
// Seul le prof donne la parole : modération du micro dès l'arrivée.
|
||||
api.addListener('videoConferenceJoined', function () {
|
||||
api.executeCommand('toggleModeration', true, 'audio');
|
||||
});
|
||||
var compter = function () { $('#v-nb').textContent = '· ' + api.getNumberOfParticipants() + ' en ligne'; };
|
||||
api.addListener('participantJoined', compter);
|
||||
api.addListener('participantLeft', compter);
|
||||
api.addListener('readyToClose', function () { $('#v-terminer').submit(); });
|
||||
$('#v-mute-all').addEventListener('click', function () { api.executeCommand('muteEveryone', 'audio'); });
|
||||
$('#v-terminer').addEventListener('submit', function () { try { api.executeCommand('endConference'); } catch (e) {} });
|
||||
|
||||
var debut = new Date(@json($seance->visio_ouverte_a?->toIso8601String())).getTime();
|
||||
setInterval(function () {
|
||||
var s = Math.max(0, Math.floor((Date.now() - debut) / 1000));
|
||||
$('#v-chrono').textContent = [Math.floor(s / 3600), Math.floor(s / 60) % 60, s % 60].map(function (n) { return String(n).padStart(2, '0'); }).join(':');
|
||||
}, 1000);
|
||||
|
||||
$('#v-chat-switch').addEventListener('click', function () {
|
||||
var actif = this.getAttribute('aria-checked') !== 'true';
|
||||
poster(URL_CHAT, { actif: actif }).then(function () { rafraichir(); });
|
||||
});
|
||||
@endif
|
||||
|
||||
function rafraichir() {
|
||||
fetch(URL_ETAT, { headers: { 'Accept': 'application/json' } }).then(function (r) { return r.json(); }).then(function (e) {
|
||||
$('#v-nb-mains').textContent = e.mains.length;
|
||||
$('#v-mains').innerHTML = e.mains.length ? e.mains.map(function (m) {
|
||||
var attente = Math.max(0, Math.floor(Date.now() / 1000) - m.depuis);
|
||||
return '<li style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px;border-radius:12px">'
|
||||
+ '<span><strong>' + esc(m.nom) + '</strong> <span style="font-size:13px;color:#5f6368">' + (attente < 60 ? attente + ' s' : Math.floor(attente / 60) + ' min') + '</span></span>'
|
||||
+ (api && m.participant_id ? '<button type="button" data-parole="' + m.eleve_id + '" data-participant="' + esc(m.participant_id) + '" style="display:inline-flex;align-items:center;height:32px;padding:0 14px;border-radius:999px;border:0;cursor:pointer;font-size:13px;font-weight:700;background:#0b57d0;color:#fff">Donner la parole</button>' : '')
|
||||
+ '</li>';
|
||||
}).join('') : '<li style="padding:8px;font-size:14px;color:#5f6368">Aucune main levée.</li>';
|
||||
$('#v-attente').innerHTML = e.attente.length ? e.attente.map(function (a) {
|
||||
return '<li style="padding:8px;font-weight:700">' + esc(a.nom) + '</li>';
|
||||
}).join('') : '<li style="padding:8px;font-size:14px;color:#5f6368">Personne n’attend.</li>';
|
||||
var sw = $('#v-chat-switch');
|
||||
if (sw.getAttribute('aria-checked') !== String(e.chat)) {
|
||||
sw.setAttribute('aria-checked', String(e.chat));
|
||||
sw.style.background = e.chat ? '#0b57d0' : '#c4c7c5';
|
||||
sw.firstElementChild.style.transform = e.chat ? 'translateX(18px)' : 'translateX(0)';
|
||||
$('#v-chat').textContent = e.chat ? 'Le chat est ouvert aux élèves.' : 'Le chat est désactivé.';
|
||||
if (api) api.executeCommand('overwriteConfig', { toolbarButtons: BOUTONS.concat(e.chat ? ['chat'] : []) });
|
||||
}
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
// « Donner la parole » : askToUnmute autorise le micro quand la modération
|
||||
// est active (documentation de l'IFrame API), puis la main est baissée.
|
||||
$('#v-mains').addEventListener('click', function (ev) {
|
||||
var b = ev.target.closest('[data-parole]');
|
||||
if (!b || !api) return;
|
||||
api.executeCommand('askToUnmute', b.dataset.participant);
|
||||
poster(URL_PAROLE.replace('__ELEVE__', b.dataset.parole)).then(rafraichir);
|
||||
});
|
||||
|
||||
rafraichir();
|
||||
setInterval(rafraichir, 2000);
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
Loading…
Add table
Add a link
Reference in a new issue