diff --git a/src/lang.json b/src/lang.json index 1701d9f..6a8307a 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" }, @@ -380,6 +404,9 @@ "x82IOl": { "defaultMessage": "Mute" }, + "y867Vs": { + "defaultMessage": "Volume" + }, "yzKwBQ": { "defaultMessage": "eg. nsec1xyz" }, 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..2c4dd32 100644 --- a/src/pages/widgets.tsx +++ b/src/pages/widgets.tsx @@ -1,14 +1,165 @@ /* 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"); + const [volume, setVolume] = useState(1); + + // 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, + volume, + }); + const queryParams = params.toString(); + return queryParams.length > 0 ? `?${queryParams}` : ""; + }, [voice, volume, minSatsForTextToSpeech]); + + function testVoice() { + if (selectedVoice) { + speak(selectedVoice, testText, volume); + } + } + + return ( + <> +

+ +

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