Skip to content

Commit

Permalink
Start feature to control donation alert based on amounts
Browse files Browse the repository at this point in the history
Planning on adding images as a possibility besides sounds but not done yet.
  • Loading branch information
zoton2 committed Dec 9, 2023
1 parent 85d9bb5 commit 4275311
Show file tree
Hide file tree
Showing 12 changed files with 392 additions and 7 deletions.
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@
"workspace": "Z2 - ESA Advanced",
"headerColor": "#c49215"
},
{
"name": "donation-alert-control",
"title": "Donation Alert Control",
"width": 3,
"file": "donation-alert-control.html",
"workspace": "Z2 - ESA Advanced",
"headerColor": "#c49215"
},
{
"name": "donation-total-milestones",
"title": "Donation Total Milestones",
Expand Down Expand Up @@ -356,6 +364,20 @@
"webp",
"gif"
]
},
{
"name": "donation-alert-assets",
"title": "Donation Alert Assets",
"allowedTypes": [
"jpg",
"jpeg",
"png",
"svg",
"webp",
"gif",
"mp3",
"wav"
]
}
]
}
Expand Down
53 changes: 53 additions & 0 deletions schemas/donationAlerts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"threshold": {
"type": "number"
},
"sound": {
"description": "This stores a reference based on the 'name' of the asset.",
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"volume": {
"type": "number"
},
"graphic": {
"description": "This stores a reference based on the 'name' of the asset.",
"oneOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"graphicDisplayTime": {
"type": "number"
}
},
"required": [
"id",
"threshold",
"sound",
"volume",
"graphic",
"graphicDisplayTime"
]
},
"default": []
}
10 changes: 8 additions & 2 deletions src/browser_shared/replicant_store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Bids, BigbuttonPlayerMap, Commentators, Countdown, CurrentRunDelay, DonationReader, DonationsToRead, DonationTotal, DonationTotalMilestones, GameLayouts, IntermissionSlides, ObsData, Omnibar, OtherStreamData, Prizes, ReaderIntroduction, ServerTimestamp, StreamDeckData, TtsVoices, UpcomingRunID, VideoPlayer } from '@esa-layouts/types/schemas';
import type { Bids, BigbuttonPlayerMap, Commentators, Countdown, CurrentRunDelay, DonationAlerts, DonationReader, DonationTotal, DonationTotalMilestones, DonationsToRead, GameLayouts, IntermissionSlides, ObsData, Omnibar, OtherStreamData, Prizes, ReaderIntroduction, ServerTimestamp, StreamDeckData, TtsVoices, UpcomingRunID, VideoPlayer } from '@esa-layouts/types/schemas';
import type NodeCGTypes from '@nodecg/types';
import clone from 'clone';
import { SpeedcontrolUtilBrowser } from 'speedcontrol-util';
Expand All @@ -7,19 +7,21 @@ import { RunDataActiveRunSurrounding } from 'speedcontrol-util/types/schemas';
import Vue from 'vue';
import type { Store } from 'vuex';
import { namespace } from 'vuex-class';
import { getModule, Module, Mutation, VuexModule } from 'vuex-module-decorators';
import { Module, Mutation, VuexModule, getModule } from 'vuex-module-decorators';

const sc = new SpeedcontrolUtilBrowser(nodecg);

