Skip to content

Commit

Permalink
Card voice from url (#14)
Browse files Browse the repository at this point in the history
* Card voice from URL
  • Loading branch information
kubk authored Feb 7, 2024
1 parent 45a7f9a commit 6be3f74
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 37 deletions.
1 change: 1 addition & 0 deletions src/screens/component-catalog/card-preview-story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const createCardPreviewForm = (card: {
example: new TextField<string>(card.example ?? ""),
answerType: new TextField<CardAnswerType>("remember"),
answerFormType: "new",
options: null,
answers: new ListField<CardAnswerFormType>([]),
answerId: "0",
},
Expand Down
4 changes: 4 additions & 0 deletions src/screens/deck-form/store/deck-form-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const mapUpsertDeckRequestToResponse = (
back: card.back,
answer_type: "remember",
answers: null,
options: null,
};
}),
},
Expand Down Expand Up @@ -75,6 +76,7 @@ vi.mock("./../../../store/deck-list-store.ts", () => {
back: "Время",
answer_type: "remember",
answers: null,
options: null,
},
{
id: 4,
Expand All @@ -85,6 +87,7 @@ vi.mock("./../../../store/deck-list-store.ts", () => {
back: "Год",
answer_type: "remember",
answers: null,
options: null,
},
{
id: 5,
Expand All @@ -95,6 +98,7 @@ vi.mock("./../../../store/deck-list-store.ts", () => {
back: "Дорога",
answer_type: "remember",
answers: null,
options: null,
},
];

Expand Down
4 changes: 4 additions & 0 deletions src/screens/deck-form/store/deck-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { showAlert } from "../../../lib/telegram/show-alert.ts";
import { fuzzySearch } from "../../../lib/string/fuzzy-search.ts";
import {
DeckCardDbType,
DeckCardOptionsDbType,
DeckSpeakFieldEnum,
DeckWithCardsDbType,
} from "../../../../functions/db/deck/decks-with-cards-schema.ts";
Expand Down Expand Up @@ -45,6 +46,7 @@ export type CardFormType = {
answerId?: string;
answerFormType?: "new" | "edit";
id?: number;
options: DeckCardOptionsDbType;
};

type DeckFormType = {
Expand Down Expand Up @@ -122,6 +124,7 @@ const createUpdateForm = (
back: createCardSideField(card.back),
example: new TextField(card.example || ""),
answerType: createAnswerTypeField(card),
options: card.options,
answers: createAnswerListField(
card.answers
? card.answers.map((answer) => ({
Expand Down Expand Up @@ -334,6 +337,7 @@ export class DeckFormStore implements CardFormStoreInterface {
back: createCardSideField(""),
example: new TextField(""),
answerType: createAnswerTypeField(),
options: null,
answers: createAnswerListField([], () => this.cardForm),
});
}
Expand Down
1 change: 1 addition & 0 deletions src/screens/deck-form/store/quick-add-card-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class QuickAddCardFormStore implements CardFormStoreInterface {
front: createCardSideField(""),
example: new TextField(""),
answerType: createAnswerTypeField(),
options: null,
answers: createAnswerListField([], () => this.cardForm),
};
isSending = false;
Expand Down
77 changes: 55 additions & 22 deletions src/screens/deck-review/store/card-preview-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { CardAnswerType } from "../../../../functions/db/custom-types.ts";
import { makeAutoObservable } from "mobx";
import { userStore } from "../../../store/user-store.ts";
import { isEnumValid } from "../../../lib/typescript/is-enum-valid.ts";
import { speak, SpeakLanguageEnum } from "../../../lib/voice-playback/speak.ts";
import {
isSpeechSynthesisSupported,
speak,
SpeakLanguageEnum,
} from "../../../lib/voice-playback/speak.ts";
import { removeAllTags } from "../../../lib/sanitize-html/remove-all-tags.ts";
import { CardFormStoreInterface } from "../../deck-form/store/card-form-store-interface.ts";
import { assert } from "../../../lib/typescript/assert.ts";
Expand All @@ -22,6 +26,7 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore {
answers: CardAnswerDbType[] = [];
answer?: CardAnswerDbType;

voice?: HTMLAudioElement;
deckSpeakLocale: string | null = null;
deckSpeakField: DeckSpeakFieldEnum | null = null;

Expand All @@ -31,7 +36,14 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore {
isOverflowing = new BooleanToggle(false);

constructor(cardFormStore: CardFormStoreInterface) {
makeAutoObservable(this, {}, { autoBind: true });
makeAutoObservable(
this,
{
isCardSpeakerVisible: false,
},
{ autoBind: true },
);

const form = cardFormStore.cardForm;
assert(form, "form is not defined");
this.id = 9999;
Expand All @@ -52,28 +64,13 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore {

this.deckSpeakLocale = deckForm.speakingCardsLocale.value ?? null;
this.deckSpeakField = deckForm.speakingCardsField.value ?? null;
}

speak() {
if (
!this.isSpeakingCardsEnabledSettings ||
!this.deckSpeakLocale ||
!this.deckSpeakField
) {
return;
}

if (!isEnumValid(this.deckSpeakLocale, SpeakLanguageEnum)) {
return;
if (form.options?.voice) {
const audio = new Audio(form.options.voice);
// Preload audio to avoid slow delay when playing voice
audio.load();
this.voice = audio;
}

const text = this[this.deckSpeakField];

speak(removeAllTags(text), this.deckSpeakLocale);
}

get isSpeakingCardsEnabledSettings() {
return userStore.isSpeakingCardsEnabled;
}

openWithAnswer(answer: CardAnswerDbType) {
Expand All @@ -93,4 +90,40 @@ export class CardPreviewStore implements LimitedCardUnderReviewStore {
this.speak();
}
}

speak() {
if (!userStore.isSpeakingCardsEnabled) {
return;
}

if (this.voice) {
this.voice.play();
return;
}

if (!this.deckSpeakLocale || !this.deckSpeakField) {
return;
}

if (!isEnumValid(this.deckSpeakLocale, SpeakLanguageEnum)) {
return;
}

const text = this[this.deckSpeakField];

speak(removeAllTags(text), this.deckSpeakLocale);
}

isCardSpeakerVisible(type: DeckSpeakFieldEnum) {
if (this.voice) {
return type === "back";
}

return (
isSpeechSynthesisSupported &&
this.isOpened &&
type === this.deckSpeakField &&
userStore.isSpeakingCardsEnabled
);
}
}
46 changes: 39 additions & 7 deletions src/screens/deck-review/store/card-under-review-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {
DeckSpeakFieldEnum,
} from "../../../../functions/db/deck/decks-with-cards-schema.ts";
import { DeckWithCardsWithReviewType } from "../../../store/deck-list-store.ts";
import { speak, SpeakLanguageEnum } from "../../../lib/voice-playback/speak.ts";
import {
isSpeechSynthesisSupported,
speak,
SpeakLanguageEnum,
} from "../../../lib/voice-playback/speak.ts";
import { isEnumValid } from "../../../lib/typescript/is-enum-valid.ts";
import { CardAnswerType } from "../../../../functions/db/custom-types.ts";
import { assert } from "../../../lib/typescript/assert.ts";
Expand All @@ -25,6 +29,7 @@ export class CardUnderReviewStore {
back: string;
example: string | null = null;
deckName?: string;
voice?: HTMLAudioElement;
deckSpeakLocale: string | null = null;
deckSpeakField: DeckSpeakFieldEnum | null = null;
answerType: CardAnswerType;
Expand All @@ -51,8 +56,17 @@ export class CardUnderReviewStore {
this.deckName = deck.name;
this.deckSpeakLocale = deck.speak_locale;
this.deckSpeakField = deck.speak_field;
if (card.options?.voice) {
const audio = new Audio(card.options.voice);
audio.load();
this.voice = audio;
}

makeAutoObservable(this, {}, { autoBind: true });
makeAutoObservable(
this,
{ isCardSpeakerVisible: false },
{ autoBind: true },
);
}

open() {
Expand All @@ -77,11 +91,16 @@ export class CardUnderReviewStore {
}

speak() {
if (
!userStore.isSpeakingCardsEnabled ||
!this.deckSpeakLocale ||
!this.deckSpeakField
) {
if (!userStore.isSpeakingCardsEnabled) {
return;
}

if (this.voice) {
this.voice.play();
return;
}

if (!this.deckSpeakLocale || !this.deckSpeakField) {
return;
}

Expand All @@ -93,4 +112,17 @@ export class CardUnderReviewStore {

speak(removeAllTags(text), this.deckSpeakLocale);
}

isCardSpeakerVisible(type: "front" | "back") {
if (this.voice) {
return type === "back";
}

return (
isSpeechSynthesisSupported &&
this.isOpened &&
type === this.deckSpeakField &&
userStore.isSpeakingCardsEnabled
);
}
}
8 changes: 8 additions & 0 deletions src/screens/deck-review/store/review-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const repeatCardsMock: DeckCardDbTypeWithType[] = [
type: "repeat",
answer_type: "remember",
answers: null,
options: null,
},
{
id: 4,
Expand All @@ -54,6 +55,7 @@ const repeatCardsMock: DeckCardDbTypeWithType[] = [
type: "repeat",
answer_type: "remember",
answers: null,
options: null,
},
{
id: 5,
Expand All @@ -65,6 +67,7 @@ const repeatCardsMock: DeckCardDbTypeWithType[] = [
type: "repeat",
answer_type: "remember",
answers: null,
options: null,
},
{
id: 6,
Expand All @@ -76,6 +79,7 @@ const repeatCardsMock: DeckCardDbTypeWithType[] = [
type: "repeat",
answer_type: "remember",
answers: null,
options: null,
},
];

Expand All @@ -90,6 +94,7 @@ const newCardsMock: DeckCardDbTypeWithType[] = [
type: "new",
answer_type: "remember",
answers: null,
options: null,
},
{
id: 4,
Expand All @@ -101,6 +106,7 @@ const newCardsMock: DeckCardDbTypeWithType[] = [
type: "new",
answer_type: "remember",
answers: null,
options: null,
},
{
id: 5,
Expand All @@ -112,6 +118,7 @@ const newCardsMock: DeckCardDbTypeWithType[] = [
type: "new",
answer_type: "remember",
answers: null,
options: null,
},
{
id: 6,
Expand All @@ -123,6 +130,7 @@ const newCardsMock: DeckCardDbTypeWithType[] = [
type: "new",
answer_type: "remember",
answers: null,
options: null,
},
];

Expand Down
9 changes: 1 addition & 8 deletions src/screens/shared/card/card-speaker.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
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";
import { LimitedCardUnderReviewStore } from "./card.tsx";
import { userStore } from "../../../store/user-store.ts";

type Props = {
card: LimitedCardUnderReviewStore;
Expand All @@ -14,12 +12,7 @@ type Props = {

export const CardSpeaker = observer((props: Props) => {
const { card, type } = props;
if (
!isSpeechSynthesisSupported ||
!card.isOpened ||
type !== card.deckSpeakField ||
!userStore.isSpeakingCardsEnabled
) {
if (!card.isCardSpeakerVisible(type)) {
return null;
}

Expand Down
1 change: 1 addition & 0 deletions src/screens/shared/card/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type LimitedCardUnderReviewStore = Pick<
| "openWithAnswer"
| "open"
| "isOverflowing"
| "isCardSpeakerVisible"
>;

type Props = {
Expand Down
Loading

0 comments on commit 6be3f74

Please sign in to comment.