From 710107fe2952418fa3fc5d97c3d2a67dc219d300 Mon Sep 17 00:00:00 2001 From: Egor Gorbachev <7gorbachevm@gmail.com> Date: Mon, 11 Dec 2023 14:38:41 +0700 Subject: [PATCH] Replay speaking card (#23) * Avoid reporting about unsupported Speech synthesis API on old Android * Allow to replay speaking card --- src/lib/typescript/enum-values.ts | 2 +- src/lib/voice-playback/speak.ts | 14 +++------ src/screens/deck-review/card-speaker.tsx | 40 ++++++++++++++++++++++++ src/screens/deck-review/card.tsx | 11 +++++-- src/store/card-under-review-store.ts | 8 +++-- src/store/deck-form-store.ts | 2 +- src/store/review-store.ts | 17 +++------- 7 files changed, 67 insertions(+), 27 deletions(-) create mode 100644 src/screens/deck-review/card-speaker.tsx diff --git a/src/lib/typescript/enum-values.ts b/src/lib/typescript/enum-values.ts index d2e6c02a..623b4f57 100644 --- a/src/lib/typescript/enum-values.ts +++ b/src/lib/typescript/enum-values.ts @@ -9,5 +9,5 @@ export function enumValues(enumObject: E): E[keyof E][] { export function enumEntries( enumObject: E, ): [keyof E, E[keyof E]][] { - return Object.entries(enumObject) as any + return Object.entries(enumObject) as any; } diff --git a/src/lib/voice-playback/speak.ts b/src/lib/voice-playback/speak.ts index 46db9e63..ea7dcc21 100644 --- a/src/lib/voice-playback/speak.ts +++ b/src/lib/voice-playback/speak.ts @@ -1,5 +1,3 @@ -import { reportHandledErrorOnce } from "../rollbar/rollbar.tsx"; - export enum SpeakLanguageEnum { USEnglish = "en-US", Italian = "it-IT", @@ -16,6 +14,7 @@ export enum SpeakLanguageEnum { Japanese = "ja-JP", Romanian = "ro-RO", Portuguese = "pt-PT", + BrazilianPortuguese = "pt-BR", Thai = "th-TH", Croatian = "hr-HR", Slovak = "sk-SK", @@ -45,15 +44,12 @@ export const languageKeyToHuman = (str: string): string => { return str.replace(/([A-Z])/g, " $1").trim(); }; -export const speak = (text: string, language: SpeakLanguageEnum) => { - const isSpeechSynthesisSupported = - "speechSynthesis" in window && - typeof SpeechSynthesisUtterance !== "undefined"; +export const isSpeechSynthesisSupported = + "speechSynthesis" in window && + typeof SpeechSynthesisUtterance !== "undefined"; +export const speak = (text: string, language: SpeakLanguageEnum) => { if (!isSpeechSynthesisSupported) { - reportHandledErrorOnce( - `Speech synthesis is not supported in this browser. Browser info: ${navigator.userAgent}`, - ); return; } diff --git a/src/screens/deck-review/card-speaker.tsx b/src/screens/deck-review/card-speaker.tsx new file mode 100644 index 00000000..47631520 --- /dev/null +++ b/src/screens/deck-review/card-speaker.tsx @@ -0,0 +1,40 @@ +import { CardUnderReviewStore } from "../../store/card-under-review-store.ts"; +import { isSpeechSynthesisSupported } from "../../lib/voice-playback/speak.ts"; +import { throttle } from "../../lib/throttle/throttle.ts"; +import { css, cx } from "@emotion/css"; +import { theme } from "../../ui/theme.tsx"; +import React from "react"; +import { observer } from "mobx-react-lite"; + +type Props = { + card: CardUnderReviewStore; + type: "front" | "back"; +}; + +export const CardSpeaker = observer((props: Props) => { + const { card, type } = props; + if ( + !isSpeechSynthesisSupported || + !card.isOpened || + type !== card.deckSpeakField || + !card.isSpeakingCardsEnabledSettings + ) { + return null; + } + + // throttle is needed to avoid user clicking on the speaker button many times in a row hence creating many sounds + return ( + + ); +}); diff --git a/src/screens/deck-review/card.tsx b/src/screens/deck-review/card.tsx index a2bd7a92..80d8b1c4 100644 --- a/src/screens/deck-review/card.tsx +++ b/src/screens/deck-review/card.tsx @@ -5,6 +5,7 @@ import { theme } from "../../ui/theme.tsx"; import { observer } from "mobx-react-lite"; import { CardUnderReviewStore } from "../../store/card-under-review-store.ts"; import { HorizontalDivider } from "../../ui/horizontal-divider.tsx"; +import { CardSpeaker } from "./card-speaker.tsx"; export const cardSize = 310; @@ -42,9 +43,15 @@ export const Card = observer(({ card, style, animate }: Props) => { color: theme.textColor, })} > -
{card.front}
+
+ {card.front} +
{card.isOpened ? : null} - {card.isOpened ?
{card.back}
: null} + {card.isOpened ? ( +
+ {card.back} +
+ ) : null} {card.isOpened && card.example ? (
{ this.form = createUpdateForm(response.id, response); - deckListStore.replaceDeck(response) + deckListStore.replaceDeck(response); }) .finally( action(() => { diff --git a/src/store/review-store.ts b/src/store/review-store.ts index 863fa633..cb3f450b 100644 --- a/src/store/review-store.ts +++ b/src/store/review-store.ts @@ -24,7 +24,6 @@ export class ReviewStore { initialCardCount?: number; isReviewSending = false; - isSpeakingCards = false; constructor() { makeAutoObservable(this, {}, { autoBind: true }); @@ -32,13 +31,13 @@ export class ReviewStore { startDeckReview( deck: DeckWithCardsWithReviewType, - isSpeakingCards?: boolean, + isSpeakingCardsEnabledSettings?: boolean, ) { if (!deck.cardsToReview.length) { return; } deck.cardsToReview.forEach((card) => { - this.cardsToReview.push(new CardUnderReviewStore(card, deck)); + this.cardsToReview.push(new CardUnderReviewStore(card, deck, !!isSpeakingCardsEnabledSettings)); }); this.initialCardCount = this.cardsToReview.length; @@ -46,13 +45,11 @@ export class ReviewStore { if (this.cardsToReview.length > 1) { this.nextCardId = this.cardsToReview[1].id; } - - this.isSpeakingCards = !!isSpeakingCards; } startAllRepeatReview( myDecks: DeckWithCardsWithReviewType[], - isSpeakingCards?: boolean, + isSpeakingCardsEnabledSettings?: boolean, ) { if (!myDecks.length) { return; @@ -62,7 +59,7 @@ export class ReviewStore { deck.cardsToReview .filter((card) => card.type === "repeat") .forEach((card) => { - this.cardsToReview.push(new CardUnderReviewStore(card, deck)); + this.cardsToReview.push(new CardUnderReviewStore(card, deck, !!isSpeakingCardsEnabledSettings)); }); }); @@ -75,8 +72,6 @@ export class ReviewStore { if (this.cardsToReview.length > 1) { this.nextCardId = this.cardsToReview[1].id; } - - this.isSpeakingCards = !!isSpeakingCards; } get currentCard() { @@ -103,9 +98,7 @@ export class ReviewStore { const currentCard = this.currentCard; assert(currentCard, "Current card should not be empty"); currentCard.open(); - if (this.isSpeakingCards) { - currentCard.speak(); - } + currentCard.speak(); } changeState(cardState: CardState) {