dev #106

Merged
Thibaut merged 8 commits from dev into master 2026-01-30 14:07:08 +00:00
15 changed files with 576 additions and 28 deletions
Showing only changes of commit d749dea6f4 - Show all commits

View File

@ -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);

View File

@ -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));
}

View File

@ -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();

View 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
}
}

View 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);
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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
}
}

View File

@ -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"))
})

View File

@ -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 <>
</>
}

View File

@ -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 = () => {

View File

@ -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 <>
</>
}

View File

@ -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 <>
</>
}

View 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></>
}

View File

@ -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))
}