diff --git a/VApp/src/components/CardCurrentQuizz.vue b/VApp/src/components/CardCurrentQuizz.vue index 9c828e7d..eff0830a 100644 --- a/VApp/src/components/CardCurrentQuizz.vue +++ b/VApp/src/components/CardCurrentQuizz.vue @@ -28,7 +28,12 @@ const quizzList = ref([]); // Fonction pour mettre à jour la liste const handleMessage = (topic, message) => { try { - quizzList.value = JSON.parse(message.toString()); + const parsed = JSON.parse(message.toString()); + if (Array.isArray(parsed)) { + quizzList.value = parsed; + } else { + console.warn('CardCurrentQuizz: Received non-array data', parsed); + } } catch (error) { console.error('Erreur de parsing JSON:', error); } diff --git a/VApp/src/components/CardSolution.vue b/VApp/src/components/CardSolution.vue index 6c84c792..788a9e54 100644 --- a/VApp/src/components/CardSolution.vue +++ b/VApp/src/components/CardSolution.vue @@ -3,14 +3,36 @@ mdi-play-network-outline Solution - - - - - - - + +
Question {{ currentQuestionIndex + 1 }}
+
{{ currentQuestion.QuestionText }}
+ + + + + {{ showSolution ? 'Masquer Solution' : 'Voir Solution' }} + + + +
+
{{ currentQuestion.MasterData.CorrectAnswer }}
+
+ mdi-information {{ currentQuestion.MasterData.MasterNotes }} +
+
+ mdi-help-circle {{ currentQuestion.MasterData.Help }} +
+
+
+
+ +
Aucun quiz chargé ou fin du quiz.
+
\ No newline at end of file diff --git a/VApp/src/components/CardTimer.vue b/VApp/src/components/CardTimer.vue index 3c2cfc05..327bc3e7 100644 --- a/VApp/src/components/CardTimer.vue +++ b/VApp/src/components/CardTimer.vue @@ -1,85 +1,39 @@ - diff --git a/VApp/src/components/GameMedia.vue b/VApp/src/components/GameMedia.vue new file mode 100644 index 00000000..0229e08c --- /dev/null +++ b/VApp/src/components/GameMedia.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/VApp/src/components/HidingOverlay.vue b/VApp/src/components/HidingOverlay.vue index 19e76829..c898d04d 100644 --- a/VApp/src/components/HidingOverlay.vue +++ b/VApp/src/components/HidingOverlay.vue @@ -18,6 +18,9 @@ case "pause": gamehiding.value = true; break; + case "hide": + gamehiding.value = true; + break; default: console.warn("Commande non reconnue :", message); gamehiding.value = true; @@ -29,6 +32,17 @@ onMounted(() => { subscribeToTopic('#', (topic, message) => { handleMessage(topic, message); + + if (topic === 'vulture/buzzer/status') { + try { + const data = JSON.parse(message); + if (data.status === 'blocked') { + gamehiding.value = true; + } + } catch (e) { + console.error('HidingOverlay JSON error', e); + } + } }); }); diff --git a/VApp/src/store/quizStore.js b/VApp/src/store/quizStore.js new file mode 100644 index 00000000..ae5d0c5d --- /dev/null +++ b/VApp/src/store/quizStore.js @@ -0,0 +1,166 @@ +import { reactive, computed } from 'vue'; +import mqtt from 'mqtt'; +import config from '@/config.js'; +import sessionConfig from '@/quizz/vulture-session-2026-01/session-configuration.json'; + +// Reactive state +const state = reactive({ + currentQuestionIndex: 0, + questions: [], + isMediaHidden: true, + packTitle: '', + timer: 0 // Timer in seconds +}); + +// MQTT Client +let client = null; +let timerInterval = null; +let initialized = false; + +// Initialize Store +function init() { + if (initialized) { + console.log('QuizStore: Already initialized'); + return; + } + initialized = true; + + // Load local config immediately + state.questions = sessionConfig.Questions || []; + state.packTitle = sessionConfig.PackTitle || ''; + + // Connect MQTT + client = mqtt.connect(config.mqttBrokerUrl); + + client.on('connect', () => { + console.log('QuizStore: MQTT Connected'); + client.subscribe('game/quiz/control'); + client.subscribe('/display/control'); + }); + + client.on('message', (topic, message) => { + const msgStr = message.toString(); + console.log('QuizStore: MQTT Message Received', topic, msgStr); + if (topic === 'game/quiz/control') { + try { + const payload = JSON.parse(msgStr); + handleRemoteCommand(payload); + } catch (e) { + console.error('QuizStore: JSON Parse Error', e); + } + } else if (topic === '/display/control') { + // Handle raw string commands from MqttButtons + if (msgStr === 'next') { + _nextQuestion(true); + } else if (msgStr === 'previous') { + _prevQuestion(true); + } else if (msgStr === 'play') { + // Start timer for picture questions + const currentQ = state.questions[state.currentQuestionIndex]; + if (currentQ?.Type === 'picture') { + const playTime = currentQ.Settings?.PlayTime; + if (playTime && playTime > 0) { + console.log('QuizStore: Starting timer for picture', playTime); + actions.startTimer(playTime); + } + } + } else if (msgStr === 'pause') { + stopTimer(); + } + } + }); +} + +function handleRemoteCommand(cmd) { + if (cmd.action === 'next') { + _nextQuestion(false); + } else if (cmd.action === 'prev') { + _prevQuestion(false); + } else if (cmd.action === 'setIndex') { + state.currentQuestionIndex = cmd.index; + } +} + +// Internal actions (boolean publish determines if we send MQTT) +function _nextQuestion(publish = true) { + if (state.currentQuestionIndex < state.questions.length - 1) { + state.currentQuestionIndex++; + if (publish && client) { + client.publish('game/quiz/control', JSON.stringify({ action: 'setIndex', index: state.currentQuestionIndex })); + } + } +} + +function _prevQuestion(publish = true) { + if (state.currentQuestionIndex > 0) { + state.currentQuestionIndex--; + if (publish && client) { + client.publish('game/quiz/control', JSON.stringify({ action: 'setIndex', index: state.currentQuestionIndex })); + } + } +} + +// Public Actions +const actions = { + init, + nextQuestion: () => _nextQuestion(true), + prevQuestion: () => _prevQuestion(true), + setQuestion: (index) => { + if (index >= 0 && index < state.questions.length) { + state.currentQuestionIndex = index; + if (client) { + client.publish('game/quiz/control', JSON.stringify({ action: 'setIndex', index: index })); + } + } + }, + startTimer: (seconds) => { + stopTimer(); + state.timer = seconds; + publishTimer(); + timerInterval = setInterval(() => { + if (state.timer > 0) { + state.timer--; + publishTimer(); + } else { + stopTimer(); + // Auto-hide by publishing pause + if (client) { + client.publish('/display/control', 'pause'); + } + } + }, 1000); + }, + stopTimer +}; + +function stopTimer() { + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = null; + } + state.timer = 0; + publishTimer(); +} + +function publishTimer() { + if (client) { + client.publish('game/timer', JSON.stringify({ time: state.timer })); + } +} + +// Getters +const getters = { + currentQuestion: computed(() => state.questions[state.currentQuestionIndex]), + isFirstQuestion: computed(() => state.currentQuestionIndex === 0), + isLastQuestion: computed(() => state.currentQuestionIndex === state.questions.length - 1), + totalQuestions: computed(() => state.questions.length), + packTitle: computed(() => state.packTitle), + currentQuestionIndex: computed(() => state.currentQuestionIndex), + timer: computed(() => state.timer) +}; + +export default { + state, + actions, + getters +}; diff --git a/VApp/src/views/GameControl.vue b/VApp/src/views/GameControl.vue index f14ba355..aaaae949 100644 --- a/VApp/src/views/GameControl.vue +++ b/VApp/src/views/GameControl.vue @@ -5,17 +5,17 @@ - +
- + + - @@ -26,9 +26,14 @@ import CardSolution from '@/components/CardSolution.vue' import CardControl from '@/components/CardControl.vue' -import CardSoundboard from '@/components/CardSoundboard.vue'; import CardButtonScore from '@/components/CardButtonScore.vue' import BuzzerValidationDialog from '@/components/BuzzerValidationDialog.vue'; +import { onMounted } from 'vue'; +import quizStore from '@/store/quizStore'; + +onMounted(() => { + quizStore.actions.init(); +}); diff --git a/VApp/src/views/GameDisplay.vue b/VApp/src/views/GameDisplay.vue index d83e6dbc..96ad0d8f 100644 --- a/VApp/src/views/GameDisplay.vue +++ b/VApp/src/views/GameDisplay.vue @@ -23,7 +23,7 @@ - 00:00 + {{ timerDisplay }}
@@ -50,17 +50,19 @@
- +
@@ -134,32 +154,37 @@ display: flex; justify-content: center; align-items: center; + box-shadow: 0 10px 30px rgba(0,0,0,0.5); + transition: transform 0.2s; } .color-blue { - background-color: rgb(var(--v-theme-BlueBuzzer), 1); + background: linear-gradient(135deg, rgb(var(--v-theme-BlueBuzzer)), #1a3a5a); border-radius: 40px 5px 40px 5px; } .color-red { - background-color: rgb(var(--v-theme-RedBuzzer), 1); + background: linear-gradient(135deg, rgb(var(--v-theme-RedBuzzer)), #5a1a1a); border-radius: 40px 5px 40px 5px; } .color-green { - background-color: rgb(var(--v-theme-GreenBuzzer), 1); + background: linear-gradient(135deg, rgb(var(--v-theme-GreenBuzzer)), #1a5a2a); border-radius: 5px 40px 5px 40px; } .color-yellow { - background-color: rgb(var(--v-theme-YellowBuzzer), 1); + background: linear-gradient(135deg, rgb(var(--v-theme-YellowBuzzer)), #5a5a1a); border-radius: 5px 40px 5px 40px; } .color-white { - background-color: white; + background-color: #1a1a1a; + border: 3px solid #333; border-radius: 40px; + box-shadow: 0 0 30px rgba(0,0,0,0.6); } .v-label-time { padding-top: 5px; - color: black; + color: white; font-size: 49px; font-family: 'Bahnschrift'; + text-shadow: 0 0 15px rgba(255, 255, 255, 0.2); } .v-label-score { color: white; @@ -167,6 +192,7 @@ font-family: 'Bahnschrift'; font-weight: bold; line-height: 1; + text-shadow: 4px 4px 8px rgba(0,0,0,0.4); } .v-label-round-score { color: rgba(255, 255, 255, 0.8); @@ -174,6 +200,7 @@ font-family: 'Bahnschrift'; font-weight: 500; margin-bottom: 2px; + text-shadow: 2px 2px 4px rgba(0,0,0,0.3); } /* Transition styles */ diff --git a/VApp/src/views/ScoreDisplay.vue b/VApp/src/views/ScoreDisplay.vue index f46ea5f5..b8f36e4a 100644 --- a/VApp/src/views/ScoreDisplay.vue +++ b/VApp/src/views/ScoreDisplay.vue @@ -58,13 +58,13 @@
-
00:00
+
{{ timerDisplay }}