Skip to content

Commit

Permalink
Add broadcast customisation to the dashboard (highlight {team,hero,pl…
Browse files Browse the repository at this point in the history
…ayer})
  • Loading branch information
slmnio committed Jun 14, 2024
1 parent 983f591 commit 036b81c
Show file tree
Hide file tree
Showing 4 changed files with 333 additions and 5 deletions.
53 changes: 49 additions & 4 deletions server/src/actions/update-broadcast.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,32 @@ const { safeInput, safeInputNoQuotes } = require("../action-utils/action-utils")
module.exports = {
key: "update-broadcast",
auth: ["client"],
optionalParams: ["match", "advertise", "playerCams", "mapAttack", "title", "manualGuests", "deskDisplayMode", "deskDisplayText", "showLiveMatch", "countdownEnd"],
optionalParams: ["match", "advertise", "playerCams", "mapAttack", "title", "manualGuests", "deskDisplayMode", "deskDisplayText", "showLiveMatch", "countdownEnd", "highlightTeamID", "highlightHeroID", "highlightPlayerID"],
/***
* @param {AnyAirtableID} match
* @param {ClientData} client
* @returns {Promise<void>}
*/
// eslint-disable-next-line no-empty-pattern
async handler({ match: matchID, advertise, playerCams, mapAttack, title, manualGuests, deskDisplayMode, deskDisplayText, showLiveMatch, countdownEnd }, { client }) {
async handler({
match: matchID,
advertise,
playerCams,
mapAttack,
title,
manualGuests,
deskDisplayMode,
deskDisplayText,
showLiveMatch,
countdownEnd,
highlightTeamID,
highlightHeroID,
highlightPlayerID
}, { client }) {
let broadcast = await this.helpers.get(client?.broadcast?.[0]);
if (!broadcast) throw ("No broadcast associated");

console.log({ matchID, advertise, playerCams, mapAttack, title, manualGuests, deskDisplayMode, deskDisplayText, showLiveMatch });
console.log({ matchID, advertise, playerCams, mapAttack, title, manualGuests, deskDisplayMode, deskDisplayText, showLiveMatch, highlightTeamID, highlightHeroID, highlightPlayerID });
let validatedData = {};

if (matchID !== undefined) {
Expand All @@ -23,9 +37,40 @@ module.exports = {
} else {
let match = await this.helpers.get(matchID);
if (!match) throw ("Unknown match");
if (match.__tableName !== "Matches") throw ("Live match object is not a Match");
validatedData["Live Match"] = [ match.id ];
}
}
if (highlightTeamID !== undefined) {
if (highlightTeamID === null) {
validatedData["Highlight Team"] = null;
} else {
let team = await this.helpers.get(highlightTeamID);
if (!team) throw ("Unknown highlight team");
if (team.__tableName !== "Teams") throw ("Highlight team object is not a Team");
validatedData["Highlight Team"] = [ team.id ];
}
}
if (highlightHeroID !== undefined) {
if (highlightHeroID === null) {
validatedData["Highlight Hero"] = null;
} else {
let hero = await this.helpers.get(highlightHeroID);
if (!hero) throw ("Unknown highlight hero");
if (hero.__tableName !== "Heroes") throw ("Highlight hero object is not a Hero");
validatedData["Highlight Hero"] = [ hero.id ];
}
}
if (highlightPlayerID !== undefined) {
if (highlightPlayerID === null) {
validatedData["Highlight Player"] = null;
} else {
let player = await this.helpers.get(highlightPlayerID);
if (!player) throw ("Unknown highlight player");
if (player.__tableName !== "Players") throw ("Highlight player object is not a Player");
validatedData["Highlight Player"] = [ player.id ];
}
}
if (mapAttack !== undefined) {
let eligibleSides = [null, "Left", "Right", "Both"];
if (!eligibleSides.includes(mapAttack)) throw ("Invalid side");
Expand All @@ -41,7 +86,7 @@ module.exports = {
validatedData["Show Cams"] = !!playerCams;
}
if (title !== undefined) {
validatedData["Title"] = safeInput(title);
validatedData["Title"] = safeInputNoQuotes(title);
}
if (manualGuests !== undefined) {
validatedData["Manual Guests"] = safeInput(manualGuests);
Expand Down
277 changes: 277 additions & 0 deletions website/src/components/website/dashboard/BroadcastCustomisation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
<template>
<div class="broadcast-customisation p-2 d-flex flex-column gap-3">
<b-form-checkbox v-model="changeInstantly" switch>Change instantly</b-form-checkbox>

<div class="form-row row flex-nowrap flex-center">
<div class="label text-nowrap flex-shrink-0 fw-bold col-2">Highlight team</div>
<div class="col-10 d-flex gap-2">
<theme-logo
:theme="hydratedBroadcast?.highlight_team?.theme"
logo-size="w-100"
class="preview-theme flex-shrink-0"
border-width=".3em"
icon-padding=".2em" />
<b-form-select
v-model="selectedHighlightTeamID"
:class="{'low-opacity': processing.highlight_team}"
:disabled="processing.highlight_team"
class="opacity-changes"
:options="teams"
@update:model-value="t => setTeam(t, changeInstantly)" />
<b-button
:class="{'low-opacity': processing.highlight_team}"
class="opacity-changes text-nowrap flex-shrink-0 disabled-low-opacity"
:disabled="changeInstantly || processing.highlight_team"
variant="success"
@click="setTeam(selectedHighlightTeamID, true)">
<i class="fas fa-save fa-fw"></i> {{ changeInstantly ? "Autosave" : "Save" }}
</b-button>
<b-button
:class="{'low-opacity': processing.highlight_team}"
:disabled="processing.highlight_team"
class="opacity-changes text-nowrap flex-shrink-0"
variant="danger"
@click="setTeam(null, true)">
<i class="fas fa-times fa-fw"></i> Clear
</b-button>
</div>
</div>


<div class="form-row row flex-nowrap flex-center">
<div class="label text-nowrap flex-shrink-0 fw-bold col-2">Highlight hero</div>
<div class="col-10 d-flex gap-2">
<div class="hero-image flex-shrink-0 bg-center" :style="resizedImage(hydratedBroadcast.highlight_hero, ['main_image'], 'h-76')"></div>
<b-form-select
v-model="selectedHighlightHeroID"
:class="{'low-opacity': processing.highlight_hero}"
:disabled="processing.highlight_hero"
class="opacity-changes"
:options="heroes"
@update:model-value="h => setHero(h, changeInstantly)" />
<b-button
:class="{'low-opacity': processing.highlight_hero}"
class="opacity-changes text-nowrap flex-shrink-0 disabled-low-opacity"
:disabled="changeInstantly || processing.highlight_hero"
variant="success"
@click="setHero(selectedHighlightHeroID, true)">
<i class="fas fa-save fa-fw"></i> {{ changeInstantly ? "Autosave" : "Save" }}
</b-button>
<b-button
:class="{'low-opacity': processing.highlight_hero}"
:disabled="processing.highlight_hero"
class="opacity-changes text-nowrap flex-shrink-0"
variant="danger"
@click="setHero(null, true)">
<i class="fas fa-times fa-fw"></i> Clear
</b-button>
</div>
</div>


<div class="form-row row flex-nowrap flex-center">
<div class="label text-nowrap flex-shrink-0 fw-bold col-2">Highlight player</div>
<div class="col-10 d-flex gap-2">
<div class="flex-shrink-0 bg-center player-name text-center flex-center">
<div>{{ hydratedBroadcast.highlight_player?.name }}</div>
</div>
<b-form-select
v-model="selectedHighlightPlayerID"
:class="{'low-opacity': processing.highlight_player}"
:disabled="processing.highlight_player"
class="opacity-changes"
:options="players"
@update:model-value="h => setPlayer(h, changeInstantly)" />
<b-button
:class="{'low-opacity': processing.highlight_player}"
class="opacity-changes text-nowrap flex-shrink-0 disabled-low-opacity"
:disabled="changeInstantly || processing.highlight_player"
variant="success"
@click="setPlayer(selectedHighlightPlayerID, true)">
<i class="fas fa-save fa-fw"></i> {{ changeInstantly ? "Autosave" : "Save" }}
</b-button>
<b-button
:class="{'low-opacity': processing.highlight_player}"
:disabled="processing.highlight_player"
class="opacity-changes text-nowrap flex-shrink-0"
variant="danger"
@click="setPlayer(null, true)">
<i class="fas fa-times fa-fw"></i> Clear
</b-button>
</div>
</div>
</div>
</template>

<script>
import { ReactiveArray, ReactiveRoot, ReactiveThing } from "@/utils/reactive";
import ThemeLogo from "@/components/website/ThemeLogo.vue";
import { authenticatedRequest } from "@/utils/dashboard";
import { sortAlpha } from "@/utils/sorts";
import { resizedImage } from "@/utils/images";
export default {
name: "BroadcastCustomisation",
components: { ThemeLogo },
props: ["broadcast"],
data: () => ({
changeInstantly: false,
selectedHighlightTeamID: null,
selectedHighlightHeroID: null,
selectedHighlightPlayerID: null,
processing: {
}
}),
computed: {
hydratedBroadcast() {
if (!this.broadcast?.id) return null;
return ReactiveRoot(this.broadcast.id, {
"event": ReactiveThing("event", {
"teams": ReactiveArray("teams", {
"theme": ReactiveThing("theme"),
"players": ReactiveArray("players")
})
}),
"highlight_team": ReactiveThing("highlight_team", {
"theme": ReactiveThing("theme")
}),
"highlight_hero": ReactiveThing("highlight_hero"),
"highlight_player": ReactiveThing("highlight_player")
});
},
teams() {
if (!this.hydratedBroadcast?.event?.id) return [
{
value: null,
text: "No team"
}
];
return [
{
value: null,
text: "No team"
},
...(this.hydratedBroadcast?.event?.teams || []).map(t => ({
text: t.name,
value: t.id
}))
];
},
heroes() {
const heroes = (ReactiveRoot("Heroes", {
"ids": ReactiveArray("ids")
})?.ids || []);
return [
{ value: null, text: "No hero" },
...["DPS", "Tank", "Support"].map(key => ({
text: key,
options: heroes.filter(h => h.role === key).sort((a,b) => sortAlpha(a?.name, b?.name)).map(h => ({
text: h.name,
value: h.id
}))
}))
];
},
players() {
if (!this.hydratedBroadcast?.event?.id) return [
{ value: null, text: "No player" }
];
return [
{ value: null, text: "No player" },
...(this.hydratedBroadcast?.event?.teams || []).map(team => ({
text: team?.name,
options: (team.players || []).sort((a,b) => sortAlpha(a,b,"name")).map(p => ({
text: p.name,
value: p.id
}))
})).sort((a,b) => sortAlpha(a,b,"text"))
];
}
},
methods: {
resizedImage,
async setTeam(teamID, instant) {
if (!instant) return;
this.processing.highlight_team = true;
try {
await authenticatedRequest("actions/update-broadcast", {
highlightTeamID: teamID
});
} finally {
this.processing.highlight_team = false;
}
},
async setHero(heroID, instant) {
if (!instant) return;
this.processing.highlight_hero = true;
try {
await authenticatedRequest("actions/update-broadcast", {
highlightHeroID: heroID
});
} finally {
this.processing.highlight_hero = false;
}
},
async setPlayer(playerID, instant) {
if (!instant) return;
this.processing.highlight_player = true;
try {
await authenticatedRequest("actions/update-broadcast", {
highlightPlayerID: playerID
});
} finally {
this.processing.highlight_player = false;
}
}
},
watch: {
"hydratedBroadcast.highlight_team": {
immediate: true,
handler(team) {
console.log("hydrate update", team);
this.selectedHighlightTeamID = team?.id || null;
}
},
"hydratedBroadcast.highlight_hero": {
immediate: true,
handler(hero) {
console.log("hydrate update", hero);
this.selectedHighlightHeroID = hero?.id || null;
}
},
"hydratedBroadcast.highlight_player": {
immediate: true,
handler(player) {
console.log("hydrate update", player);
this.selectedHighlightPlayerID = player?.id || null;
}
}
},
};
</script>
<style scoped>
.preview-theme, .hero-image, .player-name {
height: 2.5em;
width: 3em;
background-color: rgba(255, 255, 255, 0.1)
}
.player-name {
width: 6em;
}
.player-name div {
font-size: .75em;
}
.opacity-changes {
opacity: 1;
transition: opacity .3s ease;
}
.low-opacity, [disabled].disabled-low-opacity {
opacity: 0.5;
pointer-events: none;
user-select: none;
cursor: wait;
}
</style>
2 changes: 2 additions & 0 deletions website/src/utils/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ interface UpdateBroadcastData {
deskDisplayMode?: null | "Match" | "Predictions" | "Maps" | "Notice (Team 1)" | "Notice (Team 2)" | "Notice (Event)" | "Scoreboard" | "Drafted Maps" | "Interview" | "Hidden" | "Casters"
deskDisplayText?: string
countdownEnd?: any
highlightTeamID?: AnyAirtableID
highlightHeroID?: AnyAirtableID
}

interface UpdateGfxIndexData {
Expand Down
6 changes: 5 additions & 1 deletion website/src/views/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@
<DashboardModule v-if="useTeamComms" class="mb-2" icon-class="fas fa-microphone" title="Team Comms Listen-In">
<CommsControls :match="liveMatch" />
</DashboardModule>
<DashboardModule v-if="broadcast?.event" class="mb-2" icon-class="fas fa-paint-brush" title="Customisation">
<BroadcastCustomisation :broadcast="broadcast" />
</DashboardModule>
</div>
</template>

Expand Down Expand Up @@ -134,10 +137,11 @@ import ThemeLogo from "@/components/website/ThemeLogo.vue";
import GFXController from "@/views/GFXController.vue";
import BroadcastRoles from "@/components/website/dashboard/BroadcastRoles.vue";
import { useAuthStore } from "@/stores/authStore";
import BroadcastCustomisation from "@/components/website/dashboard/BroadcastCustomisation.vue";
export default {
name: "Dashboard",
components: { GFXController, BroadcastRoles, ThemeLogo, DeskTextEditor, DeskEditor, Bracket, PreviewProgramDisplay, BracketImplications, DashboardModule, DashboardClock, ScheduleEditor, BroadcastEditor, CommsControls, Commercials, Predictions, MatchEditor, MatchThumbnail, BroadcastSwitcher },
components: { BroadcastCustomisation, GFXController, BroadcastRoles, ThemeLogo, DeskTextEditor, DeskEditor, Bracket, PreviewProgramDisplay, BracketImplications, DashboardModule, DashboardClock, ScheduleEditor, BroadcastEditor, CommsControls, Commercials, Predictions, MatchEditor, MatchThumbnail, BroadcastSwitcher },
data: () => ({
titleProcessing: false
}),
Expand Down

0 comments on commit 036b81c

Please sign in to comment.