feat: add return of table state
This commit is contained in:
parent
189eb135bb
commit
d749dea6f4
@ -51,6 +51,9 @@ public class CompetitionWS {
|
||||
@Inject
|
||||
RTeam rTeam;
|
||||
|
||||
@Inject
|
||||
RState rState;
|
||||
|
||||
@Inject
|
||||
SecurityCtx securityCtx;
|
||||
|
||||
@ -95,6 +98,7 @@ public class CompetitionWS {
|
||||
getWSReceiverMethods(RRegister.class, rRegister);
|
||||
getWSReceiverMethods(RCard.class, rCard);
|
||||
getWSReceiverMethods(RTeam.class, rTeam);
|
||||
getWSReceiverMethods(RState.class, rState);
|
||||
|
||||
executor = notifyExecutor;
|
||||
}
|
||||
@ -141,6 +145,7 @@ public class CompetitionWS {
|
||||
LOGGER.debugf("Active connections: %d", connection.getOpenConnections().size());
|
||||
|
||||
waitingResponse.remove(connection);
|
||||
rState.removeConnection(connection);
|
||||
}
|
||||
|
||||
private MessageOut makeReply(MessageIn message, Object data) {
|
||||
@ -230,6 +235,30 @@ public class CompetitionWS {
|
||||
});
|
||||
}
|
||||
|
||||
public static void sendNotifyState(WebSocketConnection connection, String code, Object data) {
|
||||
String uuid = connection.pathParam("uuid");
|
||||
|
||||
List<Uni<Void>> queue = new ArrayList<>();
|
||||
queue.add(Uni.createFrom().voidItem()); // For avoid empty queue
|
||||
|
||||
connection.getOpenConnections().forEach(c -> {
|
||||
Boolean s = c.userData().get(UserData.TypedKey.forBoolean("needState"));
|
||||
if (uuid.equals(c.pathParam("uuid")) && s != null && s) {
|
||||
queue.add(c.sendText(new MessageOut(UUID.randomUUID(), code, MessageType.NOTIFY, data)));
|
||||
}
|
||||
});
|
||||
|
||||
Uni.join().all(queue)
|
||||
.andCollectFailures()
|
||||
.runSubscriptionOn(executor)
|
||||
.subscribeAsCompletionStage()
|
||||
.whenComplete((v, t) -> {
|
||||
if (t != null) {
|
||||
LOGGER.error("Error sending ws_out message", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@OnError
|
||||
Uni<Void> error(WebSocketConnection connection, ForbiddenException t) {
|
||||
return connection.close(CloseReason.INTERNAL_SERVER_ERROR);
|
||||
|
||||
@ -58,6 +58,8 @@ public class RCard {
|
||||
|
||||
@WSReceiver(code = "getCardForMatch", permission = PermLevel.VIEW)
|
||||
public Uni<List<CardModel>> getCardForMatch(WebSocketConnection connection, Long matchId) {
|
||||
if (matchId == null)
|
||||
return Uni.createFrom().nullItem();
|
||||
return getById(matchId, connection).chain(matchModel -> cardService.getForMatch(matchModel));
|
||||
}
|
||||
|
||||
|
||||
@ -51,6 +51,9 @@ public class RMatch {
|
||||
@Inject
|
||||
TradService trad;
|
||||
|
||||
@Inject
|
||||
RState rState;
|
||||
|
||||
private Uni<MatchModel> getById(long id, WebSocketConnection connection) {
|
||||
return matchRepository.findById(id)
|
||||
.invoke(Unchecked.consumer(o -> {
|
||||
@ -195,6 +198,7 @@ public class RMatch {
|
||||
return Panache.withTransaction(() -> matchRepository.persist(mm));
|
||||
})
|
||||
.invoke(mm -> toSend.add(MatchEntity.fromModel(mm)))
|
||||
.invoke(mm -> rState.setMatchEnd(connection, matchEnd))
|
||||
.chain(mm -> updateEndAndTree(mm, toSend))
|
||||
.invoke(__ -> SSMatch.sendMatch(connection, toSend))
|
||||
.replaceWithVoid();
|
||||
|
||||
149
src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java
Normal file
149
src/main/java/fr/titionfire/ffsaf/ws/recv/RState.java
Normal file
@ -0,0 +1,149 @@
|
||||
package fr.titionfire.ffsaf.ws.recv;
|
||||
|
||||
import fr.titionfire.ffsaf.ws.PermLevel;
|
||||
import fr.titionfire.ffsaf.ws.send.SSState;
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
import io.quarkus.websockets.next.UserData;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
@RegisterForReflection
|
||||
public class RState {
|
||||
|
||||
private static final HashMap<WebSocketConnection, TableState> tableStates = new HashMap<>();
|
||||
|
||||
@WSReceiver(code = "subscribeToState", permission = PermLevel.VIEW)
|
||||
public Uni<List<TableState>> sendCurrentScore(WebSocketConnection connection, Boolean subscribe) {
|
||||
connection.userData().put(UserData.TypedKey.forBoolean("needState"), subscribe);
|
||||
|
||||
if (subscribe) {
|
||||
String uuid = connection.pathParam("uuid");
|
||||
return Uni.createFrom().item(() ->
|
||||
tableStates.values().stream().filter(s -> s.getCompetitionUuid().equals(uuid)).toList()
|
||||
);
|
||||
}
|
||||
return Uni.createFrom().nullItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendState", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendState(WebSocketConnection connection, TableState tableState) {
|
||||
tableState.setCompetitionUuid(connection.pathParam("uuid"));
|
||||
|
||||
if (tableStates.containsKey(connection))
|
||||
tableState.setId(tableStates.get(connection).getId());
|
||||
if (tableState.getChronoState().isRunning() && tableState.getChronoState().state == 0)
|
||||
tableState.setState(MatchState.IN_PROGRESS);
|
||||
tableStates.put(connection, tableState);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendSelectCategory", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendSelectCategory(WebSocketConnection connection, Long catId) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setSelectedCategory(catId);
|
||||
tableState.setState(MatchState.NOT_STARTED);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendSelectMatch", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendSelectMatch(WebSocketConnection connection, Long matchId) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setSelectedMatch(matchId);
|
||||
tableState.setState(MatchState.NOT_STARTED);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendCurentChrono", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendCurentChrono(WebSocketConnection connection, ChronoState chronoState) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setChronoState(chronoState);
|
||||
if (chronoState.isRunning())
|
||||
tableState.setState(MatchState.IN_PROGRESS);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendLicenceName", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendCurrentScore(WebSocketConnection connection, String name) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setLiceName(name);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
@WSReceiver(code = "sendCurrentScore", permission = PermLevel.TABLE)
|
||||
public Uni<Void> sendCurrentScore(WebSocketConnection connection, ScoreState scoreState) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (tableState != null) {
|
||||
tableState.setScoreState(scoreState);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
return Uni.createFrom().voidItem();
|
||||
}
|
||||
|
||||
public void removeConnection(WebSocketConnection connection) {
|
||||
if (tableStates.containsKey(connection)) {
|
||||
SSState.sendRmStateFull(connection, tableStates.get(connection).getId());
|
||||
tableStates.remove(connection);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMatchEnd(WebSocketConnection connection, RMatch.MatchEnd matchEnd) {
|
||||
if (tableStates.containsKey(connection)) {
|
||||
TableState tableState = tableStates.get(connection);
|
||||
if (matchEnd.end())
|
||||
tableState.setState(MatchState.ENDED);
|
||||
else
|
||||
tableState.setState(MatchState.IN_PROGRESS);
|
||||
SSState.sendStateFull(connection, tableState);
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record ChronoState(long time, long startTime, long configTime, long configPause, int state) {
|
||||
public boolean isRunning() {
|
||||
return startTime != 0 || state != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@RegisterForReflection
|
||||
public record ScoreState(int scoreRouge, int scoreBleu) {
|
||||
}
|
||||
|
||||
@Data
|
||||
@RegisterForReflection
|
||||
public static class TableState {
|
||||
UUID id = UUID.randomUUID();
|
||||
String competitionUuid;
|
||||
Long selectedCategory;
|
||||
Long selectedMatch;
|
||||
ChronoState chronoState;
|
||||
ScoreState scoreState;
|
||||
String liceName = "???";
|
||||
MatchState state = MatchState.NOT_STARTED;
|
||||
}
|
||||
|
||||
public enum MatchState {
|
||||
NOT_STARTED,
|
||||
IN_PROGRESS,
|
||||
ENDED
|
||||
}
|
||||
}
|
||||
19
src/main/java/fr/titionfire/ffsaf/ws/send/SSState.java
Normal file
19
src/main/java/fr/titionfire/ffsaf/ws/send/SSState.java
Normal file
@ -0,0 +1,19 @@
|
||||
package fr.titionfire.ffsaf.ws.send;
|
||||
|
||||
import fr.titionfire.ffsaf.ws.CompetitionWS;
|
||||
import fr.titionfire.ffsaf.ws.recv.RState;
|
||||
import io.quarkus.websockets.next.WebSocketConnection;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class SSState {
|
||||
|
||||
public static void sendStateFull(WebSocketConnection connection, RState.TableState state) {
|
||||
CompetitionWS.sendNotifyState(connection, "sendStateFull", state);
|
||||
}
|
||||
|
||||
public static void sendRmStateFull(WebSocketConnection connection, UUID id) {
|
||||
CompetitionWS.sendNotifyState(connection, "rmStateFull", id);
|
||||
}
|
||||
|
||||
}
|
||||
@ -5,7 +5,6 @@
|
||||
"actuel": "Current",
|
||||
"administration": "Administration",
|
||||
"adresseDuServeur": "Server address",
|
||||
"advertisement": "",
|
||||
"ajouter": "Add",
|
||||
"ajouterDesCombattants": "Add fighters",
|
||||
"ajouterUn": "Add one",
|
||||
@ -40,6 +39,7 @@
|
||||
"config.obs.motDePasseDuServeur": "Server password",
|
||||
"config.obs.warn1": "/! The password will be stored in plain text; it is recommended to use it only on OBS WebSocket and to change it between each competition",
|
||||
"config.obs.ws": "ws://",
|
||||
"configurationDuNomDeLaZone": "Zone name configuration",
|
||||
"configurationObs": "OBS Configuration",
|
||||
"confirm1": "This match already has results; are you sure you want to delete it?",
|
||||
"confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!",
|
||||
@ -64,6 +64,7 @@
|
||||
"err3": "At least one type (pool or tournament) must be selected.",
|
||||
"erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ",
|
||||
"erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ",
|
||||
"etatDesTablesDeMarque": "State of marque tables",
|
||||
"exporter": "Export",
|
||||
"fermer": "Close",
|
||||
"finalesUniquement": "Finals only",
|
||||
@ -80,6 +81,7 @@
|
||||
"neRienConserver": "Keep nothing",
|
||||
"no": "No.",
|
||||
"nom": "Name",
|
||||
"nomDeLaZone": "Area name",
|
||||
"nomDeLéquipe": "team name",
|
||||
"nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>",
|
||||
"nouvelle...": "New...",
|
||||
@ -146,7 +148,7 @@
|
||||
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",
|
||||
"ttm.admin.scripte": "Copy integration script",
|
||||
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",
|
||||
"ttm.table.obs": "Short click: Load configuration and connect. Long click: Ring configuration",
|
||||
"ttm.table.obs": "Short click: Load configuration and connect.",
|
||||
"ttm.table.pub_aff": "Open public display",
|
||||
"ttm.table.pub_score": "Show scores on public display",
|
||||
"type": "Type",
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
"actuel": "Actuel",
|
||||
"administration": "Administration",
|
||||
"adresseDuServeur": "Adresse du serveur",
|
||||
"advertisement": "Advertisement",
|
||||
"ajouter": "Ajouter",
|
||||
"ajouterDesCombattants": "Ajouter des combattants",
|
||||
"ajouterUn": "Ajouter un ",
|
||||
@ -40,6 +39,7 @@
|
||||
"config.obs.motDePasseDuServeur": "Mot de passe du serveur",
|
||||
"config.obs.warn1": "/! Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en changer entre chaque compétition",
|
||||
"config.obs.ws": "ws://",
|
||||
"configurationDuNomDeLaZone": "Configuration du nom de la zone",
|
||||
"configurationObs": "Configuration OBS",
|
||||
"confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?",
|
||||
"confirm2.msg": "Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!",
|
||||
@ -64,6 +64,7 @@
|
||||
"err3": "Au moins un type (poule ou tournoi) doit être sélectionné.",
|
||||
"erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ",
|
||||
"erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ",
|
||||
"etatDesTablesDeMarque": "Etat des tables de marque",
|
||||
"exporter": "Exporter",
|
||||
"fermer": "Fermer",
|
||||
"finalesUniquement": "Finales uniquement",
|
||||
@ -80,6 +81,7 @@
|
||||
"neRienConserver": "Ne rien conserver",
|
||||
"no": "N°",
|
||||
"nom": "Nom",
|
||||
"nomDeLaZone": "Nom de la zone",
|
||||
"nomDeLéquipe": "Nom de l'équipe",
|
||||
"nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>",
|
||||
"nouvelle...": "Nouvelle...",
|
||||
@ -146,7 +148,7 @@
|
||||
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",
|
||||
"ttm.admin.scripte": "Copier le scripte d'intégration",
|
||||
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",
|
||||
"ttm.table.obs": "Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice",
|
||||
"ttm.table.obs": "Clique court : Charger la configuration et se connecter.",
|
||||
"ttm.table.pub_aff": "Ouvrir l'affichage public",
|
||||
"ttm.table.pub_score": "Afficher les scores sur l'affichage public",
|
||||
"type": "Type",
|
||||
|
||||
@ -47,6 +47,7 @@ export function WSProvider({url, onmessage, children}) {
|
||||
const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false})
|
||||
const [state, dispatch] = useReducer(reducer, {listener: []})
|
||||
const ws = useRef(null)
|
||||
const tableState = useRef({})
|
||||
const listenersRef = useRef([])
|
||||
const callbackRef = useRef({})
|
||||
const isReadyRef = useRef(isReady)
|
||||
@ -216,14 +217,14 @@ export function WSProvider({url, onmessage, children}) {
|
||||
}
|
||||
|
||||
|
||||
const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData}
|
||||
const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData, tableState}
|
||||
return <WebsocketContext.Provider value={ret}>
|
||||
{children}
|
||||
</WebsocketContext.Provider>
|
||||
}
|
||||
|
||||
export function useWS() {
|
||||
const {isReady, dispatch, send, wait_length, welcomeData} = useContext(WebsocketContext)
|
||||
const {isReady, dispatch, send, wait_length, welcomeData, tableState} = useContext(WebsocketContext)
|
||||
return {
|
||||
dispatch,
|
||||
isReady,
|
||||
@ -247,6 +248,10 @@ export function useWS() {
|
||||
send(uuidv4(), "error", "ERROR", data)
|
||||
},
|
||||
send,
|
||||
setState: (newState) => {
|
||||
tableState.current = {...tableState.current, ...newState}
|
||||
},
|
||||
tableState
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,10 +11,12 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
|
||||
import JSZip from "jszip";
|
||||
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
|
||||
import {faGlobe} from "@fortawesome/free-solid-svg-icons";
|
||||
import {faGlobe, faTableCellsLarge} from "@fortawesome/free-solid-svg-icons";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import i18n from "i18next";
|
||||
import {getToastMessage} from "../../../utils/Tools.js";
|
||||
import {copyStyles} from "../../../utils/copyStyles.js";
|
||||
import {StateWindow} from "./StateWindow.jsx";
|
||||
|
||||
const vite_url = import.meta.env.VITE_URL;
|
||||
|
||||
@ -161,12 +163,18 @@ async function downloadResourcesAsZip(resourceList) {
|
||||
progressText.textContent = i18n.t('téléchargementTerminé!');
|
||||
}
|
||||
|
||||
const windowName = "FFSAFTableStateWindow";
|
||||
|
||||
function Menu({menuActions, compUuid}) {
|
||||
const e = document.getElementById("actionMenu")
|
||||
const longPress = useRef({time: null, timer: null, button: null});
|
||||
const obsModal = useRef(null);
|
||||
const {t} = useTranslation("cm");
|
||||
|
||||
const [showStateWin, setShowStateWin] = useState(false)
|
||||
const externalWindow = useRef(null)
|
||||
const containerEl = useRef(document.createElement("div"))
|
||||
|
||||
for (const x of tto)
|
||||
x.dispose();
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
|
||||
@ -178,6 +186,32 @@ function Menu({menuActions, compUuid}) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem(windowName + "_open") === "true") {
|
||||
handleStateWin();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleStateWin = __ => {
|
||||
if (showStateWin === false || !externalWindow.current || externalWindow.current.closed) {
|
||||
externalWindow.current = window.open("", windowName, "width=800,height=600,left=200,top=200")
|
||||
externalWindow.current.document.body.innerHTML = ""
|
||||
externalWindow.current.document.body.appendChild(containerEl.current)
|
||||
copyStyles(document, externalWindow.current.document)
|
||||
|
||||
externalWindow.current.addEventListener("beforeunload", () => {
|
||||
setShowStateWin(false);
|
||||
externalWindow.current.close();
|
||||
externalWindow.current = null;
|
||||
sessionStorage.removeItem(windowName + "_open");
|
||||
});
|
||||
setShowStateWin(true);
|
||||
sessionStorage.setItem(windowName + "_open", "true");
|
||||
} else {
|
||||
externalWindow.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
const longPressDown = (button) => {
|
||||
longPress.current.button = button;
|
||||
longPress.current.time = new Date();
|
||||
@ -251,7 +285,12 @@ function Menu({menuActions, compUuid}) {
|
||||
onClick={() => copyScriptToClipboard()}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
data-bs-title={t('ttm.admin.scripte')}/>
|
||||
<FontAwesomeIcon icon={faTableCellsLarge} size="xl"
|
||||
style={{color: showStateWin ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
onClick={handleStateWin}
|
||||
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('etatDesTablesDeMarque')}/>
|
||||
</>, document.getElementById("actionMenu"))}
|
||||
{externalWindow.current && createPortal(<StateWindow document={externalWindow.current.document}/>, containerEl.current)}
|
||||
|
||||
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
|
||||
Launch OBS Modal
|
||||
@ -500,7 +539,10 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
|
||||
newTrees.push(trees2.at(i));
|
||||
}
|
||||
|
||||
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees", "cm")
|
||||
toast.promise(sendRequest('updateTrees', {
|
||||
categoryId: state.id,
|
||||
trees: newTrees
|
||||
}), getToastMessage("toast.updateTrees", "cm")
|
||||
).then(__ => {
|
||||
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm"))
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@ import React, {useEffect, useRef, useState} from "react";
|
||||
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {timePrint} from "../../../utils/Tools.js";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
|
||||
export function ChronoPanel() {
|
||||
const [config, setConfig] = useState({
|
||||
@ -10,6 +11,7 @@ export function ChronoPanel() {
|
||||
})
|
||||
const [chrono, setChrono] = useState({time: 0, startTime: 0})
|
||||
const chronoText = useRef(null)
|
||||
const [chronoState, setChronoState] = useState(0)
|
||||
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {t} = useTranslation("cm");
|
||||
@ -59,12 +61,15 @@ export function ChronoPanel() {
|
||||
|
||||
if (state_.chronoState === 0 && isRunning()) {
|
||||
state_.chronoState = 1
|
||||
setChronoState(1)
|
||||
} else if (state_.chronoState === 1 && getTime() >= config.time) {
|
||||
setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
|
||||
state_.chronoState = 2
|
||||
setChronoState(2)
|
||||
} else if (state_.chronoState === 2 && getTime() >= config.pause) {
|
||||
setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
|
||||
state_.chronoState = 1
|
||||
setChronoState(1)
|
||||
}
|
||||
|
||||
if (isRunning()) {
|
||||
@ -117,6 +122,7 @@ export function ChronoPanel() {
|
||||
<button className="btn btn-danger col" onClick={__ => {
|
||||
setChrono(prev => ({...prev, time: 0, startTime: 0}))
|
||||
state.current.chronoState = 0
|
||||
setChronoState(0)
|
||||
}}>{t('réinitialiser')}
|
||||
</button>
|
||||
</div>
|
||||
@ -181,5 +187,19 @@ export function ChronoPanel() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SendChrono chrono={chrono} config={config} chronoState={chronoState}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function SendChrono({chrono, config, chronoState}) {
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
setState({chronoState: {...chrono, configTime: config.time, configPause: config.pause, state: chronoState}});
|
||||
sendNotify("sendCurentChrono", {...chrono, configTime: config.time, configPause: config.pause, state: chronoState});
|
||||
}, [chrono]);
|
||||
|
||||
return <>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -191,12 +191,13 @@ function MatchList({matches, cat, menuActions}) {
|
||||
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1")
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {t} = useTranslation("cm");
|
||||
const {cards, getHeightCardForCombInMatch} = useCards();
|
||||
const {cards_v, getHeightCardForCombInMatch} = useCards();
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
const liceName = (cat.liceName || "N/A").split(";");
|
||||
const marches2 = matches.filter(m => m.categorie_ord !== -42)
|
||||
.sort((a, b) => a.categorie_ord - b.categorie_ord)
|
||||
.map(m => ({...m, ...win_end(m, Object.values(cards))}))
|
||||
.map(m => ({...m, ...win_end(m, cards_v), end: m.end}))
|
||||
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
|
||||
|
||||
const isActiveMatch = (index) => {
|
||||
@ -221,10 +222,6 @@ function MatchList({matches, cat, menuActions}) {
|
||||
});
|
||||
}
|
||||
}, [match]);
|
||||
//useEffect(() => {
|
||||
// if (activeMatch !== null)
|
||||
// setActiveMatch(null);
|
||||
//}, [cat])
|
||||
|
||||
useEffect(() => {
|
||||
if (match && match.poule !== lice)
|
||||
@ -240,6 +237,11 @@ function MatchList({matches, cat, menuActions}) {
|
||||
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
|
||||
}, [matches])
|
||||
|
||||
useEffect(() => {
|
||||
setState({selectedMatch: activeMatch});
|
||||
sendNotify("sendSelectMatch", activeMatch);
|
||||
}, [activeMatch]);
|
||||
|
||||
|
||||
const GetCard = ({combId, match, cat}) => {
|
||||
const c = getHeightCardForCombInMatch(combId, match)
|
||||
@ -329,6 +331,7 @@ function BuildTree({treeData, matches, menuActions}) {
|
||||
const {getComb} = useCombs()
|
||||
const publicAffDispatch = usePubAffDispatch();
|
||||
const {cards_v} = useCards();
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
const match = matches.find(m => m.id === currentMatch?.matchSelect)
|
||||
useEffect(() => {
|
||||
@ -390,6 +393,8 @@ function BuildTree({treeData, matches, menuActions}) {
|
||||
|
||||
const onMatchClick = (rect, matchId, __) => {
|
||||
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())});
|
||||
setState({selectedMatch: matchId});
|
||||
sendNotify("sendSelectMatch", matchId);
|
||||
}
|
||||
|
||||
const onClickVoid = () => {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
|
||||
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
|
||||
export function PointPanel({menuActions}) {
|
||||
const [revers, setRevers] = useState(false)
|
||||
@ -49,5 +50,18 @@ export function PointPanel({menuActions}) {
|
||||
<button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button>
|
||||
<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button>
|
||||
</div>
|
||||
<SendScore scoreRouge={scoreRouge} scoreBleu={scoreBleu}/>
|
||||
</div>
|
||||
}
|
||||
|
||||
function SendScore({scoreRouge, scoreBleu}) {
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
setState({scoreState: {scoreRouge, scoreBleu}});
|
||||
sendNotify("sendCurrentScore", {scoreRouge, scoreBleu});
|
||||
}, [scoreRouge, scoreBleu]);
|
||||
|
||||
return <>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -59,6 +59,7 @@ export function CMTable() {
|
||||
</div>
|
||||
<Menu menuActions={menuActions}/>
|
||||
<ObsAutoSyncWhitPubAff/>
|
||||
<SendCatId catId={catId}/>
|
||||
</div>
|
||||
</PubAffProvider>
|
||||
</OBSProvider>
|
||||
@ -73,6 +74,7 @@ function Menu({menuActions}) {
|
||||
const publicAffDispatch = usePubAffDispatch()
|
||||
const [showPubAff, setShowPubAff] = useState(false)
|
||||
const [showScore, setShowScore] = useState(true)
|
||||
const [zone, setZone] = useState(sessionStorage.getItem("liceName") || "???")
|
||||
const {connected, connect, disconnect} = useOBS();
|
||||
const longPress = useRef({time: null, timer: null, button: null});
|
||||
const obsModal = useRef(null);
|
||||
@ -124,7 +126,7 @@ function Menu({menuActions}) {
|
||||
|
||||
const longTimeAction = (button) => {
|
||||
if (button === "obs") {
|
||||
obsModal.current.click();
|
||||
// obsModal.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,12 +171,19 @@ function Menu({menuActions}) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleOBSSubmit = (e) => {
|
||||
const handleLiceSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const prefix = form[0].value;
|
||||
|
||||
sessionStorage.setItem("obs_prefix", prefix);
|
||||
if (prefix === "") {
|
||||
sessionStorage.removeItem("liceName");
|
||||
setZone("???");
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem("liceName", prefix);
|
||||
setZone(prefix);
|
||||
}
|
||||
|
||||
if (!e)
|
||||
@ -182,6 +191,8 @@ function Menu({menuActions}) {
|
||||
return <>
|
||||
{createPortal(
|
||||
<>
|
||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||
<span onClick={() => obsModal.current.click()} style={{cursor: "pointer"}}>Zone {zone}</span>
|
||||
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
|
||||
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
|
||||
onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top"
|
||||
@ -203,23 +214,23 @@ function Menu({menuActions}) {
|
||||
</>, document.getElementById("actionMenu"))}
|
||||
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
|
||||
|
||||
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
|
||||
Launch OBS Modal
|
||||
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#LiceNameModal"
|
||||
style={{display: 'none'}}>
|
||||
Launch Lice Name Modal
|
||||
</button>
|
||||
<div className="modal fade" id="OBSModal" tabIndex="-1" aria-labelledby="OBSModalLabel" aria-hidden="true">
|
||||
<div className="modal fade" id="LiceNameModal" tabIndex="-1" aria-labelledby="LiceNameModalLabel" aria-hidden="true">
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Configuration OBS</h5>
|
||||
<h5 className="modal-title">{t('configurationDuNomDeLaZone')}</h5>
|
||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form onSubmit={handleOBSSubmit}>
|
||||
<form onSubmit={handleLiceSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="input-group mb-3">
|
||||
<span className="input-group-text">{t('obs.préfixDesSources')}</span>
|
||||
<span className="input-group-text">sub</span>
|
||||
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1}
|
||||
defaultValue={localStorage.getItem("obs_prefix") || "1"} required/>
|
||||
<span className="input-group-text">{t('nomDeLaZone')}</span>
|
||||
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={0} maxLength={1}
|
||||
defaultValue={sessionStorage.getItem("liceName") || "1"}/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
@ -230,6 +241,40 @@ function Menu({menuActions}) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SendLiceName name={zone}/>
|
||||
</>
|
||||
}
|
||||
|
||||
function SendLiceName({name}) {
|
||||
const {sendNotify, setState} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
setState({liceName: name});
|
||||
sendNotify("sendLicenceName", name);
|
||||
}, [name]);
|
||||
|
||||
return <>
|
||||
</>
|
||||
}
|
||||
|
||||
function SendCatId({catId}) {
|
||||
const {sendNotify, setState, dispatch, tableState} = useWS();
|
||||
|
||||
useEffect(() => {
|
||||
const welcomeInfo = () => {
|
||||
sendNotify("sendState", tableState.current)
|
||||
}
|
||||
welcomeInfo();
|
||||
dispatch({type: 'addListener', payload: {callback: welcomeInfo, code: 'welcomeInfo'}})
|
||||
return () => dispatch({type: 'removeListener', payload: welcomeInfo});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setState({selectedCategory: catId});
|
||||
sendNotify("sendSelectCategory", catId);
|
||||
}, [catId]);
|
||||
|
||||
return <>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
211
src/main/webapp/src/pages/competition/editor/StateWindow.jsx
Normal file
211
src/main/webapp/src/pages/competition/editor/StateWindow.jsx
Normal file
@ -0,0 +1,211 @@
|
||||
import {useWS} from "../../../hooks/useWS.jsx";
|
||||
import {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
|
||||
import {from_sendTree} from "../../../utils/TreeUtils.js";
|
||||
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
|
||||
import {CombName, useCombsDispatch} from "../../../hooks/useComb.jsx";
|
||||
import {timePrint, win_end} from "../../../utils/Tools.js";
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'SET':
|
||||
return [...state.filter(s => s.id !== action.payload.id), action.payload]
|
||||
case 'SET_ALL':
|
||||
return action.payload
|
||||
case 'REMOVE':
|
||||
return state.filter(s => s.id !== action.payload)
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export function StateWindow({document}) {
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const [state, dispatchState] = useReducer(reducer, [])
|
||||
|
||||
const subscribeToState = () => {
|
||||
sendRequest("subscribeToState", true)
|
||||
.then((data) => {
|
||||
dispatchState({type: 'SET_ALL', payload: data});
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const sendStateFull = ({data}) => {
|
||||
dispatchState({type: 'SET', payload: data});
|
||||
}
|
||||
|
||||
const rmStateFull = ({data}) => {
|
||||
dispatchState({type: 'REMOVE', payload: data});
|
||||
}
|
||||
|
||||
const welcomeInfo = () => {
|
||||
subscribeToState();
|
||||
}
|
||||
subscribeToState();
|
||||
|
||||
dispatch({type: 'addListener', payload: {callback: welcomeInfo, code: 'welcomeInfo'}})
|
||||
dispatch({type: 'addListener', payload: {callback: sendStateFull, code: 'sendStateFull'}})
|
||||
dispatch({type: 'addListener', payload: {callback: rmStateFull, code: 'rmStateFull'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: welcomeInfo});
|
||||
dispatch({type: 'removeListener', payload: sendStateFull});
|
||||
dispatch({type: 'removeListener', payload: rmStateFull});
|
||||
|
||||
sendRequest("subscribeToState", false)
|
||||
.then(() => {
|
||||
});
|
||||
}
|
||||
}, [])
|
||||
|
||||
document.title = "État des tables de marque";
|
||||
document.body.className = "overflow-hidden";
|
||||
|
||||
console.log(state)
|
||||
return <>
|
||||
<div className="d-flex flex-row flex-wrap justify-content-around align-items-center align-content-around h-100 p-2 overflow-auto">
|
||||
{state.sort((a, b) => a.liceName.localeCompare(b.liceName)).map((table, index) =>
|
||||
<div key={index} className="card d-inline-flex flex-grow-1 align-self-stretch" style={{minWidth: "25em", maxWidth: "30em"}}>
|
||||
<ShowState table={table} dispatch={dispatchState}/>
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function readAndConvertMatch(matches, data, combsToAdd) {
|
||||
matches.push({
|
||||
...data,
|
||||
c1: data.c1?.id,
|
||||
c2: data.c2?.id,
|
||||
c1_cacheName: data.c1?.fname + " " + data.c1?.lname,
|
||||
c2_cacheName: data.c2?.fname + " " + data.c2?.lname
|
||||
})
|
||||
if (data.c1)
|
||||
combsToAdd.push(data.c1)
|
||||
if (data.c2)
|
||||
combsToAdd.push(data.c2)
|
||||
}
|
||||
|
||||
function ShowState({table}) {
|
||||
const cardDispatch = useCardsDispatch();
|
||||
const {sendRequest, dispatch} = useWS();
|
||||
const [matches, reducer] = useReducer(MarchReducer, []);
|
||||
const combDispatch = useCombsDispatch();
|
||||
const {cards_v} = useCards();
|
||||
|
||||
const [cat, setCat] = useState({id: -1, name: ""});
|
||||
|
||||
const marches2 = matches.filter(m => m.categorie === cat.id).map(m => ({...m, ...win_end(m, cards_v)}))
|
||||
|
||||
useEffect(() => {
|
||||
const categoryListener = ({data}) => {
|
||||
if (data.id !== cat.id)
|
||||
return;
|
||||
setCat({id: data.id, name: data.name});
|
||||
}
|
||||
|
||||
const matchListener = ({data: datas}) => {
|
||||
for (const data of datas) {
|
||||
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}});
|
||||
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}});
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMatch = ({data: datas}) => {
|
||||
for (const data of datas)
|
||||
reducer({type: 'REMOVE', payload: data});
|
||||
}
|
||||
|
||||
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
|
||||
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
|
||||
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
|
||||
return () => {
|
||||
dispatch({type: 'removeListener', payload: matchListener})
|
||||
dispatch({type: 'removeListener', payload: deleteMatch})
|
||||
dispatch({type: 'removeListener', payload: categoryListener})
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (table.selectedCategory !== cat.id) {
|
||||
if (!table.selectedCategory || table.selectedCategory === -1) {
|
||||
setCat({id: -1, name: ""});
|
||||
return;
|
||||
}
|
||||
sendRequest('getFullCategory', table.selectedCategory)
|
||||
.then((data) => {
|
||||
setCat({id: data.id, name: data.name});
|
||||
cardDispatch({type: 'SET_ALL', payload: data.cards});
|
||||
|
||||
let matches2 = [];
|
||||
let combsToAdd = [];
|
||||
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
||||
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
|
||||
|
||||
reducer({type: 'REPLACE_ALL', payload: matches2});
|
||||
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
|
||||
console.log(matches2);
|
||||
})
|
||||
}
|
||||
}, [table]);
|
||||
|
||||
return <>
|
||||
<div className="card-header">
|
||||
Zone de combat {table.liceName}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
Catégorie : {cat.name}<br/>
|
||||
Match terminés : {marches2.filter(m => m.end).length}/{marches2.length}<br/>
|
||||
Matchs : <PrintMatch match={matches.find(m => m.id === table.selectedMatch)}/><br/>
|
||||
Statue : {table?.state}<br/>
|
||||
Score : {table?.scoreState?.scoreRouge} - {table?.scoreState?.scoreBleu}<br/>
|
||||
Chronomètre : <PrintChrono chrono={table?.chronoState}/><br/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
function PrintMatch({match}) {
|
||||
return <>{match?.c1 && <CombName combId={match?.c1}/>} vs {match?.c2 && <CombName combId={match?.c2}/>}</>
|
||||
}
|
||||
|
||||
|
||||
function PrintChrono({chrono}) {
|
||||
const chronoText = useRef(null)
|
||||
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
|
||||
|
||||
const isRunning = () => chrono.startTime !== 0
|
||||
const getTime = () => {
|
||||
if (chrono.startTime === 0)
|
||||
return chrono.time
|
||||
return chrono.time + Date.now() - chrono.startTime
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!chrono || !chronoText.current)
|
||||
return;
|
||||
|
||||
const state_ = state.current
|
||||
const text_ = chronoText.current
|
||||
|
||||
const timer = setInterval(() => {
|
||||
let currentDuration = chrono.configTime
|
||||
if (chrono.state === 2) {
|
||||
currentDuration = (chrono.state === 0) ? 10000 : chrono.configPause
|
||||
}
|
||||
const timeStr = (chrono.state === 1 ? " Match - " : " Pause - ") + timePrint(currentDuration - getTime()) + (isRunning() ? "" : " (arrêté)")
|
||||
|
||||
if (timeStr !== state_.lastTimeStr) {
|
||||
text_.textContent = timeStr
|
||||
state_.lastTimeStr = timeStr
|
||||
}
|
||||
|
||||
if (chrono.chronoState === 0) {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, 50);
|
||||
return () => clearInterval(timer)
|
||||
}, [chrono])
|
||||
|
||||
return <><span ref={chronoText}>{state.current.lastTimeStr}</span></>
|
||||
}
|
||||
@ -272,7 +272,6 @@ export function win_end(match, cards = []) {
|
||||
export function virtual_end(match, cards) {
|
||||
if (!cards)
|
||||
return false
|
||||
console.log(cards.filter(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie)))
|
||||
return cards.some(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie))
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user