Skip to content

Commit

Permalink
Speaking cards (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubk authored Nov 28, 2023
1 parent e4747f7 commit 2ebebdd
Show file tree
Hide file tree
Showing 18 changed files with 308 additions and 104 deletions.
7 changes: 7 additions & 0 deletions src/lib/typescript/enum-values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { EnumLike } from "./is-enum-valid.ts";

export function enumValues<E extends EnumLike>(enumObject: E): E[keyof E][] {
return Object.keys(enumObject)
.filter((key) => Number.isNaN(Number(key)))
.map((key) => enumObject[key] as E[keyof E]);
}
10 changes: 10 additions & 0 deletions src/lib/typescript/is-enum-valid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { enumValues } from "./enum-values.ts";

export type EnumLike = { [key: string]: number | string };

export function isEnumValid<T extends EnumLike>(
param: unknown,
enumObject: T,
): param is T[keyof T] {
return enumValues(enumObject).includes(param as T[keyof T]);
}
48 changes: 48 additions & 0 deletions src/lib/voice-playback/speak.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export enum SpeakLanguageEnum {
AmericanEnglish = "en-US",
Italian = "it-IT",
Swedish = "sv-SE",
CanadianFrench = "fr-CA",
Malay = "ms-MY",
German = "de-DE",
BritishEnglish = "en-GB",
Hebrew = "he-IL",
AustralianEnglish = "en-AU",
Indonesian = "id-ID",
French = "fr-FR",
Bulgarian = "bg-BG",
Spanish = "es-ES",
MexicanSpanish = "es-MX",
Finnish = "fi-FI",
BrazilianPortuguese = "pt-BR",
BelgianDutch = "nl-BE",
Japanese = "ja-JP",
Romanian = "ro-RO",
Portuguese = "pt-PT",
Thai = "th-TH",
Croatian = "hr-HR",
Slovak = "sk-SK",
Hindi = "hi-IN",
Ukrainian = "uk-UA",
MainlandChinaChinese = "zh-CN",
Vietnamese = "vi-VN",
ModernStandardArabic = "ar-001",
TaiwaneseChinese = "zh-TW",
Greek = "el-GR",
Russian = "ru-RU",
Danish = "da-DK",
HongKongChinese = "zh-HK",
Hungarian = "hu-HU",
Dutch = "nl-NL",
Turkish = "tr-TR",
Korean = "ko-KR",
Polish = "pl-PL",
Czech = "cs-CZ",
}

export const speak = (text: string, language: SpeakLanguageEnum) => {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = language;

window.speechSynthesis.speak(utterance);
};
4 changes: 2 additions & 2 deletions src/screens/deck-review/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { css } from "@emotion/css";
import React from "react";
import { theme } from "../../ui/theme.tsx";
import { observer } from "mobx-react-lite";
import { CardFormStore } from "../../store/card-form-store.ts";
import { CardUnderReviewStore } from "../../store/card-under-review-store.ts";
import { HorizontalDivider } from "../../ui/horizontal-divider.tsx";

export const cardSize = 310;

type FramerMotionProps = Pick<MotionProps, "style" | "animate" | "initial">;

type Props = {
card: CardFormStore;
card: CardUnderReviewStore;
} & FramerMotionProps;

export const Card = observer(({ card, style, animate }: Props) => {
Expand Down
2 changes: 1 addition & 1 deletion src/screens/deck-review/deck-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const DeckScreen = observer(() => {
const reviewStore = useReviewStore();

if (reviewStore.isFinished) {
return <DeckFinished type={'deck'} />;
return <DeckFinished type={"deck"} />;
} else if (reviewStore.currentCardId) {
return <Review />;
}
Expand Down
5 changes: 4 additions & 1 deletion src/screens/deck-review/repeat-all-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export const RepeatAllScreen = observer(() => {
const reviewStore = useReviewStore();

useMount(() => {
reviewStore.startAllRepeatReview(deckListStore.myDecks);
reviewStore.startAllRepeatReview(
deckListStore.myDecks,
deckListStore.user?.is_speaking_card_enabled ?? false,
);
});

if (reviewStore.isFinished) {
Expand Down
2 changes: 1 addition & 1 deletion src/screens/deck-review/review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { css } from "@emotion/css";
import { theme } from "../../ui/theme.tsx";
import throttle from "just-throttle";
import { CardState } from "../../store/card-form-store.ts";
import { CardState } from "../../store/card-under-review-store.ts";
import { ProgressBar } from "../../ui/progress-bar.tsx";
import { useReviewStore } from "../../store/review-store-context.tsx";
import { Button } from "../../ui/button.tsx";
Expand Down
32 changes: 27 additions & 5 deletions src/screens/user-settings/user-settings-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { SettingsRow } from "./settings-row.tsx";
import { RadioSwitcher } from "../../ui/radio-switcher.tsx";
import { theme } from "../../ui/theme.tsx";
import { Select } from "../../ui/select.tsx";
import { Hint } from "../../ui/hint.tsx";
import { css } from "@emotion/css";
import { useBackButton } from "../../lib/telegram/use-back-button.tsx";
import { screenStore } from "../../store/screen-store.ts";
import { HintTransparent } from "../../ui/hint-transparent.tsx";

export const timeRanges = generateTimeRange();

Expand Down Expand Up @@ -43,11 +43,12 @@ export const UserSettingsMain = observer(() => {
return null;
}

const { isRemindNotifyEnabled, time } = userSettingsStore.form;
const { isRemindNotifyEnabled, isSpeakingCardsEnabled, time } =
userSettingsStore.form;

return (
<div>
<ListHeader text={"Review notification settings"} />
<ListHeader text={"Settings"} />

<div
className={css({
Expand All @@ -57,7 +58,7 @@ export const UserSettingsMain = observer(() => {
})}
>
<SettingsRow>
<span>Notifications</span>
<span>Review notifications</span>
<span
className={css({
transform: "translateY(3px)",
Expand Down Expand Up @@ -88,7 +89,28 @@ export const UserSettingsMain = observer(() => {
</SettingsRow>
)}

<Hint>⭐ Daily reminders will help you remember to repeat cards</Hint>
<HintTransparent>
Daily reminders help you remember to repeat cards
</HintTransparent>

<SettingsRow>
<span>Speaking cards</span>
<span
className={css({
transform: "translateY(3px)",
position: "relative",
})}
>
<RadioSwitcher
isOn={isSpeakingCardsEnabled.value}
onToggle={isSpeakingCardsEnabled.toggle}
/>
</span>
</SettingsRow>

<HintTransparent>
Play spoken audio for each flashcard to enhance pronunciation
</HintTransparent>
</div>
</div>
);
Expand Down
35 changes: 0 additions & 35 deletions src/store/card-form-store.ts

This file was deleted.

68 changes: 68 additions & 0 deletions src/store/card-under-review-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { makeAutoObservable } from "mobx";
import { assert } from "../lib/typescript/assert.ts";
import { DeckCardDbType } from "../../functions/db/deck/decks-with-cards-schema.ts";
import { DeckWithCardsWithReviewType } from "./deck-list-store.ts";
import { speak, SpeakLanguageEnum } from "../lib/voice-playback/speak.ts";
import { isEnumValid } from "../lib/typescript/is-enum-valid.ts";

export enum CardState {
Remember = "remember",
Forget = "forget",
}

export enum DeckSpeakField {
Front = "front",
Back = "back",
}

export class CardUnderReviewStore {
id: number;
front: string;
back: string;
example: string | null = null;
deckName?: string;
deckSpeakLocale: string | null = null;
deckSpeakField: string | null = null;

isOpened = false;
state?: CardState;

constructor(card: DeckCardDbType, deck: DeckWithCardsWithReviewType) {
this.id = card.id;
this.front = card.front;
this.back = card.back;
this.example = card.example;
this.deckName = deck.name;
this.deckSpeakLocale = deck.speak_locale;
this.deckSpeakField = deck.speak_field;

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

open() {
this.isOpened = true;
}

close() {
this.isOpened = false;
}

changeState(state: CardState) {
assert(this.isOpened, "The card should be opened before changing state");
this.state = state;
}

speak() {
if (!this.deckSpeakLocale || !this.deckSpeakField) {
return;
}
if (
!isEnumValid(this.deckSpeakField, DeckSpeakField) ||
!isEnumValid(this.deckSpeakLocale, SpeakLanguageEnum)
) {
return;
}

speak(this[this.deckSpeakField], this.deckSpeakLocale);
}
}
5 changes: 5 additions & 0 deletions src/store/deck-list-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vi.mock("../api/api.ts", () => {
language_code: "uk",
last_name: "Testov",
last_reminded_date: null,
is_speaking_card_enabled: false,
username: "test",
},
cardsToReview: [
Expand All @@ -33,6 +34,8 @@ vi.mock("../api/api.ts", () => {
share_id: "1",
author_id: 1,
description: "",
speak_locale: null,
speak_field: null,
deck_card: [
{
id: 1,
Expand Down Expand Up @@ -76,6 +79,8 @@ vi.mock("../api/api.ts", () => {
share_id: "2",
author_id: 1,
description: "",
speak_locale: null,
speak_field: null,
deck_card: [
{
id: 4,
Expand Down
22 changes: 14 additions & 8 deletions src/store/deck-list-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ export enum StartParamType {
RepeatAll = "repeat_all",
}

export type DeckCardDbTypeWithType = DeckCardDbType & {
type: "new" | "repeat";
};

export type DeckWithCardsWithReviewType = DeckWithCardsDbType & {
cardsToReview: Array<DeckCardDbType & { type: "new" | "repeat" }>;
cardsToReview: DeckCardDbTypeWithType[];
};

export class DeckListStore {
Expand Down Expand Up @@ -153,8 +157,8 @@ export class DeckListStore {
}

reviewStore.startDeckReview(
deckListStore.selectedDeck.cardsToReview,
deckListStore.selectedDeck.name,
deckListStore.selectedDeck,
this.user?.is_speaking_card_enabled ?? false,
);
}

Expand All @@ -172,11 +176,15 @@ export class DeckListStore {
});
}

get myId() {
get user() {
if (this.myInfo?.state !== "fulfilled") {
return null;
}
return this.myInfo.value.user.id;
return this.myInfo.value.user;
}

get myId() {
return this.user?.id;
}

get selectedDeck(): DeckWithCardsWithReviewType | null {
Expand Down Expand Up @@ -257,9 +265,7 @@ export class DeckListStore {
);
}

optimisticUpdateSettings(
body: Pick<UserDbType, "is_remind_enabled" | "last_reminded_date">,
) {
optimisticUpdateSettings(body: Partial<UserDbType>) {
assert(this.myInfo?.state === "fulfilled");
Object.assign(this.myInfo.value.user, body);
}
Expand Down
Loading

0 comments on commit 2ebebdd

Please sign in to comment.