diff --git a/app/app-services.ts b/app/app-services.ts index e0fbe968e87b..8c8589b32c86 100644 --- a/app/app-services.ts +++ b/app/app-services.ts @@ -114,7 +114,6 @@ export { SseService } from 'services/server-sent-events'; // WIDGETS export { WidgetSource, WidgetsService } from './services/widgets'; -export { StreamBossService } from 'services/widgets/settings/stream-boss'; export { CreditsService } from 'services/widgets/settings/credits'; export { TipJarService } from 'services/widgets/settings/tip-jar'; export { MediaShareService } from 'services/widgets/settings/media-share'; diff --git a/app/components-react/widgets/GenericGoal.tsx b/app/components-react/widgets/GenericGoal.tsx index 9445828d50e1..d234de312f5d 100644 --- a/app/components-react/widgets/GenericGoal.tsx +++ b/app/components-react/widgets/GenericGoal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Button, Menu, message } from 'antd'; import { $t } from 'services/i18n'; import { IWidgetCommonState, useWidget, WidgetModule, WidgetParams } from './common/useWidget'; diff --git a/app/components-react/widgets/StreamBoss.tsx b/app/components-react/widgets/StreamBoss.tsx new file mode 100644 index 000000000000..aedca80d9828 --- /dev/null +++ b/app/components-react/widgets/StreamBoss.tsx @@ -0,0 +1,315 @@ +import React, { useState, useEffect } from 'react'; +import { inject } from 'slap'; +import { Menu, Button, message } from 'antd'; +import { useWidget, WidgetModule } from './common/useWidget'; +import { metadata, TInputMetadata } from 'components-react/shared/inputs/metadata'; +import { $t } from 'services/i18n'; +import { UserService } from 'app-services'; +import { TPlatform } from 'services/platforms'; +import { WidgetLayout } from './common/WidgetLayout'; +import FormFactory, { TInputValue } from 'components-react/shared/inputs/FormFactory'; +import Form from 'components-react/shared/inputs/Form'; +import { authorizedHeaders, jfetch } from 'util/requests'; +import { assertIsDefined } from 'util/properties-type-guards'; +import styles from './GenericGoal.m.less'; + +interface IStreamBossState { + data: { + goal: { + boss_img: string; + boss_name: string; + current_health: number; + mode: 'fixed' | 'overkill' | 'increment'; + multiplier: number; + percent: number; + total_health: number; + } | null; + settings: { + background_color: string; + bar_bg_color: string; + bar_color: string; + bar_text_color: string; + bg_transparent: boolean; + bit_multiplier: number; + boss_heal: boolean; + donation_multiplier: boolean; + fade_time: number; + follow_multiplier: boolean; + font: string; + incr_amount: string; + kill_animation: string; + overkill_min: number; + overkill_multiplier: number; + skin: string; + sub_multiplier: number; + superchat_multiplier: number; + text_color: string; + }; + }; +} + +export function Streamboss() { + const { + setSelectedTab, + selectedTab, + isLoading, + settings, + goalSettings, + updateSetting, + resetGoal, + saveGoal, + visualMeta, + goalMeta, + battleMeta, + } = useStreamboss(); + + const hasGoal = !!goalSettings; + + const [goalCreateValues, setGoalCreateValues] = useState>({ + total_health: 0, + mode: 'fixed', + overkill_multiplier: 0, + overkill_min: 0, + incr_amount: 0, + }); + + useEffect(() => { + message.config({ top: 270 }); + }, []); + + function updateGoalCreate(key: string) { + return (val: TInputValue) => { + setGoalCreateValues({ ...goalCreateValues, [key]: val }); + }; + } + + return ( + + setSelectedTab(e.key)} selectedKeys={[selectedTab]}> + {$t('Goal')} + {$t('Manage Battle')} + {$t('Visual Settings')} + +
+ {!isLoading && selectedTab === 'goal' && !hasGoal && ( + <> + + + + )} + {!isLoading && selectedTab === 'goal' && hasGoal && ( + + )} + {!isLoading && selectedTab === 'battle' && ( + + )} + {!isLoading && selectedTab === 'visual' && ( + + )} + +
+ ); +} + +function DisplayGoal(p: { goal: IStreamBossState['data']['goal']; resetGoal: () => void }) { + if (!p.goal) return <>; + return ( +
+
+ {$t('Current Boss Name')} + {p.goal.boss_name} +
+
+ {$t('Total Health')} + {p.goal.total_health} +
+
+ {$t('Current Health')} + {p.goal.current_health} +
+
+ {$t('Mode')} + {p.goal.mode} +
+ +
+ ); +} + +export class StreambossModule extends WidgetModule { + userService = inject(UserService); + + get visualMeta() { + return { + skin: metadata.list({ + label: $t('Theme'), + options: [ + { value: 'default', label: 'Default' }, + { value: 'future', label: 'Future' }, + { value: 'noimg', label: 'No Image' }, + { value: 'pill', label: 'Slim' }, + { value: 'future-curve', label: 'Curved' }, + ], + }), + kill_animation: metadata.animation({ label: $t('Kill Animation') }), + bg_transparent: metadata.bool({ label: $t('Transparent Background') }), + background_color: metadata.color({ label: $t('Background Color') }), + text_color: metadata.color({ label: $t('Text Color') }), + bar_text_color: metadata.color({ label: $t('Health Text Color') }), + bar_color: metadata.color({ label: $t('Health Bar Color') }), + bar_bg_color: metadata.color({ label: $t('Health Bar Background Color') }), + font: metadata.fontFamily({ label: $t('Font') }), + }; + } + + get battleMeta() { + return { + fade_time: metadata.slider({ + label: $t('Fade Time (s)'), + min: 0, + max: 20, + tooltip: $t('Set to 0 to always appear on screen'), + }), + boss_heal: metadata.bool({ label: $t('Damage From Boss Heals') }), + ...this.multipliersByPlatform(), + }; + } + + get goalMeta() { + return { + total_health: metadata.number({ label: $t('Starting Health'), required: true, min: 0 }), + mode: metadata.list({ + label: $t('Mode'), + options: [ + { + label: $t('Fixed'), + value: 'fixed', + description: $t('The boss will spawn with the set amount of health everytime.'), + }, + { + label: $t('Incremental'), + value: 'incremental', + description: $t( + 'The boss will have additional health each time he is defeated. The amount is set below.', + ), + }, + { + label: $t('Overkill'), + value: 'overkill', + description: $t( + "The boss' health will change depending on how much damage is dealt on the killing blow. Excess damage multiplied by the multiplier will be the boss' new health. I.e. 150 damage with 100 health remaining and a set multiplier of 3 would result in the new boss having 150 health on spawn. \n Set your multiplier below.", + ), + }, + ], + children: { + overkill_multiplier: metadata.number({ + label: $t('Overkill Multiplier'), + displayed: this.widgetData.goal?.mode === 'overkill', + }), + overkill_min: metadata.number({ + label: $t('Overkill Min Health'), + displayed: this.widgetData.goal?.mode === 'overkill', + }), + incr_amount: metadata.number({ + label: $t('Increment Amount'), + displayed: this.widgetData.goal?.mode === 'increment', + }), + }, + }), + }; + } + + multipliersByPlatform(): Dictionary { + const platform = this.userService.platform?.type as Exclude< + TPlatform, + 'tiktok' | 'twitter' | 'instagram' | 'kick' + >; + const multipliers = { + twitch: { + bit_multiplier: metadata.number({ label: $t('Damage Per Bit') }), + sub_multiplier: metadata.number({ label: $t('Damage Per Subscriber') }), + follow_multiplier: metadata.number({ label: $t('Damage Per Follower') }), + }, + facebook: { + follow_multiplier: metadata.number({ label: $t('Damage Per Follower') }), + sub_multiplier: metadata.number({ label: $t('Damage Per Subscriber') }), + }, + youtube: { + sub_multiplier: metadata.number({ label: $t('Damage Per Membership') }), + superchat_multiplier: metadata.number({ label: $t('Damage Per Superchat Dollar') }), + follow_multiplier: metadata.number({ label: $t('Damage Per Subscriber') }), + }, + trovo: { + sub_multiplier: metadata.number({ label: $t('Damage Per Subscriber') }), + follow_multiplier: metadata.number({ label: $t('Damage Per Follower') }), + }, + }; + + return { + ...multipliers[platform], + donation_multiplier: metadata.number({ label: $t('Damage Per Dollar Donation') }), + }; + } + + get goalSettings() { + return this.widgetData.goal; + } + + get headers() { + return authorizedHeaders( + this.userService.apiToken, + new Headers({ 'Content-Type': 'application/json' }), + ); + } + + resetGoal() { + const url = this.config.goalUrl; + if (!url) return; + jfetch(new Request(url, { method: 'DELETE', headers: this.headers })); + this.setGoalData(null); + } + + async saveGoal(options: Dictionary) { + const url = this.config.goalUrl; + if (!url) return; + try { + const resp: IStreamBossState['data'] = await jfetch( + new Request(url, { + method: 'POST', + headers: this.headers, + body: JSON.stringify(options), + }), + ); + this.setGoalData(resp.goal); + } catch (e: unknown) { + message.error({ content: (e as any).result.message, duration: 2 }); + } + } + + private setGoalData(goal: IStreamBossState['data']['goal']) { + assertIsDefined(this.state.widgetData.data); + this.state.mutate(state => { + state.widgetData.data.goal = goal; + }); + } +} + +function useStreamboss() { + return useWidget(); +} diff --git a/app/components-react/widgets/common/WidgetWindow.tsx b/app/components-react/widgets/common/WidgetWindow.tsx index b546df4eb232..3f540e9a6c4a 100644 --- a/app/components-react/widgets/common/WidgetWindow.tsx +++ b/app/components-react/widgets/common/WidgetWindow.tsx @@ -26,6 +26,7 @@ import { GamePulseWidget } from 'components-react/widgets/GamePulse'; import { GamePulseModule } from 'components-react/widgets/game-pulse/useGamePulseWidget'; import { useSubscription } from '../../hooks/useSubscription'; import { useChildWindowParams } from 'components-react/hooks'; +import { Streamboss, StreambossModule } from '../StreamBoss'; // define list of Widget components and modules export const components = { @@ -48,7 +49,7 @@ export const components = { // Poll // SpinWheel SponsorBanner: [SponsorBanner, SponsorBannerModule], - // StreamBoss + StreamBoss: [Streamboss, StreambossModule], // TipJar ViewerCount: [ViewerCount, ViewerCountModule], GameWidget: [GameWidget, GameWidgetModule], diff --git a/app/components/widgets/StreamBoss.vue b/app/components/widgets/StreamBoss.vue deleted file mode 100644 index d9002d25f708..000000000000 --- a/app/components/widgets/StreamBoss.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - - - diff --git a/app/components/widgets/StreamBoss.vue.ts b/app/components/widgets/StreamBoss.vue.ts deleted file mode 100644 index c70bd1f610c5..000000000000 --- a/app/components/widgets/StreamBoss.vue.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Component } from 'vue-property-decorator'; -import WidgetEditor from 'components/windows/WidgetEditor.vue'; -import WidgetSettings from 'components/widgets/WidgetSettings.vue'; - -import { inputComponents } from 'components/widgets/inputs'; -import VFormGroup from 'components/shared/inputs/VFormGroup.vue'; -import { $t } from 'services/i18n/index'; -import ValidatedForm from 'components/shared/inputs/ValidatedForm'; -import { - IStreamBossCreateOptions, - IStreamBossData, - StreamBossService, -} from 'services/widgets/settings/stream-boss'; - -@Component({ - components: { - WidgetEditor, - VFormGroup, - ValidatedForm, - ...inputComponents, - }, -}) -export default class StreamBoss extends WidgetSettings { - $refs: { - form: ValidatedForm; - }; - - bossCreateOptions: IStreamBossCreateOptions = { - mode: 'fixed', - total_health: 4800, - }; - - textColorTooltip = $t('A hex code for the base text color.'); - - get hasGoal() { - return this.loaded && this.wData.goal; - } - - get multipliersForPlatform() { - const baseEvents = [ - { key: 'donation_multiplier', title: $t('Damage Per Dollar Donation'), isInteger: true }, - ]; - return this.service.multipliersByPlatform().concat(baseEvents); - } - - async saveGoal() { - if (await this.$refs.form.validateAndGetErrorsCount()) return; - await this.service.saveGoal(this.bossCreateOptions); - } - - get navItems() { - return [ - { value: 'goal', label: $t('Goal') }, - { value: 'manage-battle', label: $t('Manage Battle') }, - { value: 'visual', label: $t('Visual Settings') }, - { value: 'source', label: $t('Source') }, - ]; - } - - async resetGoal() { - await this.service.resetGoal(); - } -} diff --git a/app/services/sources/sources.ts b/app/services/sources/sources.ts index 623493604984..17aece4ab314 100644 --- a/app/services/sources/sources.ts +++ b/app/services/sources/sources.ts @@ -828,7 +828,7 @@ export class SourcesService extends StatefulService { // 'Poll', // 'SpinWheel', 'SponsorBanner', - // 'StreamBoss', + 'StreamBoss', // 'TipJar', 'ViewerCount', 'GameWidget', diff --git a/app/services/widgets/settings/base-goal.ts b/app/services/widgets/settings/base-goal.ts deleted file mode 100644 index 116be63e5027..000000000000 --- a/app/services/widgets/settings/base-goal.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { IWidgetApiSettings, IWidgetData, WidgetSettingsService } from 'services/widgets'; -import { InheritMutations } from 'services/core/stateful-service'; -import { handleResponse } from 'util/requests'; - -interface IBaseGoalData extends IWidgetData { - goal: Dictionary; -} - -interface IGoalWidgetApiSettings extends IWidgetApiSettings { - goalUrl: string; - goalCreateEvent?: string; - goalResetEvent?: string; -} - -@InheritMutations() -export abstract class BaseGoalService< - TGoalData extends IBaseGoalData, - TGoalCreateOptions -> extends WidgetSettingsService { - subToWebsocket() { - super.subToWebsocket(); - - this.websocketService.socketEvent.subscribe(event => { - const apiSettings = this.getApiSettings(); - if (event.type === apiSettings.goalCreateEvent || event.type === apiSettings.goalResetEvent) { - this.refreshData(); - } - }); - } - - abstract getApiSettings(): IGoalWidgetApiSettings; - - protected patchAfterFetch(data: TGoalData): TGoalData { - // fix a bug when API returning an empty array instead of null - if (Array.isArray(data.goal)) data.goal = null; - return data; - } - - async saveGoal(options: TGoalCreateOptions) { - const apiSettings = this.getApiSettings(); - return await this.request({ - url: apiSettings.goalUrl, - method: 'POST', - body: options, - }); - } - - async resetGoal() { - const apiSettings = this.getApiSettings(); - - return await this.request({ - url: apiSettings.goalUrl, - method: 'DELETE', - }); - } -} diff --git a/app/services/widgets/settings/stream-boss.ts b/app/services/widgets/settings/stream-boss.ts deleted file mode 100644 index a3a561bda757..000000000000 --- a/app/services/widgets/settings/stream-boss.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { WIDGET_INITIAL_STATE } from './widget-settings'; -import { IWidgetData, IWidgetSettings, WidgetDefinitions, WidgetType } from 'services/widgets'; -import { $t } from 'services/i18n'; -import { metadata } from 'components/widgets/inputs/index'; -import { InheritMutations } from 'services/core/stateful-service'; -import { BaseGoalService } from './base-goal'; -import { formMetadata } from 'components/shared/inputs'; -import { TPlatform } from '../../platforms'; - -export interface IStreamBossSettings extends IWidgetSettings { - background_color: string; - bar_bg_color: string; - bar_color: string; - bar_text_color: string; - bg_transparent: boolean; - bit_multiplier: number; - boss_heal: boolean; - donation_multiplier: boolean; - fade_time: number; - follow_multiplier: boolean; - font: string; - incr_amount: string; - kill_animation: string; - overkill_min: number; - overkill_multiplier: number; - skin: string; - sub_multiplier: number; - superchat_multiplier: number; - text_color: string; -} - -export interface IStreamBossData extends IWidgetData { - goal: { - boss_img: string; - boss_name: string; - current_health: number; - mode: string; - multiplier: 1; - percent: number; - total_health: number; - }; - settings: IStreamBossSettings; -} - -type TStreamBossMode = 'fixed' | 'incremental' | 'overkill'; - -export interface IStreamBossCreateOptions { - mode: TStreamBossMode; - total_health: number; -} - -@InheritMutations() -export class StreamBossService extends BaseGoalService { - static initialState = WIDGET_INITIAL_STATE; - - getApiSettings() { - const host = this.getHost(); - return { - type: WidgetType.StreamBoss, - url: WidgetDefinitions[WidgetType.StreamBoss].url(host, this.getWidgetToken()), - previewUrl: `https://${host}/widgets/streamboss?token=${this.getWidgetToken()}`, - webSettingsUrl: `https://${host}/dashboard#/widgets/streamboss`, - settingsUpdateEvent: 'streambossSettingsUpdate', - goalCreateEvent: 'newStreamboss', - goalResetEvent: 'streambossEnd', - dataFetchUrl: `https://${host}/api/v5/slobs/widget/streamboss/settings`, - settingsSaveUrl: `https://${host}/api/v5/slobs/widget/streamboss/settings`, - goalUrl: `https://${host}/api/v5/slobs/widget/streamboss`, - testers: ['Follow', 'Subscription', 'Donation', 'Bits'], - customCodeAllowed: true, - customFieldsAllowed: true, - }; - } - - getMetadata() { - return formMetadata({ - // CREATE BOSS - - total_health: metadata.number({ - title: $t('Starting Health'), - required: true, - min: 0, - }), - - mode: metadata.list({ - title: $t('Mode'), - options: [ - { - title: $t('Fixed'), - value: 'fixed', - description: $t('The boss will spawn with the set amount of health everytime.'), - }, - { - title: $t('Incremental'), - value: 'incremental', - description: $t( - 'The boss will have additional health each time he is defeated. The amount is set below.', - ), - }, - { - title: $t('Overkill'), - value: 'overkill', - description: $t( - "The boss' health will change depending on how much damage is dealt on the killing blow. Excess damage multiplied by the multiplier will be the boss' new health. I.e. 150 damage with 100 health remaining and a set multiplier of 3 would result in the new boss having 150 health on spawn. \n Set your multiplier below.", - ), - }, - ], - }), - incr_amount: metadata.number({ title: $t('Increment Amount'), isInteger: true }), - overkill_multiplier: metadata.number({ title: $t('Overkill Multiplier'), isInteger: true }), - overkill_min: metadata.number({ title: $t('Overkill Min Health'), isInteger: true }), - - // SETTINGS - - fade_time: metadata.slider({ - title: $t('Fade Time (s)'), - min: 0, - max: 20, - description: $t('Set to 0 to always appear on screen'), - }), - - boss_heal: metadata.bool({ - title: $t('Damage From Boss Heals'), - }), - - skin: metadata.list({ - title: $t('Theme'), - options: [ - { value: 'default', title: 'Default' }, - { value: 'future', title: 'Future' }, - { value: 'noimg', title: 'No Image' }, - { value: 'pill', title: 'Slim' }, - { value: 'future-curve', title: 'Curved' }, - ], - }), - - kill_animation: metadata.animation({ - title: $t('Kill Animation'), - }), - - bg_transparent: metadata.bool({ - title: $t('Transparent Background'), - }), - - background_color: metadata.color({ - title: $t('Background Color'), - }), - - text_color: metadata.color({ - title: $t('Text Color'), - }), - - bar_text_color: metadata.color({ - title: $t('Health Text Color'), - }), - - bar_color: metadata.color({ - title: $t('Health Bar Color'), - }), - - bar_bg_color: metadata.color({ - title: $t('Health Bar Background Color'), - }), - - font: metadata.fontFamily({ - title: $t('Font'), - }), - }); - } - - multipliersByPlatform(): { key: string; title: string; isInteger: boolean }[] { - const platform = this.userService.platform.type as Exclude< - TPlatform, - 'tiktok' | 'twitter' | 'instagram' | 'kick' - >; - return { - twitch: [ - { key: 'bit_multiplier', title: $t('Damage Per Bit'), isInteger: true }, - { key: 'sub_multiplier', title: $t('Damage Per Subscriber'), isInteger: true }, - { key: 'follow_multiplier', title: $t('Damage Per Follower'), isInteger: true }, - ], - facebook: [ - { key: 'follow_multiplier', title: $t('Damage Per Follower'), isInteger: true }, - { key: 'sub_multiplier', title: $t('Damage Per Subscriber'), isInteger: true }, - ], - youtube: [ - { key: 'sub_multiplier', title: $t('Damage Per Membership'), isInteger: true }, - { key: 'superchat_multiplier', title: $t('Damage Per Superchat Dollar'), isInteger: true }, - { key: 'follow_multiplier', title: $t('Damage Per Subscriber'), isInteger: true }, - ], - trovo: [ - { key: 'sub_multiplier', title: $t('Damage Per Subscriber'), isInteger: true }, - { key: 'follow_multiplier', title: $t('Damage Per Follower'), isInteger: true }, - ], - }[platform]; - } -} diff --git a/app/services/widgets/widgets-config.ts b/app/services/widgets/widgets-config.ts index 2f2eb90c1fe2..40f41506d2e6 100644 --- a/app/services/widgets/widgets-config.ts +++ b/app/services/widgets/widgets-config.ts @@ -21,7 +21,8 @@ export type TWidgetType = | WidgetType.SuperchatGoal | WidgetType.CharityGoal | WidgetType.EventList - | WidgetType.GamePulseWidget; + | WidgetType.GamePulseWidget + | WidgetType.StreamBoss; export interface IWidgetConfig { type: TWidgetType; @@ -573,9 +574,36 @@ export function getWidgetsConfig( customFieldsAllowed: true, }, - // StreamBoss: { - // - // }, + [WidgetType.StreamBoss]: { + type: WidgetType.StreamBoss, + + defaultTransform: { + width: 600, + height: 200, + + x: 0, + y: 1, + anchor: AnchorPoint.SouthWest, + }, + + settingsWindowSize: { + width: 850, + height: 700, + }, + + url: `https://${host}/widgets/streamboss?token=${token}`, + previewUrl: `https://${host}/widgets/streamboss?token=${token}`, + webSettingsUrl: `https://${host}/dashboard#/widgets/streamboss`, + settingsUpdateEvent: 'streambossSettingsUpdate', + goalCreateEvent: 'newStreamboss', + goalResetEvent: 'streambossEnd', + dataFetchUrl: `https://${host}/api/v5/slobs/widget/streamboss/settings`, + settingsSaveUrl: `https://${host}/api/v5/slobs/widget/streamboss/settings`, + goalUrl: `https://${host}/api/v5/slobs/widget/streamboss`, + testers: ['follow', 'sub', 'donation', 'bits'], + customCodeAllowed: true, + customFieldsAllowed: true, + }, // TipJar: { // diff --git a/app/services/windows.ts b/app/services/windows.ts index 58e6109b0f6e..7c94f4fea48b 100644 --- a/app/services/windows.ts +++ b/app/services/windows.ts @@ -54,7 +54,6 @@ import EventFilterMenu from 'components/windows/EventFilterMenu'; import OverlayPlaceholder from 'components/windows/OverlayPlaceholder'; import BrowserSourceInteraction from 'components/windows/BrowserSourceInteraction'; -import StreamBoss from 'components/widgets/StreamBoss.vue'; import Credits from 'components/widgets/Credits.vue'; import TipJar from 'components/widgets/TipJar.vue'; import MediaShare from 'components/widgets/MediaShare'; @@ -107,7 +106,6 @@ export function getComponents() { MultistreamChatInfo, Credits, TipJar, - StreamBoss, MediaShare, AlertBox, SpinWheel,