diff --git a/pom.xml b/pom.xml index 2c0622e..de66203 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ UTF-8 quarkus-bom io.quarkus.platform - 3.16.4 + 3.30.5 true 3.2.3 @@ -68,7 +68,7 @@ io.quarkiverse.tika quarkus-tika - 2.0.4 + 2.3.0 @@ -127,7 +127,7 @@ org.apache.xmlgraphics fop - 2.6 + 2.11 io.quarkus diff --git a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java index 02c0f90..16691d3 100644 --- a/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java +++ b/src/main/java/fr/titionfire/ffsaf/data/model/RegisterModel.java @@ -66,4 +66,13 @@ public class RegisterModel { return membre.club; return club; } + + public Categorie getCategorie2() { + Categorie tmp = this.categorie; + if (tmp == null) + tmp = membre.getCategorie(); + if (tmp == null) + return null; + return Categorie.values()[Math.min(tmp.ordinal() + this.overCategory, Categorie.values().length - 1)]; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/HelloAssoTokenService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/HelloAssoTokenService.java index 253a071..1a5b131 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/HelloAssoTokenService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/HelloAssoTokenService.java @@ -4,7 +4,6 @@ import fr.titionfire.ffsaf.rest.client.HelloAssoAuthClient; import fr.titionfire.ffsaf.rest.client.dto.TokenResponse; import io.smallrye.mutiny.Uni; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.jboss.logging.Logger; @@ -13,7 +12,6 @@ import org.jboss.logging.Logger; public class HelloAssoTokenService { private static final Logger LOG = Logger.getLogger(HelloAssoTokenService.class); - @Inject @RestClient HelloAssoAuthClient authClient; diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/LoggerService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/LoggerService.java index 025c88b..5638a7f 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/LoggerService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/LoggerService.java @@ -58,6 +58,8 @@ public class LoggerService { } public Uni append() { + if (buffer.isEmpty()) + return Uni.createFrom().voidItem(); return Panache.withTransaction(() -> repository.persist(buffer)) .invoke(__ -> buffer.clear()); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index e0a9217..023b543 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -158,7 +158,7 @@ public class MembreService { "EXISTS (SELECT 1 FROM LicenceModel l WHERE l.membre.id = m.id AND l.saison >= %s)", Utils.getSaison() - 1); - String clubFilter = "?3 = ?3"; + String clubFilter = "(TRUE OR ?3 = ?3)"; if (club != null) { if (club instanceof String club_) { if (!club_.isBlank()) { @@ -270,14 +270,16 @@ public class MembreService { if (model.getEmail() != null && !model.getEmail().isBlank()) { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { LOGGER.info("Similar membres found: " + model); - throw new DBadRequestException("Email '" + model.getEmail() + "' déja utilisé"); + throw new DBadRequestException( + "Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname()); } if (StringSimilarity.similarity(model.getLname().toUpperCase(), dataIn.getNom().toUpperCase()) > 3 || StringSimilarity.similarity( model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { LOGGER.info("Similar membres found: " + model); - throw new DBadRequestException("Email '" + model.getEmail() + "' déja utilisé"); + throw new DBadRequestException( + "Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname()); } } @@ -288,7 +290,8 @@ public class MembreService { model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) { LOGGER.info("Similar membres found: " + model); throw new DBadRequestException( - "Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide."); + "Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (tentative de changement non-autotiser de nom sur la licence " + + model.getLicence() + " pour " + model.getFname() + " " + model.getFname() + ")"); } ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model); @@ -296,8 +299,7 @@ public class MembreService { dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1), model); model.setLname(dataIn.getNom().toUpperCase()); - model.setFname(dataIn.getPrenom().toUpperCase().charAt(0) + dataIn.getPrenom().substring(1)); - + model.setFname(Utils.formatPrenom(dataIn.getPrenom())); if (dataIn.getEmail() != null && !dataIn.getEmail().isBlank()) { ls.logChange("Email", model.getEmail(), dataIn.getEmail(), model); @@ -417,7 +419,7 @@ public class MembreService { private Uni update(Uni uni, FullMemberForm membre, boolean admin) { return uni.chain(target -> { ls.logChange("Prénom", target.getFname(), membre.getFname(), target); - target.setFname(membre.getFname()); + target.setFname(Utils.formatPrenom(membre.getFname())); ls.logChange("Nom", target.getLname(), membre.getLname(), target); target.setLname(membre.getLname().toUpperCase()); ls.logChange("Pays", target.getCountry(), membre.getCountry(), target); @@ -560,8 +562,8 @@ public class MembreService { private static MembreModel getMembreModel(FullMemberForm input, ClubModel clubModel) { MembreModel model = new MembreModel(); - model.setFname(input.getFname()); - model.setLname(input.getLname()); + model.setFname(Utils.formatPrenom(input.getFname())); + model.setLname(input.getLname().toUpperCase()); model.setEmail(input.getEmail()); model.setLicence(null); model.setGenre(input.getGenre()); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java index ecd48a4..e83009c 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -1,10 +1,7 @@ package fr.titionfire.ffsaf.domain.service; import fr.titionfire.ffsaf.data.model.*; -import fr.titionfire.ffsaf.data.repository.CategoryRepository; -import fr.titionfire.ffsaf.data.repository.CompetitionRepository; -import fr.titionfire.ffsaf.data.repository.MatchRepository; -import fr.titionfire.ffsaf.data.repository.RegisterRepository; +import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.rest.data.ResultCategoryData; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.utils.*; @@ -19,7 +16,9 @@ import lombok.Builder; import org.hibernate.reactive.mutiny.Mutiny; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Stream; @WithSession @@ -35,6 +34,12 @@ public class ResultService { @Inject MembreService membreService; + @Inject + ClubRepository clubRepository; + + @Inject + CompetitionGuestRepository competitionGuestRepository; + @Inject CategoryRepository categoryRepository; @@ -53,10 +58,22 @@ public class ResultService { .collect().asList(); } + public Uni> getCategoryList(String uuid) { + return categoryRepository.list("compet.uuid = ?1", uuid) + .map(categoryModels -> { + HashMap map = new HashMap<>(); + categoryModels.stream() + .sorted(Comparator.comparing(CategoryModel::getName)) + .forEachOrdered(categoryModel -> map.put(categoryModel.getName(), categoryModel.getId())); + return map; + }); + } + public Uni> getCategory(String uuid, SecurityCtx securityCtx) { return hasAccess(uuid, securityCtx) .chain(m -> categoryRepository.list("compet.uuid = ?1", uuid) - .chain(cats -> matchRepository.list("(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True + .chain(cats -> matchRepository.list( + "(c1_id = ?1 OR c2_id = ?1 OR True) AND category IN ?2", //TODO rm OR True m.getMembre(), cats))) .map(matchModels -> { HashMap> map = new HashMap<>(); @@ -82,6 +99,8 @@ public class ResultService { CategoryModel categoryModel = matchModels.get(0).getCategory(); out.setName(categoryModel.getName()); out.setType(categoryModel.getType()); + out.setLiceName(categoryModel.getLiceName() == null ? new String[]{} : categoryModel.getLiceName().split(";")); + out.setGenTime(System.currentTimeMillis()); getArray2(matchModels, out); getTree(categoryModel.getTree(), out); @@ -173,6 +192,12 @@ public class ResultService { } } + public Uni getCategoryPublic(String uuid, long poule) { + return matchRepository.list("category.compet.uuid = ?1 AND category.id = ?2", uuid, poule) + .call(list -> Mutiny.fetch(list.get(0).getCategory().getTree())) + .map(this::getData); + } + private void getTree(List treeModels, ResultCategoryData out) { ArrayList> trees = new ArrayList<>(); treeModels.stream().filter(t -> t.getLevel() != 0).forEach(treeModel -> { @@ -185,87 +210,230 @@ public class ResultService { public Uni getAllCombArray(String uuid, SecurityCtx securityCtx) { return hasAccess(uuid, securityCtx) - .chain(cm_register -> registerRepository.list("competition.uuid = ?1", uuid) - .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) - .map(matchModels -> new Pair<>(registers, matchModels))) - .map(pair -> { - List registers = pair.getKey(); - List matchModels = pair.getValue(); + .chain(__ -> getAllCombArray(uuid)); + } - CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); + public Uni getAllCombArrayPublic(String uuid) { + return getAllCombArray(uuid); + } - List combs = matchModels.stream() - .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) - .filter(Objects::nonNull) - .distinct() - .map(combName -> { - var builder2 = CombsArrayData.CombsData.builder(); - AtomicInteger w = new AtomicInteger(0); - AtomicInteger l = new AtomicInteger(0); - AtomicInteger pointMake = new AtomicInteger(); - AtomicInteger pointTake = new AtomicInteger(); + public Uni getAllCombArray(String uuid) { + return registerRepository.list("competition.uuid = ?1", uuid) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); - matchModels.stream() - .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) - || m.getC2Name().equals(combName))) - .forEach(matchModel -> { - int win = matchModel.win(); - if ((combName.equals(matchModel.getC1Name()) && win > 0) || - combName.equals(matchModel.getC2Name()) && win < 0) { - w.getAndIncrement(); - } else { - l.getAndIncrement(); - } + CombsArrayData.CombsArrayDataBuilder builder = CombsArrayData.builder(); - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) - .forEach(score -> { - if (combName.equals(matchModel.getC1Name())) { - pointMake.addAndGet(score.getS1()); - pointTake.addAndGet(score.getS2()); - } else { - pointMake.addAndGet(score.getS2()); - pointTake.addAndGet(score.getS1()); - } - }); - }); + List combs = matchModels.stream() + .flatMap(m -> Stream.of(m.getC1Name(), m.getC2Name())) + .filter(Objects::nonNull) + .distinct() + .map(combName -> { + var builder2 = CombsArrayData.CombsData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); - Categorie categorie = null; - ClubModel club = null; + matchModels.stream() + .filter(m -> m.isEnd() && (m.getC1Name().equals(combName) + || m.getC2Name().equals(combName))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((combName.equals(matchModel.getC1Name()) && win > 0) || + combName.equals(matchModel.getC2Name()) && win < 0) { + w.getAndIncrement(); + } else { + l.getAndIncrement(); + } - Optional register = registers.stream() - .filter(r -> r.getName().equals(combName)).findFirst(); - if (register.isPresent()) { - categorie = register.get().getCategorie(); - club = register.get().getClub2(); - } + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(score -> { + if (combName.equals(matchModel.getC1Name())) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + }); + }); - builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); - builder2.name(combName); - builder2.w(w.get()); - builder2.l(l.get()); - builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); - builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName()); - builder2.pointMake(pointMake.get()); - builder2.pointTake(pointTake.get()); - builder2.ratioPoint( - (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + Categorie categorie = null; + ClubModel club = null; - return builder2.build(); - }) - .sorted(Comparator.comparing(CombsArrayData.CombsData::name)) - .toList(); + Optional register = registers.stream() + .filter(r -> r.getName().equals(combName)).findFirst(); + if (register.isPresent()) { + categorie = register.get().getCategorie(); + club = register.get().getClub2(); + } - builder.nb_insc(combs.size()); - builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); - builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum()); - builder.combs(combs); + builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.name(combName); + builder2.w(w.get()); + builder2.l(l.get()); + builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); + builder2.club((club == null) ? BUNDLE.getString("no.licence") : club.getName()); + builder2.pointMake(pointMake.get()); + builder2.pointTake(pointTake.get()); + builder2.ratioPoint( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); - return builder.build(); + return builder2.build(); + }) + .sorted(Comparator.comparing(CombsArrayData.CombsData::name)) + .toList(); + + builder.nb_insc(combs.size()); + builder.tt_match((int) matchModels.stream().filter(MatchModel::isEnd).count()); + builder.point(combs.stream().mapToInt(CombsArrayData.CombsData::pointMake).sum()); + builder.combs(combs); + + return builder.build(); + }); + } + + public Uni> getCombList(String uuid) { + return registerRepository.list("competition.uuid = ?1", uuid) + .map(models -> { + HashMap map = new HashMap<>(); + models.forEach(registerEmbeddable -> { + map.put(Utils.getFullName(registerEmbeddable.getMembre()), + registerEmbeddable.getMembre().getFname() + "¤" + registerEmbeddable.getMembre() + .getLname()); + }); + return map; + }) + .chain(map -> competitionGuestRepository.list("competition.uuid = ?1", uuid) + .map(models -> { + models.forEach(guestModel -> map.put(Utils.getFullName(guestModel), + guestModel.getFname() + "¤" + guestModel.getLname())); + return map; }) ); } + public Uni getCombArrayPublic(String uuid, String fname, String lname) { + CombArrayData.CombArrayDataBuilder builder = CombArrayData.builder(); + AtomicBoolean guest = new AtomicBoolean(false); + AtomicLong id = new AtomicLong(0); + + + return registerRepository.find("membre.fname = ?1 AND membre.lname = ?2 AND competition.uuid = ?3", fname, + lname, uuid).firstResult() + .chain(registerModel -> { + if (registerModel == null) { + return competitionGuestRepository.find("fname = ?1 AND lname = ?2 AND competition.uuid = ?3", + fname, lname, uuid).firstResult() + .chain(guestModel -> { + builder.name(Utils.getFullName(guestModel)); + builder.club(guestModel.getClub()); + guest.set(true); + id.set(guestModel.getId()); + builder.cat((guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie() + .getName(BUNDLE)); + + return matchRepository.list( + "category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)", uuid, + guestModel); + }); + } + builder.name(Utils.getFullName(registerModel.getMembre())); + builder.club((registerModel.getClub2() == null) ? BUNDLE.getString( + "no.licence") : registerModel.getClub2().getName()); + id.set(registerModel.getMembre().getId()); + builder.cat((registerModel.getCategorie2() == null) ? "---" : registerModel.getCategorie2() + .getName(BUNDLE)); + + return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid, + registerModel.getMembre()); + }) + .invoke(matchModels -> { + List pouleModels = matchModels.stream().map(MatchModel::getCategory).distinct() + .toList(); + List matchs = new ArrayList<>(); + + AtomicInteger sumW = new AtomicInteger(); + AtomicInteger sumPointMake = new AtomicInteger(0); + AtomicInteger sumPointTake = new AtomicInteger(0); + + for (MatchModel matchModel : matchModels) { + if ((matchModel.getC1_id() == null && matchModel.getC1_guest() == null) || + (matchModel.getC2_id() == null && matchModel.getC2_guest() == null)) + continue; + + var builder2 = CombArrayData.MatchsData.builder(); + builder2.date(matchModel.getDate()); + builder2.poule(pouleModels.stream().filter(p -> p.equals(matchModel.getCategory())) + .map(CategoryModel::getName).findFirst().orElse("")); + + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); + + if ((!guest.get() && matchModel.getC1_id() != null && matchModel.getC1_id().getId() == id.get()) + || (guest.get() && matchModel.getC1_guest() != null && matchModel.getC1_guest() + .getId() == id.get())) { + builder2.adv(Utils.getFullName(matchModel.getC2_id(), matchModel.getC2_guest())); + if (matchModel.isEnd()) { + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(scoreEntity -> { + pointMake.addAndGet(scoreEntity.getS1()); + pointTake.addAndGet(scoreEntity.getS2()); + }); + builder2.score(matchModel.getScores().stream() + .map(s -> new Integer[]{s.getS1(), s.getS2()}).toList()); + } else { + builder2.score(new ArrayList<>()); + } + builder2.win(matchModel.win() > 0); + } else { + builder2.adv(Utils.getFullName(matchModel.getC1_id(), matchModel.getC1_guest())); + if (matchModel.isEnd()) { + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(scoreEntity -> { + pointMake.addAndGet(scoreEntity.getS2()); + pointTake.addAndGet(scoreEntity.getS1()); + }); + builder2.score(matchModel.getScores().stream() + .map(s -> new Integer[]{s.getS2(), s.getS1()}).toList()); + } else { + builder2.score(new ArrayList<>()); + } + builder2.win(matchModel.win() < 0); + } + + builder2.ratio( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + + sumPointMake.addAndGet(pointMake.get()); + sumPointTake.addAndGet(pointTake.get()); + + matchs.add(builder2.build()); + if (builder2.win) + sumW.getAndIncrement(); + } + + builder.totalWin(sumW.get()); + builder.pointRatio( + (sumPointTake.get() == 0) ? sumPointMake.get() : (float) sumPointMake.get() / sumPointTake.get()); + builder.pointMake(sumPointMake.get()); + builder.pointTake(sumPointTake.get()); + + matchs.sort(Comparator.comparing(CombArrayData.MatchsData::poule) + .thenComparing(CombArrayData.MatchsData::adv)); + + builder.matchs(matchs); + }) + .map(__ -> builder.build()); + } + @Builder @RegisterForReflection @@ -277,96 +445,117 @@ public class ResultService { } } + @Builder + @RegisterForReflection + public static record CombArrayData(String name, String club, String cat, int totalWin, + float pointRatio, int pointMake, int pointTake, List matchs) { + @Builder + @RegisterForReflection + public static record MatchsData(Date date, String poule, String adv, List score, float ratio, + boolean win) { + } + } + + public Uni> getClubList(String uuid) { // TODO add guest club + return registerRepository.list("competition.uuid = ?1", uuid) + .map(registers -> { + HashMap registerMap = new HashMap<>(); + registers.stream().map(RegisterModel::getClub2).distinct().filter(Objects::nonNull) + .forEach(registerClub -> registerMap.put(registerClub.getName(), registerClub.getId())); + return registerMap; + }); + } + public Uni getClubArray(String uuid, SecurityCtx securityCtx) { + return hasAccess(uuid, securityCtx).chain(cm_register -> getClubArray(uuid, cm_register.getClub2())); + } + + public Uni getClubArrayPublic(String uuid, Long id) { + return clubRepository.findById(id).chain(clubModel -> getClubArray(uuid, clubModel)); + } + + public Uni getClubArray(String uuid, ClubModel clubModel) { ClubArrayData.ClubArrayDataBuilder builder = ClubArrayData.builder(); + builder.name(clubModel.getName()); - return hasAccess(uuid, securityCtx) - .invoke(cm_register -> builder.name(cm_register.getClub2().getName())) - .chain(cm_register -> registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, - cm_register.getClub2()) - .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) - .map(matchModels -> new Pair<>(registers, matchModels))) - .map(pair -> { - List registers = pair.getKey(); - List matchModels = pair.getValue(); + return registerRepository.list("competition.uuid = ?1 AND membre.club = ?2", uuid, clubModel) + .chain(registers -> matchRepository.list("category.compet.uuid = ?1", uuid) + .map(matchModels -> new Pair<>(registers, matchModels))) + .map(pair -> { + List registers = pair.getKey(); + List matchModels = pair.getValue(); - builder.nb_insc(registers.size()); + builder.nb_insc(registers.size()); - AtomicInteger tt_win = new AtomicInteger(0); - AtomicInteger tt_match = new AtomicInteger(0); + AtomicInteger tt_win = new AtomicInteger(0); + AtomicInteger tt_match = new AtomicInteger(0); - List combData = registers.stream() - .map(register -> { + List combData = registers.stream().map(register -> { + var builder2 = ClubArrayData.CombData.builder(); + AtomicInteger w = new AtomicInteger(0); + AtomicInteger l = new AtomicInteger(0); + AtomicInteger pointMake = new AtomicInteger(); + AtomicInteger pointTake = new AtomicInteger(); - var builder2 = ClubArrayData.CombData.builder(); - AtomicInteger w = new AtomicInteger(0); - AtomicInteger l = new AtomicInteger(0); - AtomicInteger pointMake = new AtomicInteger(); - AtomicInteger pointTake = new AtomicInteger(); + matchModels.stream() + .filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id()) + || register.getMembre().equals(m.getC2_id()))) + .forEach(matchModel -> { + int win = matchModel.win(); + if ((register.getMembre().equals(matchModel.getC1_id()) && win > 0) || + register.getMembre().equals(matchModel.getC2_id()) && win < 0) { + w.getAndIncrement(); + } else { + l.getAndIncrement(); + } - matchModels.stream() - .filter(m -> m.isEnd() && (register.getMembre().equals(m.getC1_id()) - || register.getMembre().equals(m.getC2_id()))) - .forEach(matchModel -> { - int win = matchModel.win(); - if ((register.getMembre() - .equals(matchModel.getC1_id()) && win > 0) || - register.getMembre() - .equals(matchModel.getC2_id()) && win < 0) { - w.getAndIncrement(); - } else { - l.getAndIncrement(); - } + matchModel.getScores().stream() + .filter(s -> s.getS1() > -900 && s.getS2() > -900) + .forEach(score -> { + if (register.getMembre().equals(matchModel.getC1_id())) { + pointMake.addAndGet(score.getS1()); + pointTake.addAndGet(score.getS2()); + } else { + pointMake.addAndGet(score.getS2()); + pointTake.addAndGet(score.getS1()); + } + }); + }); - matchModel.getScores().stream() - .filter(s -> s.getS1() > -900 && s.getS2() > -900) - .forEach(score -> { - if (register.getMembre() - .equals(matchModel.getC1_id())) { - pointMake.addAndGet(score.getS1()); - pointTake.addAndGet(score.getS2()); - } else { - pointMake.addAndGet(score.getS2()); - pointTake.addAndGet(score.getS1()); - } - }); - }); + Categorie categorie = register.getCategorie(); + if (categorie == null) + categorie = register.getMembre().getCategorie(); - Categorie categorie = register.getCategorie(); - if (categorie == null) - categorie = register.getMembre().getCategorie(); + builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.name(register.getName()); + builder2.w(w.get()); + builder2.l(l.get()); + builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); + builder2.pointMake(pointMake.get()); + builder2.pointTake(pointTake.get()); + builder2.ratioPoint( + (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); - builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); - builder2.name(register.getName()); - builder2.w(w.get()); - builder2.l(l.get()); - builder2.ratioVictoire((l.get() == 0) ? w.get() : (float) w.get() / l.get()); - builder2.pointMake(pointMake.get()); - builder2.pointTake(pointTake.get()); - builder2.ratioPoint( - (pointTake.get() == 0) ? pointMake.get() : (float) pointMake.get() / pointTake.get()); + tt_win.addAndGet(w.get()); + tt_match.addAndGet(w.get() + l.get()); - tt_win.addAndGet(w.get()); - tt_match.addAndGet(w.get() + l.get()); + return builder2.build(); + }) + .sorted(Comparator.comparing(ClubArrayData.CombData::name)) + .toList(); - return builder2.build(); - }) - .sorted(Comparator.comparing(ClubArrayData.CombData::name)) - .toList(); + builder.nb_match(tt_match.get()); + builder.match_w(tt_win.get()); + builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0) + .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L)); + builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum()); + builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum()); + builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0) + .mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L)); + builder.combs(combData); - builder.nb_match(tt_match.get()); - builder.match_w(tt_win.get()); - builder.ratioVictoire((float) combData.stream().filter(c -> c.l + c.w != 0) - .mapToDouble(ClubArrayData.CombData::ratioVictoire).average().orElse(0L)); - builder.pointMake(combData.stream().mapToInt(ClubArrayData.CombData::pointMake).sum()); - builder.pointTake(combData.stream().mapToInt(ClubArrayData.CombData::pointTake).sum()); - builder.ratioPoint((float) combData.stream().filter(c -> c.l + c.w != 0) - .mapToDouble(ClubArrayData.CombData::ratioPoint).average().orElse(0L)); - builder.combs(combData); - - return builder.build(); - }) - ); + return builder.build(); + }); } @Builder diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java new file mode 100644 index 0000000..c04c643 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/UpdateService.java @@ -0,0 +1,23 @@ +package fr.titionfire.ffsaf.domain.service; + +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.HashMap; + +@ApplicationScoped +public class UpdateService { // For public result page + static HashMap lastUpdate = new HashMap<>(); + + public void setNewData(long id) { + lastUpdate.put(id, System.currentTimeMillis()); + } + + public boolean needUpdate(long id, long last_update) { + if (!lastUpdate.containsKey(id)) { + lastUpdate.put(id, System.currentTimeMillis() - 5000); + return true; + } + + return lastUpdate.getOrDefault(id, 0L) > last_update; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java new file mode 100644 index 0000000..aee3914 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/rest/ExternalResultEndpoints.java @@ -0,0 +1,85 @@ +package fr.titionfire.ffsaf.rest; + +import fr.titionfire.ffsaf.domain.service.ResultService; +import fr.titionfire.ffsaf.domain.service.UpdateService; +import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +import java.util.HashMap; + +@Path("api/public/result/{id}") +public class ExternalResultEndpoints { + + @Inject + ResultService resultService; + + @Inject + UpdateService updateService; + + @PathParam("id") + private String id; + + @GET + @Path("/poule/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> list() { + return resultService.getCategoryList(id); + } + + @GET + @Path("/poule/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getArray(@QueryParam("poule") long poule, @DefaultValue("-1") @QueryParam("rf") long rf) { + if (poule == 0) + return Uni.createFrom().voidItem(); + + if (updateService.needUpdate(poule, rf)) { + return resultService.getCategoryPublic(id, poule); + } else { + return Uni.createFrom().voidItem(); + } + } + + @GET + @Path("/comb/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> combList() { + return resultService.getCombList(id); + } + + @GET + @Path("/comb/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getArray(@QueryParam("comb") String comb) { + if (comb.equals("0")) + return Uni.createFrom().item(""); + return resultService.getCombArrayPublic(id, comb.substring(0, comb.indexOf('¤')), comb.substring(comb.indexOf('¤') + 1)); + } + + @GET + @Path("/comb/get_all") + @Produces(MediaType.APPLICATION_JSON) + public Uni getAll() { + return resultService.getAllCombArrayPublic(id); + } + + + + @GET + @Path("/club/list") + @Produces(MediaType.APPLICATION_JSON) + public Uni> clubList() { + return resultService.getClubList(id); + } + + @GET + @Path("/club/data") + @Produces(MediaType.APPLICATION_JSON) + public Uni getClubArray(@QueryParam("club") long club) { + if (club == 0) + return Uni.createFrom().item(""); + return resultService.getClubArrayPublic(id, club); + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java index 2054e52..3ff949f 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/ResultCategoryData.java @@ -21,6 +21,8 @@ public class ResultCategoryData { HashMap> matchs = new HashMap<>(); HashMap> rankArray = new HashMap<>(); ArrayList> trees; + String[] liceName; + long genTime; @Data @AllArgsConstructor diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java index c3cdd73..62e720b 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Utils.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Utils.java @@ -1,5 +1,7 @@ package fr.titionfire.ffsaf.utils; +import fr.titionfire.ffsaf.data.model.CompetitionGuestModel; +import fr.titionfire.ffsaf.data.model.MembreModel; import io.smallrye.mutiny.Uni; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; @@ -335,4 +337,46 @@ public class Utils { return (int) ((calendar.getTimeInMillis() - now.getTimeInMillis()) / (1000 * 60 * 60 * 24)); } + + public static String formatPrenom(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + StringBuilder result = new StringBuilder(); + String[] mots = input.split(" "); + + for (String mot : mots) { + if (!mot.isEmpty()) { + String[] parties = mot.split("[-']"); + StringBuilder motFormate = new StringBuilder(); + + for (int i = 0; i < parties.length; i++) { + if (!parties[i].isEmpty()) { + String premiereLettre = parties[i].substring(0, 1).toUpperCase(); + String reste = parties[i].substring(1).toLowerCase(); + motFormate.append(premiereLettre).append(reste); + } + + if (i < parties.length - 1) { + motFormate.append(mot.charAt(mot.indexOf(parties[i]) + parties[i].length())); + } + } + result.append(motFormate).append(" "); + } + } + return result.toString().trim(); + } + + public static String getFullName(Object ...models) { + for (Object model : models){ + if (model == null) + continue; + if (model instanceof MembreModel membreModel) + return membreModel.getFname() + " " + membreModel.getLname(); + if (model instanceof CompetitionGuestModel guestModel) + return guestModel.getFname() + " " + guestModel.getLname(); + } + return ""; + } } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index c2256d2..9e3b5d3 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -21,6 +21,7 @@ import org.jboss.logging.Logger; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.time.Duration; import java.util.*; import static fr.titionfire.ffsaf.net2.Client_Thread.MAPPER; @@ -166,6 +167,7 @@ public class CompetitionWS { return Uni.createFrom().item(makeError(message, "Permission denied")).toMulti(); return ((Uni) method.invoke(entry.getValue(), connection, MAPPER.treeToValue(message.data(), method.getParameterTypes()[1]))) + .ifNoItem().after(Duration.ofSeconds(5)).fail() .map(o -> makeReply(message, o)) .onFailure() .recoverWithItem(t -> { diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java index 816cbea..317ff0d 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -118,7 +118,8 @@ public class RCategorie { uni = uni.chain(__ -> treeRepository.delete("category = ?1", cat.getId())) .chain(__ -> matchRepository.delete("category = ?1 AND category_ord = -42", cat)); } - return uni; + Uni finalUni = uni; + return Panache.withTransaction(() -> finalUni); }) .call(cat -> SSCategorie.sendCategory(connection, cat)) .replaceWithVoid(); @@ -205,6 +206,16 @@ public class RCategorie { .replaceWithVoid(); } + @WSReceiver(code = "deleteCategory", permission = PermLevel.ADMIN) + public Uni deleteCategory(WebSocketConnection connection, Long id) { + return getById(id, connection) + .call(cat -> Panache.withTransaction(() -> treeRepository.delete("category = ?1", cat.getId()) + .call(__ -> matchRepository.delete("category = ?1", cat)))) + .chain(cat -> Panache.withTransaction(() -> categoryRepository.delete(cat))) + .call(__ -> SSCategorie.sendDelCategory(connection, id)) + .replaceWithVoid(); + } + @RegisterForReflection public record JustCategorie(long id, String name, int type, String liceName) { public static JustCategorie from(CategoryModel m) { diff --git a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCategorie.java index 23162b9..e981080 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/send/SSCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/send/SSCategorie.java @@ -30,4 +30,8 @@ public class SSCategorie { public static Uni sendTreeCategory(WebSocketConnection connection, List treeEntities) { return CompetitionWS.sendNotifyToOtherEditor(connection, "sendTreeCategory", treeEntities); } + + public static Uni sendDelCategory(WebSocketConnection connection, Long id) { + return CompetitionWS.sendNotifyToOtherEditor(connection, "sendDelCategory", id); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0d7d3e6..5ffd4f2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -75,6 +75,4 @@ helloasso.client-id=changeme helloasso.client-secret=changeme quarkus.rest-client.helloasso-auth.url=${helloasso.api}/oauth2 -quarkus.rest-client.helloasso-auth.scope=javax.inject.Singleton - quarkus.rest-client.helloasso-api.url=${helloasso.api}/v5 diff --git a/src/main/webapp/.gitignore b/src/main/webapp/.gitignore index a547bf3..a461aa8 100644 --- a/src/main/webapp/.gitignore +++ b/src/main/webapp/.gitignore @@ -22,3 +22,4 @@ dist-ssr *.njsproj *.sln *.sw? +/public/result_test.html diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index 67a1ffd..c8d23ff 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -18,7 +18,9 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "jszip": "^3.10.1", "leaflet": "^1.9.4", + "obs-websocket-js": "^5.0.7", "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1058,6 +1060,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@msgpack/msgpack": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", + "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==", + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1845,6 +1855,11 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -1870,6 +1885,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -2007,7 +2027,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -2980,6 +2999,11 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3018,8 +3042,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.6", @@ -3360,6 +3383,14 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/iterator.prototype": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", @@ -3447,6 +3478,17 @@ "node": ">=4.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3474,6 +3516,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3559,8 +3609,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -3696,6 +3745,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obs-websocket-js": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-5.0.7.tgz", + "integrity": "sha512-SdSNSyrLVR6F0ogInKr7qcadV1tYaTUse/vbabxjkUL8hU3P3dyifxkZ7pEkDDrtCp3TkQ53Enx23kgZO0Cjcw==", + "dependencies": { + "@msgpack/msgpack": "^2.7.1", + "crypto-js": "^4.1.1", + "debug": "^4.3.2", + "eventemitter3": "^5.0.1", + "isomorphic-ws": "^5.0.0", + "type-fest": "^3.11.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">16.0" + } + }, + "node_modules/obs-websocket-js/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/obs-websocket-js/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3752,6 +3834,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3855,6 +3942,11 @@ "node": ">=0.8" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/proj4": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.11.0.tgz", @@ -4055,6 +4147,25 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/recharts": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", @@ -4256,6 +4367,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/safe-regex-test": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", @@ -4319,6 +4435,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", @@ -4378,6 +4499,14 @@ "node": ">=0.8" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -4723,6 +4852,11 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/uzip": { "version": "0.20201231.0", "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", @@ -4922,6 +5056,26 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 0e57209..418755f 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -20,7 +20,9 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "jszip": "^3.10.1", "leaflet": "^1.9.4", + "obs-websocket-js": "^5.0.7", "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/main/webapp/public/competition.js b/src/main/webapp/public/competition.js new file mode 100644 index 0000000..53ddd7b --- /dev/null +++ b/src/main/webapp/public/competition.js @@ -0,0 +1,946 @@ +let apiUrlRoot = ""; +const rootDiv = document.getElementById("safca_api_data"); + +const header = `

Résultat de la compétition :

` +const backButton = `Retour` +const cupImg = `` + +const voidFunction = () => { +} +let lastRf = 0; +let rfFonction = voidFunction; + +setInterval(() => { + rfFonction(); +}, 15000); + +function setSubPage(name) { + window.location.hash = name; + const location = name.split('/'); + console.log(location); + + switch (location[0]) { + case 'home': + homePage(); + break; + case 'poule': + poulePage(location); + break; + case 'comb': + combPage(location); + break; + case 'club': + clubPage(location); + break; + case 'all': + combsPage(); + break; + } +} + +function homePage() { + rootDiv.innerHTML = header; + + let content = document.createElement('div'); + content.innerHTML = ` + + ` + rootDiv.append(content) +} + +let loadingAnimationStep = 0; + +function startLoading(root) { + const id = "loading" + Math.random().toString(36); + let element = document.createElement('h2'); + element.id = id; + + const anim = setInterval(() => { + let str = "Chargement"; + for (let i = 0; i < loadingAnimationStep; i++) { + str += "."; + } + loadingAnimationStep = (loadingAnimationStep + 1) % 4; + element.innerText = str; + }, 500); + + root.append(element) + return {interval: anim, root: root, element: element}; +} + +function stopLoading(loading) { + clearInterval(loading['interval']); + loading['root'].removeChild(loading['element']); +} + +function scoreToString(score) { + const scorePrint = (s1) => { + switch (s1) { + case -997: + return "disc."; + case -998: + return "abs."; + case -999: + return "for."; + case -1000: + return ""; + default: + return String(s1); + } + } + + return score.map(o => scorePrint(o.at(0)) + "-" + scorePrint(o.at(1))).join(" | "); +} + +function dateToString(date) { + if (date === null || date === undefined) + return ""; + + const date_ = new Date(date); + const date_2 = new Date(date); + const current = new Date(); + current.setHours(0, 0, 0, 0); + date_2.setHours(0, 0, 0, 0); + + let d = Math.floor((current - date_2) / (1000 * 60 * 60 * 24)); + if (d === 0) + return "Aujourd'hui à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else if (d === 1) + return "Hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else if (d === 2) + return "Avant-hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + else + return date_.toLocaleDateString(); +} + +function buildPouleMenu(isPoule, change_view) { + const menuDiv = document.createElement('div'); + menuDiv.id = 'menu'; + menuDiv.style.borderBottom = '1px solid #9EA0A1'; + menuDiv.style.paddingBottom = '25px'; + + const ul = document.createElement('ul'); + ul.id = 'onglets'; + ul.style.position = 'absolute'; + ul.style.border = '1px solid transparent'; + ul.style.padding = '0'; + ul.style.font = 'bold 11px Batang, arial, serif'; + ul.style.listStyleType = 'none'; + ul.style.left = '50%'; + ul.style.marginTop = '0'; + ul.style.width = '430px'; + ul.style.marginLeft = '-215px'; + + function createTab(text, isActive, onClickHandler) { + const li = document.createElement('li'); + if (isActive) { + li.className = 'active'; + li.style.borderBottom = '1px solid #fff'; + li.style.backgroundColor = '#fff'; + } else { + li.style.backgroundColor = '#F4F9FD'; + } + li.style.float = 'left'; + li.style.height = '21px'; + li.style.margin = '2px 2px 0 2px !important'; + li.style.margin = '1px 2px 0 2px'; + li.style.border = '1px solid #9EA0A1'; + + const a = document.createElement('a'); + a.href = 'javascript:void(0);'; + a.onclick = onClickHandler; + a.textContent = text; + a.style.display = 'block'; + a.style.color = '#666'; + a.style.textDecoration = 'none'; + a.style.padding = '4px'; + + a.addEventListener('mouseover', function () { + a.style.background = '#fff'; + }); + + a.addEventListener('mouseout', function () { + if (!isActive) + a.style.background = '#F4F9FD'; + }); + + li.appendChild(a); + return li; + } + + const li1 = createTab('Poule', isPoule, function () { + change_view(true); + }); + ul.appendChild(li1); + const li2 = createTab('Tournois', !isPoule, function () { + change_view(false); + }); + ul.appendChild(li2); + + menuDiv.appendChild(ul); + + return menuDiv; +} + +function buildMatchArray(matchs) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +
+ + + + + + + + + + ` + for (const match of matchs) { + arrayContent += ` + + + + + + + + ` + } + arrayContent += `
RougeScoresBleuDate
${match.red}${match.red_w ? cupImg : ""}${scoreToString(match.score)}${match.blue_w ? cupImg : ""}${match.blue}${dateToString((match.red_w || match.blue_w) ? match.date : null)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function buildRankArray(rankArray) { + const arrayDiv = document.createElement('div'); + let arrayContent = `
+ + + + + + + + + + + ` + for (const row of rankArray) { + arrayContent += ` + + + + + + + + ` + + } + arrayContent += `
PlaceNomVictoireRatioPoints marquésPoints reçus
${row.rank}${row.name}${row.win}${row.pointRate.toFixed(3)}${row.pointMake}${row.pointTake}
` + arrayDiv.innerHTML = arrayContent; + return arrayDiv; +} + +function buildTree(treeData) { + return drawGraph(initTree(treeData)) +} + +function poulePage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par poule

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + let currentPoule = 0; + const loadPoule = () => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/poule/data?poule=${currentPoule}&rf=${lastRf}`) + .then(response => { + if (response.status === 204) // No content => no update + return; + + response.json().then(poule => { + lastRf = (poule.genTime !== undefined) ? poule.genTime : 0; + // console.log(poule); + + if (location.length === 3 && poule.type < 3) { + location.pop(); + window.location.hash = location.join('/'); + } + + dataContainer.replaceChildren(); + + if (poule.type === 1) { + for (const g in poule.matchs) { + if (Object.keys(poule.matchs).length > 1) { + const text = document.createElement('h4'); + text.textContent = `Groupe ${g}`; + text.style.marginTop = '2em'; + dataContainer.append(text); + } + dataContainer.append(buildMatchArray(poule.matchs[g])); + dataContainer.append(buildRankArray(poule.rankArray[g])); + } + } else if (poule.type === 2) { + dataContainer.append(buildTree(poule['trees'])); + } else { + const change_view = (isPoule) => { + dataContainer.replaceChildren(buildPouleMenu(isPoule, change_view)); + + if (isPoule) { + for (const g in poule.matchs) { + if (Object.keys(poule.matchs).length > 1) { + const text = document.createElement('h4'); + text.textContent = `Groupe ${g}`; + text.style.marginTop = '2em'; + dataContainer.append(text); + } + dataContainer.append(buildMatchArray(poule.matchs[g])); + dataContainer.append(buildRankArray(poule.rankArray[g])); + } + } else { + dataContainer.append(buildTree(poule['trees'])); + } + + location[2] = isPoule ? 1 : 2; + window.location.hash = location.join('/'); + } + change_view((location.length === 3) ? location[2] === '1' : true); + } + }) + }) + .catch(e => { + console.error(e); + dataContainer.replaceChildren(new Text("Erreur de chargement de la poule")); + }) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/poule/list`) + .then(response => response.json()) + .then(poule => { + const select = document.createElement('select'); + select.setAttribute('id', poule.id); + select.innerHTML = ``; + for (const pouleKey of Object.keys(poule).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + location[1] = Object.keys(poule).find(key => poule[key] === Number(e.target.value)); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + lastRf = 0; + currentPoule = e.target.value; + loadPoule(); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = poule[decodeURI(location[1])]; + select.value = tmp; + currentPoule = tmp + loadPoule(); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des poules"))) + .finally(() => stopLoading(loading)); + + rfFonction = () => { + if (currentPoule !== 0) + loadPoule(); + } + + rootDiv.append(content) +} + +function buildCombView(comb) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Info :

+
    +
  • Nom Prénom : ${comb.name}
  • +
  • Club : ${comb.club}
  • +
  • Catégorie : ${comb.cat}
  • +
+

Statistique :

+
    +
  • Taux de victoire : ${comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0)}% (${comb.totalWin} sur ${comb.matchs.length})
  • +
  • Points marqués : ${comb.pointMake}
  • +
  • Points reçus : ${comb.pointTake}
  • +
  • Ratio du score (point marqué / point reçu): ${comb.pointRatio.toFixed(3)}
  • +
+ +

Liste des matchs:

+ +
+ + + + + + + + + + ` + for (const match of comb.matchs) { + arrayContent += ` + + + + + + + + ` + } + arrayContent += `
DatePouleAdversaireScoresRatio
${dateToString(match.date)}${match.poule}${match.adv}${scoreToString(match.score)}${match.ratio.toFixed(3)}${match.win ? cupImg : ""}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function combPage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par combattant

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loadComb = (id) => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/data?comb=${id}`) + .then(response => response.json()) + .then(comb => { + console.log(comb); + dataContainer.replaceChildren(buildCombView(comb)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du combattant"))) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/list`) + .then(response => response.json()) + .then(combs => { + const select = document.createElement('select'); + select.innerHTML = ``; + for (const comb of Object.keys(combs).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + location[1] = Object.keys(combs).find(key => combs[key] === e.target.value); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + loadComb(e.target.value); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = combs[decodeURI(location[1])]; + select.value = tmp; + loadComb(tmp); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des combattants"))) + .finally(() => stopLoading(loading)); + + rootDiv.append(content) +} + +function buildClubView(club) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Info :

+
    +
  • Nom : ${club.name}
  • +
  • Nombre d'inscris : ${club.nb_insc}
  • +
+

Statistique :

+
    +
  • Nombre de match disputé : ${club.nb_match}
  • +
  • Nombre de victoires : ${club.match_w}
  • +
  • Ratio de victoires moyen : ${club.ratioVictoire.toFixed(3)}
  • +
  • Points marqués : ${club.pointMake}
  • +
  • Points reçus : ${club.pointTake}
  • +
  • Ratio de points moyen : ${club.ratioPoint.toFixed(3)}
  • +
+ +

Liste des menbres :

+ +
+ + + + + + + + + + + + ` + for (const comb of club.combs) { + arrayContent += ` + + + + + + + + + + ` + } + arrayContent += `
CatégorieNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatio points
${comb.cat}${comb.name}${comb.w}${comb.l}${comb.ratioVictoire.toFixed(3)}${comb.pointMake}${comb.pointTake}${comb.ratioPoint.toFixed(3)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function clubPage(location) { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + content.innerHTML = '

Recherche par club

'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loadComb = (id) => { + const loading = startLoading(content); + fetch(`${apiUrlRoot}/club/data?club=${id}`) + .then(response => response.json()) + .then(club => { + console.log(club); + dataContainer.replaceChildren(buildClubView(club)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du club"))) + .finally(() => stopLoading(loading)); + } + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/club/list`) + .then(response => response.json()) + .then(clubs => { + const select = document.createElement('select'); + select.innerHTML = ``; + for (const club of Object.keys(clubs).sort()) { + select.innerHTML += ``; + } + select.addEventListener('change', e => { + if (e.target.value === '0') + return; + location[1] = Object.keys(clubs).find(key => clubs[key] === Number(e.target.value)); + location[1] = encodeURI(location[1]); + window.location.hash = location.join('/'); + loadComb(e.target.value); + }) + content.append(select); + content.appendChild(dataContainer); + + if (location.length > 1 && location[1] !== undefined && location[1] !== '') { + const tmp = clubs[decodeURI(location[1])]; + select.value = tmp; + loadComb(tmp); + } + }) + .catch(() => rootDiv.append(new Text("Erreur de chargement des clubs"))) + .finally(() => stopLoading(loading)); + + rootDiv.append(content) +} + +function buildCombsView(combs) { + const pouleDiv = document.createElement('div'); + let arrayContent = ` +

Statistique :

+
    +
  • Nombre d'inscris : ${combs.nb_insc}
  • +
  • Nombre de match disputé : ${combs.tt_match}
  • +
  • Points marqués : ${combs.point}
  • +
+ +

Liste des combattants :

+ +
+ + + + + + + + + + + + + ` + for (const comb of combs.combs) { + arrayContent += ` + + + + + + + + + + + ` + } + arrayContent += `
CatégorieClubNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatios points
${comb.cat}${comb.club}${comb.name}${comb.w}${comb.l}${comb.ratioVictoire.toFixed(3)}${comb.pointMake}${comb.pointTake}${comb.ratioPoint.toFixed(3)}
` + pouleDiv.innerHTML = arrayContent; + return pouleDiv; +} + +function combsPage() { + rootDiv.innerHTML = header + backButton; + const content = document.createElement('div'); + content.style.marginTop = '1em'; + + const dataContainer = document.createElement('div'); + dataContainer.id = 'data-container'; + + const loading = startLoading(content); + fetch(`${apiUrlRoot}/comb/get_all`) + .then(response => response.json()) + .then(combs => { + console.log(combs); + dataContainer.replaceChildren(buildCombsView(combs)); + }) + .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement de la liste"))) + .finally(() => stopLoading(loading)); + + content.append(dataContainer); + rootDiv.append(content) +} + +window.addEventListener("load", () => { + let path = document.getElementById('safca_api_script').src; + const urlParams = new URLSearchParams(new URL(path).search); + apiUrlRoot = path.substring(0, path.lastIndexOf('/')) + "/api/public/result/" + urlParams.get("id"); + + console.log("apiUrlRoot:", apiUrlRoot) + + let hash = window.location.hash.substring(1); + if (hash.length === 0) + setSubPage('home'); + else + setSubPage(hash); +}); + +class TreeNode { + constructor(data) { + this.data = data; + this.left = null; + this.right = null; + } + + death() { + let dg = 0; + let dd = 0; + + if (this.right != null) + dg = this.right.death(); + + if (this.left != null) + dg = this.left.death(); + + return 1 + Math.max(dg, dd); + } +} + +function initTree(data_in) { + out = []; + for (const din of data_in) { + out.push(parseTree(din)); + } + + return out; +} + +function parseTree(data_in) { + if (data_in?.data == null) + return null; + + let node = new TreeNode(data_in.data); + node.left = parseTree(data_in?.left); + node.right = parseTree(data_in?.right); + + return node; +} + +const max_x = 500; +const size = 24; + +function getBounds(root) { + let px = max_x; + let py; + let maxx, minx, miny, maxy + + function drawNode(tree, px, py) { + let death = tree.death() - 1 + + if (death === 0) { + if (miny > py - size - ((size * 1.5 / 2) | 0)) miny = py - size - (size * 1.5 / 2) | 0; + if (maxy < py + size + ((size * 1.5 / 2) | 0)) maxy = py + size + (size * 1.5 / 2) | 0; + } else { + if (miny > py - size * 2 * death - ((size * 1.5 / 2) | 0)) + miny = py - size * 2 * death - ((size * 1.5 / 2) | 0); + if (maxy < py + size * 2 * death + ((size * 1.5 / 2) | 0)) + maxy = py + size * 2 * death + ((size * 1.5 / 2) | 0); + } + if (minx > px - size * 2 - size * 8) minx = px - size * 2 - size * 8; + + if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death); + if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); + } + + if (root != null) { + py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2; + + maxx = px; + minx = px; + miny = py - (size * 1.5 / 2) | 0; + maxy = py + (size * 1.5 / 2) | 0; + + for (const node of root) { + px = px - size * 2 - size * 8; + if (minx > px) minx = px; + + drawNode(node, px, py); + //graphics2D.drawRect(minx, miny, maxx - minx, maxy - miny); + py = maxy + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0))); + px = maxx; + } + } else { + minx = 0; + maxx = 0; + miny = 0; + maxy = 0; + } + + return [minx, maxx, miny, maxy]; +} + +function drawGraph(root = []) { + const canvas = document.createElement('canvas'); + canvas.id = "myCanvas"; + canvas.style.border = "1px solid grey"; + canvas.style.marginTop = "10px"; + + const ctx = canvas.getContext("2d"); + + const [minx, maxx, miny, maxy] = getBounds(root); + canvas.width = maxx - minx; + canvas.height = maxy - miny; + ctx.translate(-minx, -miny); + + + ctx.fillStyle = "#000000" + ctx.lineWidth = 2 + ctx.strokeStyle = "#000000" + + function printText(s, x, y, width, height, lineG, lineD) { + ctx.save(); + ctx.translate(x, y); + + let tSize = 17; + let ratioX = height * 1. / 20.; + + ctx.font = "100 " + tSize + "px Arial"; + + let mw = width - (ratioX * 2) | 0; + if (ctx.measureText(s).width > mw) { + let dTextSize = true; + do { + tSize--; + ctx.font = tSize + "px Arial"; + } while (ctx.measureText(s).width > mw && tSize > 10) + + if (!dTextSize || ctx.measureText(s).width > mw) { + let s = ""; + for (const string2 in s.split(" ")) { + if (ctx.measureText(s + string2).width >= mw) { + s += "..."; + break; + } else { + s += string2 + " " + } + } + } + } + + const text = ctx.measureText(s) + let dx = (width - text.width) / 2; + let dy = ((height - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2); + + ctx.fillText(s, dx, dy, width - dy); + ctx.restore(); + + ctx.beginPath(); + if (lineD) { + ctx.moveTo((ratioX * 2.5 + x + dx + text.width) | 0, y + height / 2); + ctx.lineTo(x + width, y + height / 2) + } + if (lineG) { + ctx.moveTo(x, y + height / 2); + ctx.lineTo((dx + x - ratioX * 2.5) | 0, y + height / 2) + } + ctx.stroke(); + } + + function printScores(scores, px, py, scale) { + + ctx.save(); + ctx.translate(px - size * 2, py - size * scale); + ctx.font = "100 " + 14 + "px Arial"; + ctx.textBaseline = 'top'; + + for (let i = 0; i < scores.length; i++) { + const score = scores[i].s1+"-"+scores[i].s2; + const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3; + const text = ctx.measureText(score); + let dx = (size * 2 - text.width) / 2; + let dy = ((size * 2 / div - text.actualBoundingBoxDescent) / 2) + (text.actualBoundingBoxAscent / 2); + + ctx.fillStyle = '#ffffffdd'; + ctx.fillRect(dx, size * 2 * scale / div * (i) + dy, text.width, 14); + ctx.fillStyle = "#000000"; + ctx.fillText(score, dx, size * 2 * scale / div * (i) + dy, size * 2); + } + ctx.restore(); + } + + function drawNode(tree, px, py) { + ctx.beginPath() + ctx.moveTo(px, py) + ctx.lineTo(px - size, py) + ctx.stroke(); + + let death = tree.death() - 1 + let match = tree.data + + if (death === 0) { + ctx.beginPath() + ctx.moveTo(px - size, py + size) + ctx.lineTo(px - size, py - size) + + ctx.moveTo(px - size, py + size) + ctx.lineTo(px - size * 2, py + size) + ctx.moveTo(px - size, py - size) + ctx.lineTo(px - size * 2, py - size) + ctx.stroke(); + + printScores(match.scores, px, py, 1); + + ctx.fillStyle = "#FF0000"; + printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, false, true) + + ctx.fillStyle = "#0000FF"; + printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, false, true) + + if (max_y < py + size + ((size * 1.5 / 2) | 0)) max_y = py + size + (size * 1.5 / 2) | 0; + } else { + ctx.beginPath() + ctx.moveTo(px - size, py) + ctx.lineTo(px - size, py + size * 2 * death) + ctx.moveTo(px - size, py) + ctx.lineTo(px - size, py - size * 2 * death) + + ctx.moveTo(px - size, py + size * 2 * death) + ctx.lineTo(px - size * 2, py + size * 2 * death) + ctx.moveTo(px - size, py - size * 2 * death) + ctx.lineTo(px - size * 2, py - size * 2 * death) + ctx.stroke(); + + printScores(match.scores, px, py, 1.5); + + ctx.fillStyle = "#FF0000"; + printText((match.c1FullName == null) ? "" : match.c1FullName, px - size * 2 - size * 8, py - size * 2 * death - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, true, true) + + ctx.fillStyle = "#0000FF"; + printText((match.c2FullName == null) ? "" : match.c2FullName, px - size * 2 - size * 8, py + size * 2 * death - (size * 1.5 / 2) | 0, + size * 8, (size * 1.5) | 0, true, true) + + if (max_y < py + size * 2 * death + ((size * 1.5 / 2) | 0)) + max_y = py + size * 2 * death + ((size * 1.5 / 2) | 0); + } + + ctx.stroke(); + + if (tree.left != null) drawNode(tree.left, px - size * 2 - size * 8, py - size * 2 * death); + if (tree.right != null) drawNode(tree.right, px - size * 2 - size * 8, py + size * 2 * death); + } + + function win(scores) { + let sum = 0; + for (const score of scores) { + if (score.s1 === -1000 || score.s2 === -1000) + continue; + + if (score.s1 > score.s2) + sum++; + else if (score.s1 < score.s2) + sum--; + } + return sum; + } + + let px = max_x; + let py; + let max_y + + if (root != null) { + py = (size * 2 * root.at(0).death() + (((size * 1.5 / 2) | 0) + size) * root.at(0).death()) * 2; + + max_y = py + (size * 1.5 / 2) | 0; + + for (const node of root) { + let win_name = ""; + if (node.data.end) { + if (win(node.data.scores) > 0) + win_name = (node.data.c1FullName === null) ? "???" : node.data.c1FullName; + else + win_name = (node.data.c2FullName === null) ? "???" : node.data.c2FullName; + } + + + ctx.fillStyle = "#18A918"; + printText(win_name, px - size * 2 - size * 8, py - ((size * 1.5 / 2) | 0), + size * 8, (size * 1.5) | 0, true, false); + + px = px - size * 2 - size * 8; + + drawNode(node, px, py); + + py = max_y + ((size * 2 * node.death() + ((size * 1.5 / 2) | 0))); + px = max_x; + } + } + + return canvas; +} diff --git a/src/main/webapp/public/obs_template.json b/src/main/webapp/public/obs_template.json new file mode 100644 index 0000000..da724c5 --- /dev/null +++ b/src/main/webapp/public/obs_template.json @@ -0,0 +1,828 @@ +{ + "current_scene": "Ovarlay", + "current_program_scene": "Ovarlay", + "scene_order": [ + { + "name": "Ovarlay" + }, + { + "name": "Scène 2" + } + ], + "name": "saf", + "groups": [], + "quick_transitions": [ + { + "name": "Coupure", + "duration": 300, + "hotkeys": [], + "id": 5, + "fade_to_black": false + }, + { + "name": "Fondu", + "duration": 300, + "hotkeys": [], + "id": 6, + "fade_to_black": false + } + ], + "transitions": [], + "saved_projectors": [], + "canvases": [], + "current_transition": "Fondu", + "transition_duration": 300, + "preview_locked": false, + "scaling_enabled": false, + "scaling_level": -9, + "scaling_off_x": 0.0, + "scaling_off_y": 0.0, + "virtual-camera": { + "type2": 3 + }, + "modules": { + "auto-scene-switcher": { + "interval": 300, + "non_matching_scene": "", + "switch_if_not_matching": false, + "active": false, + "switches": [] + }, + "captions": { + "source": "", + "enabled": false, + "lang_id": 1036, + "provider": "mssapi" + }, + "output-timer": { + "streamTimerHours": 0, + "streamTimerMinutes": 0, + "streamTimerSeconds": 30, + "recordTimerHours": 0, + "recordTimerMinutes": 0, + "recordTimerSeconds": 30, + "autoStartStreamTimer": false, + "autoStartRecordTimer": false, + "pauseRecordTimer": false + }, + "scripts-tool": [] + }, + "version": 1, + "sources": [ + { + "prev_ver": 536870916, + "name": "Capture d'écran", + "uuid": "62b56523-7c2e-4ae2-b4e6-9ca3a36e904b", + "id": "monitor_capture", + "versioned_id": "monitor_capture", + "settings": { + "monitor_id": "\\\\?\\DISPLAY#HWP3320#5&3974db33&0&UID143619#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}" + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "Ovarlay", + "uuid": "670a1c48-dac6-405a-959a-d40662bab4d8", + "id": "scene", + "versioned_id": "scene", + "settings": { + "custom_size": false, + "id_counter": 21, + "items": [ + { + "name": "sub1.img.blue", + "source_uuid": "0a37157d-f356-4657-a2ad-fa4193a298f8", + "visible": true, + "locked": false, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 19, + "group_item_backup": false, + "pos": { + "x": 1789.0, + "y": 950.0 + }, + "scale": { + "x": 0.11296296119689941, + "y": 0.11296296119689941 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.img.rouge", + "source_uuid": "2dfe69bc-9b10-419d-8ee1-6cd9cc53a502", + "visible": true, + "locked": false, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 14, + "group_item_backup": false, + "pos": { + "x": 9.0, + "y": 950.0 + }, + "scale": { + "x": 0.11296296119689941, + "y": 0.11296296119689941 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.comb.blue", + "source_uuid": "4e75e437-20ed-4834-bfac-b668f5cac960", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 17, + "group_item_backup": false, + "pos": { + "x": 960.0, + "y": 945.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.comb.rouge", + "source_uuid": "69594be1-dfdc-4eee-893c-fb546e830a6f", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 8, + "group_item_backup": false, + "pos": { + "x": 140.0, + "y": 945.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.score.blue", + "source_uuid": "264866ab-58e1-45ec-a32f-c440c32057cd", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 20, + "group_item_backup": false, + "pos": { + "x": 1108.0, + "y": 10.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 0.0, + "y": 0.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.score.rouge", + "source_uuid": "8a532b22-7bcd-4742-aa0e-8ea556f39a12", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 16, + "group_item_backup": false, + "pos": { + "x": 686.0, + "y": 10.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "sub1.temps", + "source_uuid": "a961104f-2cad-4f65-9ff8-d50ff36f8418", + "visible": true, + "locked": true, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 21, + "group_item_backup": false, + "pos": { + "x": 584.0, + "y": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 1.0, + "y": 1.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + } + ] + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "OBSBasic.SelectScene": [], + "libobs.show_scene_item.19": [], + "libobs.hide_scene_item.19": [], + "libobs.show_scene_item.14": [], + "libobs.hide_scene_item.14": [], + "libobs.show_scene_item.17": [], + "libobs.hide_scene_item.17": [], + "libobs.show_scene_item.8": [], + "libobs.hide_scene_item.8": [], + "libobs.show_scene_item.20": [], + "libobs.hide_scene_item.20": [], + "libobs.show_scene_item.16": [], + "libobs.hide_scene_item.16": [], + "libobs.show_scene_item.21": [], + "libobs.hide_scene_item.21": [] + }, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "Scène 2", + "uuid": "d430fe13-0e4a-4d4b-a28e-7c833eb27bf6", + "id": "scene", + "versioned_id": "scene", + "settings": { + "custom_size": false, + "id_counter": 2, + "items": [ + { + "name": "Capture d'écran", + "source_uuid": "62b56523-7c2e-4ae2-b4e6-9ca3a36e904b", + "visible": false, + "locked": false, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 2, + "group_item_backup": false, + "pos": { + "x": 0.0, + "y": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 0.0, + "y": 0.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + }, + { + "name": "Ovarlay", + "source_uuid": "670a1c48-dac6-405a-959a-d40662bab4d8", + "visible": true, + "locked": false, + "rot": 0.0, + "align": 5, + "bounds_type": 0, + "bounds_align": 0, + "bounds_crop": false, + "crop_left": 0, + "crop_top": 0, + "crop_right": 0, + "crop_bottom": 0, + "id": 1, + "group_item_backup": false, + "pos": { + "x": 0.0, + "y": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0 + }, + "bounds": { + "x": 0.0, + "y": 0.0 + }, + "scale_filter": "disable", + "blend_method": "default", + "blend_type": "normal", + "show_transition": { + "duration": 0 + }, + "hide_transition": { + "duration": 0 + }, + "private_settings": {} + } + ] + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "OBSBasic.SelectScene": [], + "libobs.show_scene_item.2": [], + "libobs.hide_scene_item.2": [], + "libobs.show_scene_item.1": [], + "libobs.hide_scene_item.1": [] + }, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "canvas_uuid": "6c69626f-6273-4c00-9d88-c5136d61696e", + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.comb.blue", + "uuid": "4e75e437-20ed-4834-bfac-b668f5cac960", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "chatlog": false, + "extents": true, + "gradient": false, + "outline": false, + "undo_sname": "sub1.comb_rouge", + "vertical": false, + "font": { + "face": "Arial", + "flags": 0, + "size": 65, + "style": "Normal" + }, + "align": "right", + "valign": "bottom", + "color": 4294917120, + "extents_cx": 835, + "extents_cy": 120, + "antialiasing": true, + "overlay": true, + "text": "Xavier Login" + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.comb.rouge", + "uuid": "69594be1-dfdc-4eee-893c-fb546e830a6f", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "chatlog": false, + "extents": true, + "gradient": false, + "outline": false, + "undo_sname": "sub1.comb_rouge", + "vertical": false, + "font": { + "face": "Arial", + "flags": 0, + "size": 65, + "style": "Normal" + }, + "align": "left", + "valign": "bottom", + "color": 4278190335, + "extents_cx": 820, + "extents_cy": 120, + "antialiasing": true, + "overlay": true, + "text": "Xavier Login" + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.img.blue", + "uuid": "0a37157d-f356-4657-a2ad-fa4193a298f8", + "id": "slideshow", + "versioned_id": "slideshow", + "settings": { + "files": [], + "transition": "slide", + "slide_time": 9000, + "transition_speed": 1300, + "use_custom_size": "1:1", + "playback_behavior": "stop_restart", + "slide_mode": "mode_auto", + "overlay": true + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "SlideShow.PlayPause": [], + "SlideShow.Restart": [], + "SlideShow.Stop": [], + "SlideShow.NextSlide": [], + "SlideShow.PreviousSlide": [] + }, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.img.rouge", + "uuid": "2dfe69bc-9b10-419d-8ee1-6cd9cc53a502", + "id": "slideshow", + "versioned_id": "slideshow", + "settings": { + "files": [], + "transition": "slide", + "slide_time": 9000, + "transition_speed": 1300, + "use_custom_size": "1:1", + "playback_behavior": "stop_restart", + "slide_mode": "mode_auto", + "overlay": true + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": { + "SlideShow.PlayPause": [], + "SlideShow.Restart": [], + "SlideShow.Stop": [], + "SlideShow.NextSlide": [], + "SlideShow.PreviousSlide": [] + }, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.score.blue", + "uuid": "264866ab-58e1-45ec-a32f-c440c32057cd", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "chatlog": false, + "extents": true, + "gradient": false, + "outline": false, + "text": "0", + "undo_sname": "sub1.comb_rouge", + "vertical": false, + "font": { + "face": "Arial", + "flags": 0, + "size": 80, + "style": "Normal" + }, + "align": "center", + "valign": "bottom", + "color": 4294917120, + "extents_wrap": true, + "extents_cx": 163, + "extents_cy": 80, + "antialiasing": true + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.score.rouge", + "uuid": "8a532b22-7bcd-4742-aa0e-8ea556f39a12", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "chatlog": false, + "extents": true, + "gradient": false, + "outline": false, + "text": "0", + "undo_sname": "sub1.comb_rouge", + "vertical": false, + "font": { + "face": "Arial", + "flags": 0, + "size": 80, + "style": "Normal" + }, + "align": "center", + "valign": "bottom", + "color": 4278190335, + "extents_wrap": true, + "extents_cx": 163, + "extents_cy": 80, + "antialiasing": true + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + }, + { + "prev_ver": 536870916, + "name": "sub1.temps", + "uuid": "a961104f-2cad-4f65-9ff8-d50ff36f8418", + "id": "text_gdiplus", + "versioned_id": "text_gdiplus_v2", + "settings": { + "extents": true, + "text": "00:10", + "font": { + "face": "Arial", + "flags": 0, + "size": 110, + "style": "Normal" + }, + "align": "center", + "color": -16711936, + "extents_wrap": true, + "extents_cx": 800, + "extents_cy": 110 + }, + "mixers": 0, + "sync": 0, + "flags": 0, + "volume": 1.0, + "balance": 0.5, + "enabled": true, + "muted": false, + "push-to-mute": false, + "push-to-mute-delay": 0, + "push-to-talk": false, + "push-to-talk-delay": 0, + "hotkeys": {}, + "deinterlace_mode": 0, + "deinterlace_field_order": 0, + "monitoring_type": 0, + "private_settings": {} + } + ] +} \ No newline at end of file diff --git a/src/main/webapp/src/assets/SimpleIconsOBS.ts b/src/main/webapp/src/assets/SimpleIconsOBS.ts new file mode 100644 index 0000000..2135033 --- /dev/null +++ b/src/main/webapp/src/assets/SimpleIconsOBS.ts @@ -0,0 +1,26 @@ +import { + IconDefinition, + IconName, + IconPrefix +} from "@fortawesome/fontawesome-svg-core"; + +export const SimpleIconsOBS: IconDefinition = { + icon: [ + // SVG viewbox width (in pixels) + 20, + + // SVG viewbox height (in pixels) + 20, + + // Aliases (not needed) + [], + + // Unicode as hex value (not needed) + "", + + // SVG path data + "M10 0C4.486 0 0 4.486 0 10s4.486 10 10 10 10-4.485 10-10S15.515 0 10 0m8.159 14.075c.55-1.617-.057-3.491-1.551-4.448-1.75-1.12-5.196 1.14-5.196 1.14s-.763 2.815-.026 4.007a5.4 5.4 0 0 1-1.77 1.709c-2.348 1.4-5.372.848-7.054-1.209a9 9 0 0 1-.235-.346c1.139 1.423 3.219 1.831 4.855.908 1.809-1.02 1.427-5.124 1.427-5.124S6.598 8.774 5.26 8.799a5.424 5.424 0 0 1 2.993-7.76c.157-.03.317-.05.476-.072a3.76 3.76 0 1 0 4.525 5.435 5.4 5.4 0 0 1 2.794.793c2.199 1.345 3.119 4.041 2.344 6.397-.072.166-.154.324-.234.483" + ], + iconName: "simple-icons-obs" as IconName, + prefix: "simple-icons" as IconPrefix +}; diff --git a/src/main/webapp/src/components/SmartLogoBackground.jsx b/src/main/webapp/src/components/SmartLogoBackground.jsx index ca5a42a..270a532 100644 --- a/src/main/webapp/src/components/SmartLogoBackground.jsx +++ b/src/main/webapp/src/components/SmartLogoBackground.jsx @@ -2,44 +2,21 @@ import {useEffect, useRef, useState, memo} from 'react'; const cache = {}; -export function SmartLogoBackground({ - src, +export function detectOptimalBackground(blob, darkBackground = '#333333', lightBackground = '#f0f0f0', defaultBackground = 'transparent', tolerance = 55, - minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond - alt = 'Logo', - style = {}, - imgClassName = '', - }) { - const canvasRef = useRef(null); - const [background, setBackground] = useState(defaultBackground); - const [load, setLoad] = useState(false) - - useEffect(() => { - if (cache[src]) { - setBackground(cache[src]); - return; - } - if (!load) - return; - - const canvas = canvasRef.current; - const ctx = canvas.getContext('2d'); + minPixels = 10) { + return new Promise((resolve) => { + const imgUrl = URL.createObjectURL(blob); const img = new Image(); - - img.crossOrigin = 'Anonymous'; - img.src = src; - - // Prevent error logging - img.onerror = function () { - return true; - } - + img.crossOrigin = "anonymous"; img.onload = () => { + const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; + const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); @@ -69,12 +46,9 @@ export function SmartLogoBackground({ const i = Math.round(y * canvas.width + x) * 4; if (isTransparent(i)) { const neighbors = [ - i - 4, // Haut - i + 4, // Bas - i - 4 * canvas.width, // Gauche - i + 4 * canvas.width, // Droite + i - 4, i + 4, // Haut/Bas + i - 4 * canvas.width, i + 4 * canvas.width, // Gauche/Droite ]; - for (const neighbor of neighbors) { if (neighbor >= 0 && neighbor < pixels.length) { if (isLightColor(neighbor)) lightBorderCount++; @@ -82,24 +56,64 @@ export function SmartLogoBackground({ } } } - - - if (lightBorderCount >= 25 || darkBorderCount >= 25) - break - } - - if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) { - cache[src] = darkBackground; - setBackground(darkBackground) - } else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) { - cache[src] = lightBackground; - setBackground(lightBackground) - } else { - cache[src] = defaultBackground; - setBackground(defaultBackground) } } - } + + URL.revokeObjectURL(imgUrl); + if (lightBorderCount > darkBorderCount && lightBorderCount >= minPixels) { + resolve(darkBackground); // Fond sombre + } else if (darkBorderCount > lightBorderCount && darkBorderCount >= minPixels) { + resolve(lightBackground); // Fond clair + } else { + resolve(defaultBackground); // Fond transparent + } + }; + img.onerror = () => { + URL.revokeObjectURL(imgUrl); + resolve(defaultBackground); // En cas d'erreur + }; + img.src = imgUrl; + }); +} + +export function SmartLogoBackground({ + src, + darkBackground = '#333333', + lightBackground = '#f0f0f0', + defaultBackground = 'transparent', + tolerance = 55, + minPixels = 10, // Seuil minimal de pixels détectés pour appliquer un fond + alt = 'Logo', + style = {}, + imgClassName = '', + }) { + const canvasRef = useRef(null); + const [background, setBackground] = useState(defaultBackground); + const [load, setLoad] = useState(false) + + useEffect(() => { + if (!load) return; + + const fetchAndDetect = async () => { + if (cache[src]) { + setBackground(cache[src]); + return; + } + + try { + const response = await fetch(src); + const blob = await response.blob(); + const detectedBackground = await detectOptimalBackground(blob, darkBackground, lightBackground, defaultBackground, tolerance, minPixels); + cache[src] = detectedBackground; + setBackground(detectedBackground); + } catch (error) { + console.error("Erreur de détection du fond:", error); + cache[src] = defaultBackground; + setBackground(defaultBackground); + } + }; + + fetchAndDetect(); }, [src, darkBackground, lightBackground, defaultBackground, tolerance, minPixels, load]); return <> diff --git a/src/main/webapp/src/hooks/useExternalWindow.jsx b/src/main/webapp/src/hooks/useExternalWindow.jsx index 233b052..db7f53d 100644 --- a/src/main/webapp/src/hooks/useExternalWindow.jsx +++ b/src/main/webapp/src/hooks/useExternalWindow.jsx @@ -1,6 +1,15 @@ import {createContext, useContext, useReducer} from "react"; -const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true, timeCb: undefined, scoreRouge: 0, scoreBleu: 0}); +const PubAffContext = createContext({ + next: [], + c1: undefined, + c2: undefined, + showScore: true, + timeCb: undefined, + timeCb2: undefined, + scoreRouge: 0, + scoreBleu: 0 +}); const PubAffDispatchContext = createContext(() => { }); @@ -11,6 +20,8 @@ function reducer(state, action) { case 'CALL_TIME': if (state.timeCb) state.timeCb(action.payload) + if (state.timeCb2) + state.timeCb2(action.payload) return state case 'CLEAR_CB_TIME': return {...state, timeCb: undefined} diff --git a/src/main/webapp/src/hooks/useOBS.jsx b/src/main/webapp/src/hooks/useOBS.jsx new file mode 100644 index 0000000..6f74831 --- /dev/null +++ b/src/main/webapp/src/hooks/useOBS.jsx @@ -0,0 +1,183 @@ +import {createContext, useContext, useEffect, useRef, useState} from "react"; +import OBSWebSocket from "obs-websocket-js"; +import {hex2rgb} from "../utils/Tools.js"; + +const OBSContext = createContext({connected: false, obs: null}) + +export function OBSProvider({children}) { + const obs = useRef(null) + const obsParm = useRef(null) + const assets = useRef(null) + const [connected, setConnected] = useState(false) + const [doReconnect, setDoReconnect] = useState(false) + + useEffect(() => { + if (!doReconnect) + return; + + const timer = setInterval(() => { + if (obs.current && !connected && doReconnect) { + console.log("Reconnecting to OBS WebSocket...") + obs.current.connect(obsParm.current.adresse, obsParm.current.password) + .then(data => { + console.log("Reconnected to OBS WebSocket", data) + setDoReconnect(false) + }) + } + }, 5000); + return () => clearInterval(timer); + }, [connected, doReconnect]); + + const connect = (adresse, password = undefined, assets_dir = undefined) => { + if (connected && obs.current) + return; + assets.current = assets_dir; + + const obs_ = new OBSWebSocket(); + obs_.connect(adresse, password) + .then(data => { + console.log("Connected to OBS WebSocket", data) + }) + .catch(err => { + console.error("Failed to connect to OBS WebSocket", err) + }); + obs_.on('ConnectionOpened', () => { + setConnected(true) + console.log("OBS WebSocket connection opened") + }); + obs_.on('ConnectionClosed', err => { + setConnected(false) + console.log("OBS WebSocket connection closed", err.code, err.message) + if (err.code === 1000 || err.code === 4009) // 1000 = Normal Closure, 4009 = Authentication Failure + return; + obsParm.current = {adresse, password} + setDoReconnect(true) + }); + obs_.on('error', err => { + console.error("OBS WebSocket error", err) + }); + + obs.current = obs_; + } + + const disconnect = () => { + if (obs.current && connected) { + obs.current.disconnect(); + obs.current = null; + } + } + + const ret = {connected, obs: obs.current, connect, disconnect, assets: assets.current} + return + {children} + +} + +function getElementName(element) { + return `sub${sessionStorage.getItem("obs_prefix") || 1}.${element}` +} + +export function useOBS() { + const {connected, obs, connect, disconnect, assets} = useContext(OBSContext) + const setTextAndColor = (element, text, color) => { + if (!connected) + return; + if (color.startsWith("#")){ + const tmp = hex2rgb(color); + color = (tmp.b << 16) + (tmp.g << 8) + tmp.r; + } + + obs.call('SetInputSettings', { + inputName: getElementName(element), + inputSettings: { + text: text, + color: color, + overlay: true, + } + }).catch(err => { + console.error("Failed to set text and color for OBS element", element, err) + }); + } + const setText = (element, text) => { + if (!connected) + return; + obs.call('SetInputSettings', { + inputName: getElementName(element), + inputSettings: { + text: text, + overlay: true, + } + }).catch(err => { + console.error("Failed to set text for OBS element", element, err) + }); + } + + const setDiapo = (element, files) => { + if (!connected) + return; + const arr = []; + for (const file of files) { + arr.push({ + hidden: false, + selected: false, + value: (assets.endsWith('/') ? assets : assets + '/') + file, + }) + } + obs.call('SetInputSettings', { + inputName: getElementName(element), + inputSettings: { + files: arr, + overlay: true, + } + }).catch(err => { + console.error("Failed to set diapo for OBS element", element, err) + }); + } + + return { + connected, + obs, + connect, + disconnect, + setText, + setTextAndColor, + setDiapo, + } +} + +export function exportOBSConfiguration(adresse, password, assets_dir) { + const config = { + adresse: adresse, + password: password, + assets_dir: assets_dir, + }; + const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config, null, 2)); + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", "obs_configuration.json"); + document.body.appendChild(downloadAnchorNode); // required for firefox + downloadAnchorNode.click(); + downloadAnchorNode.remove(); +} + +export async function importOBSConfiguration() { + return new Promise((resolve, reject) => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json'; + input.onchange = e => { + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = event => { + try { + const config = JSON.parse(event.target.result); + resolve(config); + } catch (err) { + reject(err); + } + }; + reader.readAsText(file); + }; + input.click(); + }); +} diff --git a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx index 8c56854..8454f11 100644 --- a/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMAdmin.jsx @@ -1,30 +1,40 @@ -import {useEffect, useRef, useState} from "react"; +import React, {useEffect, useRef, useState} from "react"; import {useRequestWS, useWS} from "../../../hooks/useWS.jsx"; import {LoadingProvider, useLoadingSwitcher} from "../../../hooks/useLoading.jsx"; import {toast} from "react-toastify"; import {build_tree, resize_tree} from "../../../utils/TreeUtils.js" import {ConfirmDialog} from "../../../components/ConfirmDialog.jsx"; import {CategoryContent} from "./CategoryAdminContent.jsx"; +import {exportOBSConfiguration} from "../../../hooks/useOBS.jsx"; +import {createPortal} from "react-dom"; +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"; -export function CMAdmin() { +const vite_url = import.meta.env.VITE_URL; + +export function CMAdmin({compUuid}) { const [catId, setCatId] = useState(null); const [cat, setCat] = useState(null); + const menuActions = useRef({}); const {dispatch} = useWS(); useEffect(() => { const categoryListener = ({data}) => { if (!cat || data.id !== cat.id) return - setCat({ - ...cat, + setCat(cat_ => ({ + ...cat_, name: data.name, liceName: data.liceName, type: data.type - }) + })) } dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) return () => dispatch({type: 'removeListener', payload: categoryListener}) - }, []); + }, [cat]); return <>
@@ -35,10 +45,261 @@ export function CMAdmin() {
- +
+ +
+ +} + +let tto = []; + +function resizeImageWithOptimalBackground(blob) { + return new Promise(async (resolve) => { + const background = await detectOptimalBackground(blob); + const imgUrl = URL.createObjectURL(blob); + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = 1080; + canvas.height = 1080; + const ctx = canvas.getContext('2d'); + + // Dessiner l'image centrée + const scale = Math.min(1080 / img.width, 1080 / img.height); + const newWidth = img.width * scale; + const newHeight = img.height * scale; + const x = (1080 - newWidth) / 2; + const y = (1080 - newHeight) / 2; + + ctx.filter = `drop-shadow(0 0 2rem ${background})`; + ctx.drawImage(img, x, y, newWidth, newHeight); + + // Exporter en PNG + canvas.toBlob((newBlob) => { + resolve(newBlob); + URL.revokeObjectURL(imgUrl); + }, 'image/png', 1.0); + }; + img.onerror = () => resolve(blob); // Retourne l'original en cas d'erreur + img.src = imgUrl; + }); +} + +async function downloadResourcesAsZip(resourceList) { + const zip = new JSZip(); + const modal = new bootstrap.Modal(document.getElementById('progressModal')); + const progressBar = document.getElementById('progressBar'); + const progressText = document.getElementById('progressText'); + let completed = 0; + + if (!resourceList.some(d => d.url === '/obs_template.json')) + resourceList.push({url: '/obs_template.json', name: 'saf_obs_template.json'}); + + // Afficher la modale + modal.show(); + + // Fonction pour télécharger une ressource et l'ajouter au ZIP + const addResourceToZip = async (data) => { + try { + const response = await fetch(data.url); + if (!response.ok) { + if (response.status === 404) { + return {success: false, filename: data.name || data.url.split('/').pop()}; + } + // noinspection ExceptionCaughtLocallyJS + throw new Error(`Erreur HTTP: ${response.status}`); + } + const blob = await response.blob(); + const filename = data.name || data.url.split('/').pop(); + const format = filename.split('.').pop().toLowerCase(); + + if (['png', 'jpg', 'jpeg', 'svg'].includes(format)) { + const resizedBlob = await resizeImageWithOptimalBackground(blob); + const pngFilename = filename.replace(/\.[^/.]+$/, ".png"); + zip.file(pngFilename, resizedBlob); + return {success: true, pngFilename}; + } else { + zip.file(filename, blob); + return {success: true, filename}; + } + } catch (error) { + console.error(`Impossible d'ajouter ${data.url} au ZIP:`, error); + return {success: false, filename: data.name || data.url.split('/').pop()}; + } + }; + + // Télécharger toutes les ressources et mettre à jour la progression + await Promise.all( + resourceList.map(async (data) => { + const result = await addResourceToZip(data); + completed++; + const progress = Math.round((completed / resourceList.length) * 100); + progressBar.style.width = `${progress}%`; + progressText.textContent = `Téléchargement (${completed}/${resourceList.length}) : ${result.filename}`; + return result; + }) + ); + + // Générer le ZIP et déclencher le téléchargement + const zipBlob = await zip.generateAsync({type: 'blob'}); + const zipUrl = URL.createObjectURL(zipBlob); + const a = document.createElement('a'); + a.href = zipUrl; + a.download = 'ressources.zip'; + a.click(); + URL.revokeObjectURL(zipUrl); + + // Fermer la modale + modal.hide(); + progressText.textContent = "Téléchargement terminé !"; +} + +function Menu({menuActions, compUuid}) { + const e = document.getElementById("actionMenu") + const longPress = useRef({time: null, timer: null, button: null}); + const obsModal = useRef(null); + + for (const x of tto) + x.dispose(); + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]') + tto = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) + + const longTimeAction = (button) => { + if (button === "obs") { + obsModal.current.click(); + } + } + + const longPressDown = (button) => { + longPress.current.button = button; + longPress.current.time = new Date(); + longPress.current.timer = setTimeout(() => { + longTimeAction(button); + + longPress.current.time = null; + longPress.current.button = null; + }, 1000); + } + + const longPressUp = (button) => { + clearTimeout(longPress.current.timer); + + if (longPress.current.time) { + const diff = new Date() - longPress.current.time; + if (longPress.current.button === button) { + if (diff >= 1000) { + longTimeAction(button); + } else { + if (button === "obs") { + downloadResourcesAsZip(menuActions.current.resourceList || []) + .then(__ => console.log("Ressources téléchargées")); + } + } + } + + longPress.current.time = null; + longPress.current.button = null; + } + } + + const handleOBSSubmit = (e) => { + e.preventDefault(); + const form = e.target; + const adresse = form[0].value; + const password = form[1].value; + const assets_dir = form[2].value; + + exportOBSConfiguration(adresse, password, assets_dir) + } + + const copyScriptToClipboard = () => { + navigator.clipboard.writeText(`
+ ` + ).then(() => { + toast.success("Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress."); + }).catch(err => { + toast.error("Erreur lors de la copie dans le presse-papier : " + err); + }); + } + + if (!e) + return <>; + return <> + {createPortal( + <> +
+ longPressDown("obs")} + onMouseUp={() => longPressUp("obs")} + data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title="Clique court : Télécharger les ressources. Clique long : Créer la configuration obs"/> + copyScriptToClipboard()} + data-bs-toggle="tooltip2" data-bs-placement="top" + data-bs-title="Copier le scripte d'intégration"/> + , document.getElementById("actionMenu"))} + + + + + } @@ -60,18 +321,34 @@ function CategoryHeader({cat, setCatId}) { data ]) } + const sendAddCategory = ({data}) => { + setCats([...cats, data]) + } + const sendDelCategory = ({data}) => { + setCatId(catId => { + if (catId === data) return null; + return catId; + }) + setCats([...cats.filter(c => c.id !== data)]) + } dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) - return () => dispatch({type: 'removeListener', payload: categoryListener}) + dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}}) + dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}}) + return () => { + dispatch({type: 'removeListener', payload: categoryListener}) + dispatch({type: 'removeListener', payload: sendAddCategory}) + dispatch({type: 'removeListener', payload: sendDelCategory}) + } }, [cats]); useEffect(() => { - if (cats && cats.length > 0 && !cat) { + if (cats && cats.length > 0 && !cat || (cats && !cats.find(c => c.id === cat.id))) { setCatId(cats.sort((a, b) => a.name.localeCompare(b.name))[0].id); } else if (cats && cats.length === 0) { setModal({}); bthRef.current.click(); } - }, [cats]); + }, [cats, cat]); const handleCatChange = (e) => { const selectedCatId = e.target.value; @@ -88,7 +365,7 @@ function CategoryHeader({cat, setCatId}) {
Edition de la catégorie
- {cats && cats.sort((a, b) => a.name.localeCompare(b.name)).map(c => ( ))} {cats && } @@ -98,7 +375,7 @@ function CategoryHeader({cat, setCatId}) {
{cat &&
Type: {(cat.type & 1) !== 0 ? "Poule" : ""}{cat.type === 3 ? " & " : ""}{(cat.type & 2) !== 0 ? "Tournois" : ""} | - Lice: {cat.liceName}
} + Zone: {cat.liceName}
}
@@ -311,8 +588,8 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
- - Nom des zones de combat (séparée par des ';') + setLice(e.target.value)}/>
@@ -373,6 +650,22 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
+ {state.id !== undefined && }
} diff --git a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx index 40ac209..2c744b5 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx @@ -9,7 +9,7 @@ export function ChronoPanel() { }) const [chrono, setChrono] = useState({time: 0, startTime: 0}) const chronoText = useRef(null) - const state = useRef({chronoState: 0, countBlink: 20, lastColor: "black", lastTimeStr: "00:00"}) + const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"}) const publicAffDispatch = usePubAffDispatch(); const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time})) @@ -34,12 +34,12 @@ export function ChronoPanel() { const timer = setInterval(() => { let currentDuration = config.time - let color = "black" + let color = "#000000" if (state_.chronoState === 1) { - color = (state_.countBlink < blinkRfDuration) ? "black" : "red" + color = (state_.countBlink < blinkRfDuration) ? "#000000" : "#ff0000" } else if (state_.chronoState === 2) { currentDuration = (state_.chronoState === 0) ? 10000 : config.pause - color = (state_.countBlink < blinkRfDuration) ? "green" : "red" + color = (state_.countBlink < blinkRfDuration) ? "#008000" : "#ff0000" } const timeStr = timePrint(currentDuration - getTime()) diff --git a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx index 4b8e048..a4522be 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTMatchPanel.jsx @@ -11,6 +11,7 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; import {toast} from "react-toastify"; import "./CMTMatchPanel.css" +import {useOBS} from "../../../hooks/useOBS.jsx"; function CupImg() { return { const categoryListener = ({data}) => { setCats([...cats.filter(c => c.id !== data.id), data]) } + const sendAddCategory = ({data}) => { + setCats([...cats, data]) + } + const sendDelCategory = ({data}) => { + if (catId === data) + setCatId(-1); + setCats([...cats.filter(c => c.id !== data)]) + } dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}}) - return () => dispatch({type: 'removeListener', payload: categoryListener}) + dispatch({type: 'addListener', payload: {callback: sendAddCategory, code: 'sendAddCategory'}}) + dispatch({type: 'addListener', payload: {callback: sendDelCategory, code: 'sendDelCategory'}}) + return () => { + dispatch({type: 'removeListener', payload: categoryListener}) + dispatch({type: 'removeListener', payload: sendAddCategory}) + dispatch({type: 'removeListener', payload: sendDelCategory}) + } }, [cats]); const cat = cats?.find(c => c.id === catId); + useEffect(() => { + setText("poule",cat ? cat.name : ""); + }, [cat, connected]); + return <>
Catégorie
@@ -131,10 +151,15 @@ function ListMatch({cat, matches, trees, menuActions}) { const [type, setType] = useState(1); useEffect(() => { + if (!cat) + return; if ((cat.type & type) === 0) setType(cat.type); }, [cat]); + if (!cat) + return <>; + return
{cat && cat.type === 3 && <>
    @@ -173,6 +198,10 @@ function MatchList({matches, cat, menuActions}) { .map(m => ({...m, win: win(m.scores)})) const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1; + const isActiveMatch = (index) => { + return liceName.length === 1 || (liceName[(index - firstIndex) % liceName.length] === lice) + } + const match = matches.find(m => m.id === activeMatch) useEffect(() => { if (!match) { @@ -183,7 +212,7 @@ function MatchList({matches, cat, menuActions}) { payload: { c1: match.c1, c2: match.c2, - next: marches2.filter((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice && m.id !== activeMatch).map(m => ({ + next: marches2.filter((m, index) => !m.end && isActiveMatch(index) && m.id !== activeMatch).map(m => ({ c1: m.c1, c2: m.c2 })) @@ -198,7 +227,7 @@ function MatchList({matches, cat, menuActions}) { useEffect(() => { if (match && match.poule !== lice) - setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id) + setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id) }, [lice]); useEffect(() => { @@ -207,12 +236,12 @@ function MatchList({matches, cat, menuActions}) { if (marches2.some(m => m.id === activeMatch)) return; - setActiveMatch(marches2.find((m, index) => !m.end && liceName[(index - firstIndex) % liceName.length] === lice)?.id); + setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id); }, [matches]) return <> {liceName.length > 1 && -
    - +
    + +
    +
    +
    + + +
    + +
+
+ } + +function ObsAutoSyncWhitPubAff() { + const {connected, setText, setTextAndColor, setDiapo} = useOBS(); + const oldState = useRef({timeColor: "#000000", timeStr: "--:--", c1: null, c2: null, showScore: true, scoreRouge: 0, scoreBleu: 0}); + const state = usePubAffState(); + const {getComb} = useCombs(); + + useEffect(() => { + if (state.c1 !== oldState.current.c1) { + const comb = getComb(state.c1); + setText("comb.rouge", comb ? (comb?.fname + " " + comb?.lname) : ""); + const files = [] + if (comb?.club_uuid) files.push(`club_${comb.club_uuid}.png`) + if (comb?.country) files.push(`flag_${comb.country.toLowerCase()}.png`) + setDiapo("img.rouge", files); + oldState.current.c1 = state.c1; + } + + if (state.c2 !== oldState.current.c2) { + const comb = getComb(state.c2); + setText("comb.blue", comb ? (comb?.fname + " " + comb?.lname) : ""); + const files = [] + if (comb?.club_uuid) files.push(`club_${comb.club_uuid}.png`) + if (comb?.country) files.push(`flag_${comb.country.toLowerCase()}.png`) + setDiapo("img.blue", files); + oldState.current.c2 = state.c2; + } + + if (state.showScore !== oldState.current.showScore) { + setText("score.rouge", state.showScore ? state.scoreRouge.toString() : ""); + setText("score.blue", state.showScore ? state.scoreBleu.toString() : ""); + oldState.current.showScore = state.showScore; + } + + if (state.showScore === undefined || state.showScore) { + if (state.scoreRouge !== oldState.current.scoreRouge) { + setText("score.rouge", (state.scoreRouge || 0).toString()); + oldState.current.scoreRouge = state.scoreRouge; + } + if (state.scoreBleu !== oldState.current.scoreBleu) { + setText("score.blue", (state.scoreBleu || 0).toString()); + oldState.current.scoreBleu = state.scoreBleu; + } + } + }, [state]); + + state.timeCb2 = (payload) => { + if (payload.timeStr && payload.timeColor) { + setTextAndColor("temps", payload.timeStr, payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor); + + oldState.current.timeStr = payload.timeStr; + oldState.current.lastColor = payload.timeColor; + } + } + + useEffect(() => { + if (!connected) + return; + // Initial sync + const comb = getComb(oldState.current.c1); + const comb2 = getComb(oldState.current.c2); + setText("comb.rouge", comb ? (comb?.fname + " " + comb?.lname) : ""); + setText("comb.blue", comb2 ? (comb2?.fname + " " + comb2?.lname) : ""); + setTextAndColor("temps", oldState.current.timeStr, oldState.current.timeColor === "#000000" ? "#ffffff" : oldState.current.timeColor); + setText("score.rouge", oldState.current.showScore === undefined || oldState.current.showScore ? oldState.current.scoreRouge.toString() : ""); + setText("score.blue", oldState.current.showScore === undefined || oldState.current.showScore ? oldState.current.scoreBleu.toString() : ""); + + }, [connected]) +} diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index 7df2e9d..faae064 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -17,13 +17,15 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faTrash} from "@fortawesome/free-solid-svg-icons"; import {win} from "../../../utils/Tools.js"; +const vite_url = import.meta.env.VITE_URL; + function CupImg() { return } -export function CategoryContent({cat, catId, setCat}) { +export function CategoryContent({cat, catId, setCat, menuActions}) { const setLoading = useLoadingSwitcher() const {sendRequest, dispatch} = useWS(); const [matches, reducer] = useReducer(MarchReducer, []); @@ -47,10 +49,10 @@ export function CategoryContent({cat, catId, setCat}) { const treeListener = ({data}) => { if (!cat || data.length < 1 || data[0].categorie !== cat.id) return - setCat({ - ...cat, - trees: data.map(d => from_sendTree(d, true)) - }) + setCat(cat_ => ({ + ...cat_, + trees: data.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)) + })) let matches2 = []; let combsToAdd = []; @@ -64,10 +66,16 @@ export function CategoryContent({cat, catId, setCat}) { 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)}}); - if (data.c1 !== null && !groupsRef.current.some(g => g.id === data.c1?.id)) - setGroups(prev => [...prev, {id: data.c1?.id, poule: data.poule}]); - if (data.c2 !== null && !groupsRef.current.some(g => g.id === data.c2?.id)) - setGroups(prev => [...prev, {id: data.c2?.id, poule: data.poule}]); + setGroups(prev => { + if (data.c1 !== null && !prev.some(g => g.id === data.c1?.id)) + return [...prev, {id: data.c1?.id, poule: data.poule}]; + return prev; + }) + setGroups(prev => { + if (data.c2 !== null && !prev.some(g => g.id === data.c2?.id)) + return [...prev, {id: data.c2?.id, poule: data.poule}]; + return prev; + }) } } @@ -103,7 +111,7 @@ export function CategoryContent({cat, catId, setCat}) { name: data.name, liceName: data.liceName, type: data.type, - trees: data.trees.map(d => from_sendTree(d, true)) + trees: data.trees.sort((a, b) => a.level - b.level).map(d => from_sendTree(d, true)) }) let matches2 = []; @@ -142,7 +150,7 @@ export function CategoryContent({cat, catId, setCat}) { return <>
- +
{cat && } @@ -150,7 +158,7 @@ export function CategoryContent({cat, catId, setCat}) { } -function AddComb({groups, setGroups, removeGroup}) { +function AddComb({groups, setGroups, removeGroup, menuActions}) { const {data, setData} = useRequestWS("getRegister", null) const combDispatch = useCombsDispatch() const {dispatch} = useWS() @@ -184,6 +192,21 @@ function AddComb({groups, setGroups, removeGroup}) { if (data === null) return; combDispatch({type: 'SET_ALL', payload: {source: "register", data: data}}); + + const resourceList = [] + data.forEach(d => { + if (d.club_uuid) { + const url = `${vite_url}/api/club/${d.club_uuid}/logo`; + if (!resourceList.some(d => d.url === url)) + resourceList.push({url: url, name: `club_${d.club_uuid}.png`}); + } + if (d.country) { + const url = `/flags/svg/${d.country.toLowerCase()}.svg`; + if (!resourceList.some(d => d.url === url)) + resourceList.push({url: url, name: `flag_${d.country.toLowerCase()}.svg`}); + } + }) + menuActions.current.resourceList = resourceList }, [data]); return <> @@ -554,7 +577,7 @@ function MatchList({matches, cat, groups, reducer}) { N° Poule - Lice + Zone Rouge Blue diff --git a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx index f47dbd3..59e7c3c 100644 --- a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx +++ b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx @@ -66,7 +66,7 @@ function HomeComp() { }/> - }/> + }/> }/> @@ -95,7 +95,7 @@ function WSStatus({setPerm}) { return () => dispatch({type: 'removeListener', payload: welcomeListener}) }, []) - return
+ return

{name}

Serveur: diff --git a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx index 8eac0bc..0b7273d 100644 --- a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx +++ b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx @@ -14,16 +14,16 @@ const text2Style = {fontSize: "min(1.7vw, 7vh)", fontWeight: "bold"}; export function PubAffWindow({document}) { const chronoText = useRef(null) - const state2 = useRef({lastColor: "white", lastTimeStr: "--:--"}) + const state2 = useRef({lastColor: "#ffffff", lastTimeStr: "--:--"}) const state = usePubAffState(); - document.title = "A React portal window" + document.title = "Affichage Public"; document.body.className = "bg-dark text-white overflow-hidden"; state.timeCb = (payload) => { - state2.current = {lastColor: payload.timeColor === "black" ? "white" : payload.timeColor, lastTimeStr: payload.timeStr} + state2.current = {lastColor: payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor, lastTimeStr: payload.timeStr} chronoText.current.textContent = payload.timeStr - chronoText.current.style.color = payload.timeColor === "black" ? "white" : payload.timeColor + chronoText.current.style.color = payload.timeColor === "#000000" ? "#ffffff" : payload.timeColor } const showScore = state.showScore ?? true; diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index 5821030..a74a8e3 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -153,3 +153,31 @@ export function timePrint(time, negSign = false) { String(min).padStart(2, '0') + ":" + String(sec).padStart(2, '0') } + +//create full hex +function fullHex (hex) { + let r = hex.slice(1,2); + let g = hex.slice(2,3); + let b = hex.slice(3,4); + + r = parseInt(r+r, 16); + g = parseInt(g+g, 16); + b = parseInt(b+b, 16); + + // return {r, g, b} + return { r, g, b }; +} + +//convert hex to rgb +export function hex2rgb (hex) { + if(hex.length === 4){ + return fullHex(hex); + } + + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + + // return {r, g, b} + return { r, g, b }; +}