From 6c6a5eb4c09da6c57d707652731a78938b54ddf2 Mon Sep 17 00:00:00 2001 From: Paul Sommer Date: Mon, 16 Mar 2026 21:33:40 +0100 Subject: [PATCH 1/5] feat: add own roundWinBox element and support for multiple roundWinBox sponsors --- .../src/app/observer/observer.component.html | 11 ++ .../src/app/observer/observer.component.ts | 43 ++++- .../app/roundwinbox/roundwinbox.component.css | 0 .../roundwinbox/roundwinbox.component.html | 165 ++++++++++++++++++ .../app/roundwinbox/roundwinbox.component.ts | 136 +++++++++++++++ angular/src/app/services/electron.service.ts | 2 + .../tournamentinfo.component.html | 4 - src/main.ts | 3 + src/preload.ts | 2 + src/services/connectorService.ts | 3 + src/services/formattingService.ts | 13 ++ 11 files changed, 375 insertions(+), 7 deletions(-) create mode 100644 angular/src/app/roundwinbox/roundwinbox.component.css create mode 100644 angular/src/app/roundwinbox/roundwinbox.component.html create mode 100644 angular/src/app/roundwinbox/roundwinbox.component.ts diff --git a/angular/src/app/observer/observer.component.html b/angular/src/app/observer/observer.component.html index 6121882..7e46c55 100644 --- a/angular/src/app/observer/observer.component.html +++ b/angular/src/app/observer/observer.component.html @@ -237,6 +237,17 @@ (validationChanged)="onPanelValidationChanged($event, 'tournamentinfoPanelId')" > + + + + + + ("roundWinBox") || this.roundWinBox; const loadedTimeoutInfo = this.localStorageService.getItem("timeouts"); if (loadedTimeoutInfo) { @@ -465,6 +478,7 @@ export class ObserverComponent implements OnInit { this.watermarkInfo, this.playercamsInfo, this.timeoutInfo, + this.roundWinBox, ); this.saveAllValues(); @@ -484,6 +498,7 @@ export class ObserverComponent implements OnInit { this.localStorageService.setItem("watermark", this.watermarkInfo); this.localStorageService.setItem("playercams", this.playercamsInfo); this.localStorageService.setItem("timeouts", this.timeoutInfo); + this.localStorageService.setItem("roundWinBox", this.roundWinBox); } protected onExtrasClick() { @@ -579,6 +594,7 @@ export class ObserverComponent implements OnInit { }, tournamentInfo: this.tournamentInfo, timeoutDuration: this.tournamentInfo.timeoutDuration, + roundWinBox: this.roundWinBox, sponsorInfo: this.sponsorInfo, }, }) @@ -776,14 +792,35 @@ export type TeamInfo = { }; export type TournamentInfo = { - // name just activates the round win box at this time - name: boolean; logoUrl: string; backdropUrl: string; timeoutDuration: number; showMappool: boolean; }; +export type RoundWinBox = { + type: "disabled" | "tournamentInfo" | "sponsors"; + sponsors: RoundWinBoxSponsors[]; +}; + +export type RoundWinBoxWonTeam = "all" | "left" | "right"; +export type RoundWinBoxRoundCeremonie = ( + | "all" + | "normal" + | "ace" + | "clutch" + | "teamAce" + | "flawless" + | "thrifty" +)[]; + +export type RoundWinBoxSponsors = { + wonTeam: RoundWinBoxWonTeam; + roundCeremonie: RoundWinBoxRoundCeremonie; + iconUrl: string; + backdropUrl: string; +}; + export type SeriesInfo = { enable: boolean; needed: number | null; diff --git a/angular/src/app/roundwinbox/roundwinbox.component.css b/angular/src/app/roundwinbox/roundwinbox.component.css new file mode 100644 index 0000000..e69de29 diff --git a/angular/src/app/roundwinbox/roundwinbox.component.html b/angular/src/app/roundwinbox/roundwinbox.component.html new file mode 100644 index 0000000..50a71c4 --- /dev/null +++ b/angular/src/app/roundwinbox/roundwinbox.component.html @@ -0,0 +1,165 @@ +
+
+

Round Win Box

+ + +

Recommended image size:

+

Logo: Any size with a 1:1 aspect ratio

+

Backdrop: 875x200 / 35:8 - 830x250 / 83:25 on Nobii's Style

+
+
+ + + + + + + + + + + + + @if (data.type == "tournamentInfo") { +

Configure Tournament Info in the Box above

+ } + + + +
+
+ @for (i of numSequence(data.sponsors.length + 1); track $index) { +
+
+
+ + + + + + + + +
+
+ + + + + + + + +
+
+
+ @if ($index < data.sponsors.length) { + + } +
+
+ } +
+ +
+ @if (data.sponsors.length > 0 && data.type == "sponsors") { + @for (item of data.sponsors; track $index) { +
+ @if (sponsorIconError[$index] || !data.sponsors[$index].iconUrl) { + + +

Make sure the link points directly to the image.

+

Valid links usually end in .png or .jpg.

+
+ } + +
+
+ @if (sponsorBackdropError[$index] || !data.sponsors[$index].backdropUrl) { + + +

Make sure the link points directly to the image.

+

Valid links usually end in .png or .jpg.

+
+ } + +
+ } + } @else { +
+ You can preview sponsor logos here. +
+ } +
+
+
+
diff --git a/angular/src/app/roundwinbox/roundwinbox.component.ts b/angular/src/app/roundwinbox/roundwinbox.component.ts new file mode 100644 index 0000000..0258bd2 --- /dev/null +++ b/angular/src/app/roundwinbox/roundwinbox.component.ts @@ -0,0 +1,136 @@ +import { AfterContentInit, Component, EventEmitter, Input, Output } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { BlockUIModule } from "primeng/blockui"; +import { FloatLabelModule } from "primeng/floatlabel"; +import { InputNumberModule } from "primeng/inputnumber"; +import { InputTextModule } from "primeng/inputtext"; +import { ToggleSwitchModule } from "primeng/toggleswitch"; +import { RoundWinBox } from "../observer/observer.component"; +import { Validatable, ValidationState } from "../services/validation.service"; +import { PopoverModule } from "primeng/popover"; +import { BlockableDiv } from "../blockablediv/blockablediv.component"; +import { Button } from "primeng/button"; +import { InputGroup } from "primeng/inputgroup"; +import { InputGroupAddon } from "primeng/inputgroupaddon"; +import { Select } from "primeng/select"; +import { MultiSelectModule } from "primeng/multiselect"; + +@Component({ + selector: "app-roundwinbox", + imports: [ + InputTextModule, + FormsModule, + FloatLabelModule, + ToggleSwitchModule, + InputNumberModule, + BlockUIModule, + PopoverModule, + BlockableDiv, + Button, + InputGroup, + InputGroupAddon, + Select, + MultiSelectModule, + ], + templateUrl: "./roundwinbox.component.html", + styleUrl: "./roundwinbox.component.css", +}) +export class RoundwinboxComponent implements Validatable, AfterContentInit { + @Input({ required: true }) + data!: RoundWinBox; + + @Input({ required: false }) + isSupporter: boolean = false; + + roundWinBoxTeams = ["all", "left", "right"]; + roundWinBoxRoundCeremonie = ["all", "normal", "ace", "clutch", "teamAce", "flawless", "thrifty"]; + + ngAfterContentInit(): void { + this.runValidation(); + } + + @Output() + validationChanged = new EventEmitter(); + runValidation() { + let valid: boolean = true; + if (this.data.type !== "sponsors") { + this.validationChanged.emit(ValidationState.OPTIONAL); + return; + } + + valid = valid && !this.sponsorIconError.includes(true) && !this.sponsorTypeError.includes(true); + + this.validationChanged.emit(valid ? ValidationState.VALID : ValidationState.INVALID); + } + + protected sponsorIconError: boolean[] = []; + protected sponsorBackdropError: boolean[] = []; + + protected sponsorTypeError: boolean[] = []; + + protected onSponsorDetailsClick(index: number) { + if (!this.data.sponsors[index]) { + this.data.sponsors[index] = { + wonTeam: "all", + roundCeremonie: ["all"], + iconUrl: "", + backdropUrl: "", + }; + } + + this.runValidation(); + } + + protected updateSponsor( + index: number, + field: "iconUrl" | "backdropUrl" | "wonTeam" | "roundCeremonie", + value: string, + ) { + if (!this.data.sponsors[index]) { + this.data.sponsors[index] = { + wonTeam: "all", + roundCeremonie: ["all"], + iconUrl: "", + backdropUrl: "", + }; + } + + this.data.sponsors[index][field] = value as any; + + this.runValidation(); + } + + protected onSponsorImageLoadError(index: number, type: "icon" | "backdrop") { + if (type == "icon") { + if (this.data.sponsors[index].iconUrl == "" || this.data.sponsors[index] == null) { + this.sponsorIconError[index] = false; + this.runValidation(); + return; + } else { + this.sponsorIconError[index] = true; + } + } + + if (type == "backdrop") { + if (this.data.sponsors[index].backdropUrl == "" || this.data.sponsors[index] == null) { + this.sponsorBackdropError[index] = false; + this.runValidation(); + return; + } else { + this.sponsorBackdropError[index] = true; + } + } + this.runValidation(); + } + + protected onSponsorImageLoadSuccess(index: number, type: "icon" | "backdrop") { + if (type == "icon") this.sponsorIconError[index] = false; + if (type == "backdrop") this.sponsorBackdropError[index] = false; + + this.runValidation(); + } + + numSequence(n: number): number[] { + return Array(n); + } +} diff --git a/angular/src/app/services/electron.service.ts b/angular/src/app/services/electron.service.ts index d2b9c05..e02783c 100644 --- a/angular/src/app/services/electron.service.ts +++ b/angular/src/app/services/electron.service.ts @@ -41,6 +41,7 @@ export class ElectronService { watermarkInfo: any, playercamsInfo: any, timeoutInfo: any, + roundWinBox: any, ) { this.api.processInputs( ingestIp, @@ -57,6 +58,7 @@ export class ElectronService { watermarkInfo, playercamsInfo, timeoutInfo, + roundWinBox, ); } diff --git a/angular/src/app/tournamentinfo/tournamentinfo.component.html b/angular/src/app/tournamentinfo/tournamentinfo.component.html index 13010e3..725e6f1 100644 --- a/angular/src/app/tournamentinfo/tournamentinfo.component.html +++ b/angular/src/app/tournamentinfo/tournamentinfo.component.html @@ -13,10 +13,6 @@

Tournament Info

-
- - -
ipcRenderer.send( "process-inputs", @@ -33,6 +34,7 @@ contextBridge.exposeInMainWorld("electronAPI", { watermarkInfo, playercamsInfo, timeoutInfo, + roundWinBox, ), processAuxInputs: (ingestIp: any, name: any) => ipcRenderer.send("process-aux-inputs", ingestIp, name), diff --git a/src/services/connectorService.ts b/src/services/connectorService.ts index 740452a..aafcf36 100644 --- a/src/services/connectorService.ts +++ b/src/services/connectorService.ts @@ -16,6 +16,7 @@ import { IAuxAuthenticationData, IFormattedAuxScoreboardTeam as IFormattedAuxScoreboardTeammate, IFormattedData, + IRoundWinBox, ISeedingInfo, ISeriesInfo, ITimeoutInfo, @@ -89,6 +90,7 @@ export class ConnectorService { watermarkInfo: WatermarkInfo, playercamsInfo: PlayercamsInfo, timeoutInfo: ITimeoutInfo, + roundWinBox: IRoundWinBox, win: Electron.Main.BrowserWindow, ) { if (RegExp("(http|https)://[^/]+:[0-9]+").test(ingestIp)) { @@ -201,6 +203,7 @@ export class ConnectorService { sponsorInfo: sponsorInfo, watermarkInfo: watermarkInfo, playercamsInfo: playercamsInfo, + roundWinBox: roundWinBox, }, }; this.ws.emit(SocketChannels.OBSERVER_LOGON, JSON.stringify(logonData)); diff --git a/src/services/formattingService.ts b/src/services/formattingService.ts index 6e3d8b3..98670b9 100644 --- a/src/services/formattingService.ts +++ b/src/services/formattingService.ts @@ -294,6 +294,7 @@ export interface IToolsData { sponsorInfo: SponsorInfo; watermarkInfo: WatermarkInfo; playercamsInfo: PlayercamsInfo; + roundWinBox: IRoundWinBox; } export type ISeriesInfo = { @@ -340,6 +341,18 @@ export type PlayercamsInfo = { endTime: number; }; +export type IRoundWinBox = { + type: "disabled" | "tournamentInfo" | "sponsors"; + sponsors: IRoundWinBoxSponsors[]; +}; + +export type IRoundWinBoxSponsors = { + wonTeam: "all" | "left" | "right"; + roundCeremonie: ("all" | "normal" | "ace" | "clutch" | "teamAce" | "flawless" | "thrifty")[]; + iconUrl: string; + backdropUrl: string; +}; + type BaseMapPoolInfo = { type: "past" | "present" | "future" | "error"; }; From 0a23e69f5e7dd30638a648cbadc5448b752ceccd Mon Sep 17 00:00:00 2001 From: Paul Sommer Date: Tue, 17 Mar 2026 19:58:43 +0100 Subject: [PATCH 2/5] feat: make roundWinBox multiple Sponsors Spectra+ only --- .../src/app/observer/observer.component.ts | 20 +- .../roundwinbox/roundwinbox.component.html | 243 ++++++++++-------- .../app/roundwinbox/roundwinbox.component.ts | 6 +- 3 files changed, 152 insertions(+), 117 deletions(-) diff --git a/angular/src/app/observer/observer.component.ts b/angular/src/app/observer/observer.component.ts index 95e0e77..1ec10a1 100644 --- a/angular/src/app/observer/observer.component.ts +++ b/angular/src/app/observer/observer.component.ts @@ -416,6 +416,22 @@ export class ObserverComponent implements OnInit { this.electron.confirmCloseDecision(decided === true); } + protected roundWinBoxCheckSpectraPlus() { + if (this.isSupporter) return this.roundWinBox; + if (this.roundWinBox.type != "sponsors") + return { + type: this.roundWinBox.type, + sponsors: [], + }; + const roundWinBoxFirstSponsor = this.roundWinBox.sponsors[0]; + roundWinBoxFirstSponsor.roundCeremonie = ["all"]; + roundWinBoxFirstSponsor.wonTeam = "all"; + return { + type: this.roundWinBox.type, + sponsors: [roundWinBoxFirstSponsor], + }; + } + protected onConnectClick() { if (this.tryingToConnect) return; @@ -478,7 +494,7 @@ export class ObserverComponent implements OnInit { this.watermarkInfo, this.playercamsInfo, this.timeoutInfo, - this.roundWinBox, + this.roundWinBoxCheckSpectraPlus(), ); this.saveAllValues(); @@ -594,7 +610,7 @@ export class ObserverComponent implements OnInit { }, tournamentInfo: this.tournamentInfo, timeoutDuration: this.tournamentInfo.timeoutDuration, - roundWinBox: this.roundWinBox, + roundWinBox: this.roundWinBoxCheckSpectraPlus(), sponsorInfo: this.sponsorInfo, }, }) diff --git a/angular/src/app/roundwinbox/roundwinbox.component.html b/angular/src/app/roundwinbox/roundwinbox.component.html index 50a71c4..02d2af3 100644 --- a/angular/src/app/roundwinbox/roundwinbox.component.html +++ b/angular/src/app/roundwinbox/roundwinbox.component.html @@ -23,136 +23,151 @@

Round Win Box

- @if (data.type == "tournamentInfo") { -

Configure Tournament Info in the Box above

- } +

+ Configure Tournament Info in the Box above. +

+

+ Subscribe to Spectra+ to be able to add more than one sponsor. +

@for (i of numSequence(data.sponsors.length + 1); track $index) { -
-
-
- - - - - - - - + @if ($index == 0 || $index > 0 && isSupporter) { +
+
+
+ + + + + + + + +
+
+ + + + + + + + +
-
- - + @if ($index < data.sponsors.length) { + - - - - - - + icon="pi pi-times" + class="h-full" + styleClass="w-full h-full" + severity="danger" + (onClick)="this.data.sponsors.splice($index, 1); this.sponsorIconError.splice($index, 1); this.sponsorBackdropError.splice($index, 1);" + /> + }
-
- @if ($index < data.sponsors.length) { - - } -
-
+ } }
@if (data.sponsors.length > 0 && data.type == "sponsors") { @for (item of data.sponsors; track $index) { -
- @if (sponsorIconError[$index] || !data.sponsors[$index].iconUrl) { - - -

Make sure the link points directly to the image.

-

Valid links usually end in .png or .jpg.

-
- } - -
-
- @if (sponsorBackdropError[$index] || !data.sponsors[$index].backdropUrl) { - - -

Make sure the link points directly to the image.

-

Valid links usually end in .png or .jpg.

-
- } - -
+ @if ($index == 0 || $index > 0 && isSupporter) { +
+ @if (sponsorIconError[$index] || !data.sponsors[$index].iconUrl) { + + +

Make sure the link points directly to the image.

+

Valid links usually end in .png or .jpg.

+
+ } + +
+
+ @if (sponsorBackdropError[$index] || !data.sponsors[$index].backdropUrl) { + + +

Make sure the link points directly to the image.

+

Valid links usually end in .png or .jpg.

+
+ } + +
+ } } } @else {
diff --git a/angular/src/app/roundwinbox/roundwinbox.component.ts b/angular/src/app/roundwinbox/roundwinbox.component.ts index 0258bd2..7a3b093 100644 --- a/angular/src/app/roundwinbox/roundwinbox.component.ts +++ b/angular/src/app/roundwinbox/roundwinbox.component.ts @@ -58,7 +58,11 @@ export class RoundwinboxComponent implements Validatable, AfterContentInit { return; } - valid = valid && !this.sponsorIconError.includes(true) && !this.sponsorTypeError.includes(true); + valid = + valid && + !this.sponsorIconError.includes(true) && + !this.sponsorBackdropError.includes(true) && + !this.sponsorTypeError.includes(true); this.validationChanged.emit(valid ? ValidationState.VALID : ValidationState.INVALID); } From 7a93c4b5534c5aa66b7c282ef7fa811a366535ca Mon Sep 17 00:00:00 2001 From: Paul Sommer Date: Wed, 18 Mar 2026 13:00:44 +0100 Subject: [PATCH 3/5] remove tournamentInfo.name from all Types --- src/services/formattingService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/formattingService.ts b/src/services/formattingService.ts index 98670b9..a116def 100644 --- a/src/services/formattingService.ts +++ b/src/services/formattingService.ts @@ -310,7 +310,6 @@ export type ISeedingInfo = { }; export type ITournamentInfo = { - name: string; logoUrl: string; backdropUrl: string; timeoutDuration: number; From 5712689f690c19c57fde0c5e3e62719384ad844c Mon Sep 17 00:00:00 2001 From: Paul Sommer Date: Wed, 18 Mar 2026 23:34:49 +0100 Subject: [PATCH 4/5] feat: improve separation of roundWinBox sponsors --- .../roundwinbox/roundwinbox.component.html | 179 +++++++++--------- .../app/roundwinbox/roundwinbox.component.ts | 13 -- 2 files changed, 86 insertions(+), 106 deletions(-) diff --git a/angular/src/app/roundwinbox/roundwinbox.component.html b/angular/src/app/roundwinbox/roundwinbox.component.html index 02d2af3..c4ededf 100644 --- a/angular/src/app/roundwinbox/roundwinbox.component.html +++ b/angular/src/app/roundwinbox/roundwinbox.component.html @@ -38,93 +38,85 @@

Round Win Box

-
-
- @for (i of numSequence(data.sponsors.length + 1); track $index) { - @if ($index == 0 || $index > 0 && isSupporter) { -
-
-
- - - - - - - - -
-
- - - - - - - - -
-
-
- @if ($index < data.sponsors.length) { - + @for (i of numSequence(data.sponsors.length + 1); track $index) { + @if ($index == 0 || $index > 0 && isSupporter) { +
+
+
+ + - } + + + + + + +
+
+ + + + + + + +
- } - } -
- -
- @if (data.sponsors.length > 0 && data.type == "sponsors") { - @for (item of data.sponsors; track $index) { - @if ($index == 0 || $index > 0 && isSupporter) { +
+ @if ($index < data.sponsors.length) { + + } +
+
+ @if ($index < data.sponsors.length) { +
@if (sponsorIconError[$index] || !data.sponsors[$index].iconUrl) { Round Win Box }
@if (sponsorBackdropError[$index] || !data.sponsors[$index].backdropUrl) { Round Win Box }
- } +
+ } + @if ($index < data.sponsors.length) { +
} - } @else { -
- You can preview sponsor logos here. -
} -
+ }
diff --git a/angular/src/app/roundwinbox/roundwinbox.component.ts b/angular/src/app/roundwinbox/roundwinbox.component.ts index 7a3b093..5616427 100644 --- a/angular/src/app/roundwinbox/roundwinbox.component.ts +++ b/angular/src/app/roundwinbox/roundwinbox.component.ts @@ -72,19 +72,6 @@ export class RoundwinboxComponent implements Validatable, AfterContentInit { protected sponsorTypeError: boolean[] = []; - protected onSponsorDetailsClick(index: number) { - if (!this.data.sponsors[index]) { - this.data.sponsors[index] = { - wonTeam: "all", - roundCeremonie: ["all"], - iconUrl: "", - backdropUrl: "", - }; - } - - this.runValidation(); - } - protected updateSponsor( index: number, field: "iconUrl" | "backdropUrl" | "wonTeam" | "roundCeremonie", From 0b9c5d99a74832ded64dbf3752c2dae7c2119eb8 Mon Sep 17 00:00:00 2001 From: Paul Sommer Date: Thu, 19 Mar 2026 20:10:42 +0100 Subject: [PATCH 5/5] feat: check for duplicate roundCeremonies in round win box sponsors --- .../roundwinbox/roundwinbox.component.html | 6 +++-- .../app/roundwinbox/roundwinbox.component.ts | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/angular/src/app/roundwinbox/roundwinbox.component.html b/angular/src/app/roundwinbox/roundwinbox.component.html index c4ededf..e13ec82 100644 --- a/angular/src/app/roundwinbox/roundwinbox.component.html +++ b/angular/src/app/roundwinbox/roundwinbox.component.html @@ -80,7 +80,8 @@

Round Win Box

[options]="roundWinBoxTeams" [ngModel]="data.sponsors[$index]?.wonTeam" (ngModelChange)="updateSponsor($index, 'wonTeam', $event)" - [class.ng-invalid]="sponsorTypeError[$index]" + [class.ng-dirty]="$index < data.sponsors.length ? sponsorTypeError[$index] : false" + [class.ng-invalid]="$index < data.sponsors.length ? sponsorTypeError[$index] : false" > @@ -95,7 +96,8 @@

Round Win Box

[maxSelectedLabels]="3" [filter]="false" [showToggleAll]="false" - [class.ng-invalid]="sponsorTypeError[$index]" + [class.ng-dirty]="$index < data.sponsors.length ? sponsorTypeError[$index] : false" + [class.ng-invalid]="$index < data.sponsors.length ? sponsorTypeError[$index] : false" > diff --git a/angular/src/app/roundwinbox/roundwinbox.component.ts b/angular/src/app/roundwinbox/roundwinbox.component.ts index 5616427..627a755 100644 --- a/angular/src/app/roundwinbox/roundwinbox.component.ts +++ b/angular/src/app/roundwinbox/roundwinbox.component.ts @@ -58,6 +58,32 @@ export class RoundwinboxComponent implements Validatable, AfterContentInit { return; } + this.sponsorTypeError = []; + + for (let i = 0; i < this.data.sponsors.length; i++) { + const sponsor = this.data.sponsors[i]; + for (let j = 0; j < this.data.sponsors.length; j++) { + if (i != j) { + const spons = this.data.sponsors[j]; + if ( + sponsor.wonTeam == spons.wonTeam || + sponsor.wonTeam == "all" || + spons.wonTeam == "all" + ) { + for (const ceremonie of sponsor.roundCeremonie) { + if ( + spons.roundCeremonie.includes(ceremonie) || + (ceremonie == "all" && spons.roundCeremonie.length > 0) + ) { + this.sponsorTypeError[i] = true; + this.sponsorTypeError[j] = true; + } + } + } + } + } + } + valid = valid && !this.sponsorIconError.includes(true) &&