diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index 464c8dda..9125eed9 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -106,6 +106,11 @@ const routes: Routes = [
loadComponent: () =>
import("./overlays/map-breakdown/map-breakdown").then((m) => m.MapBreakdown),
},
+ {
+ path: "toast",
+ loadComponent: () =>
+ import("./components/shopping/toast/toast-component").then((m) => m.ToastComponent),
+ },
];
@NgModule({
diff --git a/src/app/components/shopping/toast/toast-component.css b/src/app/components/shopping/toast/toast-component.css
new file mode 100644
index 00000000..c42b578f
--- /dev/null
+++ b/src/app/components/shopping/toast/toast-component.css
@@ -0,0 +1,36 @@
+@keyframes swish {
+ from {
+ clip-path: inset(0 100% 0 0);
+ }
+ to {
+ clip-path: inset(0 0 0 0);
+ }
+}
+
+.animate-swish {
+ animation: swish 300ms ease-in-out forwards;
+ will-change: clip-path, width;
+}
+
+.animate-swish-out {
+ animation: swish 300ms ease-in-out reverse forwards;
+ will-change: clip-path, width;
+}
+
+.bg-fade-defender {
+ background: linear-gradient(
+ to right,
+ rgba(var(--defender-color-rgb), 0.5) 10%,
+ rgba(var(--defender-color-rgb), 0.25) 40%,
+ transparent
+ );
+}
+
+.bg-fade-attacker {
+ background: linear-gradient(
+ to right,
+ rgba(var(--attacker-color-rgb), 0.5) 10%,
+ rgba(var(--attacker-color-rgb), 0.25) 40%,
+ transparent
+ );
+}
diff --git a/src/app/components/shopping/toast/toast-component.html b/src/app/components/shopping/toast/toast-component.html
new file mode 100644
index 00000000..f5d40b64
--- /dev/null
+++ b/src/app/components/shopping/toast/toast-component.html
@@ -0,0 +1,55 @@
+@if(this.dataModel.match().roundPhase === "shopping") {
+
+
+
+
+
+ @if (toastInfo().selectedTeam !== "none") {
+
+
![]()
+
+ } @else {
+
+ }
+
+ @if(toastInfo().title !== "") {
+
+ {{ toastInfo().title }}
+
+ }
+
+ {{ toastInfo().message }}
+
+
+ @if (toastInfo().eventLogoEnabled) {
+
+
![]()
+
+ } @else {
+
+ }
+
+
+
+
+
+}
diff --git a/src/app/components/shopping/toast/toast-component.ts b/src/app/components/shopping/toast/toast-component.ts
new file mode 100644
index 00000000..dcb275ec
--- /dev/null
+++ b/src/app/components/shopping/toast/toast-component.ts
@@ -0,0 +1,63 @@
+import { Component, computed, effect, inject, signal } from "@angular/core";
+import { DataModelService } from "../../../services/dataModel.service";
+
+@Component({
+ selector: "app-toast",
+ imports: [],
+ templateUrl: "./toast-component.html",
+ styleUrl: "./toast-component.css",
+})
+export class ToastComponent {
+ dataModel = inject(DataModelService);
+
+ toastInfo = signal(Object.assign({}, this.dataModel.toastInfo()));
+
+ shouldHide = computed(
+ () => !this.dataModel.toastInfo().active || !(this.dataModel.match().roundPhase === "shopping"),
+ );
+
+ teamSite = computed(() => {
+ if (this.dataModel.match().teams[this.toastInfo().selectedTeam == "left" ? 0 : 1].isAttacking) {
+ return "attacker";
+ } else {
+ return "defender";
+ }
+ });
+
+ tournamentIconUrl = computed(() => {
+ const logo = this.dataModel.tournamentInfo().logoUrl;
+ if (logo && logo !== "") return logo;
+ else return "assets/misc/logo.webp";
+ });
+
+ hide = true;
+ inAnimation = false;
+ outAnimation = false;
+
+ hideAnimationEffect = effect(() => {
+ if (this.shouldHide()) {
+ this.outAnimation = true;
+ setTimeout(() => {
+ this.outAnimation = false;
+ this.hide = true;
+ }, 300);
+ } else {
+ this.outAnimation = false;
+ this.hide = false;
+ this.inAnimation = true;
+ setTimeout(() => {
+ this.inAnimation = false;
+ }, 300);
+ }
+ });
+
+ delayEndToastEffect = effect(() => {
+ if (this.shouldHide()) {
+ setTimeout(() => {
+ this.toastInfo.set(Object.assign({}, this.dataModel.toastInfo()));
+ }, 350);
+ } else {
+ this.toastInfo.set(Object.assign({}, this.dataModel.toastInfo()));
+ }
+ });
+}
diff --git a/src/app/overlays/match-overlay/match-overlay.component.html b/src/app/overlays/match-overlay/match-overlay.component.html
index 28f97d8a..f37bdb56 100644
--- a/src/app/overlays/match-overlay/match-overlay.component.html
+++ b/src/app/overlays/match-overlay/match-overlay.component.html
@@ -20,3 +20,5 @@
+
+
diff --git a/src/app/overlays/match-overlay/match-overlay.component.ts b/src/app/overlays/match-overlay/match-overlay.component.ts
index b035faa1..01ff7481 100644
--- a/src/app/overlays/match-overlay/match-overlay.component.ts
+++ b/src/app/overlays/match-overlay/match-overlay.component.ts
@@ -15,6 +15,7 @@ import { EndroundBannerComponent } from "../../components/combat/endround-banner
import { TimeoutComponent } from "../../components/common/timeout/timeout.component";
import { TopBackgroundComponent } from "../../components/top/match/background/background.component";
import { OneVersusOneComponent } from "../../components/combat/1v1/1v1.component";
+import { ToastComponent } from "../../components/shopping/toast/toast-component";
@Component({
selector: "app-match-overlay",
@@ -34,6 +35,7 @@ import { OneVersusOneComponent } from "../../components/combat/1v1/1v1.component
TimeoutComponent,
TopBackgroundComponent,
OneVersusOneComponent,
+ ToastComponent,
],
templateUrl: "./match-overlay.component.html",
styleUrl: "./match-overlay.component.css",
diff --git a/src/app/overlays/testing-agent-select/testing-agent-select.component.ts b/src/app/overlays/testing-agent-select/testing-agent-select.component.ts
index c2ef93ae..2fc2756a 100644
--- a/src/app/overlays/testing-agent-select/testing-agent-select.component.ts
+++ b/src/app/overlays/testing-agent-select/testing-agent-select.component.ts
@@ -90,6 +90,14 @@ export class TestingAgentSelectComponent implements OnInit {
sponsors: [],
},
},
+ toastInfo: {
+ active: false,
+ duration: 10000,
+ title: "",
+ message: "",
+ selectedTeam: "none",
+ eventLogoEnabled: false,
+ },
timeoutState: {
techPause: false,
leftTeam: false,
diff --git a/src/app/overlays/testing/testing.component.html b/src/app/overlays/testing/testing.component.html
index ca40412a..76e93a59 100644
--- a/src/app/overlays/testing/testing.component.html
+++ b/src/app/overlays/testing/testing.component.html
@@ -17,6 +17,7 @@
+
diff --git a/src/app/overlays/testing/testing.component.ts b/src/app/overlays/testing/testing.component.ts
index 8155ecc6..7a4b202e 100644
--- a/src/app/overlays/testing/testing.component.ts
+++ b/src/app/overlays/testing/testing.component.ts
@@ -11,6 +11,7 @@ import { IMatchData } from "../../services/Types";
})
export class TestingComponent implements OnInit {
dataModel = inject(DataModelService);
+ private toastTimerRef?: ReturnType;
match: IMatchData = initialMatchData;
bgCounter = 1;
@@ -99,6 +100,14 @@ export class TestingComponent implements OnInit {
sponsors: [],
},
},
+ toastInfo: {
+ active: false,
+ duration: 10000,
+ title: "",
+ message: "",
+ selectedTeam: "none",
+ eventLogoEnabled: false,
+ },
teams: [
{
players: [
@@ -815,4 +824,45 @@ export class TestingComponent implements OnInit {
return ret;
});
}
+
+ showToast() {
+ const currentlyActive = this.dataModel.match().toastInfo.active;
+
+ if (currentlyActive) {
+ // Deactivate immediately (mimics pressing the hotkey again)
+ clearTimeout(this.toastTimerRef);
+ this.toastTimerRef = undefined;
+ this.dataModel.match.update((v) => {
+ const ret = v;
+ ret.toastInfo.active = false;
+ return ret;
+ });
+ } else {
+ // Activate
+ this.dataModel.match.update((v) => {
+ const ret = v;
+ ret.toastInfo.active = true;
+ ret.toastInfo.title = "Spezial Spectra Developer Announcement";
+ ret.toastInfo.message =
+ "This is a live toast preview with the biggest changes. Thanks for using Spectra!";
+ ret.toastInfo.selectedTeam = "left";
+ ret.toastInfo.duration = null;
+ ret.toastInfo.eventLogoEnabled = true;
+ return ret;
+ });
+
+ const duration = this.dataModel.match().toastInfo.duration;
+ if (duration !== null) {
+ clearTimeout(this.toastTimerRef);
+ this.toastTimerRef = setTimeout(() => {
+ this.dataModel.match.update((v) => {
+ const ret = v;
+ ret.toastInfo.active = false;
+ return ret;
+ });
+ this.toastTimerRef = undefined;
+ }, duration);
+ }
+ }
+ }
}
diff --git a/src/app/services/Types.ts b/src/app/services/Types.ts
index 597a53e9..fec08596 100644
--- a/src/app/services/Types.ts
+++ b/src/app/services/Types.ts
@@ -14,6 +14,7 @@ export interface IMatchData {
switchRound: number;
firstOtRound: number;
attackersWon: boolean;
+ toastInfo: IToastInfo;
}
export interface ITeamData {
@@ -84,6 +85,15 @@ export interface ITimeoutState {
timeRemaining: number;
}
+export interface IToastInfo {
+ active: boolean;
+ title: string;
+ message: string;
+ duration: number | null;
+ eventLogoEnabled: boolean;
+ selectedTeam: "left" | "right" | "none";
+}
+
//#endregion
//#region Tools
diff --git a/src/app/services/dataModel.service.ts b/src/app/services/dataModel.service.ts
index 43150052..846ba673 100644
--- a/src/app/services/dataModel.service.ts
+++ b/src/app/services/dataModel.service.ts
@@ -106,6 +106,7 @@ export class DataModelService {
public readonly sponsorInfo = computed(() => this.match().tools.sponsorInfo);
public readonly watermarkInfo = computed(() => this.match().tools.watermarkInfo);
public readonly tournamentInfo = computed(() => this.match().tools.tournamentInfo);
+ public toastInfo = computed(() => this.match().toastInfo, { equal: () => false });
public readonly playercamsInfo = computed(() => this.match().tools.playercamsInfo, {
equal: () => false,
});
@@ -195,6 +196,14 @@ export const initialMatchData: IMatchData = {
sponsors: [],
},
},
+ toastInfo: {
+ active: false,
+ duration: 10000,
+ title: "",
+ message: "",
+ eventLogoEnabled: true,
+ selectedTeam: "none",
+ },
timeoutState: {
techPause: false,
leftTeam: false,