diff --git a/app/app-services.ts b/app/app-services.ts index e0fbe968e87b..1fdfcb031f03 100644 --- a/app/app-services.ts +++ b/app/app-services.ts @@ -115,7 +115,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'; export { AlertBoxService } from 'services/widgets/settings/alert-box'; diff --git a/app/components-react/shared/inputs/FormFactory.tsx b/app/components-react/shared/inputs/FormFactory.tsx index 64d67050dc7c..578e77a9ba2f 100644 --- a/app/components-react/shared/inputs/FormFactory.tsx +++ b/app/components-react/shared/inputs/FormFactory.tsx @@ -6,7 +6,7 @@ import cloneDeep from 'lodash/cloneDeep'; import * as inputs from './inputList'; import { TInputType, TSlobsInputProps } from './inputs'; import Form, { useForm } from './Form'; -import { TInputMetadata } from './metadata'; +import { IBaseMetadata } from './metadata'; import { ButtonGroup } from 'components-react/shared/ButtonGroup'; import { $t } from 'services/i18n'; @@ -36,7 +36,7 @@ const componentTable: { }; interface IFormMetadata { - [value: string]: TInputMetadata; + [value: string]: IBaseMetadata; } export default function FormFactory(p: { @@ -93,7 +93,7 @@ export default function FormFactory(p: { function FormInput(p: { id: string; - metadata: TInputMetadata; + metadata: IBaseMetadata; values: Dictionary; onChange: (key: string) => (value: TInputValue) => void; }) { diff --git a/app/components-react/shared/inputs/metadata.ts b/app/components-react/shared/inputs/metadata.ts index 791d9a42d69d..40c32ecb55f8 100644 --- a/app/components-react/shared/inputs/metadata.ts +++ b/app/components-react/shared/inputs/metadata.ts @@ -1,14 +1,13 @@ import { Rule } from 'antd/lib/form'; import { TInputValue } from './FormFactory'; import { IListOption } from './ListInput'; -import { TInputType } from './inputs'; /** * Metadata generator for inputs * Provides some presets and helps with typechecking */ export const metadata = { - any: (options: IAnyMetadata) => options, + any: (options: IAnyMetadata & Record) => options, text: (options: ITextMetadata) => ({ ...options, type: 'text' }), textarea: (options: ITextMetadata) => ({ ...options, type: 'textarea' }), number: (options: INumberMetadata) => ({ ...options, type: 'number' }), @@ -47,29 +46,20 @@ export const metadata = { animation: (options: IAnimationMetadata) => ({ ...options, type: 'animation' }), }; -export type TInputMetadata = - | ITextMetadata - | INumberMetadata - | ISliderMetadata - | ITextBoolMetadata - | ICheckboxGroupMetadata - | IRadioGroupMetadata - | IListMetadata; - -interface IBaseMetadata { +export interface IBaseMetadata { label?: string; tooltip?: string; required?: boolean; type?: string; rules?: Rule[]; onChange?: (value: unknown) => void; - children?: Dictionary>; + children?: Dictionary; displayed?: boolean; disabled?: boolean; name?: string; } -interface ITextMetadata extends IBaseMetadata { +export interface ITextMetadata extends IBaseMetadata { value?: string; isPassword?: boolean; placeholder?: string; diff --git a/app/components-react/widgets/Credits.tsx b/app/components-react/widgets/Credits.tsx new file mode 100644 index 000000000000..84c26da7b093 --- /dev/null +++ b/app/components-react/widgets/Credits.tsx @@ -0,0 +1,224 @@ +import { Menu } from 'antd'; +import { Services } from 'components-react/service-provider'; +import Form from 'components-react/shared/inputs/Form'; +import FormFactory from 'components-react/shared/inputs/FormFactory'; +import React from 'react'; +import { TPlatform } from 'services/platforms'; +import { $t } from '../../services/i18n'; +import { IBaseMetadata, metadata } from '../shared/inputs/metadata'; +import { IWidgetCommonState, useWidget, WidgetModule } from './common/useWidget'; +import { WidgetLayout } from './common/WidgetLayout'; + +interface ICreditsState extends IWidgetCommonState { + data: { + themes: Dictionary<{ label: string }>; + settings: { + theme: string; + credit_title: string; + credit_subtitle: string; + background_color: string; + font_color: string; + font_size: 14; + font: string; + muted_chatters: string; + bits: boolean; + subscribers: boolean; + moderators: boolean; + donations: boolean; + followers: boolean; + bits_change: string; + donor_change: string; + followers_change: string; + mods_change: string; + subscribers_change: string; + delay_time: number; + roll_speed: number; + roll_time: number; + loop_credits: boolean; + }; + }; +} + +type TCreditsMeta = PartialRec< + keyof ICreditsState['data']['settings'] | 'themes' | `_${string}`, + IBaseMetadata +>; + +function fromMeta(meta: TCreditsMeta): Record { + return meta as Record; +} + +export function Credits() { + const { + isLoading, + settings, + creditsMeta, + fontMeta, + visualMeta, + updateSetting, + setSelectedTab, + selectedTab, + } = useCredits(); + + // use 1 column layout + return ( + + setSelectedTab(e.key)} selectedKeys={[selectedTab]}> + {$t('Manage Credits')} + {$t('Font Settings')} + {$t('Visual Settings')} + +
+ {!isLoading && selectedTab === 'credits' && ( + + )} + {!isLoading && selectedTab === 'font' && ( + + )} + {!isLoading && selectedTab === 'visual' && ( + + )} + +
+ ); +} + +export class CreditsModule extends WidgetModule { + get UserService() { + return Services.UserService; + } + + get includesByPlatform() { + const platform = this.UserService.views.platform?.type; + const baseEvents = { + donations: metadata.bool({ label: $t('Show Donations') }), + }; + const platformEvents: PartialRec = { + twitch: { + followers: metadata.bool({ label: $t('Show Followers') }), + subscribers: metadata.bool({ label: $t('Show Subscribers') }), + bits: metadata.bool({ label: $t('Show Cheers') }), + moderators: metadata.bool({ label: $t('Show Moderators') }), + }, + youtube: { + subscriptions: metadata.bool({ label: $t('Show Subscriptions') }), + sponsors: metadata.bool({ label: $t('Show Members') }), + superchats: metadata.bool({ label: $t('Show Super Chats') }), + }, + }; + if (!platform || !platformEvents[platform]) return fromMeta(baseEvents); + return fromMeta({ ...platformEvents[platform], ...baseEvents }); + } + + get titlesByPlatform() { + const platform = this.UserService.views.platform?.type; + const baseEvents = { + donor_change: metadata.text({ label: $t('Donors') }), + }; + const platformEvents: PartialRec = { + twitch: { + followers_change: metadata.text({ label: $t('Followers') }), + subscribers_change: metadata.text({ label: $t('Subscribers & Resubs') }), + bits_change: metadata.text({ label: $t('Cheers') }), + mods_change: metadata.text({ label: $t('Moderators') }), + }, + youtube: { + subscriptions_change: metadata.text({ label: $t('Subscriptions') }), + sponsors_change: metadata.text({ label: $t('Members') }), + superchats_change: metadata.text({ label: $t('Super Chats') }), + }, + }; + if (!platform || !platformEvents[platform]) return fromMeta(baseEvents); + return fromMeta({ ...platformEvents[platform], ...baseEvents }); + } + + get creditsMeta() { + return fromMeta({ + credit_title: metadata.text({ label: $t('Credit Title') }), + credit_subtitle: metadata.text({ + label: $t('Credit Subtitle'), + tooltip: + $t('When the credits roll, this will be the format of the subtitle. Available tokens:') + + '\r' + + [ + '{total_donated_amount}', + '{total_cheer_amount}', + '{top_donor}', + '{top_donated_amount}', + '{top_cheer_donor}', + '{username}', + '{top_cheer_amount}', + '{new_subscriber_count}', + '{new_follower_count}', + ].join(', '), + }), + _includes: { + type: 'checkboxGroup', + label: $t('Includes'), + children: this.includesByPlatform, + }, + ...this.titlesByPlatform, + }); + } + + get fontMeta() { + return fromMeta({ + font_color: metadata.color({ + label: $t('Text Color'), + tooltip: $t('A hex code for the base text color.'), + }), + font: metadata.fontFamily({ label: $t('Font') }), + font_size: metadata.fontSize({ + label: $t('Font Size'), + min: 10, + max: 80, + tooltip: $t( + 'The font size in pixels. Reasonable size typically ranges between 24px and 48px.', + ), + }), + }); + } + + get visualMeta() { + return fromMeta({ + theme: metadata.list({ + label: $t('Theme'), + options: Object.entries(this.widgetData?.themes || {}).map(([theme, val]) => ({ + label: val.label, + value: theme, + })), + }), + background_color: metadata.color({ + label: $t('Background Color'), + tooltip: $t( + 'A hex code for the widget background. This is for preview purposes only. It will not be shown in your stream.', + ), + }), + delay_time: metadata.slider({ + label: $t('Delay Time'), + tooltip: $t('Wait time before rerunning the credit reel.'), + min: 0, + max: 10, + step: 1, + }), + roll_speed: metadata.slider({ + label: $t('Roll Speed'), + tooltip: $t('Speed of the rolling credits.'), + min: 1, + max: 5, + step: 1, + }), + roll_time: metadata.slider({ + label: $t('Roll Time'), + tooltip: $t('Duration of the rolling credits.'), + min: 15, + max: 150, + step: 5, + }), + }); + } +} + +function useCredits() { + return useWidget(); +} diff --git a/app/components-react/widgets/EventList.tsx b/app/components-react/widgets/EventList.tsx index 375e3ace204f..c6f8c23e8202 100644 --- a/app/components-react/widgets/EventList.tsx +++ b/app/components-react/widgets/EventList.tsx @@ -1,14 +1,13 @@ -import React from 'react'; import { Menu } from 'antd'; -import { IWidgetCommonState, useWidget, WidgetModule } from './common/useWidget'; -import { WidgetLayout } from './common/WidgetLayout'; -import { $t } from '../../services/i18n'; -import { metadata } from '../shared/inputs/metadata'; -import FormFactory from 'components-react/shared/inputs/FormFactory'; -import { UserService } from 'app-services'; import { Services } from 'components-react/service-provider'; -import { TPlatform } from 'services/platforms'; import Form from 'components-react/shared/inputs/Form'; +import FormFactory from 'components-react/shared/inputs/FormFactory'; +import React from 'react'; +import { TPlatform } from 'services/platforms'; +import { $t } from '../../services/i18n'; +import { metadata } from '../shared/inputs/metadata'; +import { IWidgetCommonState, useWidget, WidgetModule } from './common/useWidget'; +import { WidgetLayout } from './common/WidgetLayout'; interface IEventListState extends IWidgetCommonState { data: { @@ -131,7 +130,7 @@ export class EventListModule extends WidgetModule { show_sub_tiers: metadata.bool({ label: $t('Show Sub Tiers') }), }, }; - if (!platform) return baseEvents; + if (!platform || !platformEvents[platform]) return baseEvents; return { ...platformEvents[platform], ...baseEvents }; } @@ -181,7 +180,7 @@ export class EventListModule extends WidgetModule { return { theme: metadata.list({ label: $t('Theme'), - options: Object.entries(this.widgetData?.themes || []).map(([theme, val]) => ({ + options: Object.entries(this.widgetData?.themes || {}).map(([theme, val]) => ({ label: val.label, value: theme, })), diff --git a/app/components-react/widgets/SponsorBanner.tsx b/app/components-react/widgets/SponsorBanner.tsx index 9b2781b68bbd..8802aa97b6b5 100644 --- a/app/components-react/widgets/SponsorBanner.tsx +++ b/app/components-react/widgets/SponsorBanner.tsx @@ -162,7 +162,7 @@ export class SponsorBannerModule extends WidgetModule { { label: $t('Double'), value: 'double' }, ], children: { - layout: { + layout: metadata.any({ type: 'imagepicker', label: $t('Image Layout'), options: [ @@ -170,7 +170,7 @@ export class SponsorBannerModule extends WidgetModule { { label: '', value: 'above', image: $i('images/layout-image-above.png') }, ], displayed: this.settings.placement_options === 'double', - }, + }), }, }), }; diff --git a/app/components-react/widgets/common/WidgetWindow.tsx b/app/components-react/widgets/common/WidgetWindow.tsx index 8d592570dbe8..3ed3377c38e3 100644 --- a/app/components-react/widgets/common/WidgetWindow.tsx +++ b/app/components-react/widgets/common/WidgetWindow.tsx @@ -9,7 +9,7 @@ import { useWidgetRoot, WidgetModule } from './useWidget'; import { GenericGoal, GenericGoalModule } from '../GenericGoal'; import { ChatBox, ChatBoxModule } from '../ChatBox'; // ChatHighlight -// Credits +import { Credits, CreditsModule } from '../Credits'; import { DonationTicker, DonationTickerModule } from '../DonationTicker'; import { EmoteWall, EmoteWallModule } from '../EmoteWall'; import { EventList, EventListModule } from '../EventList'; @@ -39,7 +39,7 @@ export const components = { SubscriberGoal: [GenericGoal, GenericGoalModule], ChatBox: [ChatBox, ChatBoxModule], // ChatHighlight - // Credits + Credits: [Credits, CreditsModule], DonationTicker: [DonationTicker, DonationTickerModule], EmoteWall: [EmoteWall, EmoteWallModule], EventList: [EventList, EventListModule], diff --git a/app/components/widgets/Credits.vue b/app/components/widgets/Credits.vue deleted file mode 100644 index 124afd995908..000000000000 --- a/app/components/widgets/Credits.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/app/components/widgets/Credits.vue.ts b/app/components/widgets/Credits.vue.ts deleted file mode 100644 index 53cdaa7bfd0e..000000000000 --- a/app/components/widgets/Credits.vue.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Component } from 'vue-property-decorator'; -import { CreditsService, ICreditsData } from 'services/widgets/settings/credits'; -import { inputComponents } from 'components/widgets/inputs'; -import WidgetEditor from 'components/windows/WidgetEditor.vue'; -import WidgetSettings from './WidgetSettings.vue'; -import VFormGroup from 'components/shared/inputs/VFormGroup.vue'; -import { $t } from 'services/i18n/index'; -import ValidatedForm from 'components/shared/inputs/ValidatedForm'; - -@Component({ - components: { - WidgetEditor, - VFormGroup, - ValidatedForm, - ...inputComponents, - }, -}) -export default class Credits extends WidgetSettings { - get themeOptions() { - return Object.keys(this.wData.themes).map(theme => ({ - title: this.wData.themes[theme].label, - value: theme, - })); - } - - optionIterable(map: Dictionary) { - // TODO: index - // @ts-ignore - return Object.keys(map).filter(option => this.wData.settings[option] != null); - } - - get shownCreditOptions() { - return { - // Twitch - followers: $t('Show Followers'), - subscribers: $t('Show Subscribers'), - bits: $t('Show Cheers'), - moderators: $t('Show Moderators'), - // Youtube - subscriptions: $t('Show Subscriptions'), - sponsors: $t('Show Members'), - superchats: $t('Show Super Chats'), - }; - } - - get creditNameOptions() { - return { - // Twitch - followers_change: $t('Followers'), - subscribers_change: $t('Subscribers & Resubs'), - bits_change: $t('Cheers'), - mods_change: $t('Moderators'), - // Youtube - subscriptions_change: $t('Subscriptions'), - sponsors_change: $t('Members'), - superchats_change: $t('Super Chats'), - }; - } - - rollCredits() { - this.service.testRollCredits(); - } - - get metadata() { - return this.service.getMetadata(this.themeOptions); - } - - get navItems() { - return [ - { value: 'manage-credits', label: $t('Manage Credits') }, - { value: 'visual', label: $t('Visual Settings') }, - { value: 'source', label: $t('Source') }, - ]; - } -} diff --git a/app/services/sources/sources.ts b/app/services/sources/sources.ts index 42bc75a241bd..acc84686ae47 100644 --- a/app/services/sources/sources.ts +++ b/app/services/sources/sources.ts @@ -819,7 +819,7 @@ export class SourcesService extends StatefulService { 'ChatBox', // TODO: // 'ChatHighlight', - // 'Credits', + 'Credits', 'DonationTicker', 'EmoteWall', 'EventList', diff --git a/app/services/widgets/settings/credits.ts b/app/services/widgets/settings/credits.ts deleted file mode 100644 index 33c313ccd47b..000000000000 --- a/app/services/widgets/settings/credits.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - IWidgetData, - IWidgetSettings, - WidgetDefinitions, - WidgetSettingsService, - WidgetType, -} from 'services/widgets'; -import { WIDGET_INITIAL_STATE } from './widget-settings'; -import { InheritMutations } from 'services/core/stateful-service'; -import { authorizedHeaders } from 'util/requests'; -import { formMetadata, metadata, IListOption } from 'components/shared/inputs'; -import { $t } from 'services/i18n'; - -export interface ICreditsSettings extends IWidgetSettings { - theme: string; - credit_title: string; - credit_subtitle: string; - background_color: string; - font_color: string; - font_size: 14; - font: string; - muted_chatters: string; - bits: boolean; - subscribers: boolean; - moderators: boolean; - donations: boolean; - followers: boolean; - bits_change: string; - donor_change: string; - followers_change: string; - mods_change: string; - subscribers_change: string; - delay_time: number; - roll_speed: number; - roll_time: number; - loop_credits: boolean; -} -export interface ICreditsData extends IWidgetData { - themes: any; - settings: ICreditsSettings; -} - -const creditTokens = [ - '{total_donated_amount}', - '{total_cheer_amount}', - '{top_donor}', - '{top_donated_amount}', - '{top_cheer_donor}', - '{username}', - '{top_cheer_amount}', - '{new_subscriber_count}', - '{new_follower_count}', -]; - -@InheritMutations() -export class CreditsService extends WidgetSettingsService { - static initialState = WIDGET_INITIAL_STATE; - - getApiSettings() { - const host = this.getHost(); - return { - type: WidgetType.Credits, - url: WidgetDefinitions[WidgetType.Credits].url(host, this.getWidgetToken()), - previewUrl: `https://${host}/widgets/end-credits?token=${this.getWidgetToken()}&simulate=1`, - webSettingsUrl: `https://${host}/dashboard#/widgets/credits`, - dataFetchUrl: `https://${host}/api/v5/slobs/widget/endcredits`, - settingsSaveUrl: `https://${host}/api/v5/slobs/widget/endcredits`, - settingsUpdateEvent: 'endCreditsSettingsUpdate', - customCodeAllowed: true, - customFieldsAllowed: true, - }; - } - testRollCredits() { - const headers = authorizedHeaders(this.userService.apiToken); - const request = new Request(`https://${this.getHost()}/api/v5/slobs/widget/test/endcredits`, { - headers, - }); - return fetch(request); - } - - getMetadata(themeOptions: IListOption[]) { - return formMetadata({ - title: metadata.text({ title: $t('Credit Title') }), - subtitle: metadata.text({ - title: $t('Credit Subtitle'), - tooltip: - $t('When the credits roll, this will be the format of the subtitle. Available tokens: ') + - creditTokens.join(', '), - }), - theme: metadata.list({ - title: $t('Theme'), - options: themeOptions, - }), - backgroundColor: metadata.color({ title: $t('Background Color') }), - fontFamily: metadata.fontFamily({ - title: $t('Font'), - tooltip: $t( - 'The Google Font to use for the text. Visit http://google.com/fonts to find one! Popular Fonts include:' + - ' Open Sans, Roboto, Oswald, Lato, and Droid Sans.', - ), - }), - fontSize: metadata.fontSize({ - title: $t('Font Size'), - min: 10, - max: 100, - }), - fontColor: metadata.color({ - title: $t('Text Color'), - tooltip: $t('A hex code for the base text color.'), - }), - delayTime: metadata.slider({ - title: $t('Delay Time'), - tooltip: $t('Wait time before rerunning the credit reel.'), - max: 10, - interval: 1, - min: 0, - }), - rollSpeed: metadata.slider({ - title: $t('Roll Speed'), - tooltip: $t('Speed of the rolling credits.'), - max: 5, - interval: 1, - min: 1, - }), - rollTime: metadata.slider({ - title: $t('Roll Time'), - tooltip: $t('Duration of the rolling credits.'), - max: 150, - interval: 5, - min: 15, - }), - }); - } -} diff --git a/app/services/widgets/widgets-config.ts b/app/services/widgets/widgets-config.ts index 2f2eb90c1fe2..8253a7eee692 100644 --- a/app/services/widgets/widgets-config.ts +++ b/app/services/widgets/widgets-config.ts @@ -8,6 +8,7 @@ export type TWidgetType = | WidgetType.GameWidget | WidgetType.EmoteWall | WidgetType.DonationTicker + | WidgetType.Credits | WidgetType.CustomWidget | WidgetType.ChatBox | WidgetType.SponsorBanner @@ -476,9 +477,31 @@ export function getWidgetsConfig( // // }, - // Credits: { - // - // }, + [WidgetType.Credits]: { + type: WidgetType.DonationTicker, + + defaultTransform: { + width: 600, + height: 200, + x: 1, + y: 1, + anchor: AnchorPoint.North, + }, + + settingsWindowSize: { + width: 850, + height: 700, + }, + + url: `https://${host}/widgets/end-credits?token=${token}`, + previewUrl: `https://${host}/widgets/end-credits?token=${token}&simulate=1`, + webSettingsUrl: `https://${host}/dashboard#/widgets/credits`, + dataFetchUrl: `https://${host}/api/v5/slobs/widget/endcredits`, + settingsSaveUrl: `https://${host}/api/v5/slobs/widget/endcredits`, + settingsUpdateEvent: 'endCreditsSettingsUpdate', + customCodeAllowed: true, + customFieldsAllowed: true, + }, [WidgetType.DonationTicker]: { type: WidgetType.DonationTicker, diff --git a/app/services/windows.ts b/app/services/windows.ts index 58e6109b0f6e..5c595d83a862 100644 --- a/app/services/windows.ts +++ b/app/services/windows.ts @@ -55,7 +55,6 @@ 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'; import AlertBox from 'components/widgets/AlertBox.vue'; @@ -105,7 +104,6 @@ export function getComponents() { GameOverlayEventFeed, AdvancedStatistics, MultistreamChatInfo, - Credits, TipJar, StreamBoss, MediaShare,