Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speaking cards settings #22

Merged
merged 3 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions functions/db/databaseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export interface Database {
is_public: boolean
name: string
share_id: string
speak_field: string | null
speak_locale: string | null
}
Insert: {
author_id?: number | null
Expand All @@ -64,6 +66,8 @@ export interface Database {
is_public?: boolean
name: string
share_id: string
speak_field?: string | null
speak_locale?: string | null
}
Update: {
author_id?: number | null
Expand All @@ -73,6 +77,8 @@ export interface Database {
is_public?: boolean
name?: string
share_id?: string
speak_field?: string | null
speak_locale?: string | null
}
Relationships: [
{
Expand Down
5 changes: 4 additions & 1 deletion functions/db/deck/decks-with-cards-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const deckCardSchema = z.object({
example: z.string().nullable(),
});

const deckSpeakField = z.enum(["front", "back"]);

export const deckSchema = z.object({
id: z.number(),
created_at: z.string(),
Expand All @@ -18,7 +20,7 @@ export const deckSchema = z.object({
share_id: z.string().nullable(),
is_public: z.boolean(),
speak_locale: z.string().nullable(),
speak_field: z.string().nullable(),
speak_field: deckSpeakField.nullable(),
});

export const deckWithCardsSchema = deckSchema.merge(
Expand All @@ -31,3 +33,4 @@ export const decksWithCardsSchema = z.array(deckWithCardsSchema);

export type DeckWithCardsDbType = z.infer<typeof deckWithCardsSchema>;
export type DeckCardDbType = z.infer<typeof deckCardSchema>;
export type DeckSpeakFieldEnum = z.infer<typeof deckSpeakField>;
5 changes: 3 additions & 2 deletions functions/db/deck/get-deck-by-id-and-author-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ export const getDeckByIdAndAuthorId = async (
.from("deck")
.select()
.eq("author_id", userId)
.eq("id", deckId);
.eq("id", deckId)
.single();

if (canEditDeckResult.error) {
throw new DatabaseException(canEditDeckResult.error);
}

return canEditDeckResult.data.length ? canEditDeckResult.data[0] : null;
return canEditDeckResult.data ?? null;
};
9 changes: 3 additions & 6 deletions functions/db/deck/get-deck-with-cards-by-id-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { deckWithCardsSchema } from "./decks-with-cards-schema.ts";
import { EnvType } from "../../env/env-schema.ts";
import { getDatabase } from "../get-database.ts";

export const getDeckWithCardsById = async (
env: EnvType,
deckId: number,
) => {
export const getDeckWithCardsById = async (env: EnvType, deckId: number) => {
const db = getDatabase(env);

const { data, error } = await db
Expand All @@ -20,5 +17,5 @@ export const getDeckWithCardsById = async (
throw new DatabaseException(error);
}

return deckWithCardsSchema.parse(data)
}
return deckWithCardsSchema.parse(data);
};
40 changes: 19 additions & 21 deletions functions/upsert-deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,21 @@ import { DatabaseException } from "./db/database-exception.ts";
import { createJsonResponse } from "./lib/json-response/create-json-response.ts";
import {
deckSchema,
deckWithCardsSchema
deckWithCardsSchema,
} from "./db/deck/decks-with-cards-schema.ts";
import { addDeckToMineDb } from "./db/deck/add-deck-to-mine-db.ts";
import { createForbiddenRequestResponse } from "./lib/json-response/create-forbidden-request-response.ts";
import { getDeckByIdAndAuthorId } from "./db/deck/get-deck-by-id-and-author-id.ts";
import { shortUniqueId } from "./lib/short-unique-id/short-unique-id.ts";
import { Database } from "./db/databaseTypes.ts";
import { assert } from "./lib/typescript/assert.ts";
import {
getDeckWithCardsById
} from "./db/deck/get-deck-with-cards-by-id-db.ts";
import { getDeckWithCardsById } from "./db/deck/get-deck-with-cards-by-id-db.ts";

const requestSchema = z.object({
id: z.number().nullable().optional(),
title: z.string(),
description: z.string().nullable().optional(),
speakLocale: z.string().nullable().optional(),
speakField: z.string().nullable().optional(),
cards: z.array(
z.object({
front: z.string(),
Expand All @@ -39,6 +38,7 @@ export type UpsertDeckRequest = z.infer<typeof requestSchema>;
export type UpsertDeckResponse = z.infer<typeof deckWithCardsSchema>;

type InsertDeckDatabaseType = Database["public"]["Tables"]["deck"]["Insert"];
type DeckRow = Database["public"]["Tables"]["deck"]["Row"];

export const onRequestPost = handleError(async ({ request, env }) => {
const user = await getUser(request, env);
Expand All @@ -51,39 +51,34 @@ export const onRequestPost = handleError(async ({ request, env }) => {

const envSafe = envSchema.parse(env);
const db = getDatabase(envSafe);
let databaseDeck: DeckRow | null = null;

const upsertDataDynamic: { share_id?: string; is_public?: boolean } = {};

// Is edit
if (input.data.id) {
const databaseDeck = await getDeckByIdAndAuthorId(
databaseDeck = await getDeckByIdAndAuthorId(
envSafe,
input.data.id,
user.id,
);
if (!databaseDeck) {
return createForbiddenRequestResponse();
}
// https://github.com/orgs/supabase/discussions/3447
upsertDataDynamic.share_id = databaseDeck.share_id;
upsertDataDynamic.is_public = databaseDeck.is_public;
} else {
upsertDataDynamic.share_id = shortUniqueId();
upsertDataDynamic.is_public = false;
}
assert(upsertDataDynamic.share_id !== undefined);
assert(upsertDataDynamic.is_public !== undefined);

// prettier-ignore
const upsertData: InsertDeckDatabaseType = {
id: input.data.id ? input.data.id : undefined,
author_id: user.id,
name: input.data.title,
description: input.data.description,
share_id: upsertDataDynamic.share_id,
is_public: upsertDataDynamic.is_public,
share_id: input.data.id && databaseDeck ? databaseDeck.share_id : shortUniqueId(),
is_public: input.data.id && databaseDeck ? databaseDeck.is_public : false,
speak_field: input.data.speakField,
speak_locale: input.data.speakLocale,
};

const upsertDeckResult = await db.from("deck").upsert(upsertData)
const upsertDeckResult = await db
.from("deck")
.upsert(upsertData)
.select()
.single();

Expand Down Expand Up @@ -132,5 +127,8 @@ export const onRequestPost = handleError(async ({ request, env }) => {
});
}

return createJsonResponse<UpsertDeckResponse>(await getDeckWithCardsById(envSafe, upsertedDeck.id), 200);
return createJsonResponse<UpsertDeckResponse>(
await getDeckWithCardsById(envSafe, upsertedDeck.id),
200,
);
});
6 changes: 6 additions & 0 deletions src/lib/typescript/enum-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ export function enumValues<E extends EnumLike>(enumObject: E): E[keyof E][] {
.filter((key) => Number.isNaN(Number(key)))
.map((key) => enumObject[key] as E[keyof E]);
}

export function enumEntries<E extends EnumLike>(
enumObject: E,
): [keyof E, E[keyof E]][] {
return Object.entries(enumObject) as any
}
25 changes: 14 additions & 11 deletions src/lib/voice-playback/speak.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { reportHandledErrorOnce } from "../rollbar/rollbar.tsx";

export enum SpeakLanguageEnum {
AmericanEnglish = "en-US",
USEnglish = "en-US",
Italian = "it-IT",
Swedish = "sv-SE",
CanadianFrench = "fr-CA",
Malay = "ms-MY",
German = "de-DE",
BritishEnglish = "en-GB",
UKEnglish = "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",
Expand All @@ -26,14 +21,12 @@ export enum SpeakLanguageEnum {
Slovak = "sk-SK",
Hindi = "hi-IN",
Ukrainian = "uk-UA",
MainlandChinaChinese = "zh-CN",
Chinese = "zh-CN",
Vietnamese = "vi-VN",
ModernStandardArabic = "ar-001",
TaiwaneseChinese = "zh-TW",
Arabic = "ar-001",
Greek = "el-GR",
Russian = "ru-RU",
Danish = "da-DK",
HongKongChinese = "zh-HK",
Hungarian = "hu-HU",
Dutch = "nl-NL",
Turkish = "tr-TR",
Expand All @@ -42,6 +35,16 @@ export enum SpeakLanguageEnum {
Czech = "cs-CZ",
}

export const languageKeyToHuman = (str: string): string => {
if (str === "UKEnglish") {
return "UK English";
}
if (str === "USEnglish") {
return "US English";
}
return str.replace(/([A-Z])/g, " $1").trim();
};

export const speak = (text: string, language: SpeakLanguageEnum) => {
const isSpeechSynthesisSupported =
"speechSynthesis" in window &&
Expand Down
75 changes: 67 additions & 8 deletions src/screens/deck-form/deck-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ import { useTelegramProgress } from "../../lib/telegram/use-telegram-progress.ts
import { assert } from "../../lib/typescript/assert.ts";
import { SettingsRow } from "../user-settings/settings-row.tsx";
import { Button } from "../../ui/button.tsx";
import { HintTransparent } from "../../ui/hint-transparent.tsx";
import { RadioSwitcher } from "../../ui/radio-switcher.tsx";
import { Select } from "../../ui/select.tsx";
import { enumEntries } from "../../lib/typescript/enum-values.ts";
import {
languageKeyToHuman,
SpeakLanguageEnum,
} from "../../lib/voice-playback/speak.ts";
import { DeckSpeakFieldEnum } from "../../../functions/db/deck/decks-with-cards-schema.ts";
import { theme } from "../../ui/theme.tsx";

export const DeckForm = observer(() => {
const deckFormStore = useDeckFormStore();
Expand Down Expand Up @@ -57,7 +67,7 @@ export const DeckForm = observer(() => {
<Label text={"Description"}>
<Input
field={deckFormStore.form.description}
rows={5}
rows={3}
type={"textarea"}
/>
</Label>
Expand All @@ -73,14 +83,63 @@ export const DeckForm = observer(() => {
</SettingsRow>
)}

{/*<SettingsRow>*/}
{/* <span>Speaking cards</span>*/}
{/*</SettingsRow>*/}
{/*<HintTransparent>*/}
{/* Play spoken audio for each flashcard to enhance pronunciation*/}
{/*</HintTransparent>*/}
<SettingsRow>
<span>Speaking cards</span>
<RadioSwitcher
isOn={deckFormStore.isSpeakingCardEnabled}
onToggle={deckFormStore.toggleIsSpeakingCardEnabled}
/>
</SettingsRow>
{deckFormStore.isSpeakingCardEnabled ? (
<div
className={css({
display: "flex",
justifyContent: "space-between",
marginLeft: 12,
marginRight: 12,
})}
>
<div>
<div className={css({ fontSize: 14, color: theme.hintColor })}>
Voice language
</div>
{deckFormStore.form.speakingCardsLocale.value ? (
<Select<string>
value={deckFormStore.form.speakingCardsLocale.value}
onChange={deckFormStore.form.speakingCardsLocale.onChange}
options={enumEntries(SpeakLanguageEnum).map(([name, key]) => ({
value: key,
label: languageKeyToHuman(name),
}))}
/>
) : null}
</div>

<div>
<div className={css({ fontSize: 14, color: theme.hintColor })}>
Speak field
</div>
{deckFormStore.form.speakingCardsField.value ? (
<Select<DeckSpeakFieldEnum>
value={deckFormStore.form.speakingCardsField.value}
onChange={deckFormStore.form.speakingCardsField.onChange}
options={[
{ value: "front", label: "Front side" },
{ value: "back", label: "Back side" },
]}
/>
) : null}
</div>
</div>
) : (
<>
<HintTransparent>
Play spoken audio for each flashcard to enhance pronunciation
</HintTransparent>
</>
)}

{/*<div className={css({ marginTop: 18 })}/>*/}
<div className={css({ marginTop: 18 })} />

<Button
onClick={() => {
Expand Down
17 changes: 6 additions & 11 deletions src/store/card-under-review-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { makeAutoObservable } from "mobx";
import { assert } from "../lib/typescript/assert.ts";
import { DeckCardDbType } from "../../functions/db/deck/decks-with-cards-schema.ts";
import {
DeckCardDbType,
DeckSpeakFieldEnum,
} 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";
Expand All @@ -10,19 +13,14 @@ export enum CardState {
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;
deckSpeakField: DeckSpeakFieldEnum | null = null;

isOpened = false;
state?: CardState;
Expand Down Expand Up @@ -56,10 +54,7 @@ export class CardUnderReviewStore {
if (!this.deckSpeakLocale || !this.deckSpeakField) {
return;
}
if (
!isEnumValid(this.deckSpeakField, DeckSpeakField) ||
!isEnumValid(this.deckSpeakLocale, SpeakLanguageEnum)
) {
if (!isEnumValid(this.deckSpeakLocale, SpeakLanguageEnum)) {
return;
}

Expand Down
Loading