mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-14 13:52:11 +00:00
tts config pages
This commit is contained in:
1 parent
c388ac1407
commit
9dcd247671
15 files changed
+1058
-853
No files matched your search
@@ -94,6 +94,10 @@ p {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.fw-semibold {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
+83
-75
@@ -1,47 +1,56 @@
|
||||
import React, { useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
import { TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/GetTwitchEventRule';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import { TwitchEventCategory, TWITCH_EVENT_CATEGORY_BY_STRING } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory';
|
||||
import { EventResponse } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse';
|
||||
import { EventMatchPredicate } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate';
|
||||
import { CreateTwitchEventRule, CreateTwitchEventRuleRequest } from '@storyteller/components/src/api/storyteller/twitch_event_rules/CreateTwitchEventRule';
|
||||
import { EventMatchPredicateBuilderComponent } from './event_match_predicate_builder/EventMatchPredicateBuilderComponent';
|
||||
import { EventResponseComponent } from './event_response_builder/EventResponseComponent';
|
||||
import { TwitchEventRuleElement } from './rule_cards/TwitchEventRuleElement';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleLeft, faSave } from '@fortawesome/free-solid-svg-icons';
|
||||
import React, { useState } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { TwitchEventRule } from "@storyteller/components/src/api/storyteller/twitch_event_rules/GetTwitchEventRule";
|
||||
import { Link, useHistory, useParams } from "react-router-dom";
|
||||
import {
|
||||
TwitchEventCategory,
|
||||
TWITCH_EVENT_CATEGORY_BY_STRING,
|
||||
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory";
|
||||
import { EventResponse } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse";
|
||||
import { EventMatchPredicate } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate";
|
||||
import {
|
||||
CreateTwitchEventRule,
|
||||
CreateTwitchEventRuleRequest,
|
||||
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/CreateTwitchEventRule";
|
||||
import { EventMatchPredicateBuilderComponent } from "./event_match_predicate_builder/EventMatchPredicateBuilderComponent";
|
||||
import { EventResponseComponent } from "./event_response_builder/EventResponseComponent";
|
||||
import { TwitchEventRuleElement } from "./rule_cards/TwitchEventRuleElement";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleLeft, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
sessionWrapper: SessionWrapper;
|
||||
allTtsModels: TtsModelListItem[];
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
function TtsConfigsCreateRulePage(props: Props) {
|
||||
const { event_category } = useParams() as { event_category : string };
|
||||
const { event_category } = useParams() as { event_category: string };
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
// TODO: Use centralized configs
|
||||
const indexLink = '/tts_configs';
|
||||
const indexLink = "/tts_configs";
|
||||
|
||||
// ========== In-Progress Model Edits ==========
|
||||
|
||||
// Sent to sub-components to initialize UI
|
||||
// NB: This is a HACK. The subcomponent views were originally created for the "Edit" UI
|
||||
// and this had to be done to get them to work for "Create" UI. Sending empty dictionaries
|
||||
// NB: This is a HACK. The subcomponent views were originally created for the "Edit" UI
|
||||
// and this had to be done to get them to work for "Create" UI. Sending empty dictionaries
|
||||
// or statically initialized values froze the UI.
|
||||
const [serverEventMatchPredicate] = useState<EventMatchPredicate>({});
|
||||
const [serverEventResponse] = useState<EventResponse>({});
|
||||
|
||||
const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] = useState<EventMatchPredicate>({});
|
||||
const [modifiedEventResponse, setModifiedEventResponse] = useState<EventResponse>({});
|
||||
const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] =
|
||||
useState<EventMatchPredicate>({});
|
||||
const [modifiedEventResponse, setModifiedEventResponse] =
|
||||
useState<EventResponse>({});
|
||||
const [ruleIsDisabled] = useState(false);
|
||||
|
||||
let maybeTwitchEventCategory = TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
|
||||
let maybeTwitchEventCategory =
|
||||
TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
|
||||
|
||||
if (maybeTwitchEventCategory === undefined) {
|
||||
history.push(indexLink);
|
||||
@@ -51,15 +60,19 @@ function TtsConfigsCreateRulePage(props: Props) {
|
||||
// NB: To satisfy the type system and not be a union on 'undefined'
|
||||
const twitchEventCategory = maybeTwitchEventCategory;
|
||||
|
||||
const updateModifiedEventMatchPredicate = (predicate: EventMatchPredicate) => {
|
||||
const updateModifiedEventMatchPredicate = (
|
||||
predicate: EventMatchPredicate
|
||||
) => {
|
||||
setModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
};
|
||||
|
||||
const updateModifiedEventResponse = (response: EventResponse) => {
|
||||
setModifiedEventResponse(response);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
const handleFormSubmit = async (
|
||||
ev: React.FormEvent<HTMLFormElement>
|
||||
): Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
// TODO: Check for errors.
|
||||
@@ -69,7 +82,7 @@ function TtsConfigsCreateRulePage(props: Props) {
|
||||
|
||||
// TODO: UUID idempotency token that updates on every model change.
|
||||
|
||||
const request : CreateTwitchEventRuleRequest = {
|
||||
const request: CreateTwitchEventRuleRequest = {
|
||||
idempotency_token: uuidv4(),
|
||||
event_category: twitchEventCategory,
|
||||
event_match_predicate: newEventMatchPredicate,
|
||||
@@ -84,16 +97,16 @@ function TtsConfigsCreateRulePage(props: Props) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
return <h1>Must Log In</h1>;
|
||||
}
|
||||
|
||||
// NB: This is a hypothetical version of what we'll update to
|
||||
let renderRule : TwitchEventRule = {
|
||||
let renderRule: TwitchEventRule = {
|
||||
// Not yet saved, so fake values
|
||||
token: 'virtual',
|
||||
token: "virtual",
|
||||
user_specified_rule_order: 1000,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
@@ -106,62 +119,57 @@ function TtsConfigsCreateRulePage(props: Props) {
|
||||
};
|
||||
|
||||
const titlesByCategory = new Map<TwitchEventCategory, string>([
|
||||
[TwitchEventCategory.Bits, 'Create New Bits / Cheers Rule'],
|
||||
[TwitchEventCategory.ChannelPoints, 'Create New Channel Points Rule'],
|
||||
[TwitchEventCategory.Bits, "Create New Bits Rule"],
|
||||
[TwitchEventCategory.ChannelPoints, "Create New Channel Points Rule"],
|
||||
]);
|
||||
|
||||
const pageTitle = titlesByCategory.get(twitchEventCategory);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
<h1 className="title"> {pageTitle} </h1>
|
||||
<div className="pt-5 container pb-2">
|
||||
<h1 className="fw-bold mt-5 pt-lg-5">
|
||||
<span className="word">{pageTitle}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
|
||||
<EventMatchPredicateBuilderComponent
|
||||
twitchEventCategory={twitchEventCategory}
|
||||
serverEventMatchPredicate={serverEventMatchPredicate}
|
||||
updateModifiedEventMatchPredicate={updateModifiedEventMatchPredicate}
|
||||
<div className="container mt-5">
|
||||
<form onSubmit={handleFormSubmit} className="d-flex flex-column gap-4">
|
||||
<EventMatchPredicateBuilderComponent
|
||||
twitchEventCategory={twitchEventCategory}
|
||||
serverEventMatchPredicate={serverEventMatchPredicate}
|
||||
updateModifiedEventMatchPredicate={
|
||||
updateModifiedEventMatchPredicate
|
||||
}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<EventResponseComponent
|
||||
serverEventResponse={serverEventResponse}
|
||||
updateModifiedEventResponse={updateModifiedEventResponse}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
|
||||
<h2 className="title is-4">This is the rule:</h2>
|
||||
|
||||
<div className="content">
|
||||
<TwitchEventRuleElement
|
||||
rule={renderRule}
|
||||
hideButtons={true}
|
||||
<EventResponseComponent
|
||||
serverEventResponse={serverEventResponse}
|
||||
updateModifiedEventResponse={updateModifiedEventResponse}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
|
||||
<button className="button is-large is-fullwidth is-primary">
|
||||
Save New Rule <FontAwesomeIcon icon={faSave} />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
<h2 className="fw-bold mt-4">This is the rule:</h2>
|
||||
|
||||
<Link to={indexLink} className="button is-large is-fullwidth is-info is-outlined">
|
||||
<FontAwesomeIcon icon={faAngleLeft} /> Cancel / Go Back
|
||||
</Link>
|
||||
<TwitchEventRuleElement
|
||||
rule={renderRule}
|
||||
hideButtons={true}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
<button className="btn btn-primary">
|
||||
<FontAwesomeIcon icon={faSave} className="me-2" />
|
||||
Save New Rule
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Link to={indexLink} className="btn btn-secondary mt-3 mb-5">
|
||||
<FontAwesomeIcon icon={faAngleLeft} className="me-2" />
|
||||
Cancel / Go Back
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { TtsConfigsCreateRulePage }
|
||||
export { TtsConfigsCreateRulePage };
|
||||
+94
-74
@@ -1,43 +1,58 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { GetTwitchEventRule, GetTwitchEventRuleIsError, GetTwitchEventRuleIsOk, TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/GetTwitchEventRule';
|
||||
import { EditTwitchEventRule, EditTwitchEventRuleRequest } from '@storyteller/components/src/api/storyteller/twitch_event_rules/EditTwitchEventRule';
|
||||
import { TwitchEventRuleElement } from './rule_cards/TwitchEventRuleElement';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleLeft, faSave } from '@fortawesome/free-solid-svg-icons';
|
||||
import { EventMatchPredicate } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate';
|
||||
import { EventResponse } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
import { EventResponseComponent } from './event_response_builder/EventResponseComponent';
|
||||
import { EventMatchPredicateBuilderComponent } from './event_match_predicate_builder/EventMatchPredicateBuilderComponent';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import {
|
||||
GetTwitchEventRule,
|
||||
GetTwitchEventRuleIsError,
|
||||
GetTwitchEventRuleIsOk,
|
||||
TwitchEventRule,
|
||||
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/GetTwitchEventRule";
|
||||
import {
|
||||
EditTwitchEventRule,
|
||||
EditTwitchEventRuleRequest,
|
||||
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/EditTwitchEventRule";
|
||||
import { TwitchEventRuleElement } from "./rule_cards/TwitchEventRuleElement";
|
||||
import { Link, useHistory, useParams } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faAngleLeft, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { EventMatchPredicate } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate";
|
||||
import { EventResponse } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { EventResponseComponent } from "./event_response_builder/EventResponseComponent";
|
||||
import { EventMatchPredicateBuilderComponent } from "./event_match_predicate_builder/EventMatchPredicateBuilderComponent";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
sessionWrapper: SessionWrapper;
|
||||
allTtsModels: TtsModelListItem[];
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
function TtsConfigsEditRulePage(props: Props) {
|
||||
const { token } : { token : string } = useParams();
|
||||
const { token }: { token: string } = useParams();
|
||||
|
||||
// TODO: Use centralized URL configs
|
||||
const indexLink = '/tts_configs';
|
||||
const indexLink = "/tts_configs";
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
// ========== Initial Load of Server State ==========
|
||||
|
||||
const [twitchEventRule, setTwitchEventRule] = useState<TwitchEventRule|undefined>(undefined);
|
||||
const [twitchEventRule, setTwitchEventRule] = useState<
|
||||
TwitchEventRule | undefined
|
||||
>(undefined);
|
||||
|
||||
// Sent to sub-components to initialize UI
|
||||
const [serverEventMatchPredicate, setServerEventMatchPredicate] = useState<EventMatchPredicate>({});
|
||||
const [serverEventResponse, setServerEventResponse] = useState<EventResponse>({});
|
||||
const [serverEventMatchPredicate, setServerEventMatchPredicate] =
|
||||
useState<EventMatchPredicate>({});
|
||||
const [serverEventResponse, setServerEventResponse] = useState<EventResponse>(
|
||||
{}
|
||||
);
|
||||
|
||||
// ========== In-Progress Model Edits ==========
|
||||
|
||||
const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] = useState<EventMatchPredicate>({});
|
||||
const [modifiedEventResponse, setModifiedEventResponse] = useState<EventResponse>({});
|
||||
const [modifiedEventMatchPredicate, setModifiedEventMatchPredicate] =
|
||||
useState<EventMatchPredicate>({});
|
||||
const [modifiedEventResponse, setModifiedEventResponse] =
|
||||
useState<EventResponse>({});
|
||||
const [ruleIsDisabled, setRuleIsDisabled] = useState(false);
|
||||
|
||||
const getTwitchEventRule = useCallback(async (token: string) => {
|
||||
@@ -46,15 +61,18 @@ function TtsConfigsEditRulePage(props: Props) {
|
||||
if (GetTwitchEventRuleIsOk(response)) {
|
||||
// Source of truth
|
||||
setTwitchEventRule(response.twitch_event_rule);
|
||||
setServerEventMatchPredicate(response.twitch_event_rule.event_match_predicate);
|
||||
setServerEventMatchPredicate(
|
||||
response.twitch_event_rule.event_match_predicate
|
||||
);
|
||||
setServerEventResponse(response.twitch_event_rule.event_response);
|
||||
|
||||
// In-progress modifications
|
||||
setModifiedEventMatchPredicate(response.twitch_event_rule.event_match_predicate);
|
||||
setModifiedEventMatchPredicate(
|
||||
response.twitch_event_rule.event_match_predicate
|
||||
);
|
||||
setModifiedEventResponse(response.twitch_event_rule.event_response);
|
||||
setRuleIsDisabled(response.twitch_event_rule.rule_is_disabled);
|
||||
|
||||
} else if (GetTwitchEventRuleIsError(response)) {
|
||||
} else if (GetTwitchEventRuleIsError(response)) {
|
||||
// TODO
|
||||
}
|
||||
}, []);
|
||||
@@ -63,15 +81,19 @@ function TtsConfigsEditRulePage(props: Props) {
|
||||
getTwitchEventRule(token);
|
||||
}, [getTwitchEventRule, token]);
|
||||
|
||||
const updateModifiedEventMatchPredicate = (predicate: EventMatchPredicate) => {
|
||||
const updateModifiedEventMatchPredicate = (
|
||||
predicate: EventMatchPredicate
|
||||
) => {
|
||||
setModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
};
|
||||
|
||||
const updateModifiedEventResponse = (response: EventResponse) => {
|
||||
setModifiedEventResponse(response);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
const handleFormSubmit = async (
|
||||
ev: React.FormEvent<HTMLFormElement>
|
||||
): Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
// TODO: Check for errors.
|
||||
@@ -79,7 +101,7 @@ function TtsConfigsEditRulePage(props: Props) {
|
||||
let newEventMatchPredicate = modifiedEventMatchPredicate;
|
||||
let newEventResponse = modifiedEventResponse;
|
||||
|
||||
const request : EditTwitchEventRuleRequest = {
|
||||
const request: EditTwitchEventRuleRequest = {
|
||||
event_match_predicate: newEventMatchPredicate,
|
||||
event_response: newEventResponse,
|
||||
rule_is_disabled: ruleIsDisabled,
|
||||
@@ -91,7 +113,7 @@ function TtsConfigsEditRulePage(props: Props) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
return <h1>Must Log In</h1>;
|
||||
@@ -102,7 +124,7 @@ function TtsConfigsEditRulePage(props: Props) {
|
||||
}
|
||||
|
||||
// NB: This is a hypothetical version of what we'll update to
|
||||
let renderRule : TwitchEventRule = {
|
||||
let renderRule: TwitchEventRule = {
|
||||
// Unchanged
|
||||
token: twitchEventRule.token,
|
||||
event_category: twitchEventRule.event_category,
|
||||
@@ -118,54 +140,52 @@ function TtsConfigsEditRulePage(props: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
<h1 className="title"> Edit Rule </h1>
|
||||
<div className="pt-5 container">
|
||||
<h1 className="fw-bold mt-5 pt-lg-5">
|
||||
<span className="word">Edit Rule</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
|
||||
<EventMatchPredicateBuilderComponent
|
||||
twitchEventCategory={twitchEventRule.event_category}
|
||||
serverEventMatchPredicate={serverEventMatchPredicate}
|
||||
updateModifiedEventMatchPredicate={updateModifiedEventMatchPredicate}
|
||||
<div className="container mt-5">
|
||||
<form onSubmit={handleFormSubmit} className="d-flex flex-column gap-4">
|
||||
<EventMatchPredicateBuilderComponent
|
||||
twitchEventCategory={twitchEventRule.event_category}
|
||||
serverEventMatchPredicate={serverEventMatchPredicate}
|
||||
updateModifiedEventMatchPredicate={
|
||||
updateModifiedEventMatchPredicate
|
||||
}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<EventResponseComponent
|
||||
serverEventResponse={serverEventResponse}
|
||||
updateModifiedEventResponse={updateModifiedEventResponse}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
|
||||
<h2 className="title is-4">This is the rule:</h2>
|
||||
|
||||
<div className="content">
|
||||
<TwitchEventRuleElement
|
||||
rule={renderRule}
|
||||
hideButtons={true}
|
||||
<EventResponseComponent
|
||||
serverEventResponse={serverEventResponse}
|
||||
updateModifiedEventResponse={updateModifiedEventResponse}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
|
||||
<h2 className="fw-bold mt-4">This is the rule:</h2>
|
||||
|
||||
<div className="content">
|
||||
<TwitchEventRuleElement
|
||||
rule={renderRule}
|
||||
hideButtons={true}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="button is-large is-fullwidth is-primary">
|
||||
Save Changes <FontAwesomeIcon icon={faSave} />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<Link to={indexLink} className="button is-large is-fullwidth is-info is-outlined">
|
||||
<FontAwesomeIcon icon={faAngleLeft} /> Cancel / Go Back
|
||||
</Link>
|
||||
<button className="btn btn-primary">
|
||||
<FontAwesomeIcon icon={faSave} className="me-2" />
|
||||
Save Changes
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<Link to={indexLink} className="btn btn-secondary mt-3 mb-5">
|
||||
<FontAwesomeIcon icon={faAngleLeft} className="me-2" />
|
||||
Cancel / Go Back
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { TtsConfigsEditRulePage }
|
||||
export { TtsConfigsEditRulePage };
|
||||
@@ -8,20 +8,12 @@ import {
|
||||
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules";
|
||||
import { TwitchEventRuleElement } from "./rule_cards/TwitchEventRuleElement";
|
||||
import { TwitchEventCategory } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory";
|
||||
import { DiscordLink } from "@storyteller/components/src/elements/DiscordLink";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faBox,
|
||||
faDonate,
|
||||
faFilm,
|
||||
faGem,
|
||||
faHeart,
|
||||
faLightbulb,
|
||||
faMeteor,
|
||||
faPlus,
|
||||
faSort,
|
||||
faTerminal,
|
||||
faUsers,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
+102
-71
@@ -1,60 +1,80 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { ListTwitchEventRules, ListTwitchEventRulesIsError, ListTwitchEventRulesIsOk, TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules';
|
||||
import { ReorderTwitchEventRules, ReorderTwitchEventRulesRequest } from '@storyteller/components/src/api/storyteller/twitch_event_rules/ReorderTwitchEventRules';
|
||||
import { TwitchEventCategory, TWITCH_EVENT_CATEGORY_BY_STRING } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSave } from '@fortawesome/free-solid-svg-icons';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { ReorderableTwitchEventRuleElement } from './rule_cards/ReorderableTwitchEventRuleElement';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import {
|
||||
ListTwitchEventRules,
|
||||
ListTwitchEventRulesIsError,
|
||||
ListTwitchEventRulesIsOk,
|
||||
TwitchEventRule,
|
||||
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules";
|
||||
import {
|
||||
ReorderTwitchEventRules,
|
||||
ReorderTwitchEventRulesRequest,
|
||||
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/ReorderTwitchEventRules";
|
||||
import {
|
||||
TwitchEventCategory,
|
||||
TWITCH_EVENT_CATEGORY_BY_STRING,
|
||||
} from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { ReorderableTwitchEventRuleElement } from "./rule_cards/ReorderableTwitchEventRuleElement";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
sessionWrapper: SessionWrapper;
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
function TtsConfigsReorderPage(props: Props) {
|
||||
const { event_category } = useParams() as { event_category : string };
|
||||
const { event_category } = useParams() as { event_category: string };
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
// TODO: Use centralized configs
|
||||
const indexLink = '/tts_configs';
|
||||
const indexLink = "/tts_configs";
|
||||
|
||||
const [twitchEventRules, setTwitchEventRules] = useState<TwitchEventRule[]>([]);
|
||||
const [twitchEventRules, setTwitchEventRules] = useState<TwitchEventRule[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const listTwitchEventRules = useCallback(async (twitchEventCategory: TwitchEventCategory) => {
|
||||
const response = await ListTwitchEventRules();
|
||||
const listTwitchEventRules = useCallback(
|
||||
async (twitchEventCategory: TwitchEventCategory) => {
|
||||
const response = await ListTwitchEventRules();
|
||||
|
||||
if (ListTwitchEventRulesIsOk(response)) {
|
||||
const allRules = response.twitch_event_rules;
|
||||
const releventRules = allRules.filter(rule => rule.event_category === twitchEventCategory);
|
||||
if (ListTwitchEventRulesIsOk(response)) {
|
||||
const allRules = response.twitch_event_rules;
|
||||
const releventRules = allRules.filter(
|
||||
(rule) => rule.event_category === twitchEventCategory
|
||||
);
|
||||
|
||||
setTwitchEventRules(releventRules);
|
||||
|
||||
} else if (ListTwitchEventRulesIsError(response)) {
|
||||
// TODO
|
||||
}
|
||||
}, []);
|
||||
setTwitchEventRules(releventRules);
|
||||
} else if (ListTwitchEventRulesIsError(response)) {
|
||||
// TODO
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('useEffect');
|
||||
let maybeTwitchEventCategory = TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
|
||||
console.log("useEffect");
|
||||
let maybeTwitchEventCategory =
|
||||
TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
|
||||
|
||||
if (maybeTwitchEventCategory === undefined) {
|
||||
history.push(indexLink);
|
||||
} else {
|
||||
listTwitchEventRules(maybeTwitchEventCategory);
|
||||
}
|
||||
|
||||
}, [listTwitchEventRules, event_category, history]);
|
||||
|
||||
const handleMoveUp = (ruleIndex: number) => {
|
||||
let newEventRules = [...twitchEventRules];
|
||||
|
||||
if (ruleIndex >= newEventRules.length || // Walk off end
|
||||
ruleIndex === 0) { // Can't move up.
|
||||
if (
|
||||
ruleIndex >= newEventRules.length || // Walk off end
|
||||
ruleIndex === 0
|
||||
) {
|
||||
// Can't move up.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,13 +85,16 @@ function TtsConfigsReorderPage(props: Props) {
|
||||
newEventRules[ruleIndex] = aboveRule;
|
||||
|
||||
setTwitchEventRules(newEventRules);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveDown = (ruleIndex: number) => {
|
||||
let newEventRules = [...twitchEventRules];
|
||||
|
||||
if (ruleIndex < 0 || // Invalid
|
||||
ruleIndex >= newEventRules.length - 1) { // Can't move down.
|
||||
if (
|
||||
ruleIndex < 0 || // Invalid
|
||||
ruleIndex >= newEventRules.length - 1
|
||||
) {
|
||||
// Can't move down.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -85,18 +108,19 @@ function TtsConfigsReorderPage(props: Props) {
|
||||
newEventRules[ruleIndex] = belowRule;
|
||||
|
||||
setTwitchEventRules(newEventRules);
|
||||
}
|
||||
|
||||
const handleSaveOrderings = async (ev: React.FormEvent<HTMLButtonElement>) : Promise<boolean> => {
|
||||
};
|
||||
|
||||
const handleSaveOrderings = async (
|
||||
ev: React.FormEvent<HTMLButtonElement>
|
||||
): Promise<boolean> => {
|
||||
const pairs = twitchEventRules.map((rule, index) => {
|
||||
return {
|
||||
rule_token: rule.token,
|
||||
position: index,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const request : ReorderTwitchEventRulesRequest = {
|
||||
const request: ReorderTwitchEventRulesRequest = {
|
||||
rule_token_position_pairs: pairs,
|
||||
};
|
||||
|
||||
@@ -106,7 +130,7 @@ function TtsConfigsReorderPage(props: Props) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
return <h1>Must Log In</h1>;
|
||||
@@ -114,44 +138,51 @@ function TtsConfigsReorderPage(props: Props) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
<h1 className="title"> Reorder Rules </h1>
|
||||
<h2 className="subtitle"> This affects match priority. Rules at the top come first. </h2>
|
||||
<div className="container pt-5">
|
||||
<h1 className="fw-bold mt-5 pt-lg-5 mb-4">
|
||||
<span className="word">Reorder Rules</span>
|
||||
</h1>
|
||||
<h2 className="fw-bold">
|
||||
{" "}
|
||||
This affects match priority. Rules at the top come first.{" "}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<article className="message is-dark">
|
||||
<div className="message-body">
|
||||
You can create rules for matching <em>different categories of events</em>: cheers (bits),
|
||||
channel point rewards, etc.
|
||||
|
||||
When an event occurs in your stream, we compare it against the rules in its
|
||||
category. <strong>The first matched rule</strong> (from the top) is selected, and the action
|
||||
that it specifies is taken. For now this is limited to FakeYou text to speech, but we'll be
|
||||
adding lots of new capabilities soon.
|
||||
</div>
|
||||
</article>
|
||||
<div className="container">
|
||||
<p className="pb-5">
|
||||
You can create rules for matching{" "}
|
||||
<em>different categories of events</em>: cheers (bits), channel point
|
||||
rewards, etc. When an event occurs in your stream, we compare it
|
||||
against the rules in its category.{" "}
|
||||
<strong>The first matched rule</strong> (from the top) is selected,
|
||||
and the action that it specifies is taken. For now this is limited to
|
||||
FakeYou text to speech, but we'll be adding lots of new capabilities
|
||||
soon.
|
||||
</p>
|
||||
|
||||
{twitchEventRules.map((rule, index) => {
|
||||
return <ReorderableTwitchEventRuleElement
|
||||
key={rule.token}
|
||||
rule={rule}
|
||||
ruleIndex={index}
|
||||
handleMoveUp={handleMoveUp}
|
||||
handleMoveDown={handleMoveDown}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
return (
|
||||
<ReorderableTwitchEventRuleElement
|
||||
key={rule.token}
|
||||
rule={rule}
|
||||
ruleIndex={index}
|
||||
handleMoveUp={handleMoveUp}
|
||||
handleMoveDown={handleMoveDown}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="button is-large is-fullwidth is-primary"
|
||||
onClick={handleSaveOrderings}
|
||||
<button
|
||||
className="btn btn-primary w-100 my-5"
|
||||
onClick={handleSaveOrderings}
|
||||
>
|
||||
Save Orderings <FontAwesomeIcon icon={faSave} />
|
||||
</button>
|
||||
<FontAwesomeIcon icon={faSave} className="me-2" />
|
||||
Save Orderings
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { TtsConfigsReorderPage }
|
||||
export { TtsConfigsReorderPage };
|
||||
+150
-109
@@ -1,36 +1,49 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { BitsRuleType } from './types/BitsRuleType';
|
||||
import { TwitchEventCategory } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory';
|
||||
import { EventMatchPredicate } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate';
|
||||
import { BitsCheermoteNameExactMatchForm } from './subforms/BitsCheermoteNameExactMatchForm';
|
||||
import { BitsCheermotePrefixSpendThresholdForm } from './subforms/BitsCheermotePrefixSpendThresholdForm';
|
||||
import { BitsSpendThresholdForm } from './subforms/BitsSpendThresholdForm';
|
||||
import { ChannelPointsRewardNameExactMatchForm } from './subforms/ChannelPointsRewardNameExactMatchForm';
|
||||
import { ChannelPointsRuleType } from './types/ChannelPointsRuleType';
|
||||
import { CheerUtil } from '../../../twitch/CheerUtil';
|
||||
import { CheerState, CheerStateIsCustom, CheerStateIsOfficial, cheerStateToPredicate, predicateToCheerState } from './CheerState';
|
||||
import { CHEER_LOOKUP_MAP } from '../../../twitch/Cheers';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BitsRuleType } from "./types/BitsRuleType";
|
||||
import { TwitchEventCategory } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/TwitchEventCategory";
|
||||
import { EventMatchPredicate } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate";
|
||||
import { BitsCheermoteNameExactMatchForm } from "./subforms/BitsCheermoteNameExactMatchForm";
|
||||
import { BitsCheermotePrefixSpendThresholdForm } from "./subforms/BitsCheermotePrefixSpendThresholdForm";
|
||||
import { BitsSpendThresholdForm } from "./subforms/BitsSpendThresholdForm";
|
||||
import { ChannelPointsRewardNameExactMatchForm } from "./subforms/ChannelPointsRewardNameExactMatchForm";
|
||||
import { ChannelPointsRuleType } from "./types/ChannelPointsRuleType";
|
||||
import { CheerUtil } from "../../../twitch/CheerUtil";
|
||||
import {
|
||||
CheerState,
|
||||
CheerStateIsCustom,
|
||||
CheerStateIsOfficial,
|
||||
cheerStateToPredicate,
|
||||
predicateToCheerState,
|
||||
} from "./CheerState";
|
||||
import { CHEER_LOOKUP_MAP } from "../../../twitch/Cheers";
|
||||
|
||||
// NB: This was created for "Edit" view, but we've forced it to work for "Create" view by hardcoding
|
||||
// NB: This was created for "Edit" view, but we've forced it to work for "Create" view by hardcoding
|
||||
// values that don't actually come from the server.
|
||||
|
||||
interface EventMatchPredicateBuilderComponentProps {
|
||||
// CANNOT BE CHANGED AFTER CREATION
|
||||
twitchEventCategory: TwitchEventCategory,
|
||||
twitchEventCategory: TwitchEventCategory;
|
||||
|
||||
// Original event match predicate from the server
|
||||
// Original event match predicate from the server
|
||||
// (also immutable for sanity and because we lose information on UI changes)
|
||||
serverEventMatchPredicate: EventMatchPredicate,
|
||||
serverEventMatchPredicate: EventMatchPredicate;
|
||||
|
||||
// Updates sent back up the tree
|
||||
updateModifiedEventMatchPredicate: (predicate: EventMatchPredicate) => void,
|
||||
};
|
||||
updateModifiedEventMatchPredicate: (predicate: EventMatchPredicate) => void;
|
||||
}
|
||||
|
||||
function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderComponentProps) {
|
||||
function EventMatchPredicateBuilderComponent(
|
||||
props: EventMatchPredicateBuilderComponentProps
|
||||
) {
|
||||
// ========== Core UI flow ==========
|
||||
|
||||
const [bitsRuleType, setBitsRuleType] = useState<BitsRuleType>(BitsRuleType.BitsCheermoteNameExactMatch);
|
||||
const [channelPointsRuleType, setChannelPointsRuleType] = useState<ChannelPointsRuleType>(ChannelPointsRuleType.ChannelPointsRewardNameExactMatch);
|
||||
const [bitsRuleType, setBitsRuleType] = useState<BitsRuleType>(
|
||||
BitsRuleType.BitsCheermoteNameExactMatch
|
||||
);
|
||||
const [channelPointsRuleType, setChannelPointsRuleType] =
|
||||
useState<ChannelPointsRuleType>(
|
||||
ChannelPointsRuleType.ChannelPointsRewardNameExactMatch
|
||||
);
|
||||
|
||||
// ========== Cached Values for Editing ==========
|
||||
|
||||
@@ -41,15 +54,16 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
|
||||
|
||||
// Used in:
|
||||
// ChannelPointsRewardNameExactMatch
|
||||
const [rewardName, setRewardName] = useState('');
|
||||
const [rewardName, setRewardName] = useState("");
|
||||
|
||||
// NB: useState is not always setting from props correctly (after several re-renders)
|
||||
// The following answers suggests using useEffect:
|
||||
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
|
||||
// https://stackoverflow.com/a/62982753
|
||||
useEffect(() => {
|
||||
let newBitsRuleType = BitsRuleType.BitsCheermoteNameExactMatch;
|
||||
let newChannelPointsRuleType = ChannelPointsRuleType.ChannelPointsRewardNameExactMatch;
|
||||
let newBitsRuleType = BitsRuleType.BitsCheermoteNameExactMatch;
|
||||
let newChannelPointsRuleType =
|
||||
ChannelPointsRuleType.ChannelPointsRewardNameExactMatch;
|
||||
|
||||
let newCheerState = predicateToCheerState(props.serverEventMatchPredicate);
|
||||
|
||||
@@ -57,16 +71,26 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
|
||||
case TwitchEventCategory.Bits:
|
||||
if (!!props.serverEventMatchPredicate.bits_cheermote_name_exact_match) {
|
||||
newBitsRuleType = BitsRuleType.BitsCheermoteNameExactMatch;
|
||||
} else if (!!props.serverEventMatchPredicate.bits_cheermote_prefix_spend_threshold) {
|
||||
} else if (
|
||||
!!props.serverEventMatchPredicate
|
||||
.bits_cheermote_prefix_spend_threshold
|
||||
) {
|
||||
newBitsRuleType = BitsRuleType.BitsCheermotePrefixSpendThreshold;
|
||||
} else if (!!props.serverEventMatchPredicate.bits_spend_threshold) {
|
||||
newBitsRuleType = BitsRuleType.BitsSpendThreshold;
|
||||
}
|
||||
break;
|
||||
case TwitchEventCategory.ChannelPoints: // NB: Only one rule type
|
||||
if (!!props.serverEventMatchPredicate.channel_points_reward_name_exact_match) {
|
||||
setRewardName(props.serverEventMatchPredicate.channel_points_reward_name_exact_match.reward_name);
|
||||
newChannelPointsRuleType = ChannelPointsRuleType.ChannelPointsRewardNameExactMatch;
|
||||
if (
|
||||
!!props.serverEventMatchPredicate
|
||||
.channel_points_reward_name_exact_match
|
||||
) {
|
||||
setRewardName(
|
||||
props.serverEventMatchPredicate
|
||||
.channel_points_reward_name_exact_match.reward_name
|
||||
);
|
||||
newChannelPointsRuleType =
|
||||
ChannelPointsRuleType.ChannelPointsRewardNameExactMatch;
|
||||
}
|
||||
break;
|
||||
case TwitchEventCategory.ChatCommand: // TODO: Not yet supported
|
||||
@@ -78,82 +102,90 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
|
||||
|
||||
setBitsRuleType(newBitsRuleType);
|
||||
setChannelPointsRuleType(newChannelPointsRuleType);
|
||||
|
||||
}, [props.twitchEventCategory, props.serverEventMatchPredicate]);
|
||||
|
||||
// TODO: We need to recalculate the model.
|
||||
const handleChangedBitsRuleType = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const handleChangedBitsRuleType = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const newRuleType = value as BitsRuleType;
|
||||
|
||||
let predicate = cheerStateToPredicate(cheerState, newRuleType);
|
||||
|
||||
|
||||
setBitsRuleType(newRuleType);
|
||||
props.updateModifiedEventMatchPredicate(predicate);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangedChannelPointsRuleType = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const handleChangedChannelPointsRuleType = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const ruleType = value as ChannelPointsRuleType;
|
||||
setChannelPointsRuleType(ruleType);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangedCheerPrefix = (newPrefix: string) => {
|
||||
// 1) Update Cheer State
|
||||
// 2) Update NewEventMatchPredicate
|
||||
let newCheerState : CheerState = {};
|
||||
let newCheerState: CheerState = {};
|
||||
|
||||
let maybeCheerPrefix = CHEER_LOOKUP_MAP.get(newPrefix || '');
|
||||
let maybeCheerPrefix = CHEER_LOOKUP_MAP.get(newPrefix || "");
|
||||
|
||||
if (!!maybeCheerPrefix) {
|
||||
newCheerState = {
|
||||
cheerPrefix: maybeCheerPrefix,
|
||||
bits: cheerState.bits, // Unchanged
|
||||
}
|
||||
};
|
||||
} else {
|
||||
newCheerState = {
|
||||
cheerFull: newPrefix,
|
||||
bits: cheerState.bits, // Unchanged
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let predicate = cheerStateToPredicate(newCheerState, bitsRuleType);
|
||||
|
||||
setCheerState(newCheerState);
|
||||
props.updateModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangedCheerName = (name: string) => {
|
||||
// 1) Update Cheer State
|
||||
// 2) Update NewEventMatchPredicate
|
||||
let newCheerState : CheerState = {};
|
||||
let newCheerState: CheerState = {};
|
||||
|
||||
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(name);
|
||||
|
||||
let newBitValue = bitValue;
|
||||
let maybeCheerPrefix = CHEER_LOOKUP_MAP.get(cheerPrefix || '');
|
||||
let maybeCheerPrefix = CHEER_LOOKUP_MAP.get(cheerPrefix || "");
|
||||
|
||||
if (!!maybeCheerPrefix) {
|
||||
// Official prefix.
|
||||
newCheerState = {
|
||||
cheerPrefix: maybeCheerPrefix,
|
||||
bits: newBitValue,
|
||||
}
|
||||
};
|
||||
} else if (!!cheerPrefix) {
|
||||
// Custom prefix.
|
||||
|
||||
// Test if hypothetically we should retain an independent of name bit value.
|
||||
// Hypothetically, Sus13 -> Sus12, but the bit value is 5000 in a separate field.
|
||||
// Hypothetically, Sus13 -> Sus12, but the bit value is 5000 in a separate field.
|
||||
// We don't want to repalace with 13.
|
||||
if (CheerStateIsCustom(cheerState)) {
|
||||
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(cheerState.cheerFull || '');
|
||||
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(
|
||||
cheerState.cheerFull || ""
|
||||
);
|
||||
|
||||
if ((!!cheerPrefix && !!bitValue) // We have a complete name
|
||||
&& (bitValue !== cheerState.bits)) // The bit value is not independent of the cheer name, as in eg. (Foobar1000, 1000)
|
||||
{
|
||||
if (
|
||||
!!cheerPrefix &&
|
||||
!!bitValue && // We have a complete name
|
||||
bitValue !== cheerState.bits
|
||||
) {
|
||||
// The bit value is not independent of the cheer name, as in eg. (Foobar1000, 1000)
|
||||
// Keep the old value.
|
||||
newBitValue = cheerState.bits;
|
||||
}
|
||||
@@ -161,15 +193,15 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
|
||||
|
||||
newCheerState = {
|
||||
cheerFull: name, // Should always be the full value
|
||||
bits: newBitValue,
|
||||
}
|
||||
}
|
||||
bits: newBitValue,
|
||||
};
|
||||
}
|
||||
|
||||
let predicate = cheerStateToPredicate(newCheerState, bitsRuleType);
|
||||
|
||||
setCheerState(newCheerState);
|
||||
props.updateModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangedMinimumBitsSpent = (minimumSpent: number) => {
|
||||
// 1) Update Cheer State
|
||||
@@ -180,10 +212,12 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
|
||||
newCheerState = {
|
||||
cheerPrefix: cheerState.cheerPrefix, // Unchanged
|
||||
bits: minimumSpent,
|
||||
}
|
||||
};
|
||||
} else if (CheerStateIsCustom(cheerState)) {
|
||||
// Test if the cheer name includes numbers, eg. 'Sus12', or '1984', that differ from bit spend
|
||||
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(cheerState.cheerFull || '');
|
||||
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(
|
||||
cheerState.cheerFull || ""
|
||||
);
|
||||
let cheerFull = cheerState.cheerFull;
|
||||
|
||||
if (!!cheerPrefix && !!bitValue && bitValue === cheerState.bits) {
|
||||
@@ -194,118 +228,125 @@ function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderCo
|
||||
newCheerState = {
|
||||
cheerFull: cheerFull,
|
||||
bits: minimumSpent,
|
||||
}
|
||||
|
||||
};
|
||||
} else {
|
||||
newCheerState = {
|
||||
bits: minimumSpent,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let predicate = cheerStateToPredicate(newCheerState, bitsRuleType);
|
||||
|
||||
setCheerState(newCheerState);
|
||||
props.updateModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO
|
||||
const handleChangedRewardName = (name: string) => {
|
||||
let predicate : EventMatchPredicate = {};
|
||||
let predicate: EventMatchPredicate = {};
|
||||
|
||||
switch (channelPointsRuleType) {
|
||||
case ChannelPointsRuleType.ChannelPointsRewardNameExactMatch:
|
||||
predicate.channel_points_reward_name_exact_match = {
|
||||
reward_name: name, // New value
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
props.updateModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
};
|
||||
|
||||
let ruleTypeSelect = <></>
|
||||
let matchingRulesForm = <></>
|
||||
let ruleTypeSelect = <></>;
|
||||
let matchingRulesForm = <></>;
|
||||
|
||||
if (props.twitchEventCategory === TwitchEventCategory.Bits) {
|
||||
ruleTypeSelect = (
|
||||
<>
|
||||
<select
|
||||
<select
|
||||
value={bitsRuleType}
|
||||
onChange={handleChangedBitsRuleType}>
|
||||
<option
|
||||
value={BitsRuleType.BitsSpendThreshold}
|
||||
>Bits Spend Threshold</option>
|
||||
<option
|
||||
value={BitsRuleType.BitsCheermoteNameExactMatch}
|
||||
>Cheermote Name (Exact Match)</option>
|
||||
<option
|
||||
value={BitsRuleType.BitsCheermotePrefixSpendThreshold}
|
||||
>Cheermote Prefix and Bits Spend Threshold</option>
|
||||
onChange={handleChangedBitsRuleType}
|
||||
className="form-select"
|
||||
>
|
||||
<option value={BitsRuleType.BitsSpendThreshold}>
|
||||
Bits Spend Threshold
|
||||
</option>
|
||||
<option value={BitsRuleType.BitsCheermoteNameExactMatch}>
|
||||
Cheermote Name (Exact Match)
|
||||
</option>
|
||||
<option value={BitsRuleType.BitsCheermotePrefixSpendThreshold}>
|
||||
Cheermote Prefix and Bits Spend Threshold
|
||||
</option>
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
|
||||
switch (bitsRuleType) {
|
||||
case BitsRuleType.BitsCheermoteNameExactMatch:
|
||||
matchingRulesForm = <BitsCheermoteNameExactMatchForm
|
||||
cheerState={cheerState}
|
||||
updateCheerName={handleChangedCheerName}
|
||||
/>;
|
||||
matchingRulesForm = (
|
||||
<BitsCheermoteNameExactMatchForm
|
||||
cheerState={cheerState}
|
||||
updateCheerName={handleChangedCheerName}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case BitsRuleType.BitsCheermotePrefixSpendThreshold:
|
||||
matchingRulesForm = <BitsCheermotePrefixSpendThresholdForm
|
||||
cheerState={cheerState}
|
||||
updateCheerPrefix={handleChangedCheerPrefix}
|
||||
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
|
||||
matchingRulesForm = (
|
||||
<BitsCheermotePrefixSpendThresholdForm
|
||||
cheerState={cheerState}
|
||||
updateCheerPrefix={handleChangedCheerPrefix}
|
||||
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case BitsRuleType.BitsSpendThreshold:
|
||||
matchingRulesForm = <BitsSpendThresholdForm
|
||||
cheerState={cheerState}
|
||||
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
|
||||
/>;
|
||||
matchingRulesForm = (
|
||||
<BitsSpendThresholdForm
|
||||
cheerState={cheerState}
|
||||
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (props.twitchEventCategory === TwitchEventCategory.ChannelPoints) {
|
||||
ruleTypeSelect = (
|
||||
<>
|
||||
<select
|
||||
<select
|
||||
value={channelPointsRuleType}
|
||||
onChange={handleChangedChannelPointsRuleType}>
|
||||
<option
|
||||
onChange={handleChangedChannelPointsRuleType}
|
||||
className="form-select"
|
||||
>
|
||||
<option
|
||||
value={ChannelPointsRuleType.ChannelPointsRewardNameExactMatch}
|
||||
>Channel Points Reward Name (exact match)</option>
|
||||
>
|
||||
Channel Points Reward Name (exact match)
|
||||
</option>
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
|
||||
matchingRulesForm = <ChannelPointsRewardNameExactMatchForm
|
||||
rewardName={rewardName}
|
||||
updateRewardName={handleChangedRewardName}
|
||||
/>;
|
||||
matchingRulesForm = (
|
||||
<ChannelPointsRewardNameExactMatchForm
|
||||
rewardName={rewardName}
|
||||
updateRewardName={handleChangedRewardName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="title is-4">1) Pick what to match on</h2>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div className="d-flex flex-column panel p-3 p-lg-4 gap-2">
|
||||
<h2 className="fw-bold pb-3">1) Pick what to match on</h2>
|
||||
<div className="form-group">{ruleTypeSelect}</div>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<div className="select is-medium is-fullwidth">
|
||||
{ruleTypeSelect}
|
||||
</div>
|
||||
<div className="d-flex flex-column panel p-3 p-lg-4 gap-2">
|
||||
<h2 className="fw-bold pb-3">2) Configure the matching</h2>
|
||||
<div className="form-group">{matchingRulesForm}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 className="title is-4">2) Configure the matching</h2>
|
||||
|
||||
{matchingRulesForm}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { EventMatchPredicateBuilderComponent }
|
||||
export { EventMatchPredicateBuilderComponent };
|
||||
+93
-80
@@ -1,68 +1,83 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faGem } from '@fortawesome/free-solid-svg-icons';
|
||||
import { CHEER_BIT_LEVELS, CHEER_PREFIXES, CHEER_PREFIX_TO_STRING_MAP } from '../../../../twitch/Cheers';
|
||||
import { CheerState, CheerStateIsCustom, CheerStateIsOfficial } from '../CheerState';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faGem } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
CHEER_BIT_LEVELS,
|
||||
CHEER_PREFIXES,
|
||||
CHEER_PREFIX_TO_STRING_MAP,
|
||||
} from "../../../../twitch/Cheers";
|
||||
import {
|
||||
CheerState,
|
||||
CheerStateIsCustom,
|
||||
CheerStateIsOfficial,
|
||||
} from "../CheerState";
|
||||
|
||||
// TODO: Don't duplicate
|
||||
const CHEER_REGEX = /^([A-Za-z]+)(\d+)?$/;
|
||||
|
||||
interface BitsCheermoteNameExactMatchProps {
|
||||
cheerState: CheerState,
|
||||
updateCheerName: (cheerNameOrPrefix: string) => void,
|
||||
};
|
||||
cheerState: CheerState;
|
||||
updateCheerName: (cheerNameOrPrefix: string) => void;
|
||||
}
|
||||
|
||||
function BitsCheermoteNameExactMatchForm(props: BitsCheermoteNameExactMatchProps) {
|
||||
function BitsCheermoteNameExactMatchForm(
|
||||
props: BitsCheermoteNameExactMatchProps
|
||||
) {
|
||||
// The two dropdowns
|
||||
const [cheerPrefix, setCheerPrefix] = useState<string>('');
|
||||
const [cheerPrefix, setCheerPrefix] = useState<string>("");
|
||||
const [bitsValue, setBitsValue] = useState<number>(1);
|
||||
// The freeform text input
|
||||
const [manualCheerValue, setManualCheerValue] = useState<string>('');
|
||||
const [manualCheerValue, setManualCheerValue] = useState<string>("");
|
||||
|
||||
// NB: useState is not always setting from props correctly (after several re-renders)
|
||||
// The following answers suggests using useEffect:
|
||||
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
|
||||
// https://stackoverflow.com/a/62982753
|
||||
useEffect(() => {
|
||||
let newPrefix = '';
|
||||
let newFullName = '';
|
||||
let newPrefix = "";
|
||||
let newFullName = "";
|
||||
let newBits = 1;
|
||||
let hasBits = false;
|
||||
|
||||
if (!!props.cheerState.bits && !isNaN(props.cheerState.bits) && props.cheerState.bits > 0) {
|
||||
if (
|
||||
!!props.cheerState.bits &&
|
||||
!isNaN(props.cheerState.bits) &&
|
||||
props.cheerState.bits > 0
|
||||
) {
|
||||
newBits = props.cheerState.bits;
|
||||
hasBits = true;
|
||||
}
|
||||
|
||||
if (CheerStateIsOfficial(props.cheerState)) {
|
||||
if (!!props.cheerState.cheerPrefix) {
|
||||
newPrefix = CHEER_PREFIX_TO_STRING_MAP.get((props.cheerState.cheerPrefix)) || '';
|
||||
newPrefix =
|
||||
CHEER_PREFIX_TO_STRING_MAP.get(props.cheerState.cheerPrefix) || "";
|
||||
}
|
||||
|
||||
|
||||
if (hasBits) {
|
||||
newFullName = `${newPrefix}${newBits}`;
|
||||
} else {
|
||||
newFullName = newPrefix;
|
||||
}
|
||||
|
||||
} else if (CheerStateIsCustom(props.cheerState)) {
|
||||
newFullName = props.cheerState.cheerFull || '';
|
||||
newFullName = props.cheerState.cheerFull || "";
|
||||
}
|
||||
|
||||
setCheerPrefix(newPrefix);
|
||||
setBitsValue(newBits);
|
||||
setManualCheerValue(newFullName);
|
||||
|
||||
}, [props.cheerState]);
|
||||
|
||||
const updateCheerPrefix = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const updateCheerPrefix = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
setCheerPrefix(value);
|
||||
recalcuateFieldValue(value, bitsValue);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const updateBitsValue = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const updateBitsValue = (ev: React.FormEvent<HTMLSelectElement>): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const numericValue = parseInt(value);
|
||||
if (!isNaN(numericValue)) {
|
||||
@@ -71,13 +86,15 @@ function BitsCheermoteNameExactMatchForm(props: BitsCheermoteNameExactMatchProps
|
||||
recalcuateFieldValue(cheerPrefix, numericValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const updateTextCheerValue = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
|
||||
const updateTextCheerValue = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
|
||||
// Cheer name is the full value, eg. 'Corgo100'
|
||||
const matches = value.trim().match(CHEER_REGEX)
|
||||
const matches = value.trim().match(CHEER_REGEX);
|
||||
|
||||
if (!!matches && matches.length > 1) {
|
||||
setCheerPrefix(matches[1]); // First match group
|
||||
@@ -93,77 +110,73 @@ function BitsCheermoteNameExactMatchForm(props: BitsCheermoteNameExactMatchProps
|
||||
setManualCheerValue(value);
|
||||
props.updateCheerName(value);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// When the dropdowns are used, replace any manual text entry.
|
||||
const recalcuateFieldValue = (prefix: string, bits: number) => {
|
||||
const cheerValue = `${prefix}${bits}`;
|
||||
setManualCheerValue(cheerValue);
|
||||
props.updateCheerName(cheerValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="field is-grouped">
|
||||
<div className="control">
|
||||
<label className="label">Pick the cheer</label>
|
||||
<div className="select is-medium">
|
||||
<select
|
||||
onChange={updateCheerPrefix}
|
||||
value={cheerPrefix}
|
||||
>
|
||||
<option
|
||||
key={`option-*`}
|
||||
value=""
|
||||
>Select cheer...</option>
|
||||
{CHEER_PREFIXES.map(cheerPrefix => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${cheerPrefix}`}
|
||||
value={cheerPrefix}
|
||||
>{cheerPrefix}</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Pick the cheer</label>
|
||||
<select
|
||||
onChange={updateCheerPrefix}
|
||||
value={cheerPrefix}
|
||||
className="form-select"
|
||||
>
|
||||
<option key={`option-*`} value="">
|
||||
Select cheer...
|
||||
</option>
|
||||
{CHEER_PREFIXES.map((cheerPrefix) => {
|
||||
return (
|
||||
<option key={`option-${cheerPrefix}`} value={cheerPrefix}>
|
||||
{cheerPrefix}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="control">
|
||||
<label className="label">Then the bit value</label>
|
||||
<div className="control">
|
||||
<div className="select is-medium">
|
||||
<select
|
||||
onChange={updateBitsValue}
|
||||
value={bitsValue}
|
||||
>
|
||||
{CHEER_BIT_LEVELS.map(level => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${cheerPrefix}-${level}`}
|
||||
value={level}
|
||||
>{level}</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Then the bit value</label>
|
||||
<select
|
||||
onChange={updateBitsValue}
|
||||
value={bitsValue}
|
||||
className="form-select"
|
||||
>
|
||||
{CHEER_BIT_LEVELS.map((level) => {
|
||||
return (
|
||||
<option key={`option-${cheerPrefix}-${level}`} value={level}>
|
||||
{level}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="control is-expanded">
|
||||
<label className="label">To match against this (or set something custom)</label>
|
||||
<p className="control has-icons-left is-large">
|
||||
<input
|
||||
value={manualCheerValue}
|
||||
onChange={updateTextCheerValue}
|
||||
className="input is-medium is-primary"
|
||||
type="text"
|
||||
placeholder="Cheermote full name (including bit value)" />
|
||||
<span className="icon is-small is-left">
|
||||
<label className="sub-title">
|
||||
To match against this (or set something custom)
|
||||
</label>
|
||||
<div className="input-icon">
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faGem} />
|
||||
</span>
|
||||
</p>
|
||||
<input
|
||||
value={manualCheerValue}
|
||||
onChange={updateTextCheerValue}
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Cheermote full name (including bit value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { BitsCheermoteNameExactMatchForm }
|
||||
export { BitsCheermoteNameExactMatchForm };
|
||||
+131
-123
@@ -1,19 +1,29 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faGem } from '@fortawesome/free-solid-svg-icons';
|
||||
import { CHEER_BIT_LEVELS, CHEER_PREFIXES, CHEER_PREFIX_TO_STRING_MAP } from '../../../../twitch/Cheers';
|
||||
import { CheerState, CheerStateIsCustom, CheerStateIsOfficial } from '../CheerState';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faGem } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
CHEER_BIT_LEVELS,
|
||||
CHEER_PREFIXES,
|
||||
CHEER_PREFIX_TO_STRING_MAP,
|
||||
} from "../../../../twitch/Cheers";
|
||||
import {
|
||||
CheerState,
|
||||
CheerStateIsCustom,
|
||||
CheerStateIsOfficial,
|
||||
} from "../CheerState";
|
||||
|
||||
interface BitsCheermotePrefixSpendThresholdProps {
|
||||
cheerState: CheerState,
|
||||
cheerState: CheerState;
|
||||
|
||||
updateCheerPrefix: (cheerNameOrPrefix: string) => void,
|
||||
updateMinimumBitsSpent: (minimumSpent: number) => void,
|
||||
};
|
||||
updateCheerPrefix: (cheerNameOrPrefix: string) => void;
|
||||
updateMinimumBitsSpent: (minimumSpent: number) => void;
|
||||
}
|
||||
|
||||
function BitsCheermotePrefixSpendThresholdForm(props: BitsCheermotePrefixSpendThresholdProps) {
|
||||
const [cheerPrefix, setCheerPrefix] = useState<string>('');
|
||||
const [customCheerPrefix, setCustomCheerPrefix] = useState<string>('');
|
||||
function BitsCheermotePrefixSpendThresholdForm(
|
||||
props: BitsCheermotePrefixSpendThresholdProps
|
||||
) {
|
||||
const [cheerPrefix, setCheerPrefix] = useState<string>("");
|
||||
const [customCheerPrefix, setCustomCheerPrefix] = useState<string>("");
|
||||
|
||||
const [bitsValue, setBitsValue] = useState<number>(1);
|
||||
const [customBitsValue, setCustomBitsValue] = useState<number>(1);
|
||||
@@ -24,172 +34,170 @@ function BitsCheermotePrefixSpendThresholdForm(props: BitsCheermotePrefixSpendTh
|
||||
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
|
||||
// https://stackoverflow.com/a/62982753
|
||||
useEffect(() => {
|
||||
let newPrefix = '';
|
||||
let newCustomPrefix = '';
|
||||
let newPrefix = "";
|
||||
let newCustomPrefix = "";
|
||||
let newBits = 1;
|
||||
let newCustomBits = 1;
|
||||
|
||||
if (!!props.cheerState.bits && !isNaN(props.cheerState.bits) && props.cheerState.bits > 0) {
|
||||
if (
|
||||
!!props.cheerState.bits &&
|
||||
!isNaN(props.cheerState.bits) &&
|
||||
props.cheerState.bits > 0
|
||||
) {
|
||||
newBits = props.cheerState.bits;
|
||||
newCustomBits = props.cheerState.bits;
|
||||
}
|
||||
|
||||
if (CheerStateIsOfficial(props.cheerState)) {
|
||||
if (!!props.cheerState.cheerPrefix) {
|
||||
newPrefix = CHEER_PREFIX_TO_STRING_MAP.get((props.cheerState.cheerPrefix)) || '';
|
||||
newCustomPrefix = CHEER_PREFIX_TO_STRING_MAP.get((props.cheerState.cheerPrefix)) || '';
|
||||
newPrefix =
|
||||
CHEER_PREFIX_TO_STRING_MAP.get(props.cheerState.cheerPrefix) || "";
|
||||
newCustomPrefix =
|
||||
CHEER_PREFIX_TO_STRING_MAP.get(props.cheerState.cheerPrefix) || "";
|
||||
}
|
||||
} else if (CheerStateIsCustom(props.cheerState)) {
|
||||
newCustomPrefix = props.cheerState.cheerFull || '';
|
||||
newCustomPrefix = props.cheerState.cheerFull || "";
|
||||
}
|
||||
|
||||
setCheerPrefix(newPrefix);
|
||||
setCustomCheerPrefix(newCustomPrefix);
|
||||
setBitsValue(newBits);
|
||||
setCustomBitsValue(newCustomBits);
|
||||
|
||||
}, [props.cheerState]);
|
||||
|
||||
const updateCheerPrefix = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const updateCheerPrefix = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
setCheerPrefix(value);
|
||||
setCustomCheerPrefix(value);
|
||||
props.updateCheerPrefix(value);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const updateTextCheerValue = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
|
||||
const updateTextCheerValue = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
setCheerPrefix(value);
|
||||
setCustomCheerPrefix(value);
|
||||
props.updateCheerPrefix(value);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleBitSelect = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const handleBitSelect = (ev: React.FormEvent<HTMLSelectElement>): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const numericValue = parseInt(value);
|
||||
setBitsValue(numericValue);
|
||||
setCustomBitsValue(numericValue);
|
||||
props.updateMinimumBitsSpent(numericValue);
|
||||
return true;
|
||||
}
|
||||
const handleCustomBitsValueUpdate = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
|
||||
};
|
||||
const handleCustomBitsValueUpdate = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
const numericValue = parseInt(value);
|
||||
setCustomBitsValue(numericValue);
|
||||
props.updateMinimumBitsSpent(numericValue);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<article className="message is-warning">
|
||||
<div className="message-body">
|
||||
This rule type requires <strong>both</strong> the cheermote
|
||||
to match and the bits spent to be equal or greater than the amount specified.
|
||||
</div>
|
||||
</article>
|
||||
<div className="alert alert-warning">
|
||||
This rule type requires <strong>both</strong> the cheermote to match and
|
||||
the bits spent to be equal or greater than the amount specified.
|
||||
</div>
|
||||
|
||||
<h1 className="title is-5">Cheermote matching</h1>
|
||||
|
||||
<div className="field is-grouped">
|
||||
<div className="control">
|
||||
<label className="label">Pick the cheer prefix</label>
|
||||
<div className="select is-medium">
|
||||
<select
|
||||
onChange={updateCheerPrefix}
|
||||
value={cheerPrefix}
|
||||
>
|
||||
<option
|
||||
key={`option-*`}
|
||||
value=""
|
||||
>Select cheer...</option>
|
||||
{CHEER_PREFIXES.map(cheerPrefix => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${cheerPrefix}`}
|
||||
value={cheerPrefix}
|
||||
>{cheerPrefix}</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<h2 className="mt-3">Cheermote matching</h2>
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Pick the cheer prefix</label>
|
||||
<select
|
||||
onChange={updateCheerPrefix}
|
||||
value={cheerPrefix}
|
||||
className="form-select"
|
||||
>
|
||||
<option key={`option-*`} value="">
|
||||
Select cheer...
|
||||
</option>
|
||||
{CHEER_PREFIXES.map((cheerPrefix) => {
|
||||
return (
|
||||
<option key={`option-${cheerPrefix}`} value={cheerPrefix}>
|
||||
{cheerPrefix}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="control is-expanded">
|
||||
<label className="label">To match against this (or set something custom)</label>
|
||||
<p className="control has-icons-left is-large">
|
||||
<input
|
||||
<div className="form-group">
|
||||
<label className="sub-title">
|
||||
To match against this (or set something custom)
|
||||
</label>
|
||||
<div className="input-icon">
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faGem} />
|
||||
</span>
|
||||
<input
|
||||
value={customCheerPrefix}
|
||||
onChange={updateTextCheerValue}
|
||||
className="input is-medium is-primary"
|
||||
type="text"
|
||||
placeholder="Cheermote full name (including bit value)" />
|
||||
<span className="icon is-small is-left">
|
||||
<FontAwesomeIcon icon={faGem} />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article className="message">
|
||||
<div className="message-body">
|
||||
The "Cheer prefix" can be an actual cheer prefix, such as "SeemsGood", but it can also be an arbitrary
|
||||
string value. So if you make it "ManBearPig", that will be searched for in cheers.
|
||||
|
||||
Cheermotes will be removed from the words used in TTS, but currently custom strings will not (though
|
||||
this may change in the future).
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<h1 className="title is-5">Bits spent</h1>
|
||||
|
||||
<div className="field is-grouped">
|
||||
<div className="control">
|
||||
<label className="label">Preset thresholds</label>
|
||||
<div className="control">
|
||||
<div className="select is-medium">
|
||||
<select
|
||||
onChange={handleBitSelect}
|
||||
value={bitsValue}
|
||||
>
|
||||
{CHEER_BIT_LEVELS.map(level => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${level}`}
|
||||
value={level}
|
||||
>Spend at least {level} bits</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Cheermote full name (including bit value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="control is-expanded">
|
||||
<label className="label">Or a custom minimum amount.</label>
|
||||
<p className="control has-icons-left is-large">
|
||||
<input
|
||||
value={customBitsValue}
|
||||
onChange={handleCustomBitsValueUpdate}
|
||||
className="input is-medium is-primary"
|
||||
type="text"
|
||||
placeholder="Cheermote full name (including bit value)" />
|
||||
<span className="icon is-small is-left">
|
||||
<p>
|
||||
The "Cheer prefix" can be an actual cheer prefix, such as "SeemsGood",
|
||||
but it can also be an arbitrary string value. So if you make it
|
||||
"ManBearPig", that will be searched for in cheers. Cheermotes will be
|
||||
removed from the words used in TTS, but currently custom strings will
|
||||
not (though this may change in the future).
|
||||
</p>
|
||||
|
||||
<h2 className="mt-3">Bits spent</h2>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Preset thresholds</label>
|
||||
<select
|
||||
onChange={handleBitSelect}
|
||||
value={bitsValue}
|
||||
className="form-select"
|
||||
>
|
||||
{CHEER_BIT_LEVELS.map((level) => {
|
||||
return (
|
||||
<option key={`option-${level}`} value={level}>
|
||||
Spend at least {level} bits
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Or a custom minimum amount.</label>
|
||||
<div className="input-icon">
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faGem} />
|
||||
</span>
|
||||
</p>
|
||||
<input
|
||||
value={customBitsValue}
|
||||
onChange={handleCustomBitsValueUpdate}
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Cheermote full name (including bit value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Remember that all Cheermotes in a single message add up, eg.{" "}
|
||||
<em>“Cheer100 + Cheer100 + Party100”</em> is 300 bits. A
|
||||
custom amount should give you flexibility to charge, say, 300 or 500
|
||||
bits. Or 420. Whatever you want.
|
||||
</p>
|
||||
</div>
|
||||
<article className="message">
|
||||
<div className="message-body">
|
||||
Remember that all Cheermotes in a single message add up,
|
||||
eg. <em>“Cheer100 + Cheer100 + Party100”</em> is 300 bits.
|
||||
A custom amount should give you flexibility to charge, say, 300 or 500 bits.
|
||||
Or 420. Whatever you want.
|
||||
</div>
|
||||
</article>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { BitsCheermotePrefixSpendThresholdForm }
|
||||
export { BitsCheermotePrefixSpendThresholdForm };
|
||||
+57
-57
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faGem } from '@fortawesome/free-solid-svg-icons';
|
||||
import { CHEER_BIT_LEVELS } from '../../../../twitch/Cheers';
|
||||
import { CheerState } from '../CheerState';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faGem } from "@fortawesome/free-solid-svg-icons";
|
||||
import { CHEER_BIT_LEVELS } from "../../../../twitch/Cheers";
|
||||
import { CheerState } from "../CheerState";
|
||||
|
||||
interface BitsSpendThresholdFormProps {
|
||||
cheerState: CheerState,
|
||||
updateMinimumBitsSpent: (minimumSpent: number) => void,
|
||||
};
|
||||
cheerState: CheerState;
|
||||
updateMinimumBitsSpent: (minimumSpent: number) => void;
|
||||
}
|
||||
|
||||
function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
|
||||
// The dropdown
|
||||
@@ -19,13 +19,17 @@ function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
|
||||
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
|
||||
// https://stackoverflow.com/a/62982753
|
||||
useEffect(() => {
|
||||
if (!!props.cheerState.bits && !isNaN(props.cheerState.bits) && props.cheerState.bits > 0) {
|
||||
if (
|
||||
!!props.cheerState.bits &&
|
||||
!isNaN(props.cheerState.bits) &&
|
||||
props.cheerState.bits > 0
|
||||
) {
|
||||
setBitsValue(props.cheerState.bits);
|
||||
setCustomAmount(props.cheerState.bits);
|
||||
}
|
||||
}, [props.cheerState]);
|
||||
|
||||
const handleBitSelect = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const handleBitSelect = (ev: React.FormEvent<HTMLSelectElement>): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const numericValue = parseInt(value);
|
||||
|
||||
@@ -37,9 +41,11 @@ function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomAmountUpdate = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
|
||||
const handleCustomAmountUpdate = (
|
||||
ev: React.FormEvent<HTMLInputElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
const numericValue = parseInt(value);
|
||||
|
||||
@@ -51,60 +57,54 @@ function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="field is-grouped">
|
||||
<div className="control">
|
||||
<label className="label">Preset thresholds</label>
|
||||
<div className="control">
|
||||
<div className="select is-medium">
|
||||
<select
|
||||
onChange={handleBitSelect}
|
||||
value={bitsValue}
|
||||
>
|
||||
<option
|
||||
key={`option-*`}
|
||||
value=""
|
||||
>Choose or type...</option>
|
||||
{CHEER_BIT_LEVELS.map(level => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${level}`}
|
||||
value={level}
|
||||
>Spend at least {level} bits</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Preset thresholds</label>
|
||||
<select
|
||||
onChange={handleBitSelect}
|
||||
value={bitsValue}
|
||||
className="form-select"
|
||||
>
|
||||
<option key={`option-*`} value="">
|
||||
Choose or type...
|
||||
</option>
|
||||
{CHEER_BIT_LEVELS.map((level) => {
|
||||
return (
|
||||
<option key={`option-${level}`} value={level}>
|
||||
Spend at least {level} bits
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="control is-expanded">
|
||||
<label className="label">Or a custom minimum amount.</label>
|
||||
<p className="control has-icons-left is-large">
|
||||
<input
|
||||
value={customAmount}
|
||||
onChange={handleCustomAmountUpdate}
|
||||
className="input is-medium is-primary"
|
||||
type="text"
|
||||
placeholder="Cheermote full name (including bit value)" />
|
||||
<span className="icon is-small is-left">
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Or a custom minimum amount.</label>
|
||||
<div className="form-group input-icon">
|
||||
<span className="form-control-feedback">
|
||||
<FontAwesomeIcon icon={faGem} />
|
||||
</span>
|
||||
<input
|
||||
value={customAmount}
|
||||
onChange={handleCustomAmountUpdate}
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Cheermote full name (including bit value)"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3">
|
||||
Remember that all Cheermotes in a single message add up, eg.{" "}
|
||||
<em>“Cheer100 + Cheer100 + Party100”</em> is 300 bits. A
|
||||
custom amount should give you flexibility to charge, say, 300 or 500
|
||||
bits. Or 420. Whatever you want.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<article className="message">
|
||||
<div className="message-body">
|
||||
Remember that all Cheermotes in a single message add up,
|
||||
eg. <em>“Cheer100 + Cheer100 + Party100”</em> is 300 bits.
|
||||
A custom amount should give you flexibility to charge, say, 300 or 500 bits.
|
||||
Or 420. Whatever you want.
|
||||
</div>
|
||||
</article>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { BitsSpendThresholdForm }
|
||||
export { BitsSpendThresholdForm };
|
||||
+20
-17
@@ -1,12 +1,14 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
interface ChannelPointsRewardNameExactMatchProps {
|
||||
rewardName: string,
|
||||
updateRewardName: (name: string) => void,
|
||||
};
|
||||
rewardName: string;
|
||||
updateRewardName: (name: string) => void;
|
||||
}
|
||||
|
||||
function ChannelPointsRewardNameExactMatchForm(props: ChannelPointsRewardNameExactMatchProps) {
|
||||
const [rewardName, setRewardName] = useState<string>('');
|
||||
function ChannelPointsRewardNameExactMatchForm(
|
||||
props: ChannelPointsRewardNameExactMatchProps
|
||||
) {
|
||||
const [rewardName, setRewardName] = useState<string>("");
|
||||
|
||||
// NB: useState is not always setting from props correctly (after several re-renders)
|
||||
// The following answers suggests using useEffect:
|
||||
@@ -16,28 +18,29 @@ function ChannelPointsRewardNameExactMatchForm(props: ChannelPointsRewardNameExa
|
||||
setRewardName(props.rewardName);
|
||||
}, [props.rewardName]);
|
||||
|
||||
const updateRewardName = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
|
||||
const updateRewardName = (ev: React.FormEvent<HTMLInputElement>): boolean => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
setRewardName(value);
|
||||
props.updateRewardName(value);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="field">
|
||||
<label className="label">Match the following reward name</label>
|
||||
<div className="control is-medium">
|
||||
<input
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Match the following reward name</label>
|
||||
<div>
|
||||
<input
|
||||
value={rewardName}
|
||||
onChange={updateRewardName}
|
||||
className="input is-medium"
|
||||
type="text"
|
||||
placeholder="Reward Name" />
|
||||
className="form-control"
|
||||
type="text"
|
||||
placeholder="Reward Name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ChannelPointsRewardNameExactMatchForm }
|
||||
export { ChannelPointsRewardNameExactMatchForm };
|
||||
+86
-64
@@ -1,35 +1,39 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
import { TtsSingleVoiceForm } from './TtsSingleVoiceForm';
|
||||
import { EventResponseType } from './EventResponseType';
|
||||
import { EventResponse } from '@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse';
|
||||
import { buildEventResponse } from './BuildEventResponse';
|
||||
import { TtsRandomVoiceForm } from './TtsRandomVoiceForm';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { TtsSingleVoiceForm } from "./TtsSingleVoiceForm";
|
||||
import { EventResponseType } from "./EventResponseType";
|
||||
import { EventResponse } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse";
|
||||
import { buildEventResponse } from "./BuildEventResponse";
|
||||
import { TtsRandomVoiceForm } from "./TtsRandomVoiceForm";
|
||||
|
||||
// NB: This was created for "Edit" view, but we've forced it to work for "Create" view by hardcoding
|
||||
// NB: This was created for "Edit" view, but we've forced it to work for "Create" view by hardcoding
|
||||
// values that don't actually come from the server.
|
||||
|
||||
interface EventResponseComponentProps {
|
||||
// Original response from server.
|
||||
// (also immutable for sanity and because we may lose info on UI changes)
|
||||
serverEventResponse: EventResponse,
|
||||
serverEventResponse: EventResponse;
|
||||
|
||||
// Updates sent back up the tree
|
||||
updateModifiedEventResponse: (response: EventResponse) => void,
|
||||
updateModifiedEventResponse: (response: EventResponse) => void;
|
||||
|
||||
// FakeYou voices
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
};
|
||||
allTtsModels: TtsModelListItem[];
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
function EventResponseComponent(props: EventResponseComponentProps) {
|
||||
// ========== Core UI flow ==========
|
||||
|
||||
const [eventResponseType, setEventResponseType] = useState<EventResponseType>(EventResponseType.TtsSingleVoice);
|
||||
const [eventResponseType, setEventResponseType] = useState<EventResponseType>(
|
||||
EventResponseType.TtsSingleVoice
|
||||
);
|
||||
|
||||
// ========== Cached Values for Editing ==========
|
||||
|
||||
const [selectedTtsModelTokens, setSelectedTtsModelTokens] = useState<string[]>([]);
|
||||
const [selectedTtsModelTokens, setSelectedTtsModelTokens] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
|
||||
// NB: useState is not always setting from props correctly (after several re-renders)
|
||||
// The following answers suggests using useEffect:
|
||||
@@ -37,44 +41,52 @@ function EventResponseComponent(props: EventResponseComponentProps) {
|
||||
// https://stackoverflow.com/a/62982753
|
||||
useEffect(() => {
|
||||
let newEventResponseType = EventResponseType.TtsSingleVoice;
|
||||
let modelTokens : string[] = [];
|
||||
|
||||
let modelTokens: string[] = [];
|
||||
|
||||
if (!!props.serverEventResponse.tts_single_voice) {
|
||||
newEventResponseType = EventResponseType.TtsSingleVoice;
|
||||
|
||||
if (!!props.serverEventResponse.tts_single_voice.tts_model_token) {
|
||||
modelTokens.push(props.serverEventResponse.tts_single_voice.tts_model_token)
|
||||
modelTokens.push(
|
||||
props.serverEventResponse.tts_single_voice.tts_model_token
|
||||
);
|
||||
}
|
||||
|
||||
} else if (!!props.serverEventResponse.tts_random_voice) {
|
||||
newEventResponseType = EventResponseType.TtsRandomVoice;
|
||||
|
||||
if (!!props.serverEventResponse.tts_random_voice.tts_model_tokens &&
|
||||
props.serverEventResponse.tts_random_voice.tts_model_tokens.length > 0)
|
||||
{
|
||||
modelTokens = [...props.serverEventResponse.tts_random_voice.tts_model_tokens];
|
||||
if (
|
||||
!!props.serverEventResponse.tts_random_voice.tts_model_tokens &&
|
||||
props.serverEventResponse.tts_random_voice.tts_model_tokens.length > 0
|
||||
) {
|
||||
modelTokens = [
|
||||
...props.serverEventResponse.tts_random_voice.tts_model_tokens,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
setEventResponseType(newEventResponseType);
|
||||
setSelectedTtsModelTokens(modelTokens);
|
||||
|
||||
}, [props.serverEventResponse]);
|
||||
|
||||
const handleChangedEventResponseType = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const handleChangedEventResponseType = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const newResponseType = value as EventResponseType;
|
||||
|
||||
const newEventResponse = buildEventResponse(selectedTtsModelTokens, newResponseType);
|
||||
const newEventResponse = buildEventResponse(
|
||||
selectedTtsModelTokens,
|
||||
newResponseType
|
||||
);
|
||||
|
||||
setEventResponseType(newResponseType);
|
||||
props.updateModifiedEventResponse(newEventResponse);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangedTtsModelToken = (token: string) => {
|
||||
// We may be only updating to a single token, but we want to be able to navigate back to
|
||||
// We may be only updating to a single token, but we want to be able to navigate back to
|
||||
// the "random voices" view, etc.
|
||||
let updatedTokens = [...selectedTtsModelTokens];
|
||||
|
||||
@@ -84,72 +96,82 @@ function EventResponseComponent(props: EventResponseComponentProps) {
|
||||
updatedTokens.push(token);
|
||||
}
|
||||
|
||||
const newEventResponse = buildEventResponse(updatedTokens, eventResponseType);
|
||||
const newEventResponse = buildEventResponse(
|
||||
updatedTokens,
|
||||
eventResponseType
|
||||
);
|
||||
|
||||
setSelectedTtsModelTokens(updatedTokens);
|
||||
props.updateModifiedEventResponse(newEventResponse);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangedTtsModelTokens = (tokens: string[]) => {
|
||||
let updatedTokens = [...tokens];
|
||||
|
||||
const newEventResponse = buildEventResponse(updatedTokens, eventResponseType);
|
||||
const newEventResponse = buildEventResponse(
|
||||
updatedTokens,
|
||||
eventResponseType
|
||||
);
|
||||
|
||||
setSelectedTtsModelTokens(updatedTokens);
|
||||
props.updateModifiedEventResponse(newEventResponse);
|
||||
}
|
||||
};
|
||||
|
||||
let responseForm = <></>
|
||||
let responseForm = <></>;
|
||||
|
||||
switch (eventResponseType) {
|
||||
case EventResponseType.TtsSingleVoice:
|
||||
responseForm = <TtsSingleVoiceForm
|
||||
selectedTtsModelTokens={selectedTtsModelTokens}
|
||||
updateSelectedTtsModelToken={handleChangedTtsModelToken}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
responseForm = (
|
||||
<TtsSingleVoiceForm
|
||||
selectedTtsModelTokens={selectedTtsModelTokens}
|
||||
updateSelectedTtsModelToken={handleChangedTtsModelToken}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case EventResponseType.TtsRandomVoice:
|
||||
responseForm = <TtsRandomVoiceForm
|
||||
selectedTtsModelTokens={selectedTtsModelTokens}
|
||||
updateSelectedTtsModelTokens={handleChangedTtsModelTokens}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
responseForm = (
|
||||
<TtsRandomVoiceForm
|
||||
selectedTtsModelTokens={selectedTtsModelTokens}
|
||||
updateSelectedTtsModelTokens={handleChangedTtsModelTokens}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="title is-4">3) Pick how to respond</h2>
|
||||
<div className="panel p-3 p-lg-4">
|
||||
<h2 className="fw-bold pb-3">3) Pick how to respond</h2>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Response Type</label>
|
||||
<div className="control">
|
||||
<div className="select is-medium is-fullwidth">
|
||||
<select
|
||||
<div className="form-group">
|
||||
<label className="sub-title">Response Type</label>
|
||||
<div className="control">
|
||||
<select
|
||||
value={eventResponseType}
|
||||
onChange={handleChangedEventResponseType}>
|
||||
<option value={EventResponseType.TtsSingleVoice}>Respond with a TTS voice</option>
|
||||
<option value={EventResponseType.TtsRandomVoice}>Respond with a Random TTS voice from list</option>
|
||||
onChange={handleChangedEventResponseType}
|
||||
className="form-select"
|
||||
>
|
||||
<option value={EventResponseType.TtsSingleVoice}>
|
||||
Respond with a TTS voice
|
||||
</option>
|
||||
<option value={EventResponseType.TtsRandomVoice}>
|
||||
Respond with a Random TTS voice from list
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 className="title is-4">4) Configure the response</h2>
|
||||
|
||||
{responseForm}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div className="panel p-3 p-lg-4">
|
||||
<h2 className="fw-bold pb-3">4) Configure the response</h2>
|
||||
{responseForm}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { EventResponseComponent }
|
||||
export { EventResponseComponent };
|
||||
+81
-78
@@ -1,37 +1,36 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
import { FakeYouExternalLink } from '@storyteller/components/src/elements/FakeYouExternalLink';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import React, { useEffect } from "react";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { FakeYouExternalLink } from "@storyteller/components/src/elements/FakeYouExternalLink";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
interface TtsRandomVoiceFormProps {
|
||||
selectedTtsModelTokens: string[],
|
||||
updateSelectedTtsModelTokens: (tokens: string[]) => void,
|
||||
selectedTtsModelTokens: string[];
|
||||
updateSelectedTtsModelTokens: (tokens: string[]) => void;
|
||||
|
||||
// FakeYou voices
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
};
|
||||
allTtsModels: TtsModelListItem[];
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
|
||||
// NB: useState is not always setting from props correctly (after several re-renders)
|
||||
// The following answers suggests using useEffect:
|
||||
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
|
||||
// https://stackoverflow.com/a/62982753
|
||||
useEffect(() => {
|
||||
}, [props.selectedTtsModelTokens]);
|
||||
useEffect(() => {}, [props.selectedTtsModelTokens]);
|
||||
|
||||
const handleAddVoice = (ev: React.FormEvent<HTMLButtonElement>) : boolean => {
|
||||
const handleAddVoice = (ev: React.FormEvent<HTMLButtonElement>): boolean => {
|
||||
ev.preventDefault();
|
||||
|
||||
let newModelTokens = [...props.selectedTtsModelTokens];
|
||||
|
||||
newModelTokens.push('');
|
||||
newModelTokens.push("");
|
||||
|
||||
props.updateSelectedTtsModelTokens(newModelTokens);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelSelect = (ttsModelToken: string, voiceIndex: number) => {
|
||||
let newModelTokens = [...props.selectedTtsModelTokens];
|
||||
@@ -42,7 +41,7 @@ function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
|
||||
|
||||
newModelTokens[voiceIndex] = ttsModelToken;
|
||||
props.updateSelectedTtsModelTokens(newModelTokens);
|
||||
}
|
||||
};
|
||||
|
||||
const handleModelDelete = (voiceIndex: number) => {
|
||||
let newModelTokens = [...props.selectedTtsModelTokens];
|
||||
@@ -55,11 +54,11 @@ function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
|
||||
newModelTokens.splice(voiceIndex, 1);
|
||||
|
||||
props.updateSelectedTtsModelTokens(newModelTokens);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const deleteDisabled = props.selectedTtsModelTokens.length < 2;
|
||||
|
||||
let selectBoxes : JSX.Element[] = [];
|
||||
let selectBoxes: JSX.Element[] = [];
|
||||
|
||||
// No tokens - show one box (unselected)
|
||||
// One or more tokens - show boxes for each (with selections)
|
||||
@@ -75,8 +74,8 @@ function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
|
||||
deleteDisabled={deleteDisabled}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
)
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (selectBoxes.length === 0) {
|
||||
@@ -90,55 +89,54 @@ function TtsRandomVoiceForm(props: TtsRandomVoiceFormProps) {
|
||||
deleteDisabled={false}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
)
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectBoxes}
|
||||
|
||||
<button
|
||||
className="button is-large is-fullwidth is-info"
|
||||
onClick={handleAddVoice}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} /> Add Additional Voice
|
||||
<button className="btn btn-primary mt-3" onClick={handleAddVoice}>
|
||||
<FontAwesomeIcon icon={faPlus} className="me-2" />
|
||||
Add Additional Voice
|
||||
</button>
|
||||
|
||||
<br />
|
||||
|
||||
<article className="message">
|
||||
<div className="message-body">
|
||||
<FakeYouExternalLink>Check out the voices at FakeYou</FakeYouExternalLink> to search for voices, experiment with them,
|
||||
and see how they sound. Sorry that this list is a nightmare to navigate. It will improve over time.
|
||||
</div>
|
||||
</article>
|
||||
<p className="mt-4">
|
||||
<FakeYouExternalLink>
|
||||
Check out the voices at FakeYou
|
||||
</FakeYouExternalLink>{" "}
|
||||
to search for voices, experiment with them, and see how they sound.
|
||||
Sorry that this list is a nightmare to navigate. It will improve over
|
||||
time.
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface VoiceDropdownProps {
|
||||
// Actual token or empty string
|
||||
selectedTtsModelToken: string,
|
||||
selectedTtsModelToken: string;
|
||||
|
||||
// Position in the list, 0-indexed
|
||||
voiceIndex: number,
|
||||
voiceIndex: number;
|
||||
|
||||
// Callbacks
|
||||
handleModelSelect: (token: string, index: number) => void,
|
||||
handleModelDelete: (index: number) => void,
|
||||
handleModelSelect: (token: string, index: number) => void;
|
||||
handleModelDelete: (index: number) => void;
|
||||
|
||||
// Whether to disable the delete button
|
||||
deleteDisabled: boolean,
|
||||
deleteDisabled: boolean;
|
||||
|
||||
// FakeYou voices
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
allTtsModels: TtsModelListItem[];
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
function VoiceDropdown(props: VoiceDropdownProps) {
|
||||
|
||||
const handleSelectChange = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const handleSelectChange = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
): boolean => {
|
||||
const maybeModelToken = (ev.target as HTMLSelectElement).value;
|
||||
|
||||
if (!maybeModelToken) {
|
||||
@@ -153,47 +151,52 @@ function VoiceDropdown(props: VoiceDropdownProps) {
|
||||
|
||||
props.handleModelSelect(maybeModelToken, props.voiceIndex);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveClick = (ev: React.FormEvent<HTMLButtonElement>) : boolean => {
|
||||
const handleRemoveClick = (
|
||||
ev: React.FormEvent<HTMLButtonElement>
|
||||
): boolean => {
|
||||
props.handleModelDelete(props.voiceIndex);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={`dropdown-${props.voiceIndex}`}>
|
||||
<label className="label">{props.voiceIndex+1}. TTS Voice Model (used as a Random Voice)</label>
|
||||
<label className="sub-title">
|
||||
{props.voiceIndex + 1}. TTS Voice Model (used as a Random Voice)
|
||||
</label>
|
||||
|
||||
<div className="field is-grouped">
|
||||
<div className="control is-expanded">
|
||||
|
||||
<div className="select is-medium is-fullwidth">
|
||||
<select
|
||||
value={props.selectedTtsModelToken}
|
||||
onChange={handleSelectChange}
|
||||
>
|
||||
<option
|
||||
key={`option-${props.voiceIndex}-*`}
|
||||
value="">
|
||||
Select a voice...</option>
|
||||
{props.allTtsModels.map(ttsModel => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${props.voiceIndex}-${ttsModel.model_token}`}
|
||||
value={ttsModel.model_token}
|
||||
>{ttsModel.title}</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div className="d-flex gap-3">
|
||||
<select
|
||||
value={props.selectedTtsModelToken}
|
||||
onChange={handleSelectChange}
|
||||
className="form-select"
|
||||
>
|
||||
<option key={`option-${props.voiceIndex}-*`} value="">
|
||||
Select a voice...
|
||||
</option>
|
||||
{props.allTtsModels.map((ttsModel) => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${props.voiceIndex}-${ttsModel.model_token}`}
|
||||
value={ttsModel.model_token}
|
||||
>
|
||||
{ttsModel.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button
|
||||
|
||||
<div className="mt-3">
|
||||
<button
|
||||
onClick={handleRemoveClick}
|
||||
className="button is-medium is-danger is-inverted"
|
||||
className="btn btn-destructive"
|
||||
disabled={props.deleteDisabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} /> Remove
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="me-2" />
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,4 +206,4 @@ function VoiceDropdown(props: VoiceDropdownProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export { TtsRandomVoiceForm }
|
||||
export { TtsRandomVoiceForm };
|
||||
+46
-41
@@ -1,18 +1,18 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
import { FakeYouExternalLink } from '@storyteller/components/src/elements/FakeYouExternalLink';
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import { FakeYouExternalLink } from "@storyteller/components/src/elements/FakeYouExternalLink";
|
||||
|
||||
interface TtsSingleVoiceFormProps {
|
||||
selectedTtsModelTokens: string[],
|
||||
updateSelectedTtsModelToken: (token: string) => void,
|
||||
selectedTtsModelTokens: string[];
|
||||
updateSelectedTtsModelToken: (token: string) => void;
|
||||
|
||||
// FakeYou voices
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
};
|
||||
allTtsModels: TtsModelListItem[];
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
function TtsSingleVoiceForm(props: TtsSingleVoiceFormProps) {
|
||||
const [ttsModelToken, setTtsModelToken] = useState('');
|
||||
const [ttsModelToken, setTtsModelToken] = useState("");
|
||||
|
||||
// NB: useState is not always setting from props correctly (after several re-renders)
|
||||
// The following answers suggests using useEffect:
|
||||
@@ -24,7 +24,9 @@ function TtsSingleVoiceForm(props: TtsSingleVoiceFormProps) {
|
||||
}
|
||||
}, [props.selectedTtsModelTokens]);
|
||||
|
||||
const handleModelSelect = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const handleModelSelect = (
|
||||
ev: React.FormEvent<HTMLSelectElement>
|
||||
): boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
|
||||
if (!!value && !props.allTtsModelsByToken.has(value)) {
|
||||
@@ -34,42 +36,45 @@ function TtsSingleVoiceForm(props: TtsSingleVoiceFormProps) {
|
||||
setTtsModelToken(value);
|
||||
props.updateSelectedTtsModelToken(value);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="field">
|
||||
<label className="label">TTS Voice Model</label>
|
||||
<div className="control">
|
||||
<div className="select is-medium is-fullwidth">
|
||||
<select
|
||||
value={ttsModelToken}
|
||||
onChange={handleModelSelect}
|
||||
>
|
||||
<option value="">Select a voice...</option>
|
||||
{props.allTtsModels.map(ttsModel => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${ttsModel.model_token}`}
|
||||
value={ttsModel.model_token}
|
||||
>{ttsModel.title}</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<div className="d-flex flex-column gap-4">
|
||||
<div className="form-group">
|
||||
<label className="sub-title">TTS Voice Model</label>
|
||||
<select
|
||||
value={ttsModelToken}
|
||||
onChange={handleModelSelect}
|
||||
className="form-select"
|
||||
>
|
||||
<option value="">Select a voice...</option>
|
||||
{props.allTtsModels.map((ttsModel) => {
|
||||
return (
|
||||
<option
|
||||
key={`option-${ttsModel.model_token}`}
|
||||
value={ttsModel.model_token}
|
||||
>
|
||||
{ttsModel.title}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
<p>
|
||||
We apologize that this list is a nightmare to navigate right now. It
|
||||
will improve over time. In the mean time,{" "}
|
||||
<FakeYouExternalLink>
|
||||
check out the voices at FakeYou
|
||||
</FakeYouExternalLink>{" "}
|
||||
to find the voices you want, experiment with them, and see which ones
|
||||
work best for your stream. The names of the voices at FakeYou are the
|
||||
same as in this dropdown, so you can search at FakeYou, then add them
|
||||
here when you're done looking.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<article className="message">
|
||||
<div className="message-body">
|
||||
We apologize that this list is a nightmare to navigate right now. It will improve over time.
|
||||
In the mean time, <FakeYouExternalLink>check out the voices at FakeYou</FakeYouExternalLink> to
|
||||
find the voices you want, experiment with them, and see which ones work best for your stream. The
|
||||
names of the voices at FakeYou are the same as in this dropdown, so you can search at FakeYou, then
|
||||
add them here when you're done looking.</div>
|
||||
</article>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { TtsSingleVoiceForm }
|
||||
export { TtsSingleVoiceForm };
|
||||
+104
-51
@@ -1,48 +1,89 @@
|
||||
import React from 'react';
|
||||
import { TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faArrowDown, faArrowUp, faExternalLinkAlt, } from '@fortawesome/free-solid-svg-icons';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
import React from "react";
|
||||
import { TwitchEventRule } from "@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faExternalLinkAlt,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { TtsModelListItem } from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
|
||||
interface Props {
|
||||
rule: TwitchEventRule,
|
||||
rule: TwitchEventRule;
|
||||
|
||||
// Index of the rule in the parents' list
|
||||
ruleIndex: number,
|
||||
ruleIndex: number;
|
||||
|
||||
// Update callbacks
|
||||
handleMoveUp: (index: number) => void,
|
||||
handleMoveDown: (index: number) => void,
|
||||
handleMoveUp: (index: number) => void;
|
||||
handleMoveDown: (index: number) => void;
|
||||
|
||||
// FakeYou voices
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
function ReorderableTwitchEventRuleElement(props: Props) {
|
||||
let subtitle = <></>;
|
||||
let description = <></>;
|
||||
|
||||
if (props.rule.event_match_predicate.bits_cheermote_name_exact_match !== undefined) {
|
||||
subtitle = (
|
||||
<>Cheermote name matches "{props.rule.event_match_predicate.bits_cheermote_name_exact_match.cheermote_name}"</>
|
||||
);
|
||||
} else if (props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold !== undefined) {
|
||||
if (
|
||||
props.rule.event_match_predicate.bits_cheermote_name_exact_match !==
|
||||
undefined
|
||||
) {
|
||||
subtitle = (
|
||||
<>
|
||||
Cheermote prefix is "{props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold.cheermote_prefix}"
|
||||
and spend at least {props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold.minimum_bits_spent} bits
|
||||
Cheermote name matches "
|
||||
{
|
||||
props.rule.event_match_predicate.bits_cheermote_name_exact_match
|
||||
.cheermote_name
|
||||
}
|
||||
"
|
||||
</>
|
||||
);
|
||||
} else if (props.rule.event_match_predicate.bits_spend_threshold !== undefined) {
|
||||
} else if (
|
||||
props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold !==
|
||||
undefined
|
||||
) {
|
||||
subtitle = (
|
||||
<>
|
||||
Spend at least {props.rule.event_match_predicate.bits_spend_threshold.minimum_bits_spent} bits
|
||||
Cheermote prefix is "
|
||||
{
|
||||
props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold
|
||||
.cheermote_prefix
|
||||
}
|
||||
" and spend at least{" "}
|
||||
{
|
||||
props.rule.event_match_predicate.bits_cheermote_prefix_spend_threshold
|
||||
.minimum_bits_spent
|
||||
}{" "}
|
||||
bits
|
||||
</>
|
||||
);
|
||||
} else if (props.rule.event_match_predicate.channel_points_reward_name_exact_match !== undefined) {
|
||||
} else if (
|
||||
props.rule.event_match_predicate.bits_spend_threshold !== undefined
|
||||
) {
|
||||
subtitle = (
|
||||
<>
|
||||
Reward name matches "{props.rule.event_match_predicate.channel_points_reward_name_exact_match.reward_name}"
|
||||
Spend at least{" "}
|
||||
{
|
||||
props.rule.event_match_predicate.bits_spend_threshold
|
||||
.minimum_bits_spent
|
||||
}{" "}
|
||||
bits
|
||||
</>
|
||||
);
|
||||
} else if (
|
||||
props.rule.event_match_predicate.channel_points_reward_name_exact_match !==
|
||||
undefined
|
||||
) {
|
||||
subtitle = (
|
||||
<>
|
||||
Reward name matches "
|
||||
{
|
||||
props.rule.event_match_predicate
|
||||
.channel_points_reward_name_exact_match.reward_name
|
||||
}
|
||||
"
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -61,7 +102,14 @@ function ReorderableTwitchEventRuleElement(props: Props) {
|
||||
|
||||
description = (
|
||||
<>
|
||||
TTS with voice: {modelName} <a href={link} target="_blank" rel="noreferrer"><FontAwesomeIcon icon={faExternalLinkAlt} /></a>
|
||||
<p>
|
||||
<span className="fw-semibold">TTS with voice:</span>
|
||||
<br />
|
||||
{modelName}{" "}
|
||||
<a href={link} target="_blank" rel="noreferrer">
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
} else if (props.rule.event_response.tts_random_voice !== undefined) {
|
||||
@@ -78,50 +126,55 @@ function ReorderableTwitchEventRuleElement(props: Props) {
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
{modelName} <a href={link} target="_blank" rel="noreferrer"><FontAwesomeIcon icon={faExternalLinkAlt} /></a>
|
||||
{modelName}{" "}
|
||||
<a href={link} target="_blank" rel="noreferrer">
|
||||
<FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
description = (
|
||||
<>
|
||||
TTS with a random voice from the following {modelNameAndLinks.length} voice(s):
|
||||
<ul>{modelNameAndLinks}</ul>
|
||||
<p>
|
||||
<span className="fw-semibold">
|
||||
TTS with a random voice from the following{" "}
|
||||
{modelNameAndLinks.length} voice(s):
|
||||
</span>
|
||||
<ul>{modelNameAndLinks}</ul>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={props.rule.token}>
|
||||
<div className="card">
|
||||
<div className="card-content">
|
||||
<div className="title is-5">
|
||||
{subtitle}
|
||||
</div>
|
||||
<div>
|
||||
{description}
|
||||
</div>
|
||||
<div className="panel p-3 p-lg-4">
|
||||
<div className="mb-4">
|
||||
<h4 className="fw-bold mb-4">{subtitle}</h4>
|
||||
<div>{description}</div>
|
||||
</div>
|
||||
<div className="d-flex gap-3">
|
||||
<button
|
||||
onClick={() => props.handleMoveUp(props.ruleIndex)}
|
||||
className="btn btn-secondary w-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowUp} className="me-2" />
|
||||
Up
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => props.handleMoveDown(props.ruleIndex)}
|
||||
className="btn btn-secondary w-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowDown} className="me-2" />
|
||||
Down
|
||||
</button>
|
||||
</div>
|
||||
<footer className="card-footer">
|
||||
<div className="card-footer-item">
|
||||
<span className="icon">
|
||||
<button onClick={() => props.handleMoveUp(props.ruleIndex)} className="button is-ghost">
|
||||
<FontAwesomeIcon icon={faArrowUp} /> Up
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-footer-item">
|
||||
<span className="icon">
|
||||
<button onClick={() => props.handleMoveDown(props.ruleIndex)} className="button is-ghost">
|
||||
<FontAwesomeIcon icon={faArrowDown} /> Down
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { ReorderableTwitchEventRuleElement }
|
||||
export { ReorderableTwitchEventRuleElement };
|
||||
+7
-5
@@ -98,7 +98,7 @@ function TwitchEventRuleElement(props: Props) {
|
||||
description = (
|
||||
<>
|
||||
<p>
|
||||
TTS with voice:
|
||||
<span className="fw-semibold">TTS with voice:</span>
|
||||
<br />
|
||||
{modelName}{" "}
|
||||
<a href={link} target="_blank" rel="noreferrer">
|
||||
@@ -132,8 +132,10 @@ function TwitchEventRuleElement(props: Props) {
|
||||
description = (
|
||||
<>
|
||||
<p>
|
||||
TTS with a random voice from the following {modelNameAndLinks.length}{" "}
|
||||
voice(s):
|
||||
<span className="fw-semibold">
|
||||
TTS with a random voice from the following{" "}
|
||||
{modelNameAndLinks.length} voice(s):
|
||||
</span>
|
||||
<ul>{modelNameAndLinks}</ul>
|
||||
</p>
|
||||
</>
|
||||
@@ -143,7 +145,7 @@ function TwitchEventRuleElement(props: Props) {
|
||||
const editUrl = `/tts_configs/edit/${props.rule.token}`;
|
||||
const deleteUrl = `/tts_configs/delete/${props.rule.token}`;
|
||||
|
||||
let buttons = <></>;
|
||||
let buttons = undefined;
|
||||
if (!hideButtons) {
|
||||
buttons = (
|
||||
<>
|
||||
@@ -163,7 +165,7 @@ function TwitchEventRuleElement(props: Props) {
|
||||
|
||||
return (
|
||||
<div key={props.rule.token}>
|
||||
<div className="panel p-4">
|
||||
<div className="panel p-3 p-lg-4">
|
||||
<div>
|
||||
<h4 className="fw-bold mb-4">{subtitle}</h4>
|
||||
<div className="mb-4">{description}</div>
|
||||
|
||||
Reference in new issue
Block a user