// Declaring replicants.
export const reps: {
assetsDonationAlertAssets: NodeCGTypes.ClientReplicant<NodeCGTypes.AssetFile[]>;
assetsIntermissionSlides: NodeCGTypes.ClientReplicant<NodeCGTypes.AssetFile[]>;
assetsReaderIntroductionImages: NodeCGTypes.ClientReplicant<NodeCGTypes.AssetFile[]>;
bids: NodeCGTypes.ClientReplicant<Bids>;
bigbuttonPlayerMap: NodeCGTypes.ClientReplicant<BigbuttonPlayerMap>;
commentators: NodeCGTypes.ClientReplicant<Commentators>;
countdown: NodeCGTypes.ClientReplicant<Countdown>;
currentRunDelay: NodeCGTypes.ClientReplicant<CurrentRunDelay>;
donationAlerts: NodeCGTypes.ClientReplicant<DonationAlerts>;
donationReader: NodeCGTypes.ClientReplicant<DonationReader>;
donationsToRead: NodeCGTypes.ClientReplicant<DonationsToRead>;
donationTotal: NodeCGTypes.ClientReplicant<DonationTotal>;
Expand All @@ -42,13 +44,15 @@ export const reps: {
videoPlayer: NodeCGTypes.ClientReplicant<VideoPlayer>;
[k: string]: NodeCGTypes.ClientReplicant<unknown>;
} = {
assetsDonationAlertAssets: nodecg.Replicant('assets:donation-alert-assets'),
assetsIntermissionSlides: nodecg.Replicant('assets:intermission-slides'),
assetsReaderIntroductionImages: nodecg.Replicant('assets:reader-introduction-images'),
bids: nodecg.Replicant('bids'),
bigbuttonPlayerMap: nodecg.Replicant('bigbuttonPlayerMap'),
commentators: nodecg.Replicant('commentators'),
countdown: nodecg.Replicant('countdown'),
currentRunDelay: nodecg.Replicant('currentRunDelay'),
donationAlerts: nodecg.Replicant('donationAlerts'),
donationReader: nodecg.Replicant('donationReader'),
donationsToRead: nodecg.Replicant('donationsToRead'),
donationTotal: nodecg.Replicant('donationTotal'),
Expand All @@ -73,13 +77,15 @@ export const reps: {

// All the replicant types.
export interface ReplicantTypes {
assetsDonationAlertAssets: NodeCGTypes.AssetFile[];
assetsIntermissionSlides: NodeCGTypes.AssetFile[];
assetsReaderIntroductionImages: NodeCGTypes.AssetFile[];
bids: Bids;
bigbuttonPlayerMap: BigbuttonPlayerMap;
commentators: Commentators;
countdown: Countdown;
currentRunDelay: CurrentRunDelay;
donationAlerts: DonationAlerts;
donationReader: DonationReader;
donationsToRead: DonationsToRead;
donationTotal: DonationTotal;
Expand Down
138 changes: 138 additions & 0 deletions src/dashboard/donation-alert-control/components/Alert.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<template>
<media-card
class="d-flex align-center px-2"
:style="{ 'text-align': 'unset', height: '40px', 'margin-top': index > 0 ? '10px' : 0 }"
>
<v-dialog v-model="dialog">
<v-card>
<v-card-text class="pa-4 pb-0">
<v-form v-model="isFormValid">
<v-text-field
v-model="thresholdEdit"
label="Amount Threshold in Dollars"
prepend-icon="mdi-cash"
autocomplete="off"
:rules="[isRequired, isNumber, isZeroOrBigger]"
filled
dense
/>
<v-text-field
v-model="soundEdit"
label="Sound Asset Name"
prepend-icon="mdi-music-box"
autocomplete="off"
:rules="[isRequired, isValidAsset]"
filled
dense
/>
<v-text-field
v-model="volumeEdit"
label="Volume in dB"
prepend-icon="mdi-volume-high"
autocomplete="off"
:rules="[isRequired, isNumber, isZeroOrSmaller]"
filled
dense
/>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="save" :disabled="!isFormValid">Save</v-btn>
<v-btn @click="dialog = false">Cancel</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-icon class="mr-2" @click="test">
mdi-play
</v-icon>
<div class="flex-grow-1">
${{ alert.threshold }} - {{ alert.sound || 'N/A' }} ({{ alert.volume }} dB)
<!-- {{ alert.graphic || 'N/A' }} ({{ alert.graphicDisplayTime }}s) -->
</div>
<v-icon @click="edit">
mdi-pencil
</v-icon>
<v-icon @click="remove">
mdi-delete
</v-icon>
</media-card>
</template>

<script lang="ts">
import { replicantNS } from '@esa-layouts/browser_shared/replicant_store';
import MediaCard from '@esa-layouts/dashboard/_misc/components/MediaCard.vue';
import { DonationAlerts } from '@esa-layouts/types/schemas';
import type NodeCGTypes from '@nodecg/types';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { storeModule } from '../store';
@Component({
components: {
MediaCard,
},
})
export default class extends Vue {
@Prop({ type: Object, required: true }) readonly alert!: DonationAlerts[0];
@Prop({ type: Number, required: true }) readonly index!: number;
@replicantNS.State(
(s) => s.reps.assetsDonationAlertAssets,
) readonly assets!: NodeCGTypes.AssetFile[];
dialog = false;
thresholdEdit = '0';
soundEdit = '';
volumeEdit = '0';
isFormValid = false;
isRequired(val: string): boolean | string {
return !!val || 'Required';
}
isNumber(val: string): boolean | string {
return !Number.isNaN(Number(val)) || 'Must be a number';
}
isZeroOrBigger(val: string): boolean | string {
const num = Number(val);
return (num >= 0) || 'Must be equal to or bigger than 0';
}
isZeroOrSmaller(val: string): boolean | string {
const num = Number(val);
return (num <= 0) || 'Must be equal to or smaller than 0';
}
isValidAsset(val: string): boolean | string {
const soundAsset = this.assets.find((v) => v.name === val);
return !!soundAsset || 'Asset name must match a file uploaded';
}
test(): void {
nodecg.sendMessage('omnibarPlaySound', { amount: this.alert.threshold });
}
edit(): void {
this.dialog = true;
this.thresholdEdit = this.alert.threshold.toString() ?? '0';
this.soundEdit = this.alert.sound ?? '';
this.volumeEdit = this.alert.volume.toString() ?? '0';
}
save(): void {
const item: DonationAlerts[0] = {
id: this.alert.id,
threshold: Number(this.thresholdEdit),
sound: this.soundEdit,
volume: Number(this.volumeEdit),
graphic: this.alert.graphic, // TODO
graphicDisplayTime: this.alert.graphicDisplayTime, // TODO
};
storeModule.editItem(item);
this.dialog = false;
}
remove(): void {
storeModule.removeItem(this.alert.id);
}
}
</script>
16 changes: 16 additions & 0 deletions src/dashboard/donation-alert-control/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint no-new: off, @typescript-eslint/explicit-function-return-type: off */

import { setUpReplicants } from '@esa-layouts/browser_shared/replicant_store';
import Vue from 'vue';
import vuetify from '../_misc/vuetify';
import App from './main.vue';
import store from './store';

setUpReplicants(store).then(() => {
new Vue({
vuetify,
store,
el: '#App',
render: (h) => h(App),
});
});
32 changes: 32 additions & 0 deletions src/dashboard/donation-alert-control/main.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<v-app>
<v-btn @click="addBlank">Add New Donation Alert Tier</v-btn>
<div v-if="!donationAlerts.length" class="pa-3 font-italic">
No donation alert tiers created, add a new one with the button above.
</div>
<div v-else :style="{ 'margin-top': '10px' }">
<alert v-for="(alert, i) in donationAlerts" :key="alert.id" :alert="alert" :index="i" />
</div>
</v-app>
</template>

<script lang="ts">
import { replicantNS } from '@esa-layouts/browser_shared/replicant_store';
import { DonationAlerts } from '@esa-layouts/types/schemas';
import { Component, Vue } from 'vue-property-decorator';
import Alert from './components/Alert.vue';
import { storeModule } from './store';
@Component({
components: {
Alert,
},
})
export default class DonationAlertControl extends Vue {
@replicantNS.State((s) => s.reps.donationAlerts) readonly donationAlerts!: DonationAlerts;
addBlank(): void {
storeModule.addBlankItem();
}
}
</script>
59 changes: 59 additions & 0 deletions src/dashboard/donation-alert-control/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ReplicantModule, ReplicantTypes, replicantModule } from '@esa-layouts/browser_shared/replicant_store';
import { DonationAlerts } from '@esa-layouts/types/schemas';
import clone from 'clone';
import { v4 as uuid } from 'uuid';
import Vue from 'vue';
import Vuex, { Store } from 'vuex';
import { Module, Mutation, VuexModule, getModule } from 'vuex-module-decorators';

Vue.use(Vuex);

@Module({ name: 'OurModule' })
class OurModule extends VuexModule {
// Helper getter to return all replicants.
get reps(): ReplicantTypes {
return this.context.rootState.ReplicantModule.reps;
}

@Mutation
addBlankItem(): void {
const items = clone(replicantModule.repsTyped.donationAlerts);
items.push({
id: uuid(),
threshold: 0,
sound: null,
volume: 50,
graphic: null,
graphicDisplayTime: 5,
});
replicantModule.setReplicant({ name: 'donationAlerts', val: items });
}

@Mutation
editItem(data: DonationAlerts[0]): void {
const items = clone(replicantModule.repsTyped.donationAlerts);
const itemIndex = items.findIndex((i) => i.id === data.id);
if (itemIndex >= 0) {
items[itemIndex] = clone(data);
replicantModule.setReplicant({ name: 'donationAlerts', val: items });
}
}

@Mutation
removeItem(id: string): void {
const items = clone(replicantModule.repsTyped.donationAlerts);
const itemIndex = items.findIndex((i) => i.id === id);
if (itemIndex >= 0) {
items.splice(itemIndex, 1);
replicantModule.setReplicant({ name: 'donationAlerts', val: items });
}
}
}

const store = new Store({
strict: process.env.NODE_ENV !== 'production',
state: {},
modules: { ReplicantModule, OurModule },
});
export default store;
export const storeModule = getModule(OurModule, store);
Loading

0 comments on commit 4275311

Please sign in to comment.