delete lots of old code, fix permission check
No files matched your search
@@ -17,14 +17,6 @@
|
||||
"build-storyteller": "lerna exec --scope @storyteller/storyteller.io -- yarn build",
|
||||
"start-storyteller": "PORT=7008 BROWSER=none lerna exec --scope @storyteller/storyteller.io -- yarn start",
|
||||
"start-storyteller-w": "set PORT=7008 && lerna exec --scope @storyteller/storyteller.io -- yarn start-w",
|
||||
"build-powerstream": "lerna exec --scope @storyteller/power.stream -- yarn build",
|
||||
"start-powerstream": "PORT=7001 BROWSER=none lerna exec --scope @storyteller/power.stream -- yarn start",
|
||||
"start-powerstream-w": "set PORT=7001 && lerna exec --scope @storyteller/power.stream -- yarn start-w",
|
||||
"build-powerstream-dashboard": "lerna exec --scope @storyteller/power.stream-dashboard -- yarn build",
|
||||
"start-powerstream-dashboard": "PORT=7002 BROWSER=none lerna exec --scope @storyteller/power.stream-dashboard -- yarn start",
|
||||
"start-powerstream-dashboard-w": "set PORT=7002 && lerna exec --scope @storyteller/power.stream-dashboard -- yarn start-w",
|
||||
"build-powerstream-obs": "lerna exec --scope @storyteller/power.stream-obs -- yarn build",
|
||||
"start-powerstream-obs": "PORT=7007 BROWSER=none lerna exec --scope @storyteller/power.stream-obs -- yarn start",
|
||||
"build-trumped": "lerna exec --scope @storyteller/trumped.com -- yarn build",
|
||||
"start-trumped": "PORT=7000 BROWSER=none lerna exec --scope @storyteller/trumped.com -- yarn start",
|
||||
"start-trumped-80": "PORT=80 BROWSER=none lerna exec --scope @storyteller/trumped.com -- yarn start",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -1,46 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
@@ -1,24 +0,0 @@
|
||||
const path = require("path");
|
||||
const { getLoader, loaderByName } = require("@craco/craco");
|
||||
|
||||
const packages = [];
|
||||
packages.push(path.join(__dirname, "../components"));
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
configure: (webpackConfig, arg) => {
|
||||
const { isFound, match } = getLoader(
|
||||
webpackConfig,
|
||||
loaderByName("babel-loader")
|
||||
);
|
||||
if (isFound) {
|
||||
const include = Array.isArray(match.loader.include)
|
||||
? match.loader.include
|
||||
: [match.loader.include];
|
||||
|
||||
match.loader.include = include.concat(packages);
|
||||
}
|
||||
return webpackConfig;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"name": "@storyteller/dashboard.power.stream",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@storyteller/components": "0.1.0",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^16.11.21",
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"bulma": "^0.9.0",
|
||||
"craco": "^0.0.3",
|
||||
"react": "^16.13.1",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "5.0.0",
|
||||
"typescript": "^4.5.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"start-w": "set BROWSER=none && craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,108 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="%PUBLIC_URL%/apple-touch-icon.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="32x32"
|
||||
href="%PUBLIC_URL%/favicon-32x32.png"
|
||||
/>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
sizes="16x16"
|
||||
href="%PUBLIC_URL%/favicon-16x16.png"
|
||||
/>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#209cee" />
|
||||
<meta name="description" content="Storyteller supercharges your stream" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300;400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Storyteller. Stream tech.</title>
|
||||
|
||||
<!-- Twitter, Discord, etc. -->
|
||||
<meta property="og:title" content="Storyteller, tech for your stream." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://storyteller.io" />
|
||||
<meta property="og:image" content="%PUBLIC_URL%/apple-touch-icon.png" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Storyteller. You can supercharge your stream."
|
||||
/>
|
||||
|
||||
<!-- Include this to make the og:image larger -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-9FYFXYQ42S"
|
||||
></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
|
||||
gtag("config", "G-9FYFXYQ42S");
|
||||
</script>
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fira+Sans+Condensed:ital,wght@0,300;0,400;1,300&family=Fugaz+One&family=Merriweather:wght@300&family=Fira+Sans:ital,wght@0,200;0,300;0,400;1,200;1,300;1,400&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"short_name": "Storyteller",
|
||||
"name": "Stream tech",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 89 KiB |
@@ -1,3 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
Before Width: | Height: | Size: 47 KiB |
@@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
//@import-normalize; /* bring in normalize.css styles */
|
||||
|
||||
// $page-width: 700px;
|
||||
$primary: #fc6b68;
|
||||
$white: #fff;
|
||||
$light-text-color: #777;
|
||||
$button-text-color: #666;
|
||||
|
||||
$body-font-family: "Fira Sans", serif;
|
||||
$nav-font-family: "Fira Sans", sans-serif;
|
||||
$footer-font-family: "Fira Sans Condensed", sans-serif;
|
||||
$textarea-font-family: "Fira Sans", sans-serif;
|
||||
$headings-font-family: "Merriweather", serif;
|
||||
|
||||
// body {
|
||||
// font-family: $body-font-family;
|
||||
// font-weight: 300; // For Fira Sans
|
||||
// }
|
||||
|
||||
// .mainwrap {
|
||||
// max-width: 1000px;
|
||||
// margin: auto;
|
||||
// }
|
||||
|
||||
.git-sha {
|
||||
color: #999;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: "Fira Sans", sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:active,
|
||||
button:hover,
|
||||
label:focus,
|
||||
.btn:active,
|
||||
.btn.focus {
|
||||
outline: 0px !important;
|
||||
-webkit-appearance: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 400;
|
||||
color: #fc6b68;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
color: #fc6b68;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ff8d8b;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
p {
|
||||
font-weight: 300;
|
||||
margin-bottom: 0;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.pre-heading {
|
||||
font-size: 18px;
|
||||
color: #fc6b68;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.rotateimg180 {
|
||||
-webkit-transform: rotate(180deg);
|
||||
-moz-transform: rotate(180deg);
|
||||
-ms-transform: rotate(180deg);
|
||||
-o-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.container,
|
||||
.container-fluid,
|
||||
.container-lg,
|
||||
.container-md,
|
||||
.container-sm,
|
||||
.container-xl,
|
||||
.container-xxl {
|
||||
width: 100%;
|
||||
padding-right: var(--bs-gutter-x, 1rem);
|
||||
padding-left: var(--bs-gutter-x, 1rem);
|
||||
}
|
||||
|
||||
.noselect {
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Old versions of Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently
|
||||
supported by Chrome, Edge, Opera and Firefox */
|
||||
}
|
||||
|
||||
/*margin top/bottom for panels*/
|
||||
.py-6 {
|
||||
padding-top: 1.6rem !important;
|
||||
padding-bottom: 0rem !important;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.navbar-main {
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
transition: all 0.1s;
|
||||
border-color: rgba(255, 255, 255, 0.07) !important;
|
||||
border-bottom: 0px solid;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.navbar-fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.navbar-main.scrolled {
|
||||
background-color: #2e222c;
|
||||
border-color: rgba(255, 255, 255, 0.07) !important;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
.navbar-dark .navbar-toggler {
|
||||
border: none;
|
||||
}
|
||||
|
||||
// .nav-bg-dark {
|
||||
// background-color: #17101c !important;
|
||||
// }
|
||||
|
||||
.nav-pills .nav-link.active,
|
||||
.nav-pills .show > .nav-link {
|
||||
color: #fff;
|
||||
background-color: $primary;
|
||||
|
||||
&:hover {
|
||||
background-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-pills .nav-link {
|
||||
border-radius: 5px;
|
||||
color: rgba(#fff, 0.7);
|
||||
transition: 0.2s;
|
||||
padding: 12px 14px !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba($white, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
content: "";
|
||||
width: 20px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.p-page-top {
|
||||
padding-top: 110px;
|
||||
}
|
||||
|
||||
.p-page {
|
||||
padding-top: 100px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.offcanvas {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.offcanvas-header {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
padding-left: 16px !important;
|
||||
padding-right: 16px !important;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.parent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
span.word {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fw-semibold {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fs-7 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span.word:after {
|
||||
background-color: #fc6b68;
|
||||
content: " ";
|
||||
height: 15%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin-left: 0rem;
|
||||
top: 74%;
|
||||
width: calc(100%);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.bg-gradient {
|
||||
background: rgb(13, 16, 25) !important;
|
||||
background: linear-gradient(
|
||||
325deg,
|
||||
rgba(13, 16, 25, 1) 0%,
|
||||
rgba(42, 21, 34, 1) 55%,
|
||||
rgba(16, 14, 26, 1) 100%
|
||||
) !important;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.bg-hero {
|
||||
background: rgb(18, 14, 26);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(18, 14, 26, 1) 0%,
|
||||
rgba(48, 23, 32, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
outline: none;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
padding: 10px 24px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-primary:active,
|
||||
.btn-primary:focus {
|
||||
background-color: #fc6b68;
|
||||
border: 2.5px solid;
|
||||
border-color: #fc6b68;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #ff8d8b;
|
||||
border: 2.5px solid;
|
||||
border-color: #ff8d8b;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-secondary:focus,
|
||||
.btn-secondary:active {
|
||||
background-color: #fff;
|
||||
color: #443a40;
|
||||
border: 2.5px solid;
|
||||
border-color: #fff;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #ccc;
|
||||
color: #443a40;
|
||||
border: 2.5px solid;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn-destructive,
|
||||
.btn-destructive:active,
|
||||
.btn-destructive:focus {
|
||||
background-color: #fff;
|
||||
color: #fc6b68;
|
||||
border: 2.5px solid;
|
||||
border-color: #fff;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn-destructive:hover {
|
||||
background-color: #fdcfce;
|
||||
color: #fc6b68;
|
||||
border: 2.5px solid;
|
||||
border-color: #fdcfce;
|
||||
}
|
||||
|
||||
.btn-hero {
|
||||
padding-left: 35px !important;
|
||||
padding-right: 35px !important;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0rem;
|
||||
}
|
||||
|
||||
.nav-login {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
.nav-login:hover {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-decoration: none;
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
max-height: 1080px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
/* background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:svgjs='http://svgjs.com/svgjs' width='1920' height='300' preserveAspectRatio='none' viewBox='0 0 1920 300'%3e%3cg mask='url(%26quot%3b%23SvgjsMask1327%26quot%3b)' fill='none'%3e%3cpath d='M486 46L1358 45' stroke-width='10' stroke='url(%23SvgjsLinearGradient1328)' stroke-linecap='round' class='Right'%3e%3c/path%3e%3cpath d='M312 288L-212 287' stroke-width='6' stroke='url(%23SvgjsLinearGradient1328)' stroke-linecap='round' class='Right'%3e%3c/path%3e%3cpath d='M1583 12L2334 11' stroke-width='8' stroke='url(%23SvgjsLinearGradient1329)' stroke-linecap='round' class='Left'%3e%3c/path%3e%3cpath d='M1219 65L1985 64' stroke-width='10' stroke='url(%23SvgjsLinearGradient1329)' stroke-linecap='round' class='Left'%3e%3c/path%3e%3cpath d='M1877 57L2438 56' stroke-width='8' stroke='url(%23SvgjsLinearGradient1328)' stroke-linecap='round' class='Right'%3e%3c/path%3e%3cpath d='M347 173L-797 172' stroke-width='10' stroke='url(%23SvgjsLinearGradient1328)' stroke-linecap='round' class='Right'%3e%3c/path%3e%3cpath d='M762 234L-332 233' stroke-width='6' stroke='url(%23SvgjsLinearGradient1329)' stroke-linecap='round' class='Left'%3e%3c/path%3e%3cpath d='M621 133L2031 132' stroke-width='8' stroke='url(%23SvgjsLinearGradient1328)' stroke-linecap='round' class='Right'%3e%3c/path%3e%3c/g%3e%3cdefs%3e%3cmask id='SvgjsMask1327'%3e%3crect width='1920' height='300' fill='white'%3e%3c/rect%3e%3c/mask%3e%3clinearGradient x1='0%25' y1='0%25' x2='100%25' y2='0%25' id='SvgjsLinearGradient1328'%3e%3cstop stop-color='rgba(247%2c 84%2c 92%2c 0)' offset='0'%3e%3c/stop%3e%3cstop stop-color='rgba(247%2c 84%2c 92%2c 0.05)' offset='1'%3e%3c/stop%3e%3c/linearGradient%3e%3clinearGradient x1='100%25' y1='0%25' x2='0%25' y2='0%25' id='SvgjsLinearGradient1329'%3e%3cstop stop-color='rgba(247%2c 84%2c 92%2c 0)' offset='0'%3e%3c/stop%3e%3cstop stop-color='rgba(247%2c 84%2c 92%2c 0.05)' offset='1'%3e%3c/stop%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e"); */
|
||||
background-position: 0 120px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
padding-top: 100px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hero-text {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.hero-img-section {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-img {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
max-height: 110%;
|
||||
max-width: 140%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-floor {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
min-height: 50%;
|
||||
}
|
||||
|
||||
@keyframes animatedBackground {
|
||||
from {
|
||||
background-position: 0px 0;
|
||||
}
|
||||
to {
|
||||
background-position: -1448px 0;
|
||||
}
|
||||
}
|
||||
|
||||
#hero-audio-wave {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin-bottom: 12rem;
|
||||
opacity: 15%;
|
||||
width: 115%;
|
||||
min-height: 150px;
|
||||
// background-image: url("/assets/hero-audio-wave.png");
|
||||
background-repeat: repeat-x;
|
||||
animation: animatedBackground 20s linear infinite;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-color: #ffffff12 !important;
|
||||
border: 2px solid;
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
-webkit-box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
|
||||
|
||||
&.inner {
|
||||
background-color: rgba(255, 255, 255, 0.01);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-outline {
|
||||
border-color: rgba(252, 107, 104, 0.5) !important;
|
||||
}
|
||||
|
||||
.hero-floating-panel {
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
bottom: 180px;
|
||||
z-index: 3;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 1rem 1.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-floating-panel-left {
|
||||
margin-left: -800px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.hero-floating-panel-right {
|
||||
margin-right: -760px;
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
|
||||
.hero-floating-panel-text {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
position: static;
|
||||
text-align: center;
|
||||
text-shadow: 0px 3px 26px #00000060;
|
||||
margin-top: -35px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-top: 4rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
//import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
// NB(2022-02-01): This broke when converting to a monorepo.
|
||||
// I hadn't been using the tests anyhow.
|
||||
//render(<App />);
|
||||
//const linkElement = screen.getByText(/learn react/i);
|
||||
//expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,209 +0,0 @@
|
||||
// import 'bulma/css/bulma.css'
|
||||
import "./App.scss";
|
||||
|
||||
import React from "react";
|
||||
import { BrowserRouter, Route, Switch } from "react-router-dom";
|
||||
import { GitSha } from "@storyteller/components/src/elements/GitSha";
|
||||
import {
|
||||
ListTtsModels,
|
||||
TtsModelListItem,
|
||||
} from "@storyteller/components/src/api/tts/ListTtsModels";
|
||||
import StreamPage from "./pages/stream/StreamPage";
|
||||
import i18n from "i18next";
|
||||
import { ComingSoonPage } from "./pages/coming-soon/ComingSoonPage";
|
||||
import { LoginPage } from "./pages/login/LoginPage";
|
||||
import { SignupPage } from "./pages/signup/SignupPage";
|
||||
import { TtsConfigsIndexPage } from "./pages/tts_configs/TtsConfigsIndexPage";
|
||||
import {
|
||||
DetectLocale,
|
||||
DetectLocaleIsOk,
|
||||
} from "@storyteller/components/src/api/locale/DetectLocale";
|
||||
import { IndexPage } from "./pages/index/IndexPage";
|
||||
import { STORYTELLER_MERGED_TRANSLATIONS } from "./_i18n/StorytellerTranslations";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { TopNav } from "./layout/TopNav";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { TtsConfigsCreateRulePage } from "./pages/tts_configs/TtsConfigsCreateRulePage";
|
||||
import { TtsConfigsDeleteRulePage } from "./pages/tts_configs/TtsConfigsDeleteRulePage";
|
||||
import { TtsConfigsEditRulePage } from "./pages/tts_configs/TtsConfigsEditRulePage";
|
||||
import { TtsConfigsReorderPage } from "./pages/tts_configs/TtsConfigsReorderPage";
|
||||
import { ObsConfigsPage } from "./pages/obs_configs/ObsConfigsPage";
|
||||
import { Dashboard } from "./pages/dashboard/Dashboard";
|
||||
|
||||
i18n
|
||||
.use(initReactI18next) // passes i18n down to react-i18next
|
||||
.init({
|
||||
// the translations
|
||||
// (tip move them in a JSON file and import them,
|
||||
// or even better, manage them via a UI: https://react.i18next.com/guides/multiple-translation-files#manage-your-translations-with-a-management-gui)
|
||||
resources: STORYTELLER_MERGED_TRANSLATIONS,
|
||||
//lng: 'en', // if you're using a language detector, do not define the lng option
|
||||
fallbackLng: "en",
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
|
||||
},
|
||||
});
|
||||
|
||||
interface Props {}
|
||||
|
||||
interface State {
|
||||
sessionWrapper: SessionWrapper;
|
||||
allTtsModels: TtsModelListItem[];
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>;
|
||||
}
|
||||
|
||||
// Root element is a non-functional component for easier global lifecycle management
|
||||
class App extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
sessionWrapper: SessionWrapper.emptySession(),
|
||||
allTtsModels: [],
|
||||
allTtsModelsByToken: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await this.querySession();
|
||||
await this.queryLanguage();
|
||||
await this.queryTtsModels();
|
||||
setInterval(async () => {
|
||||
await this.querySession();
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
querySession = async () => {
|
||||
const sessionWrapper = await SessionWrapper.lookupSession();
|
||||
this.setState({
|
||||
sessionWrapper: sessionWrapper,
|
||||
});
|
||||
};
|
||||
|
||||
queryLanguage = async () => {
|
||||
let locale = await DetectLocale();
|
||||
if (DetectLocaleIsOk(locale)) {
|
||||
// TODO
|
||||
}
|
||||
};
|
||||
|
||||
queryTtsModels = async () => {
|
||||
if (this.state.allTtsModels.length > 0) {
|
||||
return; // Already queried.
|
||||
}
|
||||
const models = await ListTtsModels();
|
||||
if (models) {
|
||||
let allTtsModelsByToken = new Map();
|
||||
|
||||
models.forEach((item) => {
|
||||
allTtsModelsByToken.set(item.model_token, item);
|
||||
});
|
||||
|
||||
//dynamicallyCategorizeModels(models);
|
||||
this.setState({
|
||||
allTtsModels: models,
|
||||
allTtsModelsByToken: allTtsModelsByToken,
|
||||
});
|
||||
//if (!maybeSelectedTtsModel && models.length > 0) {
|
||||
// let model = models[0];
|
||||
// const featuredModels = models.filter(m => m.is_front_page_featured);
|
||||
// if (featuredModels.length > 0) {
|
||||
// // Random featured model
|
||||
// model = featuredModels[
|
||||
// Math.floor(Math.random()*featuredModels.length)
|
||||
// ];
|
||||
// }
|
||||
// setMaybeSelectedTtsModel(model);
|
||||
//}
|
||||
}
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<div id="viewable">
|
||||
<TopNav
|
||||
sessionWrapper={this.state.sessionWrapper}
|
||||
querySessionCallback={this.querySession}
|
||||
/>
|
||||
|
||||
<Switch>
|
||||
<Route path="/stream">
|
||||
<StreamPage />
|
||||
</Route>
|
||||
<Route path="/coming-soon">
|
||||
<ComingSoonPage />
|
||||
</Route>
|
||||
<Route path="/signup">
|
||||
<SignupPage
|
||||
sessionWrapper={this.state.sessionWrapper}
|
||||
querySessionCallback={this.querySession}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<LoginPage
|
||||
sessionWrapper={this.state.sessionWrapper}
|
||||
querySessionAction={this.querySession}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/tts_configs/create/:event_category">
|
||||
<TtsConfigsCreateRulePage
|
||||
sessionWrapper={this.state.sessionWrapper}
|
||||
allTtsModels={this.state.allTtsModels}
|
||||
allTtsModelsByToken={this.state.allTtsModelsByToken}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/tts_configs/reorder/:event_category">
|
||||
<TtsConfigsReorderPage
|
||||
sessionWrapper={this.state.sessionWrapper}
|
||||
allTtsModelsByToken={this.state.allTtsModelsByToken}
|
||||
/>
|
||||
</Route>
|
||||
<Route exact={true} path="/tts_configs/delete/:token">
|
||||
<TtsConfigsDeleteRulePage
|
||||
sessionWrapper={this.state.sessionWrapper}
|
||||
allTtsModelsByToken={this.state.allTtsModelsByToken}
|
||||
/>
|
||||
</Route>
|
||||
<Route exact={true} path="/tts_configs/edit/:token">
|
||||
<TtsConfigsEditRulePage
|
||||
sessionWrapper={this.state.sessionWrapper}
|
||||
allTtsModels={this.state.allTtsModels}
|
||||
allTtsModelsByToken={this.state.allTtsModelsByToken}
|
||||
/>
|
||||
</Route>
|
||||
<Route exact={true} path="/dashboard">
|
||||
<Dashboard sessionWrapper={this.state.sessionWrapper} />
|
||||
</Route>
|
||||
|
||||
<Route exact={true} path="/tts_configs">
|
||||
<TtsConfigsIndexPage
|
||||
sessionWrapper={this.state.sessionWrapper}
|
||||
allTtsModelsByToken={this.state.allTtsModelsByToken}
|
||||
/>
|
||||
</Route>
|
||||
<Route exact={true} path="/obs_configs">
|
||||
<ObsConfigsPage sessionWrapper={this.state.sessionWrapper} />
|
||||
</Route>
|
||||
<Route exact={true} path="/">
|
||||
<IndexPage sessionWrapper={this.state.sessionWrapper} />
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
{/* <footer className="footer">
|
||||
<div className="content has-text-centered">
|
||||
<p>Copyright © 2022 Storyteller</p>
|
||||
<p>
|
||||
<GitSha />
|
||||
</p>
|
||||
</div>
|
||||
</footer> */}
|
||||
</div>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -1,72 +0,0 @@
|
||||
import { COMMON_TRANSLATIONS, MergeDeepDictionary } from '@storyteller/components/src/_i18n/CommonTranslations';
|
||||
|
||||
// Use \u{00a0} = character literal
|
||||
|
||||
const STORYTELLER_TRANSLATIONS : any = {
|
||||
// English: 46.6% Twitch (#1), 30+% FakeYou (#1)
|
||||
en: {
|
||||
translation: {
|
||||
commonUi: {
|
||||
},
|
||||
pages: {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Spanish: 10.5% Twitch (#2), 20+% FakeYou (#2)
|
||||
es: {
|
||||
translation: {
|
||||
commonUi: {
|
||||
},
|
||||
pages: {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// ---------- OTHER LANGUAGES ----------
|
||||
|
||||
// German: 6.5% Twitch (#4)
|
||||
de: {
|
||||
translation: {
|
||||
}
|
||||
},
|
||||
// French: 5.6% Twitch (#6)
|
||||
fr: {
|
||||
translation: {
|
||||
}
|
||||
},
|
||||
// Indonesian 2% FakeYou
|
||||
id: {
|
||||
translation: {
|
||||
}
|
||||
},
|
||||
// Japanese: 2.5% Twitch (#8)
|
||||
ja: {
|
||||
translation: {
|
||||
},
|
||||
},
|
||||
// Korean: 5.4% Twitch (#7)
|
||||
ko: {
|
||||
translation: {
|
||||
}
|
||||
},
|
||||
// Portuguese: 6.2% Twitch (#5)
|
||||
pt: {
|
||||
translation: {
|
||||
},
|
||||
},
|
||||
// 6.5% Twitch (#3)
|
||||
ru: {
|
||||
translation: {
|
||||
}
|
||||
},
|
||||
// Turkish: 4% FakeYou (#3)
|
||||
tr: {
|
||||
translation: {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Type these as i18next dictionaries
|
||||
const STORYTELLER_MERGED_TRANSLATIONS : any = MergeDeepDictionary(STORYTELLER_TRANSLATIONS, COMMON_TRANSLATIONS);
|
||||
|
||||
export { STORYTELLER_MERGED_TRANSLATIONS }
|
||||
@@ -1,13 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
/*font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;*/
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
@@ -1,87 +0,0 @@
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TopNavSessionButton } from "./TopNavSessionButton";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
querySessionCallback: () => void;
|
||||
}
|
||||
|
||||
function TopNav(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<nav
|
||||
className="navbar navbar-expand-lg navbar-dark navbar-fixed navbar-main"
|
||||
aria-label="Offcanvas navbar large"
|
||||
>
|
||||
<div className="container">
|
||||
<Link className="navbar-brand me-5 pr-8" to="/">
|
||||
<img
|
||||
src="/assets/storyteller-logo.png"
|
||||
alt="Fake You Logo"
|
||||
height="34"
|
||||
/>
|
||||
</Link>
|
||||
<button
|
||||
className="navbar-toggler p-0 border-0"
|
||||
type="button"
|
||||
data-bs-toggle="offcanvas"
|
||||
data-bs-target="#offcanvasNavbar2"
|
||||
aria-controls="offcanvasNavbar2"
|
||||
title="toggle navbar"
|
||||
>
|
||||
<span className="sr-only">Navbar toggle</span>
|
||||
<span className="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div
|
||||
className="offcanvas offcanvas-end text-white nav-bg-dark"
|
||||
id="offcanvasNavbar2"
|
||||
aria-labelledby="offcanvasNavbar2Label"
|
||||
>
|
||||
<div className="offcanvas-header">
|
||||
<a className="navbar-brand me-5 pr-8" href="index.html">
|
||||
<img src="/assets/storyteller-logo.png" alt="" height="34" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close btn-close-white"
|
||||
data-bs-dismiss="offcanvas"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<div className="offcanvas-body">
|
||||
<ul className="navbar-nav justify-content-start flex-grow-1 align-items-lg-center">
|
||||
{/* <li className="nav-item">
|
||||
<a
|
||||
className="nav-link"
|
||||
aria-current="page"
|
||||
href="tts-configs.html"
|
||||
>
|
||||
TTS Configs
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li className="nav-item">
|
||||
<a className="nav-link" aria-current="page" href="#">
|
||||
OBS Configs
|
||||
</a>
|
||||
</li> */}
|
||||
</ul>
|
||||
<div className="d-grid gap-2 d-flex justify-content-start align-items-center pt-4 ps-3 pt-lg-0 ps-lg-0">
|
||||
<a className="nav-login me-3" href="">
|
||||
Login
|
||||
</a>
|
||||
<button type="button" className="btn btn-primary">
|
||||
Sign up
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { TopNav };
|
||||
@@ -1,80 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { Logout } from '@storyteller/components/src/api/session/Logout';
|
||||
import { t } from 'i18next';
|
||||
import { Gravatar } from '@storyteller/components/src/elements/Gravatar';
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
enableAlpha: boolean,
|
||||
querySessionAction: () => void,
|
||||
closeHamburgerAction: () => void,
|
||||
}
|
||||
|
||||
function TopNavSessionButton(props: Props) {
|
||||
let history = useHistory();
|
||||
|
||||
if (!props.enableAlpha) {
|
||||
return <nav />
|
||||
}
|
||||
|
||||
const logoutHandler = async () => {
|
||||
await Logout();
|
||||
props.querySessionAction();
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
let loggedIn = props.sessionWrapper.isLoggedIn();
|
||||
let displayName = props.sessionWrapper.getDisplayName();
|
||||
let gravatarHash = props.sessionWrapper.getEmailGravatarHash();
|
||||
let gravatar = <span />;
|
||||
|
||||
if (displayName === undefined) {
|
||||
displayName = 'My Account';
|
||||
}
|
||||
|
||||
if (gravatarHash !== undefined) {
|
||||
gravatar = <Gravatar email_hash={gravatarHash} size={15} />
|
||||
}
|
||||
|
||||
let sessionLink = <span />;
|
||||
let logoutLink = <span />;
|
||||
|
||||
if (loggedIn) {
|
||||
let url = `https://fakeyou.com/profile/${displayName}`;
|
||||
sessionLink = (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => props.closeHamburgerAction()}
|
||||
className="button is-alert is-inverted is-pulled-right"
|
||||
> {gravatar} {displayName} (FakeYou)</a>
|
||||
);
|
||||
logoutLink = <button
|
||||
className="button is-alert is-inverted is-pulled-right"
|
||||
onClick={async () => {
|
||||
await logoutHandler();
|
||||
props.closeHamburgerAction();
|
||||
}}
|
||||
>{t('common.logout')}</button>;
|
||||
} else {
|
||||
sessionLink = (
|
||||
<Link
|
||||
to="/signup"
|
||||
className="button is-danger is-pulled-right"
|
||||
onClick={() => props.closeHamburgerAction()}
|
||||
>{t('common.signUpLogin')}</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{logoutLink}
|
||||
{sessionLink}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export { TopNavSessionButton };
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,59 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
function ComingSoonPage() {
|
||||
return (
|
||||
<div>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<h1 className="title">
|
||||
Coming Soon
|
||||
</h1>
|
||||
<p className="subtitle">
|
||||
We're almost done!
|
||||
</p>
|
||||
<div className="content">
|
||||
<p>We'll be launching the following features shortly:</p>
|
||||
|
||||
<ul>
|
||||
<li>Audience rewards
|
||||
<ul>
|
||||
<li>Stream TTS</li>
|
||||
<li>Deepfake Emotes (TTS + Green Screen W2L)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Voice and video
|
||||
<ul>
|
||||
<li>Real time voice conversion (change your voice to someone else's)</li>
|
||||
<li>VoxelCam 3D volumetric camera (a webcam, but 3D, and directly injectable
|
||||
into games)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Big Secret Project
|
||||
<ul>
|
||||
<li>???</li>
|
||||
</ul>
|
||||
</li>
|
||||
{/*<li>Storyteller Engine
|
||||
<ul>
|
||||
<li>This is a high quality cinematic game engine.
|
||||
You don't need to own an expensive computer. It can run in the cloud.</li>
|
||||
<li>Real time 3D motion capture of one or more streamers / actors.
|
||||
Think VTubing, but Star Wars quality.</li>
|
||||
<li>Dedicated control plane for camera, lighting, environmental control OPs.</li>
|
||||
<li>Embed rich content: games, browser panes, video feeds</li>
|
||||
<li>Let your audience control your world</li>
|
||||
</ul>
|
||||
</li>*/}
|
||||
</ul>
|
||||
|
||||
{/*<p>You'll be able to make films, Hollywood-quality streams, and so much more! </p>*/}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export { ComingSoonPage }
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from "react";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { DashboardPage } from "./DashboardPage";
|
||||
import { Test } from "./Test";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
class Dashboard extends React.Component<Props> {
|
||||
// function Dashboard(props: Props) {
|
||||
// if (!props.sessionWrapper.isLoggedIn()) {
|
||||
// return (
|
||||
// <div className="container vh-100 d-flex align-items-center">
|
||||
// <div className="w-100">
|
||||
// <h1 className="text-center">Must Log In</h1>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-gradient">
|
||||
<div className="container vh-100">
|
||||
<div className="p-page-top row gx-5 h-100">
|
||||
<div className="d-none d-lg-flex col-lg-3 pb-5">
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div className="col-lg-9">
|
||||
{/* Page routes */}
|
||||
<Switch>
|
||||
<Route path="/">
|
||||
<DashboardPage />
|
||||
</Route>
|
||||
<Route path="/test">
|
||||
<Test />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export { Dashboard };
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Feed } from "./components/Feed";
|
||||
|
||||
function DashboardPage() {
|
||||
// if (!props.sessionWrapper.isLoggedIn()) {
|
||||
// return (
|
||||
// <div className="container vh-100 d-flex align-items-center">
|
||||
// <div className="w-100">
|
||||
// <h1 className="text-center">Must Log In</h1>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="parent">
|
||||
<h1 className="word mb-4">
|
||||
<span className="word">Dashboard</span>
|
||||
</h1>
|
||||
<button className="btn btn-primary w-100 mb-4">
|
||||
Link your Twitch account
|
||||
</button>
|
||||
<Feed />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DashboardPage };
|
||||
@@ -1,71 +0,0 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCog,
|
||||
faHome,
|
||||
faVideo,
|
||||
faVolumeUp,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
function Sidebar(props: Props) {
|
||||
// if (!props.sessionWrapper.isLoggedIn()) {
|
||||
// return (
|
||||
// <div className="container vh-100 d-flex align-items-center">
|
||||
// <div className="w-100">
|
||||
// <h1 className="text-center">Must Log In</h1>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="panel p-3">
|
||||
<div className="d-flex flex-column flex-shrink-0">
|
||||
<ul className="nav nav-pills flex-column gap-2">
|
||||
<li className="nav-item">
|
||||
<Link to="/" className="nav-link active d-flex" aria-current="page">
|
||||
<div className="icon">
|
||||
<FontAwesomeIcon icon={faHome} />
|
||||
</div>
|
||||
Dashboard
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/test" className="nav-link d-flex">
|
||||
<div className="icon">
|
||||
<FontAwesomeIcon icon={faVolumeUp} />
|
||||
</div>
|
||||
TTS Configs
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/" className="nav-link d-flex">
|
||||
<div className="icon">
|
||||
<FontAwesomeIcon icon={faVideo} />
|
||||
</div>
|
||||
OBS Setup
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/" className="nav-link d-flex">
|
||||
<div className="icon">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Settings
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/" className="nav-link d-flex">
|
||||
<div className="icon">
|
||||
<FontAwesomeIcon icon={faCog} />
|
||||
</div>
|
||||
Support
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Sidebar };
|
||||
@@ -1,32 +0,0 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faCog,
|
||||
faHome,
|
||||
faVideo,
|
||||
faVolumeUp,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
function Test(props: Props) {
|
||||
// if (!props.sessionWrapper.isLoggedIn()) {
|
||||
// return (
|
||||
// <div className="container vh-100 d-flex align-items-center">
|
||||
// <div className="w-100">
|
||||
// <h1 className="text-center">Must Log In</h1>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="parent">
|
||||
<h1 className="word mb-4">
|
||||
<span className="word">TESTY TEST</span>
|
||||
</h1>
|
||||
<button className="btn btn-primary w-100">
|
||||
Link your Twitch account
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Test };
|
||||
@@ -1,61 +0,0 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faUndo, faVolumeUp } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
function Feed() {
|
||||
// if (!props.sessionWrapper.isLoggedIn()) {
|
||||
// return (
|
||||
// <div className="container vh-100 d-flex align-items-center">
|
||||
// <div className="w-100">
|
||||
// <h1 className="text-center">Must Log In</h1>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="panel p-4 d-flex flex-column gap-3">
|
||||
<div className="d-flex">
|
||||
<h4 className="flex-grow-1 fw-bold">Queued TTS</h4>
|
||||
<div>icons</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="panel inner p-2 d-flex gap-4 align-items-center">
|
||||
<FontAwesomeIcon icon={faVolumeUp} className="ms-2 fs-4" />
|
||||
<div className="flex-grow-1">
|
||||
<p className="fw-semibold text-white fs-7">BFlatastic sent a TTS</p>
|
||||
<p className="fs-7">Hello, this is a test message.</p>
|
||||
</div>
|
||||
<button className="btn btn-primary">
|
||||
<FontAwesomeIcon icon={faUndo} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="panel inner p-2 d-flex gap-4 align-items-center">
|
||||
<FontAwesomeIcon icon={faVolumeUp} className="ms-2 fs-4" />
|
||||
<div className="flex-grow-1">
|
||||
<p className="fw-semibold text-white fs-7">BFlatastic sent a TTS</p>
|
||||
<p className="fs-7">Hello, this is a test message.</p>
|
||||
</div>
|
||||
<button className="btn btn-primary">
|
||||
<FontAwesomeIcon icon={faUndo} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="panel inner p-2 d-flex gap-4 align-items-center">
|
||||
<FontAwesomeIcon icon={faVolumeUp} className="ms-2 fs-4" />
|
||||
<div className="flex-grow-1">
|
||||
<p className="fw-semibold text-white fs-7">BFlatastic sent a TTS</p>
|
||||
<p className="fs-7">Hello, this is a test message.</p>
|
||||
</div>
|
||||
<button className="btn btn-primary btn-small">
|
||||
<FontAwesomeIcon icon={faUndo} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Feed };
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from "react";
|
||||
import { FakeYouExternalLink } from "@storyteller/components/src/elements/FakeYouExternalLink";
|
||||
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
|
||||
import { LoggedInIndex } from "./subpages/LoggedInIndex";
|
||||
import { LoggedOutIndex } from "./subpages/LoggedOutIndex";
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper;
|
||||
}
|
||||
|
||||
function IndexPage(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-hero">
|
||||
<div className="hero-section d-flex flex-column align-items-center">
|
||||
<div className="hero-inner container-fluid d-flex flex-column align-items-center justify-content-center">
|
||||
<h1 className="display-3 fw-bold mb-3 parent">
|
||||
Text to Speech For <span className="word">Your Stream</span>
|
||||
</h1>
|
||||
<p className="hero-text lead mb-4">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
</p>
|
||||
<div className="mt-4 d-flex gap-3 flex-column flex-md-row">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-hero load-hidden"
|
||||
>
|
||||
Sign Up Now
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary btn-hero load-hidden"
|
||||
>
|
||||
Join Discord
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hero-img-section noselect">
|
||||
<img className="hero-img" src="assets/hero-kitsune.png" alt="" />
|
||||
<div className="d-none d-md-flex" id="hero-audio-wave"></div>
|
||||
<img
|
||||
className="hero-floor d-none d-md-flex"
|
||||
src="/assets/hero-bg-floor.png"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<div className="panel hero-floating-panel hero-floating-panel-left d-none d-lg-block">
|
||||
<h6 className="pb-0">Tracer (Overwatch)</h6>
|
||||
<p className="hero-floating-panel-text">
|
||||
“Look out world! Tracer's here.”
|
||||
</p>
|
||||
</div>
|
||||
<div className="panel hero-floating-panel hero-floating-panel-right d-none d-lg-block">
|
||||
<h6 className="pb-0">Sonic the Hedgehog</h6>
|
||||
<p className="hero-floating-panel-text">
|
||||
"This is what speed looks like."
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { IndexPage };
|
||||
@@ -1,87 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCogs, faVideo } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faDiscord, faTwitch } from '@fortawesome/free-brands-svg-icons';
|
||||
import { DiscordLink } from '@storyteller/components/src/elements/DiscordLink';
|
||||
import { StorytellerUrlConfig } from '@storyteller/components/src/urls/StorytellerUrlConfig';
|
||||
import { CheckTwitchOauth, CheckTwitchOauthIsError, CheckTwitchOauthIsOk } from '@storyteller/components/src/api/storyteller/twitch_oauth/CheckTwitchOauth';
|
||||
|
||||
function LoggedInIndex() {
|
||||
const oauthUrl = new StorytellerUrlConfig().twitchOauthEnrollRedirect();
|
||||
|
||||
const [oauthTokenFound, setOauthTokenFound] = useState(false);
|
||||
|
||||
const checkTwitchOauth = useCallback(async () => {
|
||||
const result = await CheckTwitchOauth();
|
||||
|
||||
if (CheckTwitchOauthIsOk(result)) {
|
||||
setOauthTokenFound(result.oauth_token_found);
|
||||
} else if (CheckTwitchOauthIsError(result)) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkTwitchOauth();
|
||||
}, [checkTwitchOauth])
|
||||
|
||||
let linkText = <>Link Your Twitch <FontAwesomeIcon icon={faTwitch} /></> ;
|
||||
|
||||
if (oauthTokenFound) {
|
||||
linkText = <>Re-link Your Twitch <FontAwesomeIcon icon={faTwitch} /></> ;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
|
||||
<a
|
||||
href={oauthUrl}
|
||||
className="button is-large is-info is-fullwidth"
|
||||
>
|
||||
{linkText}
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<Link
|
||||
to="/tts_configs"
|
||||
className="button is-large is-info is-fullwidth"
|
||||
>
|
||||
Configure TTS <FontAwesomeIcon icon={faCogs} />
|
||||
</Link>
|
||||
|
||||
<br />
|
||||
|
||||
<Link
|
||||
to="/obs_configs"
|
||||
className="button is-large is-info is-fullwidth"
|
||||
>
|
||||
Configure OBS <FontAwesomeIcon icon={faVideo} />
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-3"> <FontAwesomeIcon icon={faDiscord} /> Join us in Discord! </h1>
|
||||
<h2 className="subtitle is-5">
|
||||
We'd like to chat with you.
|
||||
</h2>
|
||||
<p>
|
||||
<DiscordLink text="Please join us in Discord" iconAfterText={true} /> so that we know
|
||||
what you're thinking.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { LoggedInIndex }
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faRocket, faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
function LoggedOutIndex() {
|
||||
return (
|
||||
<div>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
|
||||
<h1 className="title is-4"> <FontAwesomeIcon icon={faRocket} /> You'll need an account to continue…</h1>
|
||||
<h1 className="subtitle is-6">(If you have a FakeYou.com account, it'll work here too.)</h1>
|
||||
|
||||
<Link
|
||||
to="/signup"
|
||||
className="button is-large is-info is-fullwidth"
|
||||
>
|
||||
Sign Up <FontAwesomeIcon icon={faSignInAlt} />
|
||||
</Link>
|
||||
|
||||
<br />
|
||||
|
||||
<Link
|
||||
to="/login"
|
||||
className="button is-large is-info is-outlined is-fullwidth"
|
||||
>
|
||||
Log In <FontAwesomeIcon icon={faSignInAlt} />
|
||||
</Link>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { LoggedOutIndex }
|
||||
@@ -1,113 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { iconPasswordField, iconUser } from '@storyteller/components/src/icons/SemanticIcons';
|
||||
import { CreateSession, CreateSessionIsError, CreateSessionIsSuccess } from '@storyteller/components/src/api/session/CreateSession';
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
querySessionAction: () => void,
|
||||
}
|
||||
|
||||
function LoginPage(props: Props) {
|
||||
let history = useHistory();
|
||||
|
||||
const [password, setPassword] = useState('')
|
||||
const [usernameOrEmail, setUsernameOrEmail] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
if (props.sessionWrapper.isLoggedIn()) {
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
const handleUsernameOrEmailChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
ev.preventDefault();
|
||||
const usernameOrEmailValue = (ev.target as HTMLInputElement).value;
|
||||
setUsernameOrEmail(usernameOrEmailValue);
|
||||
setErrorMessage("");
|
||||
return false;
|
||||
};
|
||||
|
||||
const handlePasswordChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
ev.preventDefault();
|
||||
const passwordValue = (ev.target as HTMLInputElement).value;
|
||||
setPassword(passwordValue);
|
||||
setErrorMessage("");
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
const request = {
|
||||
username_or_email: usernameOrEmail,
|
||||
password: password,
|
||||
}
|
||||
|
||||
const response = await CreateSession(request);
|
||||
|
||||
if (CreateSessionIsError(response)) {
|
||||
setErrorMessage(response.error_message);
|
||||
} else if (CreateSessionIsSuccess(response)) {
|
||||
console.log('querying new session');
|
||||
props.querySessionAction();
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
let errorWarning = <span />
|
||||
if (errorMessage) {
|
||||
errorWarning = (
|
||||
<div className="notification is-danger is-light">
|
||||
<strong>Login Error:</strong> {errorMessage}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<h1 className="title is-1"> Login </h1>
|
||||
<h1 className="subtitle is-6">(If you have a FakeYou.com account, it'll work here too.)</h1>
|
||||
|
||||
{errorWarning}
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<div className="field">
|
||||
<label className="label">Username or Email</label>
|
||||
<div className="control has-icons-left has-icons-right">
|
||||
<input className="input" type="text" placeholder="Username or Email" value={usernameOrEmail} onChange={handleUsernameOrEmailChange} />
|
||||
<span className="icon is-small is-left">
|
||||
<FontAwesomeIcon icon={iconUser} />
|
||||
</span>
|
||||
</div>
|
||||
{/*<p className="help"></p>*/}
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Password</label>
|
||||
<div className="control has-icons-left has-icons-right">
|
||||
<input className="input" type="password" placeholder="Password" value={password} onChange={handlePasswordChange} />
|
||||
<span className="icon is-small is-left">
|
||||
<FontAwesomeIcon icon={iconPasswordField} />
|
||||
</span>
|
||||
</div>
|
||||
{/*<p className="help"></p>*/}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<button className="button is-link is-large is-fullwidth">Login</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { LoginPage };
|
||||
@@ -1,151 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faDiscord, faTwitch } from '@fortawesome/free-brands-svg-icons';
|
||||
import { CheckTwitchOauth, CheckTwitchOauthIsError, CheckTwitchOauthIsOk } from '@storyteller/components/src/api/storyteller/twitch_oauth/CheckTwitchOauth';
|
||||
import { StorytellerUrlConfig } from '@storyteller/components/src/urls/StorytellerUrlConfig';
|
||||
import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
}
|
||||
|
||||
function ObsConfigsPage(props: Props) {
|
||||
const [maybeTwitchUsername, setMaybeTwitchUsername] = useState("");
|
||||
|
||||
const checkTwitchOauth = useCallback(async () => {
|
||||
const result = await CheckTwitchOauth();
|
||||
|
||||
if (CheckTwitchOauthIsOk(result)) {
|
||||
setMaybeTwitchUsername(result.maybe_twitch_username || "");
|
||||
} else if (CheckTwitchOauthIsError(result)) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkTwitchOauth();
|
||||
}, [checkTwitchOauth])
|
||||
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
return <h1>Must Log In</h1>;
|
||||
}
|
||||
|
||||
const username = !!maybeTwitchUsername ? maybeTwitchUsername : 'YOUR_USERNAME';
|
||||
|
||||
const obsUrl = new StorytellerUrlConfig().obsPageFortwitch(username);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<section className="hero is-small">
|
||||
<div className="hero-body">
|
||||
|
||||
<div className="columns is-vcentered">
|
||||
|
||||
<div className="column is-one-third">
|
||||
<div className="mascot">
|
||||
<img src="/mascot/kitsune_pose5_black_2000.webp" alt="FakeYou's mascot!" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="column">
|
||||
<p className="title is-1">
|
||||
OBS Setup
|
||||
</p>
|
||||
<p className="subtitle is-3">
|
||||
Ready to Broadcast!
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
|
||||
<h1 className="title is-2">Your OBS Browser Source</h1>
|
||||
|
||||
<p>Once you've linked Twitch with Storyteller, you can add the following page as an <em>OBS Browser Source</em>:</p>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="field">
|
||||
<p className="control has-icons-left">
|
||||
<input className="input is-large" type="text" value={obsUrl} readOnly={true} />
|
||||
<span className="icon is-small is-left">
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<a
|
||||
href={obsUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="button is-large is-info is-fullwidth"
|
||||
>
|
||||
Open In New Tab <FontAwesomeIcon icon={faExternalLinkAlt} />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h1 className="title is-2">Setup Instructions</h1>
|
||||
|
||||
<p>If you're not familiar with OBS or you need a refresher, here's a quick glance at the setup.</p>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 className="title is-4">Step 1) Click <FontAwesomeIcon icon={faPlus} /> to add a new Browser Source</h1>
|
||||
|
||||
<img src="/obs-tutorial/obs-step1.webp" alt="step 1" />
|
||||
|
||||
<p>The bottom of OBS should have a plus icon (<FontAwesomeIcon icon={faPlus} />) to add new sources.
|
||||
You'll want to add a new <strong>Browser Source</strong>.</p>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 className="title is-4">Step 2) Paste the URL into the configuration window</h1>
|
||||
|
||||
<img src="/obs-tutorial/obs-step2.webp" alt="step 2" />
|
||||
|
||||
<br />
|
||||
|
||||
<p>Remember, the URL is <code>{obsUrl}</code></p>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 className="title is-4">Step 3) Activate the Page</h1>
|
||||
|
||||
<img src="/obs-tutorial/obs-step3.webp" alt="step 3" />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<p>Right click the layer and select <em>“Interact”</em>.
|
||||
This will let you click the button and activate the service.</p>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h1 className="title is-2">Good to go! <FontAwesomeIcon icon={faTwitch} /></h1>
|
||||
|
||||
<p>You can now hide the layer and make it invisible. The audio levels are also independently adjustable.</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p>Have fun! We'll be back soon with some even more jaw-dropping utilities.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { ObsConfigsPage }
|
||||
@@ -1,331 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CreateAccount, CreateAccountIsError, CreateAccountIsSuccess } from '@storyteller/components/src/api/user/CreateAccount';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SessionWrapper } from '@storyteller/components/src/session/SessionWrapper';
|
||||
import { iconEmailField, iconPasswordField, iconUser } from '@storyteller/components/src/icons/SemanticIcons';
|
||||
|
||||
enum FieldTriState {
|
||||
EMPTY_FALSE,
|
||||
FALSE,
|
||||
TRUE,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
querySessionCallback : () => void,
|
||||
}
|
||||
|
||||
function SignupPage(props: Props) {
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [usernameValid, setUsernameValid] = useState(FieldTriState.EMPTY_FALSE);
|
||||
const [usernameInvalidReason, setUsernameInvalidReason] = useState('');
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [emailValid, setEmailValid] = useState(FieldTriState.EMPTY_FALSE);
|
||||
const [emailInvalidReason, setEmailInvalidReason] = useState('');
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordValid, setPasswordValid] = useState(FieldTriState.EMPTY_FALSE);
|
||||
const [passwordInvalidReason, setPasswordInvalidReason] = useState('');
|
||||
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState('');
|
||||
const [passwordConfirmationValid, setPasswordConfirmationValid] = useState(FieldTriState.EMPTY_FALSE);
|
||||
const [passwordConfirmationInvalidReason, setPasswordConfirmationInvalidReason] = useState('');
|
||||
|
||||
const handleUsernameChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const newUsername = (ev.target as HTMLInputElement).value;
|
||||
|
||||
let usernameValid = FieldTriState.EMPTY_FALSE;
|
||||
let usernameInvalidReason = "";
|
||||
|
||||
if (newUsername.length > 1) {
|
||||
if (newUsername.length < 3) {
|
||||
usernameValid = FieldTriState.FALSE;
|
||||
usernameInvalidReason = "username is too short";
|
||||
} else if (newUsername.length > 15) {
|
||||
usernameValid = FieldTriState.FALSE;
|
||||
usernameInvalidReason = "username is too long";
|
||||
} else {
|
||||
usernameValid = FieldTriState.TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
setUsername(newUsername);
|
||||
setUsernameValid(usernameValid);
|
||||
setUsernameInvalidReason(usernameInvalidReason);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const handleEmailChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const newEmail = (ev.target as HTMLInputElement).value;
|
||||
|
||||
let emailValid = FieldTriState.EMPTY_FALSE;
|
||||
let emailInvalidReason = "";
|
||||
|
||||
if (newEmail.length > 1) {
|
||||
if (newEmail.length < 3) {
|
||||
emailValid = FieldTriState.FALSE;
|
||||
emailInvalidReason = "email is too short";
|
||||
} else if (!newEmail.includes("@")) {
|
||||
emailValid = FieldTriState.FALSE;
|
||||
emailInvalidReason = "email is invalid";
|
||||
} else {
|
||||
emailValid = FieldTriState.TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
setEmail(newEmail);
|
||||
setEmailValid(emailValid);
|
||||
setEmailInvalidReason(emailInvalidReason);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const handlePasswordChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const newPassword = (ev.target as HTMLInputElement).value;
|
||||
|
||||
let passwordValid = FieldTriState.EMPTY_FALSE;
|
||||
let passwordInvalidReason = "";
|
||||
let passwordConfirmationValid = FieldTriState.EMPTY_FALSE;
|
||||
let passwordConfirmationInvalidReason = "";
|
||||
|
||||
if (newPassword.length > 1) {
|
||||
if (newPassword.length < 5) {
|
||||
passwordValid = FieldTriState.FALSE;
|
||||
passwordInvalidReason = "password is too short";
|
||||
} else {
|
||||
passwordValid = FieldTriState.TRUE;
|
||||
}
|
||||
|
||||
if (newPassword !== passwordConfirmation) {
|
||||
passwordConfirmationValid = FieldTriState.FALSE;
|
||||
passwordConfirmationInvalidReason = "passwords do not match";
|
||||
} else {
|
||||
passwordConfirmationValid = FieldTriState.TRUE;
|
||||
passwordConfirmationInvalidReason = "";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
setPassword(newPassword);
|
||||
setPasswordValid(passwordValid);
|
||||
setPasswordInvalidReason(passwordInvalidReason);
|
||||
setPasswordConfirmationValid(passwordConfirmationValid);
|
||||
setPasswordConfirmationInvalidReason(passwordConfirmationInvalidReason)
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const handlePasswordConfirmationChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const newPasswordConfirmation = (ev.target as HTMLInputElement).value;
|
||||
|
||||
let passwordConfirmationValid = FieldTriState.EMPTY_FALSE;
|
||||
let passwordConfirmationInvalidReason = "";
|
||||
|
||||
if (newPasswordConfirmation.length > 1) {
|
||||
|
||||
if (newPasswordConfirmation !== password) {
|
||||
passwordConfirmationValid = FieldTriState.FALSE;
|
||||
passwordConfirmationInvalidReason = "passwords do not match";
|
||||
} else {
|
||||
passwordConfirmationValid = FieldTriState.TRUE;
|
||||
passwordConfirmationInvalidReason = "";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
setPasswordConfirmation(newPasswordConfirmation);
|
||||
setPasswordConfirmationValid(passwordConfirmationValid);
|
||||
setPasswordConfirmationInvalidReason(passwordConfirmationInvalidReason);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!usernameValid ||
|
||||
!emailValid ||
|
||||
!passwordValid ||
|
||||
!passwordConfirmationValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const request = {
|
||||
username: username,
|
||||
email_address: email,
|
||||
password: password,
|
||||
password_confirmation: passwordConfirmation,
|
||||
};
|
||||
|
||||
const response = await CreateAccount(request);
|
||||
|
||||
if (CreateAccountIsError(response)) {
|
||||
if ('email_address' in response.error_fields) {
|
||||
setEmailValid(FieldTriState.FALSE);
|
||||
setEmailInvalidReason(response.error_fields['email_address'] || "");
|
||||
}
|
||||
if ('username' in response.error_fields) {
|
||||
setUsernameValid(FieldTriState.FALSE);
|
||||
setUsernameInvalidReason(response.error_fields['username'] || "");
|
||||
}
|
||||
} else if (CreateAccountIsSuccess(response)) {
|
||||
console.log('querying new session');
|
||||
props.querySessionCallback();
|
||||
|
||||
// TODO: Switch to functional component.
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (props.sessionWrapper.isLoggedIn()) {
|
||||
return <div>Invalid view for logged in users.</div>;
|
||||
}
|
||||
|
||||
let usernameInputClass = "input";
|
||||
let usernameHelpClass = "help";
|
||||
switch (usernameValid) {
|
||||
case FieldTriState.EMPTY_FALSE:
|
||||
break;
|
||||
case FieldTriState.FALSE:
|
||||
usernameInputClass += " is-danger";
|
||||
usernameHelpClass += " is-danger";
|
||||
break;
|
||||
case FieldTriState.TRUE:
|
||||
usernameInputClass += " is-success";
|
||||
usernameHelpClass += " is-success";
|
||||
break;
|
||||
}
|
||||
|
||||
let emailInputClass = "input";
|
||||
let emailHelpClass = "help";
|
||||
switch (emailValid) {
|
||||
case FieldTriState.EMPTY_FALSE:
|
||||
break;
|
||||
case FieldTriState.FALSE:
|
||||
emailInputClass += " is-danger";
|
||||
emailHelpClass += " is-danger";
|
||||
break;
|
||||
case FieldTriState.TRUE:
|
||||
emailInputClass += " is-success";
|
||||
emailHelpClass += " is-success";
|
||||
break;
|
||||
}
|
||||
|
||||
let passwordInputClass = "input";
|
||||
let passwordHelpClass = "help";
|
||||
switch (passwordValid) {
|
||||
case FieldTriState.EMPTY_FALSE:
|
||||
break;
|
||||
case FieldTriState.FALSE:
|
||||
passwordInputClass += " is-danger";
|
||||
passwordHelpClass += " is-danger";
|
||||
break;
|
||||
case FieldTriState.TRUE:
|
||||
passwordInputClass += " is-success";
|
||||
passwordHelpClass += " is-success";
|
||||
break;
|
||||
}
|
||||
|
||||
let passwordConfirmationInputClass = "input";
|
||||
let passwordConfirmationHelpClass = "help";
|
||||
switch (passwordConfirmationValid) {
|
||||
case FieldTriState.EMPTY_FALSE:
|
||||
break;
|
||||
case FieldTriState.FALSE:
|
||||
passwordConfirmationInputClass += " is-danger";
|
||||
passwordConfirmationHelpClass += " is-danger";
|
||||
break;
|
||||
case FieldTriState.TRUE:
|
||||
passwordConfirmationInputClass += " is-success";
|
||||
passwordConfirmationHelpClass += " is-success";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="title is-1"> Sign Up </h1>
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
<div className="field">
|
||||
<label className="label">Username</label>
|
||||
<div className="control has-icons-left has-icons-right">
|
||||
<input className={usernameInputClass} type="text" placeholder="Username" value={username} onChange={handleUsernameChange} />
|
||||
<span className="icon is-small is-left">
|
||||
<FontAwesomeIcon icon={iconUser} />
|
||||
</span>
|
||||
</div>
|
||||
<p className={usernameHelpClass}>{usernameInvalidReason}</p>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Email</label>
|
||||
<div className="control has-icons-left has-icons-right">
|
||||
<input className={emailInputClass} type="email" placeholder="Email" value={email} onChange={handleEmailChange} />
|
||||
<span className="icon is-small is-left">
|
||||
<FontAwesomeIcon icon={iconEmailField} />
|
||||
</span>
|
||||
</div>
|
||||
<p className={emailHelpClass}>{emailInvalidReason}</p>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Password</label>
|
||||
<div className="control has-icons-left has-icons-right">
|
||||
<input className={passwordInputClass} type="password" placeholder="Password" value={password} onChange={handlePasswordChange} />
|
||||
<span className="icon is-small is-left">
|
||||
<FontAwesomeIcon icon={iconPasswordField} />
|
||||
</span>
|
||||
</div>
|
||||
<p className={passwordHelpClass}>{passwordInvalidReason}</p>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label className="label">Password Confirmation</label>
|
||||
<div className="control has-icons-left has-icons-right">
|
||||
<input className={passwordConfirmationInputClass} type="password" placeholder="Password confirmation" value={passwordConfirmation} onChange={handlePasswordConfirmationChange} />
|
||||
<span className="icon is-small is-left">
|
||||
<FontAwesomeIcon icon={iconPasswordField} />
|
||||
</span>
|
||||
</div>
|
||||
<p className={passwordConfirmationHelpClass}>
|
||||
{passwordConfirmationInvalidReason}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="notification is-warning">
|
||||
<strong>Remember your password!</strong> We don't have password reset currently, and it'll be a
|
||||
few more weeks before it's added (there are more important features to work on). If you lose your
|
||||
password, please let us know in Discord.
|
||||
</div>
|
||||
|
||||
<button className="button is-link is-large is-fullwidth">Sign up</button>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<Link to="/login"
|
||||
className="button is-info is-large is-fullwidth is-inverted"
|
||||
>Already have an account? Log in instead!</Link>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { SignupPage };
|
||||
@@ -1,84 +0,0 @@
|
||||
import { faTwitch } from '@fortawesome/free-brands-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { useState } from 'react';
|
||||
import { useHistory, withRouter } from 'react-router-dom';
|
||||
|
||||
function StreamPage() {
|
||||
const history = useHistory();
|
||||
|
||||
const [twitchUsername, setTwitchUsername] = useState('')
|
||||
|
||||
const handleTwitchUsernameChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
ev.preventDefault();
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
setTwitchUsername(value);
|
||||
return false;
|
||||
};
|
||||
|
||||
const openObs = (ev: React.FormEvent<HTMLButtonElement>) => {
|
||||
ev.preventDefault();
|
||||
let url = `/obs/${twitchUsername}`;
|
||||
history.push(url);
|
||||
return false;
|
||||
};
|
||||
|
||||
const buttonDisabled = twitchUsername.trim().length === 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<h1 className="title">
|
||||
Stream TTS
|
||||
</h1>
|
||||
<p className="subtitle">
|
||||
Early Alpha Preview
|
||||
</p>
|
||||
<div className="content">
|
||||
<p>This is an early demo of our Stream TTS, powered by FakeYou.
|
||||
It does not currently offer customization, though our plans are to
|
||||
allow for a high degree of configurability: set your own voices, rewards, etc.</p>
|
||||
|
||||
<p>You don't need to set up any software on your end. Simply point OBS or your
|
||||
broadcast software the URL below:</p>
|
||||
|
||||
<br />
|
||||
|
||||
<button className="button is-large is-danger">
|
||||
1. Connect with Twitch
|
||||
</button>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div className="field">
|
||||
<p className="control has-icons-left">
|
||||
<input
|
||||
className="input is-large"
|
||||
type="text"
|
||||
placeholder="Twitch Username"
|
||||
onChange={handleTwitchUsernameChange}/>
|
||||
<span className="icon is-large is-left">
|
||||
<FontAwesomeIcon icon={faTwitch} />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<button
|
||||
className="button is-large is-danger"
|
||||
disabled={buttonDisabled}
|
||||
onClick={openObs}
|
||||
>
|
||||
2. Open OBS Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(StreamPage);
|
||||
@@ -1,167 +0,0 @@
|
||||
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>,
|
||||
}
|
||||
|
||||
function TtsConfigsCreateRulePage(props: Props) {
|
||||
const { event_category } = useParams() as { event_category : string };
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
// TODO: Use centralized 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
|
||||
// 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 [ruleIsDisabled] = useState(false);
|
||||
|
||||
let maybeTwitchEventCategory = TWITCH_EVENT_CATEGORY_BY_STRING.get(event_category);
|
||||
|
||||
if (maybeTwitchEventCategory === undefined) {
|
||||
history.push(indexLink);
|
||||
return <></>;
|
||||
}
|
||||
|
||||
// NB: To satisfy the type system and not be a union on 'undefined'
|
||||
const twitchEventCategory = maybeTwitchEventCategory;
|
||||
|
||||
const updateModifiedEventMatchPredicate = (predicate: EventMatchPredicate) => {
|
||||
setModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
|
||||
const updateModifiedEventResponse = (response: EventResponse) => {
|
||||
setModifiedEventResponse(response);
|
||||
}
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
// TODO: Check for errors.
|
||||
|
||||
let newEventMatchPredicate = modifiedEventMatchPredicate;
|
||||
let newEventResponse = modifiedEventResponse;
|
||||
|
||||
// TODO: UUID idempotency token that updates on every model change.
|
||||
|
||||
const request : CreateTwitchEventRuleRequest = {
|
||||
idempotency_token: uuidv4(),
|
||||
event_category: twitchEventCategory,
|
||||
event_match_predicate: newEventMatchPredicate,
|
||||
event_response: newEventResponse,
|
||||
rule_is_disabled: ruleIsDisabled,
|
||||
user_specified_rule_order: 1000,
|
||||
};
|
||||
|
||||
const result = await CreateTwitchEventRule(request);
|
||||
if (result.success) {
|
||||
history.push(indexLink);
|
||||
}
|
||||
|
||||
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 = {
|
||||
// Not yet saved, so fake values
|
||||
token: 'virtual',
|
||||
user_specified_rule_order: 1000,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date(),
|
||||
|
||||
// Updated in UI
|
||||
event_category: twitchEventCategory,
|
||||
event_match_predicate: modifiedEventMatchPredicate,
|
||||
event_response: modifiedEventResponse,
|
||||
rule_is_disabled: ruleIsDisabled,
|
||||
};
|
||||
|
||||
const titlesByCategory = new Map<TwitchEventCategory, string>([
|
||||
[TwitchEventCategory.Bits, 'Create New Bits / Cheers Rule'],
|
||||
[TwitchEventCategory.ChannelPoints, 'Create New Channel Points Rule'],
|
||||
]);
|
||||
|
||||
const pageTitle = titlesByCategory.get(twitchEventCategory);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
<h1 className="title"> {pageTitle} </h1>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
|
||||
<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}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button className="button is-large is-fullwidth is-primary">
|
||||
Save New Rule <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>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { TtsConfigsCreateRulePage }
|
||||
@@ -1,92 +0,0 @@
|
||||
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 { DeleteTwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/DeleteTwitchEventRule';
|
||||
import { TwitchEventRuleElement } from './rule_cards/TwitchEventRuleElement';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faAngleLeft, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
}
|
||||
|
||||
function TtsConfigsDeleteRulePage(props: Props) {
|
||||
const { token } : { token : string } = useParams();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const [twitchEventRule, setTwitchEventRule] = useState<TwitchEventRule|undefined>(undefined);
|
||||
|
||||
const getTwitchEventRule = useCallback(async (token: string) => {
|
||||
const response = await GetTwitchEventRule(token);
|
||||
|
||||
if (GetTwitchEventRuleIsOk(response)) {
|
||||
setTwitchEventRule(response.twitch_event_rule);
|
||||
} else if (GetTwitchEventRuleIsError(response)) {
|
||||
// TODO
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getTwitchEventRule(token);
|
||||
}, [getTwitchEventRule, token]);
|
||||
|
||||
const indexLink = '/tts_configs';
|
||||
|
||||
const handleDeleteFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
const result = await DeleteTwitchEventRule(token);
|
||||
if (result.success) {
|
||||
history.push(indexLink);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
return <h1>Must Log In</h1>;
|
||||
}
|
||||
|
||||
if (twitchEventRule === undefined) {
|
||||
return <h1>Loading...</h1>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
<h1 className="title"> Delete Rule ? </h1>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<TwitchEventRuleElement
|
||||
rule={twitchEventRule}
|
||||
hideButtons={true}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken} />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<form onSubmit={handleDeleteFormSubmit}>
|
||||
<button className="button is-large is-fullwidth is-danger">
|
||||
<FontAwesomeIcon icon={faTrash} /> Delete
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<Link to={indexLink} className="button is-large is-fullwidth is-info is-outlined">
|
||||
<FontAwesomeIcon icon={faAngleLeft} /> Cancel / Go Back
|
||||
</Link>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { TtsConfigsDeleteRulePage }
|
||||
@@ -1,171 +0,0 @@
|
||||
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>,
|
||||
}
|
||||
|
||||
function TtsConfigsEditRulePage(props: Props) {
|
||||
const { token } : { token : string } = useParams();
|
||||
|
||||
// TODO: Use centralized URL configs
|
||||
const indexLink = '/tts_configs';
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
// ========== Initial Load of Server State ==========
|
||||
|
||||
const [twitchEventRule, setTwitchEventRule] = useState<TwitchEventRule|undefined>(undefined);
|
||||
|
||||
// Sent to sub-components to initialize UI
|
||||
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 [ruleIsDisabled, setRuleIsDisabled] = useState(false);
|
||||
|
||||
const getTwitchEventRule = useCallback(async (token: string) => {
|
||||
const response = await GetTwitchEventRule(token);
|
||||
|
||||
if (GetTwitchEventRuleIsOk(response)) {
|
||||
// Source of truth
|
||||
setTwitchEventRule(response.twitch_event_rule);
|
||||
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);
|
||||
setModifiedEventResponse(response.twitch_event_rule.event_response);
|
||||
setRuleIsDisabled(response.twitch_event_rule.rule_is_disabled);
|
||||
|
||||
} else if (GetTwitchEventRuleIsError(response)) {
|
||||
// TODO
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getTwitchEventRule(token);
|
||||
}, [getTwitchEventRule, token]);
|
||||
|
||||
const updateModifiedEventMatchPredicate = (predicate: EventMatchPredicate) => {
|
||||
setModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
|
||||
const updateModifiedEventResponse = (response: EventResponse) => {
|
||||
setModifiedEventResponse(response);
|
||||
}
|
||||
|
||||
const handleFormSubmit = async (ev: React.FormEvent<HTMLFormElement>) : Promise<boolean> => {
|
||||
ev.preventDefault();
|
||||
|
||||
// TODO: Check for errors.
|
||||
|
||||
let newEventMatchPredicate = modifiedEventMatchPredicate;
|
||||
let newEventResponse = modifiedEventResponse;
|
||||
|
||||
const request : EditTwitchEventRuleRequest = {
|
||||
event_match_predicate: newEventMatchPredicate,
|
||||
event_response: newEventResponse,
|
||||
rule_is_disabled: ruleIsDisabled,
|
||||
};
|
||||
|
||||
const result = await EditTwitchEventRule(token, request);
|
||||
if (result.success) {
|
||||
history.push(indexLink);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
return <h1>Must Log In</h1>;
|
||||
}
|
||||
|
||||
if (twitchEventRule === undefined) {
|
||||
return <h1>Loading...</h1>;
|
||||
}
|
||||
|
||||
// NB: This is a hypothetical version of what we'll update to
|
||||
let renderRule : TwitchEventRule = {
|
||||
// Unchanged
|
||||
token: twitchEventRule.token,
|
||||
event_category: twitchEventRule.event_category,
|
||||
user_specified_rule_order: twitchEventRule.user_specified_rule_order,
|
||||
created_at: twitchEventRule.created_at,
|
||||
updated_at: twitchEventRule.updated_at,
|
||||
|
||||
// Updated in UI
|
||||
event_match_predicate: modifiedEventMatchPredicate,
|
||||
event_response: modifiedEventResponse,
|
||||
rule_is_disabled: ruleIsDisabled,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
<h1 className="title"> Edit Rule </h1>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
|
||||
<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}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
</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>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { TtsConfigsEditRulePage }
|
||||
@@ -1,212 +0,0 @@
|
||||
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 { 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';
|
||||
|
||||
interface Props {
|
||||
sessionWrapper: SessionWrapper,
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
}
|
||||
|
||||
function TtsConfigsIndexPage(props: Props) {
|
||||
const [twitchEventRules, setTwitchEventRules] = useState<TwitchEventRule[]>([]);
|
||||
|
||||
const listTwitchEventRules = useCallback(async () => {
|
||||
const response = await ListTwitchEventRules();
|
||||
|
||||
if (ListTwitchEventRulesIsOk(response)) {
|
||||
setTwitchEventRules(response.twitch_event_rules);
|
||||
} else if (ListTwitchEventRulesIsError(response)) {
|
||||
// TODO
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
listTwitchEventRules();
|
||||
}, [listTwitchEventRules]);
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
return <h1>Must Log In</h1>;
|
||||
}
|
||||
|
||||
const cheerEventRules = twitchEventRules.filter(rule => rule.event_category === TwitchEventCategory.Bits);
|
||||
|
||||
const channelPointsEventRules = twitchEventRules.filter(rule => rule.event_category === TwitchEventCategory.ChannelPoints);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<section className="hero is-small">
|
||||
<div className="hero-body">
|
||||
|
||||
<div className="columns is-vcentered">
|
||||
|
||||
<div className="column is-one-third">
|
||||
<div className="mascot">
|
||||
<img src="/mascot/kitsune_pose8_black_2000.webp" alt="Storyteller's mascot!" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="column">
|
||||
<p className="title is-1">
|
||||
Stream Setup
|
||||
</p>
|
||||
<p className="subtitle is-3">
|
||||
Configure TTS (and soon much more!)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<h1 className="title is-3"> <FontAwesomeIcon icon={faGem} /> Bits / Cheers </h1>
|
||||
<h2 className="subtitle is-5">
|
||||
These settings are best for small and medium-sized channels.
|
||||
</h2>
|
||||
<p>
|
||||
</p>
|
||||
{cheerEventRules.map(rule => {
|
||||
return <TwitchEventRuleElement
|
||||
key={rule.token}
|
||||
rule={rule}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/tts_configs/create/bits"
|
||||
className="button is-large is-fullwidth is-primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} /> Create New Bits / Cheer Rule
|
||||
</Link>
|
||||
|
||||
<br />
|
||||
|
||||
<Link
|
||||
to="/tts_configs/reorder/bits"
|
||||
className="button is-large is-fullwidth is-primary is-outlined"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSort} /> Reorder Rules
|
||||
</Link>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-3"> <FontAwesomeIcon icon={faBox} /> Channel Points / Rewards </h1>
|
||||
<h2 className="subtitle is-5">
|
||||
These settings are best for small channels.
|
||||
</h2>
|
||||
{channelPointsEventRules.map(rule => {
|
||||
return <TwitchEventRuleElement
|
||||
key={rule.token}
|
||||
rule={rule}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/tts_configs/create/channel_points"
|
||||
className="button is-large is-fullwidth is-primary"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} /> Create Channel Points Rule
|
||||
</Link>
|
||||
|
||||
<br />
|
||||
|
||||
<Link
|
||||
to="/tts_configs/reorder/channel_points"
|
||||
className="button is-large is-fullwidth is-primary is-outlined"
|
||||
>
|
||||
<FontAwesomeIcon icon={faSort} /> Reorder Rules
|
||||
</Link>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-1"> <FontAwesomeIcon icon={faMeteor} /> Coming Soon…</h1>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-4"> <FontAwesomeIcon icon={faHeart} /> Subs, Resubs, and Gifted Subs </h1>
|
||||
<h2 className="subtitle is-6">
|
||||
These settings are great for all channel sizes.
|
||||
</h2>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-4"> <FontAwesomeIcon icon={faDonate} /> Donation Services (Streamlabs, StreamElements, etc.) </h1>
|
||||
<h2 className="subtitle is-6">
|
||||
These settings are great for large channels, but work for channels of all sizes.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-5"> <FontAwesomeIcon icon={faTerminal} /> Text <code>/slash</code> Commands</h1>
|
||||
<h2 className="subtitle is-6">
|
||||
These settings are great for very small channels.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-4"> <FontAwesomeIcon icon={faFilm} /> User-generated Animated Deepfake Rewards </h1>
|
||||
<h2 className="subtitle is-6">
|
||||
A lip-synced, green screened <em>“Famous Person”</em> pops up and says something.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-4"> <FontAwesomeIcon icon={faUsers} /> Multi Voice and Sound Effects </h1>
|
||||
<h2 className="subtitle is-6">
|
||||
User-selected voices can be strung together in a single reward, interspersed with sound effects.
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div className="content">
|
||||
<h1 className="title is-2"> <FontAwesomeIcon icon={faLightbulb} /> Suggestions? </h1>
|
||||
<h2 className="subtitle is-4">
|
||||
Let us know what to build!
|
||||
</h2>
|
||||
<p>Can you think of anything we haven't
|
||||
provided? <DiscordLink text="Please let us know in Discord" iconAfterText={true} /> so
|
||||
that we can build it!</p>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { TtsConfigsIndexPage }
|
||||
@@ -1,157 +0,0 @@
|
||||
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>,
|
||||
}
|
||||
|
||||
function TtsConfigsReorderPage(props: Props) {
|
||||
const { event_category } = useParams() as { event_category : string };
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
// TODO: Use centralized configs
|
||||
const indexLink = '/tts_configs';
|
||||
|
||||
const [twitchEventRules, setTwitchEventRules] = useState<TwitchEventRule[]>([]);
|
||||
|
||||
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);
|
||||
|
||||
setTwitchEventRules(releventRules);
|
||||
|
||||
} else if (ListTwitchEventRulesIsError(response)) {
|
||||
// TODO
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
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.
|
||||
return false;
|
||||
}
|
||||
|
||||
let aboveIndex = ruleIndex - 1;
|
||||
let aboveRule = newEventRules[aboveIndex];
|
||||
|
||||
newEventRules[aboveIndex] = newEventRules[ruleIndex];
|
||||
newEventRules[ruleIndex] = aboveRule;
|
||||
|
||||
setTwitchEventRules(newEventRules);
|
||||
}
|
||||
|
||||
const handleMoveDown = (ruleIndex: number) => {
|
||||
let newEventRules = [...twitchEventRules];
|
||||
|
||||
if (ruleIndex < 0 || // Invalid
|
||||
ruleIndex >= newEventRules.length - 1) { // Can't move down.
|
||||
return false;
|
||||
}
|
||||
|
||||
let belowIndex = ruleIndex + 1;
|
||||
|
||||
console.log(ruleIndex, belowIndex);
|
||||
|
||||
let belowRule = newEventRules[belowIndex];
|
||||
|
||||
newEventRules[belowIndex] = newEventRules[ruleIndex];
|
||||
newEventRules[ruleIndex] = belowRule;
|
||||
|
||||
setTwitchEventRules(newEventRules);
|
||||
}
|
||||
|
||||
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 = {
|
||||
rule_token_position_pairs: pairs,
|
||||
};
|
||||
|
||||
const result = await ReorderTwitchEventRules(request);
|
||||
if (result.success) {
|
||||
history.push(indexLink);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!props.sessionWrapper.isLoggedIn()) {
|
||||
return <h1>Must Log In</h1>;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
{twitchEventRules.map((rule, index) => {
|
||||
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}
|
||||
>
|
||||
Save Orderings <FontAwesomeIcon icon={faSave} />
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { TtsConfigsReorderPage }
|
||||
@@ -1,247 +0,0 @@
|
||||
import { EventMatchPredicate } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventMatchPredicate";
|
||||
import { CHEER_LOOKUP_MAP, CHEER_PREFIX_TO_STRING_MAP, TwitchCheerPrefix } from "../../../twitch/Cheers";
|
||||
import { CheerUtil } from "../../../twitch/CheerUtil";
|
||||
import { BitsRuleType } from "./types/BitsRuleType";
|
||||
|
||||
// TODO: This needs tests badly.
|
||||
|
||||
export interface CheerStateOfficial {
|
||||
cheerPrefix?: TwitchCheerPrefix,
|
||||
bits?: number,
|
||||
}
|
||||
|
||||
export interface CheerStateCustom {
|
||||
// NB: This can contain a numeric suffix independent of bits.
|
||||
// eg. third party cheers from 7tv.app: Sus11, Sus12, 1984, 4Weird.
|
||||
cheerFull?: string,
|
||||
bits?: number,
|
||||
}
|
||||
|
||||
// Due to the complexity of the forms, this is used as the source of truth while editing.
|
||||
// Each type of predicate can serialize and deserialize into the predicates.
|
||||
export type CheerState = CheerStateOfficial | CheerStateCustom;
|
||||
|
||||
export function CheerStateIsOfficial(cheerState: CheerState): cheerState is CheerStateOfficial {
|
||||
return cheerState.hasOwnProperty('cheerPrefix');
|
||||
}
|
||||
|
||||
export function CheerStateIsCustom(cheerState: CheerState): cheerState is CheerStateCustom {
|
||||
return cheerState.hasOwnProperty('cheerFull');
|
||||
}
|
||||
|
||||
// Convert "exact match" strings into CheerState
|
||||
export function convertExactMatchToCheerState(freeformText: string) : CheerState {
|
||||
const trimmed = freeformText.trim();
|
||||
|
||||
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(trimmed);
|
||||
|
||||
let maybeCheer = CHEER_LOOKUP_MAP.get(cheerPrefix || '');
|
||||
|
||||
if (maybeCheer !== undefined) {
|
||||
return {
|
||||
cheerPrefix: maybeCheer,
|
||||
bits: bitValue,
|
||||
}
|
||||
}
|
||||
|
||||
// NB: In this case, bits value may be wrong.
|
||||
// eg. "1984"
|
||||
return {
|
||||
cheerFull: freeformText,
|
||||
bits: bitValue,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert "prefix + bits" into CheerState
|
||||
export function convertPrefixAndBitsToCheerState(freeformText?: string, bits?: number) : CheerState {
|
||||
const trimmed = !!freeformText ? freeformText.trim() : '';
|
||||
|
||||
let { cheerPrefix, bitValue } = CheerUtil.parseCheerString(trimmed);
|
||||
|
||||
let maybeCheer = CHEER_LOOKUP_MAP.get(cheerPrefix || '');
|
||||
|
||||
// NB: Prefer function args over parsed value since we might be parsing
|
||||
// custom cheers such as "Sus11" or "1984" (7tv.app)
|
||||
let maybeBits = bits || bitValue;
|
||||
|
||||
if (maybeCheer !== undefined) {
|
||||
return {
|
||||
cheerPrefix: maybeCheer,
|
||||
bits: maybeBits,
|
||||
}
|
||||
}
|
||||
|
||||
// NB: In this case, bits value may be wrong.
|
||||
// eg. "1984"
|
||||
return {
|
||||
cheerFull: freeformText,
|
||||
bits: maybeBits,
|
||||
};
|
||||
}
|
||||
|
||||
// Turn a cheer state into a predicate
|
||||
export function predicateToCheerState(predicate: EventMatchPredicate) : CheerState
|
||||
{
|
||||
if (!!predicate.bits_cheermote_name_exact_match) {
|
||||
return convertExactMatchToCheerState(predicate.bits_cheermote_name_exact_match.cheermote_name)
|
||||
} else if (!!predicate.bits_cheermote_prefix_spend_threshold) {
|
||||
return convertPrefixAndBitsToCheerState(
|
||||
predicate.bits_cheermote_prefix_spend_threshold.cheermote_prefix,
|
||||
predicate.bits_cheermote_prefix_spend_threshold.minimum_bits_spent,
|
||||
);
|
||||
} else if (!!predicate.bits_spend_threshold) {
|
||||
return {
|
||||
bits: predicate.bits_spend_threshold.minimum_bits_spent,
|
||||
}
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Turn a cheer state into a predicate
|
||||
export function cheerStateToPredicate(
|
||||
cheerState: CheerState,
|
||||
bitsRuleType: BitsRuleType) : EventMatchPredicate
|
||||
{
|
||||
let predicate : EventMatchPredicate = {};
|
||||
|
||||
switch (bitsRuleType) {
|
||||
case BitsRuleType.BitsCheermoteNameExactMatch:
|
||||
let cheermoteName = '';
|
||||
if (CheerStateIsOfficial(cheerState)) {
|
||||
let cheerPrefix = '';
|
||||
let bitsValue = '';
|
||||
if (!!cheerState.cheerPrefix) {
|
||||
cheerPrefix = CHEER_PREFIX_TO_STRING_MAP.get(cheerState.cheerPrefix) || '';
|
||||
}
|
||||
if (!!cheerState.bits && !isNaN(cheerState.bits) && cheerState.bits > 1) {
|
||||
bitsValue = cheerState.bits.toString();
|
||||
}
|
||||
|
||||
if (!!cheerPrefix) {
|
||||
// Don't bother setting unless we have a prefix
|
||||
cheermoteName = `${cheerPrefix}${bitsValue}`
|
||||
}
|
||||
|
||||
} else if (CheerStateIsCustom(cheerState)) {
|
||||
// Cheer name is the full value, eg. 'ZomboCom1000'
|
||||
cheermoteName = cheerState.cheerFull || '';
|
||||
}
|
||||
|
||||
predicate.bits_cheermote_name_exact_match = {
|
||||
cheermote_name: cheermoteName,
|
||||
}
|
||||
break;
|
||||
|
||||
case BitsRuleType.BitsCheermotePrefixSpendThreshold:
|
||||
let newBitsValue = 1;
|
||||
if (!!cheerState.bits && !isNaN(cheerState.bits) && cheerState.bits > 1) {
|
||||
newBitsValue = cheerState.bits;
|
||||
}
|
||||
|
||||
let newPrefix = '';
|
||||
if (CheerStateIsOfficial(cheerState)) {
|
||||
if (!!cheerState.cheerPrefix) {
|
||||
newPrefix = CHEER_PREFIX_TO_STRING_MAP.get(cheerState.cheerPrefix) || '';
|
||||
}
|
||||
} else if (CheerStateIsCustom(cheerState)) {
|
||||
newPrefix = cheerState.cheerFull || '';
|
||||
}
|
||||
|
||||
predicate.bits_cheermote_prefix_spend_threshold = {
|
||||
cheermote_prefix: newPrefix,
|
||||
minimum_bits_spent: newBitsValue,
|
||||
}
|
||||
break;
|
||||
|
||||
case BitsRuleType.BitsSpendThreshold:
|
||||
let newBits = 1;
|
||||
if (!!cheerState.bits && !isNaN(cheerState.bits) && cheerState.bits > 1) {
|
||||
newBits = cheerState.bits;
|
||||
}
|
||||
predicate.bits_spend_threshold = {
|
||||
minimum_bits_spent: newBits,
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return predicate;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
Truth table-ish breakdown (incomplete).
|
||||
|
||||
"Corgo" is an official cheer
|
||||
"Zombo", "Sus12", and "1984" are custom cheers
|
||||
|
||||
------------------------------------------------------------------------------------------------
|
||||
|
||||
Backend Re-serialize to backend
|
||||
|
||||
Bits 1000 => CheerState { bits: 1000 }
|
||||
|
||||
ExactMatch Corgo100 => CheerState { official: Corgo, bits: 100 } => ExactMatch Corgo100
|
||||
ExactMatch Zombo100 => CheerState { full: Zombo100, bits: 100 } => ExactMatch Zombo100 (from "full")
|
||||
|
||||
PrefixBits (Corgo, 100) => CheerState { official: Corgo, bits: 100 } => PrefixBits (Corgo, 100)
|
||||
PrefixBits (Corgo, 123) => CheerState { official: Corgo, bits: 123 } => PrefixBits (Corgo, 123)
|
||||
PrefixBits (Zombo, 123) => CheerState { full: Zombo, bits: 123 } => PrefixBits (Zombo, 123)
|
||||
|
||||
------------------------------------------------------------------------------------------------
|
||||
|
||||
Backend To another [view type] w/ UI state
|
||||
|
||||
Bits 1000 => CheerState { bits: 1000 } => [ExactMatch] PrefixSelect: (n/a) BitSelect: 1000 FreeForm: (n/a)
|
||||
Bits 1234 => CheerState { bits: 1234 } => [ExactMatch] PrefixSelect: (n/a) BitSelect: (n/a) FreeForm: (n/a) [CANNOT POPULATE]
|
||||
|
||||
Bits 1000 => CheerState { bits: 1000 } => [PrefixBits] PefixSelect: (n/a) FreeForm: (n/a) Bits: 1000
|
||||
Bits 1234 => CheerState { bits: 1234 } => [PrefixBits] PefixSelect: (n/a) FreeForm: (n/a) Bits: 1234
|
||||
|
||||
ExactMatch Corgo100 => CheerState { official: Corgo, bits: 100 } => [ExactMatch] PrefixSelect: Corgo BitSelect: 100 FreeForm: Corgo100
|
||||
ExactMatch Zombo100 => CheerState { full: Zombo100, bits: 100 } => [ExactMatch] PrefixSelect: (n/a) BitSelect: 100 FreeForm: Zombo100
|
||||
ExactMatch Corgo100 => CheerState { official: Corgo, bits: 100 } => [Bits] BitsSelect: 100 BitsFreeForm: 100
|
||||
*) ExactMatch Corgo100 => CheerState { official: Corgo, bits: 100 } => [PrefixBits] PrefixSelect: Corgo FreeForm: Corgo Bits: 100
|
||||
*) ExactMatch Zombo100 => CheerState { full: Zombo100, bits: 100 } => [PrefixBits] PrefixSelect: (n/a) FreeForm: Zombo100 Bits: 100
|
||||
|
||||
PrefixBits (Corgo, 100) => CheerState { official: Corgo, bits: 100 } => PrefixBits (Corgo, 100)
|
||||
PrefixBits (Corgo, 123) => CheerState { official: Corgo, bits: 123 } => PrefixBits (Corgo, 123)
|
||||
PrefixBits (Zombo, 123) => CheerState { full: Zombo, bits: 123 } => PrefixBits (Zombo, 123)
|
||||
|
||||
# Notably, prefix is usually always "full":
|
||||
PrefixBits (1884, 1000) => CheerState { full: 1984, bits: 1000 } => [PrefixBits] PrefixSelect: (n/a) FreeForm: 1984 Bits: 1000
|
||||
PrefixBits (Sus12, 1000) => CheerState { full: Sus12, bits: 1000 } => [PrefixBits] PrefixSelect: (n/a) FreeForm: Sus12 Bits: 1000
|
||||
PrefixBits (Corgo1000, 5000) => CheerState { full: Corgo1000, bits: 5000} => [PrefixBits] PrefixSelect: (n/a) FreeForm: Corgo1000 Bits: 5000
|
||||
|
||||
PrefixBits (1884, 1000) => CheerState { full: 1984, bits: 1000 } => [ExactMatch] PrefixSelect: (n/a) FreeForm: 1984 Bits: 1000
|
||||
PrefixBits (Sus12, 1000) => CheerState { full: Sus12, bits: 1000 } => [ExactMatch] PrefixSelect: (n/a) FreeForm: Sus12 Bits: 1000
|
||||
PrefixBits (Corgo1000, 5000) => CheerState { full: Corgo1000, bits: 5000} => [ExactMatch] PrefixSelect: (Corgo) FreeForm: Corgo1000 Bits: 1000 (*ignore bits*)
|
||||
|
||||
|
||||
# Unless it is *only* an official prefix
|
||||
PrefixBits (Corgo, 5000) => CheerState { official: Corgo, bits: 5000} => [PrefixBits] PrefixSelect: Corgo FreeForm: Corgo Bits: 5000
|
||||
PrefixBits (Corgo, 5000) => CheerState { official: Corgo, bits: 5000} => [ExactMatch] PrefixSelect: Corgo FreeForm: Corgo5000
|
||||
|
||||
|
||||
------------------------------------------------------------------------------------------------
|
||||
|
||||
CheerState UI CheerState
|
||||
|
||||
CheerState { official: Corgo, bits: 5000} => [Bits] BitsSelect: 5000, BitsFreeForm: 5000 => CheerState { official: (retain), bits: 5000 }
|
||||
CheerState { official: Corgo, bits: 5000} => [PrefixBits] PrefixSelect: Corgo FreeForm: Corgo Bits: 5000 => CheerState { official: Corgo, bits: 5000 }
|
||||
CheerState { official: Corgo, bits: 5000} => [ExactMatch] PrefixSelect: Corgo FreeForm: Corgo5000 => CheerState { official: Corgo, bits: 5000 } [split freeform]
|
||||
|
||||
CheerState { full: Zombo, bits: 5000} => [Bits] BitsSelect: 5000, BitsFreeForm: 5000
|
||||
CheerState { full: Zombo, bits: 5000} => [PrefixBits] PrefixSelect: (n/a) FreeForm: Zombo Bits: 5000
|
||||
CheerState { full: Zombo, bits: 5000} => [ExactMatch] PrefixSelect: (n/a) FreeForm: Zombo5000
|
||||
|
||||
CheerState { full: Zombo, bits: 5000} => [Bits] BitsSelect: 5000, BitsFreeForm: 5000
|
||||
CheerState { full: Zombo, bits: 5000} => [Bits] BitsSelect: 5000, BitsFreeForm: 5000
|
||||
|
||||
CheerState { full: 1984, bits: 5000} => [PrefixBits] PrefixSelect: Corgo FreeForm: Corgo Bits: 5000
|
||||
CheerState { full: Sus12, bits: 5000} => [ExactMatch] PrefixSelect: Corgo FreeForm: Corgo5000
|
||||
CheerState { full: Corgo1000, bits: 5000} => [ExactMatch] PrefixSelect: Corgo FreeForm: Corgo5000
|
||||
|
||||
|
||||
*/
|
||||
@@ -1,311 +0,0 @@
|
||||
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
|
||||
// values that don't actually come from the server.
|
||||
|
||||
interface EventMatchPredicateBuilderComponentProps {
|
||||
// CANNOT BE CHANGED AFTER CREATION
|
||||
twitchEventCategory: TwitchEventCategory,
|
||||
|
||||
// Original event match predicate from the server
|
||||
// (also immutable for sanity and because we lose information on UI changes)
|
||||
serverEventMatchPredicate: EventMatchPredicate,
|
||||
|
||||
// Updates sent back up the tree
|
||||
updateModifiedEventMatchPredicate: (predicate: EventMatchPredicate) => void,
|
||||
};
|
||||
|
||||
function EventMatchPredicateBuilderComponent(props: EventMatchPredicateBuilderComponentProps) {
|
||||
// ========== Core UI flow ==========
|
||||
|
||||
const [bitsRuleType, setBitsRuleType] = useState<BitsRuleType>(BitsRuleType.BitsCheermoteNameExactMatch);
|
||||
const [channelPointsRuleType, setChannelPointsRuleType] = useState<ChannelPointsRuleType>(ChannelPointsRuleType.ChannelPointsRewardNameExactMatch);
|
||||
|
||||
// ========== Cached Values for Editing ==========
|
||||
|
||||
// A very complicated container that juggles bits and cheermote state.
|
||||
// The state transitions are complicated, and I've tried to document as best as possible.
|
||||
// TODO: This really needs some testing.
|
||||
const [cheerState, setCheerState] = useState<CheerState>({});
|
||||
|
||||
// Used in:
|
||||
// ChannelPointsRewardNameExactMatch
|
||||
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 newCheerState = predicateToCheerState(props.serverEventMatchPredicate);
|
||||
|
||||
switch (props.twitchEventCategory) {
|
||||
case TwitchEventCategory.Bits:
|
||||
if (!!props.serverEventMatchPredicate.bits_cheermote_name_exact_match) {
|
||||
newBitsRuleType = BitsRuleType.BitsCheermoteNameExactMatch;
|
||||
} 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;
|
||||
}
|
||||
break;
|
||||
case TwitchEventCategory.ChatCommand: // TODO: Not yet supported
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
setCheerState(newCheerState);
|
||||
|
||||
setBitsRuleType(newBitsRuleType);
|
||||
setChannelPointsRuleType(newChannelPointsRuleType);
|
||||
|
||||
}, [props.twitchEventCategory, props.serverEventMatchPredicate]);
|
||||
|
||||
// TODO: We need to recalculate the model.
|
||||
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 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 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 { cheerPrefix, bitValue } = CheerUtil.parseCheerString(name);
|
||||
|
||||
let newBitValue = bitValue;
|
||||
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.
|
||||
// We don't want to repalace with 13.
|
||||
if (CheerStateIsCustom(cheerState)) {
|
||||
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)
|
||||
{
|
||||
// Keep the old value.
|
||||
newBitValue = cheerState.bits;
|
||||
}
|
||||
}
|
||||
|
||||
newCheerState = {
|
||||
cheerFull: name, // Should always be the full value
|
||||
bits: newBitValue,
|
||||
}
|
||||
}
|
||||
|
||||
let predicate = cheerStateToPredicate(newCheerState, bitsRuleType);
|
||||
|
||||
setCheerState(newCheerState);
|
||||
props.updateModifiedEventMatchPredicate(predicate);
|
||||
}
|
||||
|
||||
const handleChangedMinimumBitsSpent = (minimumSpent: number) => {
|
||||
// 1) Update Cheer State
|
||||
// 2) Update NewEventMatchPredicate
|
||||
let newCheerState = {};
|
||||
|
||||
if (CheerStateIsOfficial(cheerState)) {
|
||||
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 cheerFull = cheerState.cheerFull;
|
||||
|
||||
if (!!cheerPrefix && !!bitValue && bitValue === cheerState.bits) {
|
||||
// Let's replace the cheer.
|
||||
cheerFull = CheerUtil.joinCheerAndPrefix(cheerPrefix, minimumSpent);
|
||||
}
|
||||
|
||||
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 = {};
|
||||
|
||||
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 = <></>
|
||||
|
||||
if (props.twitchEventCategory === TwitchEventCategory.Bits) {
|
||||
ruleTypeSelect = (
|
||||
<>
|
||||
<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>
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
|
||||
switch (bitsRuleType) {
|
||||
case BitsRuleType.BitsCheermoteNameExactMatch:
|
||||
matchingRulesForm = <BitsCheermoteNameExactMatchForm
|
||||
cheerState={cheerState}
|
||||
updateCheerName={handleChangedCheerName}
|
||||
/>;
|
||||
break;
|
||||
case BitsRuleType.BitsCheermotePrefixSpendThreshold:
|
||||
matchingRulesForm = <BitsCheermotePrefixSpendThresholdForm
|
||||
cheerState={cheerState}
|
||||
updateCheerPrefix={handleChangedCheerPrefix}
|
||||
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
|
||||
/>
|
||||
break;
|
||||
case BitsRuleType.BitsSpendThreshold:
|
||||
matchingRulesForm = <BitsSpendThresholdForm
|
||||
cheerState={cheerState}
|
||||
updateMinimumBitsSpent={handleChangedMinimumBitsSpent}
|
||||
/>;
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (props.twitchEventCategory === TwitchEventCategory.ChannelPoints) {
|
||||
ruleTypeSelect = (
|
||||
<>
|
||||
<select
|
||||
value={channelPointsRuleType}
|
||||
onChange={handleChangedChannelPointsRuleType}>
|
||||
<option
|
||||
value={ChannelPointsRuleType.ChannelPointsRewardNameExactMatch}
|
||||
>Channel Points Reward Name (exact match)</option>
|
||||
</select>
|
||||
</>
|
||||
);
|
||||
|
||||
matchingRulesForm = <ChannelPointsRewardNameExactMatchForm
|
||||
rewardName={rewardName}
|
||||
updateRewardName={handleChangedRewardName}
|
||||
/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="title is-4">1) Pick what to match on</h2>
|
||||
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<div className="select is-medium is-fullwidth">
|
||||
{ruleTypeSelect}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 className="title is-4">2) Configure the matching</h2>
|
||||
|
||||
{matchingRulesForm}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { EventMatchPredicateBuilderComponent }
|
||||
@@ -1,169 +0,0 @@
|
||||
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,
|
||||
};
|
||||
|
||||
function BitsCheermoteNameExactMatchForm(props: BitsCheermoteNameExactMatchProps) {
|
||||
// The two dropdowns
|
||||
const [cheerPrefix, setCheerPrefix] = useState<string>('');
|
||||
const [bitsValue, setBitsValue] = useState<number>(1);
|
||||
// The freeform text input
|
||||
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 newBits = 1;
|
||||
let hasBits = false;
|
||||
|
||||
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)) || '';
|
||||
}
|
||||
|
||||
if (hasBits) {
|
||||
newFullName = `${newPrefix}${newBits}`;
|
||||
} else {
|
||||
newFullName = newPrefix;
|
||||
}
|
||||
|
||||
} else if (CheerStateIsCustom(props.cheerState)) {
|
||||
newFullName = props.cheerState.cheerFull || '';
|
||||
}
|
||||
|
||||
setCheerPrefix(newPrefix);
|
||||
setBitsValue(newBits);
|
||||
setManualCheerValue(newFullName);
|
||||
|
||||
}, [props.cheerState]);
|
||||
|
||||
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 value = (ev.target as HTMLSelectElement).value;
|
||||
const numericValue = parseInt(value);
|
||||
if (!isNaN(numericValue)) {
|
||||
// NB: The form shouldn't yield NaN, but just in case.
|
||||
setBitsValue(numericValue);
|
||||
recalcuateFieldValue(cheerPrefix, numericValue);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if (!!matches && matches.length > 1) {
|
||||
setCheerPrefix(matches[1]); // First match group
|
||||
if (matches.length === 3 && matches[2] !== undefined) {
|
||||
// NB: Second match group can be 'undefined' if no number is present. (Zero-width matching?)
|
||||
const maybeBits = parseInt(matches[2]);
|
||||
if (!isNaN(maybeBits)) {
|
||||
setBitsValue(maybeBits); // Second match group
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<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>
|
||||
<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">
|
||||
<FontAwesomeIcon icon={faGem} />
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { BitsCheermoteNameExactMatchForm }
|
||||
@@ -1,195 +0,0 @@
|
||||
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,
|
||||
|
||||
updateCheerPrefix: (cheerNameOrPrefix: string) => void,
|
||||
updateMinimumBitsSpent: (minimumSpent: number) => void,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// This handles initialization after mount.
|
||||
// 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 newCustomPrefix = '';
|
||||
let newBits = 1;
|
||||
let newCustomBits = 1;
|
||||
|
||||
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)) || '';
|
||||
}
|
||||
} else if (CheerStateIsCustom(props.cheerState)) {
|
||||
newCustomPrefix = props.cheerState.cheerFull || '';
|
||||
}
|
||||
|
||||
setCheerPrefix(newPrefix);
|
||||
setCustomCheerPrefix(newCustomPrefix);
|
||||
setBitsValue(newBits);
|
||||
setCustomBitsValue(newCustomBits);
|
||||
|
||||
}, [props.cheerState]);
|
||||
|
||||
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 value = (ev.target as HTMLInputElement).value;
|
||||
setCheerPrefix(value);
|
||||
setCustomCheerPrefix(value);
|
||||
props.updateCheerPrefix(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
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 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>
|
||||
|
||||
<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>
|
||||
<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={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>
|
||||
</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">
|
||||
<FontAwesomeIcon icon={faGem} />
|
||||
</span>
|
||||
</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 { BitsCheermotePrefixSpendThresholdForm }
|
||||
@@ -1,110 +0,0 @@
|
||||
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,
|
||||
};
|
||||
|
||||
function BitsSpendThresholdForm(props: BitsSpendThresholdFormProps) {
|
||||
// The dropdown
|
||||
const [bitsValue, setBitsValue] = useState<number>(1);
|
||||
// The freeform text input
|
||||
const [customAmount, setCustomAmount] = useState<number>(1);
|
||||
|
||||
// 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(() => {
|
||||
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 value = (ev.target as HTMLSelectElement).value;
|
||||
const numericValue = parseInt(value);
|
||||
|
||||
if (!isNaN(numericValue) && numericValue > 0) {
|
||||
// Only update on valid positive integers
|
||||
setBitsValue(numericValue);
|
||||
setCustomAmount(numericValue);
|
||||
props.updateMinimumBitsSpent(numericValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const handleCustomAmountUpdate = (ev: React.FormEvent<HTMLInputElement>) : boolean => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
const numericValue = parseInt(value);
|
||||
|
||||
if (!isNaN(numericValue) && numericValue > 0) {
|
||||
// Only update on valid positive integers
|
||||
setBitsValue(numericValue);
|
||||
setCustomAmount(numericValue);
|
||||
props.updateMinimumBitsSpent(numericValue);
|
||||
}
|
||||
|
||||
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>
|
||||
<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">
|
||||
<FontAwesomeIcon icon={faGem} />
|
||||
</span>
|
||||
</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 }
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface ChannelPointsRewardNameExactMatchProps {
|
||||
rewardName: string,
|
||||
updateRewardName: (name: string) => void,
|
||||
};
|
||||
|
||||
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:
|
||||
// https://stackoverflow.com/a/54866051 (less clear by also using useState(), but good comments)
|
||||
// https://stackoverflow.com/a/62982753
|
||||
useEffect(() => {
|
||||
setRewardName(props.rewardName);
|
||||
}, [props.rewardName]);
|
||||
|
||||
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
|
||||
value={rewardName}
|
||||
onChange={updateRewardName}
|
||||
className="input is-medium"
|
||||
type="text"
|
||||
placeholder="Reward Name" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { ChannelPointsRewardNameExactMatchForm }
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
// For bits / cheermotes
|
||||
export enum BitsRuleType {
|
||||
BitsCheermoteNameExactMatch = "bits_cheermote_name_exact_match",
|
||||
BitsCheermotePrefixSpendThreshold = "bits_cheermote_prefix_spend_threshold",
|
||||
BitsSpendThreshold = "bits_spend_threshold",
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
// For channel points
|
||||
export enum ChannelPointsRuleType {
|
||||
ChannelPointsRewardNameExactMatch = 'channel_points_reward_name_exact_match',
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { EventResponse } from "@storyteller/components/src/api/storyteller/twitch_event_rules/shared/EventResponse";
|
||||
import { EventResponseType } from "./EventResponseType";
|
||||
|
||||
export function buildEventResponse(ttsModelTokens: string[], eventResponseType: EventResponseType) :EventResponse {
|
||||
let eventResponse : EventResponse = {};
|
||||
|
||||
switch (eventResponseType) {
|
||||
case EventResponseType.TtsSingleVoice:
|
||||
let ttsModelToken = '';
|
||||
if (ttsModelTokens.length > 0) {
|
||||
ttsModelToken = ttsModelTokens[0];
|
||||
}
|
||||
eventResponse.tts_single_voice = {
|
||||
tts_model_token: ttsModelToken,
|
||||
}
|
||||
break;
|
||||
case EventResponseType.TtsRandomVoice:
|
||||
eventResponse.tts_random_voice= {
|
||||
tts_model_tokens: [...ttsModelTokens],
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return eventResponse;
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
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
|
||||
// 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,
|
||||
|
||||
// Updates sent back up the tree
|
||||
updateModifiedEventResponse: (response: EventResponse) => void,
|
||||
|
||||
// FakeYou voices
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
};
|
||||
|
||||
function EventResponseComponent(props: EventResponseComponentProps) {
|
||||
// ========== Core UI flow ==========
|
||||
|
||||
const [eventResponseType, setEventResponseType] = useState<EventResponseType>(EventResponseType.TtsSingleVoice);
|
||||
|
||||
// ========== Cached Values for Editing ==========
|
||||
|
||||
const [selectedTtsModelTokens, setSelectedTtsModelTokens] = 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 newEventResponseType = EventResponseType.TtsSingleVoice;
|
||||
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)
|
||||
}
|
||||
|
||||
} 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];
|
||||
}
|
||||
}
|
||||
|
||||
setEventResponseType(newEventResponseType);
|
||||
setSelectedTtsModelTokens(modelTokens);
|
||||
|
||||
}, [props.serverEventResponse]);
|
||||
|
||||
const handleChangedEventResponseType = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
const newResponseType = value as EventResponseType;
|
||||
|
||||
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
|
||||
// the "random voices" view, etc.
|
||||
let updatedTokens = [...selectedTtsModelTokens];
|
||||
|
||||
if (updatedTokens.length > 0) {
|
||||
updatedTokens[0] = token;
|
||||
} else if (updatedTokens.length === 0) {
|
||||
updatedTokens.push(token);
|
||||
}
|
||||
|
||||
const newEventResponse = buildEventResponse(updatedTokens, eventResponseType);
|
||||
|
||||
setSelectedTtsModelTokens(updatedTokens);
|
||||
props.updateModifiedEventResponse(newEventResponse);
|
||||
}
|
||||
|
||||
const handleChangedTtsModelTokens = (tokens: string[]) => {
|
||||
let updatedTokens = [...tokens];
|
||||
|
||||
const newEventResponse = buildEventResponse(updatedTokens, eventResponseType);
|
||||
|
||||
setSelectedTtsModelTokens(updatedTokens);
|
||||
props.updateModifiedEventResponse(newEventResponse);
|
||||
}
|
||||
|
||||
let responseForm = <></>
|
||||
|
||||
switch (eventResponseType) {
|
||||
case EventResponseType.TtsSingleVoice:
|
||||
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}
|
||||
/>
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="title is-4">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
|
||||
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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h2 className="title is-4">4) Configure the response</h2>
|
||||
|
||||
{responseForm}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { EventResponseComponent }
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
export enum EventResponseType {
|
||||
TtsSingleVoice = "tts_single_voice",
|
||||
TtsRandomVoice = "tts_random_voice",
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
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,
|
||||
|
||||
// FakeYou voices
|
||||
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]);
|
||||
|
||||
const handleAddVoice = (ev: React.FormEvent<HTMLButtonElement>) : boolean => {
|
||||
ev.preventDefault();
|
||||
|
||||
let newModelTokens = [...props.selectedTtsModelTokens];
|
||||
|
||||
newModelTokens.push('');
|
||||
|
||||
props.updateSelectedTtsModelTokens(newModelTokens);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const handleModelSelect = (ttsModelToken: string, voiceIndex: number) => {
|
||||
let newModelTokens = [...props.selectedTtsModelTokens];
|
||||
|
||||
if (voiceIndex >= newModelTokens.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
newModelTokens[voiceIndex] = ttsModelToken;
|
||||
props.updateSelectedTtsModelTokens(newModelTokens);
|
||||
}
|
||||
|
||||
const handleModelDelete = (voiceIndex: number) => {
|
||||
let newModelTokens = [...props.selectedTtsModelTokens];
|
||||
|
||||
if (newModelTokens.length <= 1 || voiceIndex >= newModelTokens.length) {
|
||||
// We must have at least one item in the list
|
||||
return;
|
||||
}
|
||||
|
||||
newModelTokens.splice(voiceIndex, 1);
|
||||
|
||||
props.updateSelectedTtsModelTokens(newModelTokens);
|
||||
}
|
||||
|
||||
const deleteDisabled = props.selectedTtsModelTokens.length < 2;
|
||||
|
||||
let selectBoxes : JSX.Element[] = [];
|
||||
|
||||
// No tokens - show one box (unselected)
|
||||
// One or more tokens - show boxes for each (with selections)
|
||||
|
||||
props.selectedTtsModelTokens.forEach((token, index) => {
|
||||
selectBoxes.push(
|
||||
<VoiceDropdown
|
||||
key={index}
|
||||
selectedTtsModelToken={token}
|
||||
voiceIndex={index}
|
||||
handleModelSelect={handleModelSelect}
|
||||
handleModelDelete={handleModelDelete}
|
||||
deleteDisabled={deleteDisabled}
|
||||
allTtsModels={props.allTtsModels}
|
||||
allTtsModelsByToken={props.allTtsModelsByToken}
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
if (selectBoxes.length === 0) {
|
||||
selectBoxes.push(
|
||||
<VoiceDropdown
|
||||
key={0}
|
||||
selectedTtsModelToken={""}
|
||||
voiceIndex={0}
|
||||
handleModelSelect={handleModelSelect}
|
||||
handleModelDelete={handleModelDelete}
|
||||
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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface VoiceDropdownProps {
|
||||
// Actual token or empty string
|
||||
selectedTtsModelToken: string,
|
||||
|
||||
// Position in the list, 0-indexed
|
||||
voiceIndex: number,
|
||||
|
||||
// Callbacks
|
||||
handleModelSelect: (token: string, index: number) => void,
|
||||
handleModelDelete: (index: number) => void,
|
||||
|
||||
// Whether to disable the delete button
|
||||
deleteDisabled: boolean,
|
||||
|
||||
// FakeYou voices
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
}
|
||||
|
||||
function VoiceDropdown(props: VoiceDropdownProps) {
|
||||
|
||||
const handleSelectChange = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const maybeModelToken = (ev.target as HTMLSelectElement).value;
|
||||
|
||||
if (!maybeModelToken) {
|
||||
// Clear selection
|
||||
props.handleModelSelect(maybeModelToken, props.voiceIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!!maybeModelToken && !props.allTtsModelsByToken.has(maybeModelToken)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
props.handleModelSelect(maybeModelToken, props.voiceIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
<div className="control">
|
||||
<button
|
||||
onClick={handleRemoveClick}
|
||||
className="button is-medium is-danger is-inverted"
|
||||
disabled={props.deleteDisabled}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} /> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { TtsRandomVoiceForm }
|
||||
@@ -1,75 +0,0 @@
|
||||
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,
|
||||
|
||||
// FakeYou voices
|
||||
allTtsModels: TtsModelListItem[],
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
};
|
||||
|
||||
function TtsSingleVoiceForm(props: TtsSingleVoiceFormProps) {
|
||||
const [ttsModelToken, setTtsModelToken] = 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(() => {
|
||||
if (props.selectedTtsModelTokens.length > 0) {
|
||||
setTtsModelToken(props.selectedTtsModelTokens[0]);
|
||||
}
|
||||
}, [props.selectedTtsModelTokens]);
|
||||
|
||||
const handleModelSelect = (ev: React.FormEvent<HTMLSelectElement>) : boolean => {
|
||||
const value = (ev.target as HTMLSelectElement).value;
|
||||
|
||||
if (!!value && !props.allTtsModelsByToken.has(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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>
|
||||
</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 }
|
||||
@@ -1,127 +0,0 @@
|
||||
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,
|
||||
|
||||
// Index of the rule in the parents' list
|
||||
ruleIndex: number,
|
||||
|
||||
// Update callbacks
|
||||
handleMoveUp: (index: number) => void,
|
||||
handleMoveDown: (index: number) => void,
|
||||
|
||||
// FakeYou voices
|
||||
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) {
|
||||
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
|
||||
</>
|
||||
);
|
||||
} else if (props.rule.event_match_predicate.bits_spend_threshold !== undefined) {
|
||||
subtitle = (
|
||||
<>
|
||||
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}"
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.rule.event_response.tts_single_voice !== undefined) {
|
||||
const token = props.rule.event_response.tts_single_voice.tts_model_token;
|
||||
|
||||
let model = props.allTtsModelsByToken.get(token);
|
||||
let modelName = token;
|
||||
|
||||
if (!!model) {
|
||||
modelName = model.title;
|
||||
}
|
||||
|
||||
let link = `https://fakeyou.com/tts/${token}`;
|
||||
|
||||
description = (
|
||||
<>
|
||||
TTS with voice: {modelName} <a href={link} target="_blank" rel="noreferrer"><FontAwesomeIcon icon={faExternalLinkAlt} /></a>
|
||||
</>
|
||||
);
|
||||
} else if (props.rule.event_response.tts_random_voice !== undefined) {
|
||||
const tokens = props.rule.event_response.tts_random_voice.tts_model_tokens;
|
||||
|
||||
let modelNameAndLinks = tokens.map((token, index) => {
|
||||
let model = props.allTtsModelsByToken.get(token);
|
||||
let link = `https://fakeyou.com/tts/${token}`;
|
||||
|
||||
let modelName = token;
|
||||
if (!!model) {
|
||||
modelName = model.title;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={props.rule.token}>
|
||||
<div className="card">
|
||||
<div className="card-content">
|
||||
<div className="title is-5">
|
||||
{subtitle}
|
||||
</div>
|
||||
<div>
|
||||
{description}
|
||||
</div>
|
||||
</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 }
|
||||
@@ -1,134 +0,0 @@
|
||||
import React from 'react';
|
||||
import { TwitchEventRule } from '@storyteller/components/src/api/storyteller/twitch_event_rules/ListTwitchEventRules';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEdit, faExternalLinkAlt, faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { TtsModelListItem } from '@storyteller/components/src/api/tts/ListTtsModels';
|
||||
|
||||
interface Props {
|
||||
rule: TwitchEventRule,
|
||||
allTtsModelsByToken: Map<string, TtsModelListItem>,
|
||||
hideButtons?: boolean,
|
||||
}
|
||||
|
||||
function TwitchEventRuleElement(props: Props) {
|
||||
const hideButtons = !!props.hideButtons;
|
||||
|
||||
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) {
|
||||
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
|
||||
</>
|
||||
);
|
||||
} else if (props.rule.event_match_predicate.bits_spend_threshold !== undefined) {
|
||||
subtitle = (
|
||||
<>
|
||||
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}"
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.rule.event_response.tts_single_voice !== undefined) {
|
||||
const token = props.rule.event_response.tts_single_voice.tts_model_token;
|
||||
|
||||
let model = props.allTtsModelsByToken.get(token);
|
||||
let modelName = token;
|
||||
|
||||
if (!!model) {
|
||||
modelName = model.title;
|
||||
}
|
||||
|
||||
let link = `https://fakeyou.com/tts/${token}`;
|
||||
|
||||
description = (
|
||||
<>
|
||||
TTS with voice: {modelName} <a href={link} target="_blank" rel="noreferrer"><FontAwesomeIcon icon={faExternalLinkAlt} /></a>
|
||||
</>
|
||||
);
|
||||
} else if (props.rule.event_response.tts_random_voice !== undefined) {
|
||||
const tokens = props.rule.event_response.tts_random_voice.tts_model_tokens;
|
||||
|
||||
let modelNameAndLinks = tokens.map((token, index) => {
|
||||
let model = props.allTtsModelsByToken.get(token);
|
||||
let link = `https://fakeyou.com/tts/${token}`;
|
||||
|
||||
let modelName = token;
|
||||
if (!!model) {
|
||||
modelName = model.title;
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const editUrl = `/tts_configs/edit/${props.rule.token}`;
|
||||
const deleteUrl = `/tts_configs/delete/${props.rule.token}`;
|
||||
|
||||
let buttons = <></>;
|
||||
if (!hideButtons) {
|
||||
buttons = (
|
||||
<>
|
||||
<footer className="card-footer">
|
||||
<div className="card-footer-item">
|
||||
<span>
|
||||
<Link to={editUrl}>
|
||||
<FontAwesomeIcon icon={faEdit} /> Edit
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-footer-item">
|
||||
<span>
|
||||
<Link to={deleteUrl}>
|
||||
<FontAwesomeIcon icon={faTrash} /> Delete
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={props.rule.token}>
|
||||
<div className="card">
|
||||
<div className="card-content">
|
||||
<div className="title is-5">
|
||||
{subtitle}
|
||||
</div>
|
||||
<div>
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
{buttons}
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { TwitchEventRuleElement }
|
||||
@@ -1 +0,0 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
@@ -1,5 +0,0 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
const CHEER_REGEX = /^([A-Za-z]+)(\d+)?$/;
|
||||
|
||||
interface ParsedCheer {
|
||||
cheerPrefix?: string,
|
||||
bitValue?: number,
|
||||
}
|
||||
|
||||
export class CheerUtil {
|
||||
|
||||
public static parseCheerString(cheerString: string) : ParsedCheer {
|
||||
const matches = cheerString.trim().match(CHEER_REGEX)
|
||||
|
||||
let cheerPrefix = undefined;
|
||||
let bitValue = undefined;
|
||||
|
||||
if (!!matches && matches.length > 1) {
|
||||
cheerPrefix = matches[1];
|
||||
if (matches.length === 3 && matches[2] !== undefined) {
|
||||
// NB(1): The second match group can be 'undefined' if no number is present. (Optional matching.)
|
||||
let maybeBits = parseInt(matches[2]);
|
||||
if (!isNaN(maybeBits) && maybeBits > 0) {
|
||||
bitValue = maybeBits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cheerPrefix: cheerPrefix,
|
||||
bitValue: bitValue,
|
||||
};
|
||||
}
|
||||
|
||||
public static joinCheerAndPrefix(cheerPrefix?: string, bitValue?: number) : string {
|
||||
return `${cheerPrefix || ''}${bitValue || ''}`;
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
|
||||
export enum TwitchCheerPrefix {
|
||||
Cheer = "Cheer",
|
||||
DoodleCheer = "DoodleCheer",
|
||||
BibleThump = "BibleThump",
|
||||
Cheerwhal = "cheerwhal",
|
||||
Corgo = "Corgo",
|
||||
Uni = "uni",
|
||||
ShowLove = "ShowLove",
|
||||
Party = "Party",
|
||||
SeemsGood = "SeemsGood",
|
||||
Pride = "Pride",
|
||||
Kappa = "Kappa",
|
||||
FrankerZ = "FrankerZ",
|
||||
HeyGuys = "HeyGuys",
|
||||
DansGame = "DansGame",
|
||||
EleGiggle = "EleGiggle",
|
||||
TriHard = "TriHard",
|
||||
Kreygasm = "Kreygasm",
|
||||
FourHead = "4Head",
|
||||
SwiftRage = "SwiftRage",
|
||||
NotLikeThis = "NotLikeThis",
|
||||
FailFish = "FailFish",
|
||||
VoHiYo = "VoHiYo",
|
||||
PjSalt = "PJSalt",
|
||||
MrDestructoid = "MrDestructoid",
|
||||
BDay = "bday",
|
||||
RipChear = "RIPCheer",
|
||||
Shamrock = "Shamrock",
|
||||
BitBoss = "BitBoss",
|
||||
Streamlabs = "Streamlabs",
|
||||
Muxy = "Muxy",
|
||||
HolidayCheer = "HolidayCheer",
|
||||
}
|
||||
|
||||
// TODO: Immutability
|
||||
/// This list seems pretty comprehensive
|
||||
/// https://github.com/nossebro/TwitchPubSubMirror/blob/master/TwitchPubSubMirror_StreamlabsSystem.py
|
||||
export const CHEER_PREFIXES : string[] = [
|
||||
"Cheer",
|
||||
"DoodleCheer",
|
||||
"BibleThump",
|
||||
"cheerwhal",
|
||||
"Corgo",
|
||||
"uni",
|
||||
"ShowLove",
|
||||
"Party",
|
||||
"SeemsGood",
|
||||
"Pride",
|
||||
"Kappa",
|
||||
"FrankerZ",
|
||||
"HeyGuys",
|
||||
"DansGame",
|
||||
"EleGiggle",
|
||||
"TriHard",
|
||||
"Kreygasm",
|
||||
"4Head",
|
||||
"SwiftRage",
|
||||
"NotLikeThis",
|
||||
"FailFish",
|
||||
"VoHiYo",
|
||||
"PJSalt",
|
||||
"MrDestructoid",
|
||||
"bday",
|
||||
"RIPCheer",
|
||||
"Shamrock",
|
||||
"BitBoss",
|
||||
"Streamlabs",
|
||||
"Muxy",
|
||||
"HolidayCheer"
|
||||
];
|
||||
|
||||
// TODO: Immutability
|
||||
export const CHEER_LOOKUP_MAP : Map<string, TwitchCheerPrefix> = new Map([
|
||||
["Cheer", TwitchCheerPrefix.Cheer],
|
||||
["DoodleCheer", TwitchCheerPrefix.DoodleCheer],
|
||||
["BibleThump", TwitchCheerPrefix.BibleThump],
|
||||
["cheerwhal", TwitchCheerPrefix.Cheerwhal],
|
||||
["Corgo", TwitchCheerPrefix.Corgo],
|
||||
["uni", TwitchCheerPrefix.Uni],
|
||||
["ShowLove", TwitchCheerPrefix.ShowLove],
|
||||
["Party", TwitchCheerPrefix.Party],
|
||||
["SeemsGood", TwitchCheerPrefix.SeemsGood],
|
||||
["Pride", TwitchCheerPrefix.Pride],
|
||||
["Kappa", TwitchCheerPrefix.Kappa],
|
||||
["FrankerZ", TwitchCheerPrefix.FrankerZ],
|
||||
["HeyGuys", TwitchCheerPrefix.HeyGuys],
|
||||
["DansGame", TwitchCheerPrefix.DansGame],
|
||||
["EleGiggle", TwitchCheerPrefix.EleGiggle],
|
||||
["TriHard", TwitchCheerPrefix.TriHard],
|
||||
["Kreygasm", TwitchCheerPrefix.Kreygasm],
|
||||
["4Head", TwitchCheerPrefix.FourHead],
|
||||
["SwiftRage", TwitchCheerPrefix.SwiftRage],
|
||||
["NotLikeThis", TwitchCheerPrefix.NotLikeThis],
|
||||
["FailFish", TwitchCheerPrefix.NotLikeThis],
|
||||
["VoHiYo", TwitchCheerPrefix.VoHiYo],
|
||||
["PJSalt", TwitchCheerPrefix.PjSalt],
|
||||
["MrDestructoid", TwitchCheerPrefix.MrDestructoid],
|
||||
["bday", TwitchCheerPrefix.BDay],
|
||||
["RIPCheer", TwitchCheerPrefix.BDay],
|
||||
["Shamrock", TwitchCheerPrefix.Shamrock],
|
||||
["BitBoss", TwitchCheerPrefix.BitBoss],
|
||||
["Streamlabs", TwitchCheerPrefix.Streamlabs],
|
||||
["Muxy", TwitchCheerPrefix.Muxy],
|
||||
["HolidayCheer", TwitchCheerPrefix.HolidayCheer],
|
||||
]);
|
||||
|
||||
// TODO: Immutability
|
||||
export const CHEER_PREFIX_TO_STRING_MAP : Map<TwitchCheerPrefix, string> = new Map(
|
||||
Array.from(CHEER_LOOKUP_MAP, entry => [entry[1], entry[0]])
|
||||
)
|
||||
|
||||
// TODO: Immutability
|
||||
/// Levels of cheering
|
||||
export const CHEER_BIT_LEVELS = [1, 100, 1000, 5000, 10000];
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -555,7 +555,7 @@ export default function MediaPage() {
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{ canAccessStudio && mediaFile?.media_type === MediaFileType.Video ? (
|
||||
{ canAccessStudio() && mediaFile?.media_type === MediaFileType.Video ? (
|
||||
<Button
|
||||
{...{
|
||||
icon: faArrowRightArrowLeft,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
BROWSER=none
|
||||
HTTPS=true
|
||||
PORT=7002
|
||||
@@ -1,23 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -1,46 +0,0 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
@@ -1,24 +0,0 @@
|
||||
const path = require("path");
|
||||
const { getLoader, loaderByName } = require("@craco/craco");
|
||||
|
||||
const packages = [];
|
||||
packages.push(path.join(__dirname, "../components"));
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
configure: (webpackConfig, arg) => {
|
||||
const { isFound, match } = getLoader(
|
||||
webpackConfig,
|
||||
loaderByName("babel-loader")
|
||||
);
|
||||
if (isFound) {
|
||||
const include = Array.isArray(match.loader.include)
|
||||
? match.loader.include
|
||||
: [match.loader.include];
|
||||
|
||||
match.loader.include = include.concat(packages);
|
||||
}
|
||||
return webpackConfig;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
{
|
||||
"name": "@storyteller/power.stream-dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@storyteller/components": "0.1.0",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/node": "^16.11.21",
|
||||
"@types/react": "^16.9.36",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"bulma": "^0.9.0",
|
||||
"craco": "^0.0.3",
|
||||
"react": "^16.13.1",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "5.0.0",
|
||||
"react-wavesurfer.js": "^0.0.5",
|
||||
"typescript": "^4.5.5",
|
||||
"wavesurfer.js": "^6.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"start-w": "set BROWSER=none && craco start",
|
||||
"build": "craco build",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="72" height="74" viewBox="0 0 72 74" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35.9831 0.750001L35.983 0.750001C29.0106 0.753442 22.1963 2.87328 16.4015 6.84078C10.6069 10.8082 6.09228 16.4449 3.42761 23.0375C0.762958 29.6301 0.0675919 36.8832 1.42919 43.88C2.7908 50.8768 6.14842 57.304 11.0784 62.3487C16.0085 67.3934 22.2897 70.8291 29.1282 72.2203C35.9667 73.6116 43.0546 72.8958 49.4954 70.1636C55.9362 67.4314 61.4401 62.806 65.312 56.8729C69.1838 50.9399 71.25 43.9655 71.25 36.8313C71.2489 32.0926 70.3361 27.4005 68.5636 23.0228C66.7912 18.645 64.1936 14.6671 60.9191 11.3164C57.6445 7.96571 53.7571 5.30777 49.4785 3.49466C45.1999 1.68155 40.6141 0.748858 35.9831 0.750001ZM35.9834 9.88613C41.1864 9.88618 46.2731 11.4657 50.6003 14.4257C54.9276 17.3858 58.3012 21.5937 60.2935 26.5179C62.2859 31.4422 62.8073 36.8611 61.7915 42.0891C60.7757 47.3171 58.2686 52.1185 54.588 55.8865C50.9075 59.6544 46.219 62.2197 41.1157 63.2589C36.0124 64.2982 30.7226 63.7649 25.915 61.7262C21.1074 59.6875 16.9973 56.2347 14.1051 51.8035C11.213 47.3723 9.66895 42.162 9.66891 36.8315C9.67555 29.6854 12.4515 22.835 17.3856 17.7837C22.3196 12.7325 29.0083 9.89292 35.9834 9.88613Z" fill="#FC6B68" stroke="#FC6B68" stroke-width="0.5"/>
|
||||
<path d="M39.948 19.1899C37.7975 18.6843 35.9834 20.5208 35.9834 22.73V23.7178C35.9834 25.7327 37.6794 27.3026 39.5344 28.0893C40.6603 28.5667 41.6832 29.2664 42.5449 30.1486C43.4066 31.0307 44.0901 32.0779 44.5564 33.2305C45.3283 35.1382 46.9311 36.8659 48.989 36.8659H49.6985C51.9077 36.8659 53.7419 35.0547 53.2691 32.8967C52.54 29.5693 50.9025 26.4913 48.5099 24.0419C46.1388 21.6144 43.1647 19.9462 39.948 19.1899Z" fill="#FC6B68"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 460 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 363 KiB |
|
Before Width: | Height: | Size: 501 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="18" height="2" viewBox="0 0 18 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="18" height="2" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 144 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="18" height="2" viewBox="0 0 18 2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="18" height="2" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 144 B |
|
Before Width: | Height: | Size: 4.3 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="8" width="18" height="2" fill="white"/>
|
||||
<rect x="10" width="18" height="2" transform="rotate(90 10 0)" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 230 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="8" width="18" height="2" fill="white"/>
|
||||
<rect x="10" width="18" height="2" transform="rotate(90 10 0)" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 230 B |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |