From 29b208b136a322679673d06ff55f19ad219570f7 Mon Sep 17 00:00:00 2001 From: verbiricha Date: Mon, 4 Sep 2023 11:38:56 +0200 Subject: [PATCH 1/3] feat: text to speech and animation for zap alerts --- src/index.css | 8 +- src/lang.json | 24 ++++++ src/pages/alerts.css | 72 ++++++++++++----- src/pages/widgets.tsx | 154 ++++++++++++++++++++++++++++++++---- src/pages/widgets/views.tsx | 7 +- src/pages/widgets/zaps.tsx | 107 +++++++++++++++++++------ src/text2speech.ts | 53 +++++++++++++ src/translations/en.json | 10 ++- 8 files changed, 370 insertions(+), 65 deletions(-) create mode 100644 src/text2speech.ts diff --git a/src/index.css b/src/index.css index dcd7ab0..c844f47 100644 --- a/src/index.css +++ b/src/index.css @@ -142,12 +142,16 @@ a { .btn-border { border: 1px solid transparent; color: inherit; - background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box; + background: + linear-gradient(black, black) padding-box, + linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box; transition: 0.3s; } .btn-border:hover { - background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box; + background: + linear-gradient(black, black) padding-box, + linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box; } .btn-primary { diff --git a/src/lang.json b/src/lang.json index 1701d9f..2d21fe8 100644 --- a/src/lang.json +++ b/src/lang.json @@ -71,6 +71,12 @@ "6Z2pvJ": { "defaultMessage": "Stream Providers" }, + "6pr6hJ": { + "defaultMessage": "Minimum amount for text to speech" + }, + "8YT6ja": { + "defaultMessage": "Insert text to speak" + }, "9WRlF4": { "defaultMessage": "Send" }, @@ -86,6 +92,9 @@ "Atr2p4": { "defaultMessage": "NSFW Content" }, + "AukrPM": { + "defaultMessage": "No viewer data available" + }, "AyGauy": { "defaultMessage": "Login" }, @@ -248,6 +257,9 @@ "cyR7Kh": { "defaultMessage": "Back" }, + "d5zWyh": { + "defaultMessage": "Test voice" + }, "dVD/AR": { "defaultMessage": "Top Zappers" }, @@ -269,6 +281,9 @@ "hGQqkW": { "defaultMessage": "Schedule" }, + "heyxZL": { + "defaultMessage": "Enable text to speech" + }, "hpl4BP": { "defaultMessage": "Chat Widget" }, @@ -299,6 +314,9 @@ "ljmS5P": { "defaultMessage": "Endpoint" }, + "mnJYBQ": { + "defaultMessage": "Voice" + }, "mtNGwh": { "defaultMessage": "A short description of the content" }, @@ -341,6 +359,9 @@ "s7V+5p": { "defaultMessage": "Confirm your age" }, + "sInm1h": { + "defaultMessage": "Zap message" + }, "tG1ST3": { "defaultMessage": "Incoming Zap" }, @@ -371,6 +392,9 @@ "wEQDC6": { "defaultMessage": "Edit" }, + "wMKVFz": { + "defaultMessage": "Select voice..." + }, "wOy57k": { "defaultMessage": "Add stream goal" }, diff --git a/src/pages/alerts.css b/src/pages/alerts.css index 2af593e..d283147 100644 --- a/src/pages/alerts.css +++ b/src/pages/alerts.css @@ -1,49 +1,83 @@ -.zap-alert-widgets .zap-alert { - animation: cssAnimation 0s ease-in 10s forwards; - animation-fill-mode: forwards; +.zap-alerts-widget .zap-alert { + animation: fadeInOut 10s; +} + +@keyframes fadeInOut { + 0% { + opacity: 0; + } + 16% { + opacity: 1; + } + 84% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.text-to-speech-settings { + display: flex; + flex-direction: column; + gap: 4px; +} + +.text-to-speech-settings .labeled-input { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.text-to-speech-settings .labeled-input label { + font-size: 14px; + color: var(--text-muted); +} + +.text-to-speech-settings textarea { + resize: vertical; } .zap-alert { display: inline-flex; flex-direction: column; align-items: center; + border-radius: 17px; + background: #2d2d2d; + margin-top: 24px; } -.zap-alert > div:nth-of-type(1) { +.zap-alert > .zap-alert-title { width: fit-content; + text-align: center; + left: 120px; + margin-top: -20px; font-size: 21px; font-weight: 600; border-radius: 33px; background: linear-gradient(135deg, #882bff 0%, #f83838 100%); - margin: 0; padding: 8px 24px; - margin-bottom: -11px; z-index: 2; } -.zap-alert > div:nth-of-type(2) { +.zap-alert > .zap-alert-header { z-index: 1; display: inline-flex; justify-content: center; align-items: center; - border-radius: 17px; - background: #2d2d2d; - padding: 27px 90px; + padding: 12px 90px 24px 90px; font-size: 29px; font-weight: 600; } -.zap-alert .highlight { - color: #ff4468; +.zap-alert-body { + padding: 0 24px 24px 24px; + margin-top: -20px; + margin-bottom: 0; } -@keyframes cssAnimation { - from { - opacity: 1; - } - to { - opacity: 0; - } +.zap-alert .highlight { + color: #ff4468; } .views { diff --git a/src/pages/widgets.tsx b/src/pages/widgets.tsx index 7d58bca..551c38a 100644 --- a/src/pages/widgets.tsx +++ b/src/pages/widgets.tsx @@ -1,14 +1,149 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import "./widgets.css"; +import { useState, useMemo } from "react"; +import { useIntl, FormattedMessage } from "react-intl"; + import { NostrPrefix, createNostrLink } from "@snort/system"; import Copy from "element/copy"; import { useCurrentStreamFeed } from "hooks/current-stream-feed"; +import { getVoices, speak, toTextToSpeechParams } from "text2speech"; import { useLogin } from "hooks/login"; -import { FormattedMessage } from "react-intl"; import { eventToLink, hexToBech32 } from "utils"; import { ZapAlertItem } from "./widgets/zaps"; import { TopZappersWidget } from "./widgets/top-zappers"; import { Views } from "./widgets/views"; +import groupBy from "lodash/groupBy"; + +interface ZapAlertConfigurationProps { + npub: string; + baseUrl: string; +} + +function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) { + const login = useLogin(); + const { formatMessage, formatDisplayName } = useIntl(); + + const [testText, setTestText] = useState(""); + const [textToSpeech, setTextToSpeech] = useState(false); + const [voice, setVoice] = useState(null); + const [minSatsForTextToSpeech, setMinSatsForTextToSpeech] = useState("21"); + + // Google propietary voices are not available on OBS browser + const voices = getVoices().filter(v => !v.name.includes("Google")); + const groupedVoices = useMemo(() => { + return groupBy(voices, v => v.lang); + }, [voices]); + const languages = useMemo(() => { + return Object.keys(groupedVoices).sort(); + }, [groupedVoices]); + const selectedVoice = useMemo(() => { + return voices.find(v => v.voiceURI === voice); + }, [voice]); + + const isTextToSpeechSupported = "speechSynthesis" in window; + const isTextToSpeechEnabled = voices.length > 0 && textToSpeech; + + const query = useMemo(() => { + const params = toTextToSpeechParams({ + voiceURI: voice, + minSats: voice ? Number(minSatsForTextToSpeech) : null, + }); + const queryParams = params.toString(); + return queryParams.length > 0 ? `?${queryParams}` : ""; + }, [voice, minSatsForTextToSpeech]); + + function testVoice() { + if (selectedVoice) { + speak(selectedVoice, testText); + } + } + + return ( + <> +

+ +

+ + +
+
setTextToSpeech(!textToSpeech)} + style={{ cursor: isTextToSpeechSupported ? "pointer" : "not-allowed" }}> + setTextToSpeech(ev.target.checked)} + /> + +
+ {isTextToSpeechEnabled && ( + <> +
+ + setMinSatsForTextToSpeech(ev.target.value)} + /> +
+
+ + +
+ {voice && ( + <> +
+ +