diff --git a/src/api/create-cached-card-input-modes-request.ts b/src/api/create-cached-card-input-modes-request.ts new file mode 100644 index 00000000..3ab061df --- /dev/null +++ b/src/api/create-cached-card-input-modes-request.ts @@ -0,0 +1,8 @@ +import { RequestStore } from "../lib/mobx-request/request-store.ts"; +import { cardInputModeListRequest } from "./api.ts"; + +export const createCachedCardInputModesRequest = () => { + return new RequestStore(cardInputModeListRequest, { + cacheId: "cardInputModeList", + }); +}; diff --git a/src/screens/ai-mass-creation/store/ai-mass-creation-store.test.ts b/src/screens/ai-mass-creation/store/ai-mass-creation-store.test.ts index 09698866..41495552 100644 --- a/src/screens/ai-mass-creation/store/ai-mass-creation-store.test.ts +++ b/src/screens/ai-mass-creation/store/ai-mass-creation-store.test.ts @@ -1,4 +1,4 @@ -import { test, vi, expect } from "vitest"; +import { expect, test, vi } from "vitest"; import { AiMassCreationStore } from "./ai-mass-creation-store.ts"; import { when } from "mobx"; import { isFormValid } from "mobx-form-lite"; diff --git a/src/screens/app.tsx b/src/screens/app.tsx index b4cdd2b3..3ae338a6 100644 --- a/src/screens/app.tsx +++ b/src/screens/app.tsx @@ -40,7 +40,6 @@ import { SnackbarProviderWrapper } from "./shared/snackbar/snackbar-provider-wra import { Debug } from "./debug/debug.tsx"; import { BrowserHeader } from "./shared/browser-platform/browser-header.tsx"; import { BrowserMainButton } from "./shared/browser-platform/browser-main-button.tsx"; -import { CardInputModeScreen } from "./card-input-mode/card-input-mode-screen.tsx"; export const App = observer(() => { useRestoreFullScreenExpand(); @@ -170,11 +169,6 @@ export const App = observer(() => { )} - {screenStore.screen.type === "cardInputMode" && ( - - - - )} ); diff --git a/src/screens/card-input-mode/card-input-mode-screen.tsx b/src/screens/card-input-mode/card-input-mode-screen.tsx index deecfacf..e634f458 100644 --- a/src/screens/card-input-mode/card-input-mode-screen.tsx +++ b/src/screens/card-input-mode/card-input-mode-screen.tsx @@ -1,7 +1,6 @@ import { observer } from "mobx-react-lite"; import { Screen } from "../shared/screen.tsx"; import { useBackButton } from "../../lib/platform/use-back-button.ts"; -import { screenStore } from "../../store/screen-store.ts"; import { useState } from "react"; import { CardInputModeStore } from "./store/card-input-mode-store.ts"; import { useMount } from "../../lib/react/use-mount.ts"; @@ -10,12 +9,16 @@ import { RadioList } from "../../ui/radio-list/radio-list.tsx"; import { useMainButton } from "../../lib/platform/use-main-button.ts"; import { useProgress } from "../../lib/platform/use-progress.tsx"; import { t } from "../../translations/t.ts"; +import { DeckFormStore } from "../deck-form/deck-form/store/deck-form-store.ts"; -export const CardInputModeScreen = observer(() => { - const [store] = useState(() => new CardInputModeStore()); +type Props = { deckFormStore: DeckFormStore }; + +export const CardInputModeScreen = observer((props: Props) => { + const { deckFormStore } = props; + const [store] = useState(() => new CardInputModeStore(deckFormStore)); useBackButton(() => { - screenStore.back(); + deckFormStore.quitInnerScreen(); }); useMount(() => { @@ -31,7 +34,7 @@ export const CardInputModeScreen = observer(() => { }); return ( - + {store.cardInputModesRequest.result.status === "loading" ? ( ) : null} diff --git a/src/screens/card-input-mode/store/card-input-mode-store.ts b/src/screens/card-input-mode/store/card-input-mode-store.ts index f1886b51..265514a7 100644 --- a/src/screens/card-input-mode/store/card-input-mode-store.ts +++ b/src/screens/card-input-mode/store/card-input-mode-store.ts @@ -1,36 +1,28 @@ import { RequestStore } from "../../../lib/mobx-request/request-store.ts"; -import { - cardInputModeListRequest, - deckChangeInputModeRequest, -} from "../../../api/api.ts"; +import { deckChangeInputModeRequest } from "../../../api/api.ts"; import { makeAutoObservable } from "mobx"; import { TextField } from "mobx-form-lite"; -import { screenStore } from "../../../store/screen-store.ts"; import { assert } from "../../../lib/typescript/assert.ts"; import { notifyError, notifySuccess } from "../../shared/snackbar/snackbar.tsx"; import { deckListStore } from "../../../store/deck-list-store.ts"; import { t } from "../../../translations/t.ts"; - -export const createCachedCardInputModesRequest = () => { - return new RequestStore(cardInputModeListRequest, { - cacheId: "cardInputModeList", - }); -}; +import { DeckFormStore } from "../../deck-form/deck-form/store/deck-form-store.ts"; +import { createCachedCardInputModesRequest } from "../../../api/create-cached-card-input-modes-request.ts"; export class CardInputModeStore { cardInputModesRequest = createCachedCardInputModesRequest(); deckChangeInputModeRequest = new RequestStore(deckChangeInputModeRequest); modeId = new TextField(null); - constructor() { + constructor(private deckFormStore: DeckFormStore) { makeAutoObservable(this, {}, { autoBind: true }); } load() { - const { screen } = screenStore; - assert(screen.type === "cardInputMode"); + assert(this.deckFormStore.deckForm, "Deck form should be loaded"); + const cardInputModeId = this.deckFormStore.deckForm.cardInputModeId; - this.modeId.onChange(screen.cardInputModeId); + this.modeId.onChange(cardInputModeId); this.cardInputModesRequest.execute(); } @@ -39,11 +31,12 @@ export class CardInputModeStore { return; } - const { screen } = screenStore; - assert(screen.type === "cardInputMode"); + assert(this.deckFormStore.deckForm, "Deck form should be loaded"); + const deckId = this.deckFormStore.deckForm.id; + assert(deckId, "Deck id should be defined"); const result = await this.deckChangeInputModeRequest.execute({ - deckId: screen.deckId, + deckId: deckId, cardInputModeId: this.modeId.value, }); @@ -55,11 +48,8 @@ export class CardInputModeStore { return; } - deckListStore.updateDeckCardInputMode(screen.deckId, this.modeId.value); + deckListStore.updateDeckCardInputMode(deckId, this.modeId.value); notifySuccess(t("card_input_mode_changed")); - screenStore.go({ - type: "deckForm", - deckId: screen.deckId, - }); + this.deckFormStore.quitInnerScreen(); } } diff --git a/src/screens/deck-form/card-form/generated-card-form-view.tsx b/src/screens/deck-form/card-form/generated-card-form-view.tsx index fa637825..e0ac3f96 100644 --- a/src/screens/deck-form/card-form/generated-card-form-view.tsx +++ b/src/screens/deck-form/card-form/generated-card-form-view.tsx @@ -14,9 +14,6 @@ import { Input } from "../../../ui/input.tsx"; import { HintTransparent } from "../../../ui/hint-transparent.tsx"; import { CardRowLoading } from "../../shared/card-row-loading.tsx"; import { CardRow } from "../../../ui/card-row.tsx"; -import { css } from "@emotion/css"; -import { theme } from "../../../ui/theme.tsx"; -import { screenStore } from "../../../store/screen-store.ts"; type Props = { cardFormStore: CardFormStoreInterface }; @@ -68,21 +65,6 @@ export const GeneratedCardFormView = observer((props: Props) => { return ( {inputMode.title} - { - screenStore.go({ - type: "cardInputMode", - cardInputModeId: cardInputModeId, - deckId: localStore.deckId, - }); - }} - className={css({ - cursor: "pointer", - color: theme.linkColor, - })} - > - {t("edit")} - ); })() diff --git a/src/screens/deck-form/card-form/manual-card-form-view.tsx b/src/screens/deck-form/card-form/manual-card-form-view.tsx index 74ebee30..e30321b5 100644 --- a/src/screens/deck-form/card-form/manual-card-form-view.tsx +++ b/src/screens/deck-form/card-form/manual-card-form-view.tsx @@ -25,6 +25,7 @@ import { ButtonGrid } from "../../../ui/button-grid.tsx"; import { boolNarrow } from "../../../lib/typescript/bool-narrow.ts"; import { CardAnswerErrors } from "./card-answer-errors.tsx"; import { css } from "@emotion/css"; +import { screenStore } from "../../../store/screen-store.ts"; type Props = { cardFormStore: CardFormStoreInterface }; @@ -40,6 +41,13 @@ export const ManualCardFormView = observer((props: Props) => { useProgress(() => cardFormStore.isSending); useBackButton(() => { + + const screen = screenStore.screen; + // Avoid duplicated 'deckForm' in the router history + if (screen.type === "deckForm" && screen.cardId) { + screenStore.back(); + } + cardFormStore.onBackCard(); }); diff --git a/src/screens/deck-form/card-form/store/ai-generated-card-form-store.ts b/src/screens/deck-form/card-form/store/ai-generated-card-form-store.ts index a821e074..45a78ef9 100644 --- a/src/screens/deck-form/card-form/store/ai-generated-card-form-store.ts +++ b/src/screens/deck-form/card-form/store/ai-generated-card-form-store.ts @@ -1,5 +1,4 @@ import { createCardSideField } from "../../deck-form/store/deck-form-store.ts"; -import { createCachedCardInputModesRequest } from "../../../card-input-mode/store/card-input-mode-store.ts"; import { RequestStore } from "../../../../lib/mobx-request/request-store.ts"; import { makeAutoObservable } from "mobx"; import { formTouchAll, isFormValid } from "mobx-form-lite"; @@ -8,6 +7,7 @@ import { assert } from "../../../../lib/typescript/assert.ts"; import { notifyError } from "../../../shared/snackbar/snackbar.tsx"; import { aiSingleCardGenerateRequest } from "../../../../api/api.ts"; import { deckListStore } from "../../../../store/deck-list-store.ts"; +import { createCachedCardInputModesRequest } from "../../../../api/create-cached-card-input-modes-request.ts"; export class AiGeneratedCardFormStore { form = { @@ -26,7 +26,6 @@ export class AiGeneratedCardFormStore { return; } - const result = await this.aiSingleCardGenerateRequest.execute({ text: this.form.prompt.value, deckId: this.deckId, @@ -47,7 +46,11 @@ export class AiGeneratedCardFormStore { const { card } = result.data.data; deckListStore.addCardOptimistic(card); - screenStore.go({ type: "deckForm", deckId: card.deck_id, cardId: card.id }); + screenStore.goOnce({ + type: "deckForm", + deckId: card.deck_id, + cardId: card.id, + }); } get deckId() { diff --git a/src/screens/deck-form/deck-form/deck-form-screen.tsx b/src/screens/deck-form/deck-form/deck-form-screen.tsx index f1dfd30f..88634e35 100644 --- a/src/screens/deck-form/deck-form/deck-form-screen.tsx +++ b/src/screens/deck-form/deck-form/deck-form-screen.tsx @@ -6,6 +6,7 @@ import { CardList } from "./card-list.tsx"; import { CardFormWrapper } from "../card-form/card-form-wrapper.tsx"; import { PreventTelegramSwipeDownClosingIos } from "../../../lib/platform/telegram/prevent-telegram-swipe-down-closing.tsx"; import { SpeakingCards } from "./speaking-cards.tsx"; +import { CardInputModeScreen } from "../../card-input-mode/card-input-mode-screen.tsx"; export const DeckFormScreen = observer(() => { const deckFormStore = useDeckFormStore(); @@ -34,6 +35,18 @@ export const DeckFormScreen = observer(() => { ); } - // No PreventTelegramSwipeDownClosingIos because the description textarea usually requires scroll - return ; + if (deckFormStore.deckFormScreen === "cardInputMode") { + return ( + + + + ); + } + + if (deckFormStore.deckFormScreen === "deckForm") { + // No PreventTelegramSwipeDownClosingIos because the description textarea usually requires scroll + return ; + } + + return deckFormStore.deckFormScreen satisfies never; }); diff --git a/src/screens/deck-form/deck-form/deck-form.tsx b/src/screens/deck-form/deck-form/deck-form.tsx index 0ef21212..9251cf73 100644 --- a/src/screens/deck-form/deck-form/deck-form.tsx +++ b/src/screens/deck-form/deck-form/deck-form.tsx @@ -46,6 +46,7 @@ export const DeckForm = observer(() => { useProgress(() => deckFormStore.isSending); if (!deckFormStore.deckForm) { + console.log("Deck form is not loaded"); return null; } @@ -174,19 +175,7 @@ export const DeckForm = observer(() => { /> ), onClick: () => { - if ( - !deckFormStore.deckForm || - !isFormValid(deckFormStore.deckForm) - ) { - return; - } - const deckId = deckFormStore.deckForm.id; - assert(deckId, "Deck id should be defined"); - screenStore.go({ - type: "cardInputMode", - deckId: deckId, - cardInputModeId: deckFormStore.deckForm.cardInputModeId, - }); + deckFormStore.goCardInputMode(); }, } : undefined, diff --git a/src/screens/deck-form/deck-form/store/deck-form-store.ts b/src/screens/deck-form/deck-form/store/deck-form-store.ts index efda0118..68081520 100644 --- a/src/screens/deck-form/deck-form/store/deck-form-store.ts +++ b/src/screens/deck-form/deck-form/store/deck-form-store.ts @@ -177,7 +177,7 @@ export class DeckFormStore implements CardFormStoreInterface { deckForm?: DeckFormType; upsertDeckRequest = new RequestStore(upsertDeckRequest); cardInnerScreen = new TextField(null); - deckInnerScreen?: "cardList" | "speakingCards"; + deckInnerScreen?: "cardList" | "speakingCards" | "cardInputMode"; cardFilter = { text: new TextField(""), sortBy: new TextField("createdAt"), @@ -203,10 +203,6 @@ export class DeckFormStore implements CardFormStoreInterface { } loadForm() { - if (this.deckForm) { - return; - } - const screen = screenStore.screen; assert(screen.type === "deckForm"); @@ -254,6 +250,17 @@ export class DeckFormStore implements CardFormStoreInterface { this.deckInnerScreen = "cardList"; } + goCardInputMode() { + if (!this.deckForm) { + return; + } + if (!isFormValid(this.deckForm)) { + formTouchAll(this.deckForm); + return; + } + this.deckInnerScreen = "cardInputMode"; + } + quitInnerScreen() { this.deckInnerScreen = undefined; } diff --git a/src/screens/deck-list/view-more-decks-toggle.tsx b/src/screens/deck-list/view-more-decks-toggle.tsx index 52814a47..da38ae8a 100644 --- a/src/screens/deck-list/view-more-decks-toggle.tsx +++ b/src/screens/deck-list/view-more-decks-toggle.tsx @@ -26,7 +26,7 @@ export const ViewMoreDecksToggle = observer(() => { )} onClick={deckListStore.isMyDecksExpanded.toggle} > - + diff --git a/src/screens/deck-review/deck-preview.tsx b/src/screens/deck-review/deck-preview.tsx index 7de78294..d6390cc2 100644 --- a/src/screens/deck-review/deck-preview.tsx +++ b/src/screens/deck-review/deck-preview.tsx @@ -119,7 +119,7 @@ export const DeckPreview = observer(() => { icon={"mdi-plus-circle mdi-24px"} outline onClick={() => { - screenStore.go({ + screenStore.goOnce({ type: "cardQuickAddForm", deckId: deck.id, }); diff --git a/src/screens/deck-review/review-deck-name.tsx b/src/screens/deck-review/review-deck-name.tsx index 3e70a16a..51a92b99 100644 --- a/src/screens/deck-review/review-deck-name.tsx +++ b/src/screens/deck-review/review-deck-name.tsx @@ -2,7 +2,7 @@ import { useReviewStore } from "./store/review-store-context.tsx"; import { css } from "@emotion/css"; import { theme } from "../../ui/theme.tsx"; import React from "react"; -import { m, AnimatePresence } from "framer-motion"; +import { AnimatePresence, m } from "framer-motion"; import { observer } from "mobx-react-lite"; import { LazyLoadFramerMotion } from "../../lib/framer-motion/lazy-load-framer-motion.tsx"; diff --git a/src/screens/plans/format-paid-until.test.ts b/src/screens/plans/format-paid-until.test.ts index 9d8fcecc..744803c2 100644 --- a/src/screens/plans/format-paid-until.test.ts +++ b/src/screens/plans/format-paid-until.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { formatPaidUntil } from "./format-paid-until.tsx"; describe("formatPaidUntil", () => { diff --git a/src/store/screen-store.test.ts b/src/store/screen-store.test.ts new file mode 100644 index 00000000..178f0e2a --- /dev/null +++ b/src/store/screen-store.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from "vitest"; +import { ScreenStore } from "./screen-store.ts"; + +test("screen store - push", () => { + const store = new ScreenStore(); + expect(store.screen).toEqual({ type: "main" }); + store.go({ type: "plans" }); + expect(store.screen).toEqual({ type: "plans" }); + store.back(); + expect(store.screen).toEqual({ type: "main" }); +}); + +test("screen store - push 1", () => { + const store = new ScreenStore(); + expect(store.screen).toEqual({ type: "main" }); + store.goOnce({ type: "plans" }); + expect(store.screen).toEqual({ type: "plans" }); + store.go({ type: "deckCatalog" }); + expect(store.screen).toEqual({ type: "deckCatalog" }); + store.back(); + expect(store.screen).toEqual({ type: "main" }); +}); + +test("screen store - push 2", () => { + const store = new ScreenStore(); + expect(store.screen).toEqual({ type: "main" }); + store.goOnce({ type: "plans" }); + expect(store.screen).toEqual({ type: "plans" }); + store.back(); + expect(store.screen).toEqual({ type: "main" }); +}); diff --git a/src/store/screen-store.ts b/src/store/screen-store.ts index 877cefad..d3bd0e59 100644 --- a/src/store/screen-store.ts +++ b/src/store/screen-store.ts @@ -22,11 +22,6 @@ type Route = | { type: "shareDeck"; deckId: number; shareId: string } | { type: "shareFolder"; folderId: number; shareId: string } | { type: "aiMassCreation"; deckId: number; deckTitle: string | null } - | { - type: "cardInputMode"; - deckId: number; - cardInputModeId: string | null; - } | { type: "plans" } | { type: "debug" } | { type: "componentCatalog" } @@ -34,27 +29,40 @@ type Route = | { type: "userStatistics" } | { type: "userSettings" }; -export type RouteType = Route["type"]; - export class ScreenStore { - history: Route[] = [{ type: "main" }]; + private history: Route[] = [{ type: "main" }]; + private onceRoute?: Route; constructor() { makeAutoObservable(this, {}, { autoBind: true }); makeLoggable(this); } - go(historyData: Route) { - this.history.push(historyData); + go(route: Route) { + if (this.onceRoute) { + this.onceRoute = undefined; + } + this.history.push(route); + } + + goOnce(route: Route) { + this.onceRoute = route; } back() { + if (this.onceRoute) { + this.onceRoute = undefined; + return; + } if (this.history.length > 1) { this.history.pop(); } } get screen(): Route { + if (this.onceRoute) { + return this.onceRoute; + } return this.history[this.history.length - 1]; } diff --git a/src/ui/dropdown.tsx b/src/ui/dropdown.tsx index 35b04f69..de2ff407 100644 --- a/src/ui/dropdown.tsx +++ b/src/ui/dropdown.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, ReactNode, useState } from "react"; +import React, { ReactNode, useEffect, useRef, useState } from "react"; import { css, cx } from "@emotion/css"; import { theme } from "./theme.tsx"; import { tapScale } from "../lib/animations/tap-scale.ts";