From faf3aee64c837743a3a6d8e0f643f52373dd031a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Advaita=20K=E1=B9=9B=E1=B9=A3=E1=B9=87a=20D=C4=81sa?= Date: Sun, 19 Feb 2023 13:34:13 +0100 Subject: [PATCH] feat: cleanup (#156) --- .eslintrc.js | 17 ++-- playwright.config.ts | 36 ++++---- src/App.vue | 2 +- src/app/Application.ts | 10 +-- src/app/Events.ts | 2 +- src/app/composables/useDialog.ts | 2 +- src/app/composables/useToast.ts | 6 +- src/app/decks/UserMovesCards.ts | 4 +- .../decks/inbox/components/InboxDeckPage.vue | 73 +++++++++-------- .../inbox/components/cards/InboxCard.vue | 4 +- .../decks/inbox/composables/useInboxDeck.ts | 51 ++++++++++++ .../inbox/composables/useInboxDeckTutorial.ts | 51 ++++++++++++ .../controllers/InboxDeckCardsController.ts | 82 ------------------- .../InboxDeckTutorialController.ts | 54 ------------ src/app/decks/inbox/index.ts | 7 +- src/app/decks/inbox/stores/useInboxStore.ts | 2 +- .../decks/inbox/tasks/SyncInboxDeckTask.ts | 38 +++++++++ .../review/components/ReviewDeckPage.vue | 61 ++++++++------ .../review/components/cards/ReviewCard.vue | 4 +- .../cards/ReviewCardSwipeOverlay.vue | 2 +- .../decks/review/composables/useReviewDeck.ts | 30 +++++++ .../composables/useReviewDeckTutorial.ts | 49 +++++++++++ src/app/decks/review/index.ts | 7 +- src/app/decks/review/stores/useReviewStore.ts | 2 +- .../decks/review/tasks/SyncReviewDeckTask.ts | 37 +++++++++ .../useCases/ReviewDeckCardsController.ts | 60 -------------- .../useCases/ReviewDeckTutorialController.ts | 52 ------------ src/app/decks/shared/CardViewModel.ts | 4 +- src/app/decks/shared/CardsDeck.vue | 10 +-- src/app/decks/shared/Deck.ts | 4 +- src/app/decks/shared/OrderedCollection.ts | 4 +- src/app/decks/shared/Vector3d.ts | 10 +-- src/app/decks/shared/VerseTextLines.vue | 4 +- src/app/decks/shared/index.ts | 25 +++--- .../decks/shared/stores/useTutorialStore.ts | 2 +- src/app/decks/shared/theme/DeckBehaviour.ts | 9 -- .../shared/theme/StackedDeckBehaviour.ts | 40 +++++---- .../shared/tutorial/TutorialCardViewModel.ts | 2 +- src/app/library/components/LibraryPage.vue | 42 +++++----- src/app/library/composables/useAddVerse.ts | 32 ++++++++ src/app/library/composables/useLibrary.ts | 50 +++++++++++ .../controllers/LibraryAddVerseController.ts | 49 ----------- .../controllers/LibraryVersesController.ts | 49 ----------- src/app/library/index.ts | 6 +- src/app/library/models/DummyLibraryVerse.ts | 6 +- .../components/GeneralSettingsPage.vue | 2 +- .../components/account/AccountPage.vue | 4 +- src/app/settings/stores/useAccountStore.ts | 12 +-- src/app/settings/stores/useLocaleStore.ts | 10 +-- src/app/shared/index.ts | 3 +- src/app/shared/tasks/RefreshTokenTask.ts | 41 ++++++++++ .../controllers/StatisticsController.ts | 30 ------- src/app/statistics/index.ts | 2 +- .../statistics/stores/useStatisticsStore.ts | 4 +- .../statistics/tasks/UpdateStatisticsTask.ts | 25 ++++++ src/app/utils/sync.ts | 8 +- src/init/{stage-1 => app}/initCommands.ts | 6 +- src/init/{stage-1 => app}/initLocale.ts | 12 +-- src/init/{stage-1 => app}/initParams.ts | 6 +- src/init/{stage-1 => app}/initStaticData.ts | 2 +- src/init/{stage-1 => app}/initStores.ts | 6 +- src/init/app/initTasks.ts | 18 ++++ src/init/index.ts | 30 ++++--- .../initAppStateChange.ts | 5 +- src/init/infrastructure/initDeviceStorage.ts | 9 ++ src/init/infrastructure/initEmitter.ts | 11 +++ .../{stage-0 => infrastructure}/initI18n.ts | 6 +- src/init/infrastructure/initLogging.ts | 24 ++++++ .../{stage-0 => infrastructure}/initPinia.ts | 0 .../{stage-0 => infrastructure}/initSentry.ts | 1 + src/init/initialization.ts | 4 +- src/init/stage-0/initDeviceStorage.ts | 11 --- src/init/stage-0/initEmitter.ts | 16 ---- src/init/stage-0/initLogging.ts | 15 ---- src/init/stage-1/initControllers.ts | 26 ------ src/init/stage-1/initSyncTask.ts | 32 -------- src/main.ts | 15 ++-- src/services/AuthService.ts | 10 +-- .../persistence/InboxCardSerializer.ts | 22 ++--- src/services/persistence/PouchRepository.ts | 14 ++-- .../persistence/ReviewCardSerializer.ts | 22 ++--- .../persistence/VerseStatusSerializer.ts | 18 ++-- tests/e2e/components/Account.ts | 4 +- tests/e2e/components/Application.ts | 2 +- tests/e2e/components/InboxDeckPage.ts | 12 +-- tests/e2e/components/LibraryPage.ts | 6 +- tests/e2e/components/ReviewDeckPage.ts | 14 ++-- tests/e2e/components/Settings.ts | 4 +- tests/e2e/components/TabsBar.ts | 10 +-- tests/e2e/fixtures/basic.ts | 2 +- tests/e2e/inbox/InboxDeck.spec.ts | 20 ++--- tests/e2e/library/AddToInbox.spec.ts | 4 +- tests/e2e/library/Search.spec.ts | 2 +- tests/e2e/review/GradeButtons.spec.ts | 4 +- tests/e2e/review/ReviewDeck.spec.ts | 2 +- tests/e2e/review/Schedule.spec.ts | 8 +- tests/e2e/review/SwipeCards.spec.ts | 2 +- tests/e2e/review/TutorialCards.spec.ts | 2 +- tests/e2e/scenarios/accounts.ts | 12 +-- tests/e2e/scenarios/cards.ts | 8 +- tests/e2e/settings/Language.spec.ts | 6 +- tests/e2e/settings/Registration.spec.ts | 4 +- tests/e2e/settings/Sync.spec.ts | 20 ++--- 103 files changed, 863 insertions(+), 860 deletions(-) create mode 100644 src/app/decks/inbox/composables/useInboxDeck.ts create mode 100644 src/app/decks/inbox/composables/useInboxDeckTutorial.ts delete mode 100644 src/app/decks/inbox/controllers/InboxDeckCardsController.ts delete mode 100644 src/app/decks/inbox/controllers/InboxDeckTutorialController.ts create mode 100644 src/app/decks/inbox/tasks/SyncInboxDeckTask.ts create mode 100644 src/app/decks/review/composables/useReviewDeck.ts create mode 100644 src/app/decks/review/composables/useReviewDeckTutorial.ts create mode 100644 src/app/decks/review/tasks/SyncReviewDeckTask.ts delete mode 100644 src/app/decks/review/useCases/ReviewDeckCardsController.ts delete mode 100644 src/app/decks/review/useCases/ReviewDeckTutorialController.ts delete mode 100644 src/app/decks/shared/theme/DeckBehaviour.ts create mode 100644 src/app/library/composables/useAddVerse.ts create mode 100644 src/app/library/composables/useLibrary.ts delete mode 100644 src/app/library/controllers/LibraryAddVerseController.ts delete mode 100644 src/app/library/controllers/LibraryVersesController.ts create mode 100644 src/app/shared/tasks/RefreshTokenTask.ts delete mode 100644 src/app/statistics/controllers/StatisticsController.ts create mode 100644 src/app/statistics/tasks/UpdateStatisticsTask.ts rename src/init/{stage-1 => app}/initCommands.ts (70%) rename src/init/{stage-1 => app}/initLocale.ts (72%) rename src/init/{stage-1 => app}/initParams.ts (84%) rename src/init/{stage-1 => app}/initStaticData.ts (95%) rename src/init/{stage-1 => app}/initStores.ts (92%) create mode 100644 src/init/app/initTasks.ts rename src/init/{stage-0 => infrastructure}/initAppStateChange.ts (72%) create mode 100644 src/init/infrastructure/initDeviceStorage.ts create mode 100644 src/init/infrastructure/initEmitter.ts rename src/init/{stage-0 => infrastructure}/initI18n.ts (76%) create mode 100644 src/init/infrastructure/initLogging.ts rename src/init/{stage-0 => infrastructure}/initPinia.ts (100%) rename src/init/{stage-0 => infrastructure}/initSentry.ts (99%) delete mode 100644 src/init/stage-0/initDeviceStorage.ts delete mode 100644 src/init/stage-0/initEmitter.ts delete mode 100644 src/init/stage-0/initLogging.ts delete mode 100644 src/init/stage-1/initControllers.ts delete mode 100644 src/init/stage-1/initSyncTask.ts diff --git a/.eslintrc.js b/.eslintrc.js index b6ee438a..d3d1cf08 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,8 +5,8 @@ module.exports = { }, 'extends': [ 'plugin:vue/vue3-essential', - "plugin:vue/vue3-strongly-recommended", - "plugin:vue/vue3-recommended", + 'plugin:vue/vue3-strongly-recommended', + 'plugin:vue/vue3-recommended', 'eslint:recommended', 'plugin:import/recommended', '@vue/typescript/recommended', @@ -25,18 +25,19 @@ module.exports = { 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'vue/no-deprecated-slot-attribute': 'off', '@typescript-eslint/no-explicit-any': 'off', - "import/order": [ - "error", + 'import/order': [ + 'error', { groups: [ - "builtin", "external", "internal", "parent", "sibling", "index", "object" + 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object' ] } ], - "semi": ["error", "never"], - "@typescript-eslint/ban-ts-comment": "off" + 'semi': ['error', 'never'], + '@typescript-eslint/ban-ts-comment': 'off', + 'quotes': ['error', 'single'], }, - ignorePatterns: ["dist/**"], + ignorePatterns: ['dist/**'], overrides: [ { files: [ diff --git a/playwright.config.ts b/playwright.config.ts index 8f746aa6..6b56170f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -49,26 +49,26 @@ const config: PlaywrightTestConfig = { /* Configure projects for major browsers */ projects: [ - { - name: 'chromium', - use: { - ...devices['Desktop Chrome'], - }, - }, + // { + // name: 'chromium', + // use: { + // ...devices['Desktop Chrome'], + // }, + // }, - { - name: 'firefox', - use: { - ...devices['Desktop Firefox'], - }, - }, + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, - { - name: 'webkit', - use: { - ...devices['Desktop Safari'], - }, - }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, /* Test against mobile viewports. */ { diff --git a/src/App.vue b/src/App.vue index a30385fc..0f2d0c64 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,7 +9,7 @@ import { IonApp, IonRouterOutlet } from '@ionic/vue' // Notch! try { - if (window.location.search.includes("demo")) { + if (window.location.search.includes('demo')) { var css = `:root { --ion-safe-area-top: 20px; --ion-safe-area-bottom: 22px; diff --git a/src/app/Application.ts b/src/app/Application.ts index 5d9ae73f..808abbc5 100644 --- a/src/app/Application.ts +++ b/src/app/Application.ts @@ -1,6 +1,6 @@ import { InMemoryRepository } from '@akdasa-studios/framework' import { SyncRepository } from '@akdasa-studios/framework-sync' -import { Application, InboxCard, Repositories, ReviewCard, Verse, VerseStatus } from "@akdasa-studios/shlokas-core" +import { Application, InboxCard, Repositories, ReviewCard, Verse, VerseStatus } from '@akdasa-studios/shlokas-core' import { Capacitor } from '@capacitor/core' import { CouchDB, InboxCardDeserializer, @@ -13,7 +13,7 @@ export let couchDB: CouchDB export async function createShlokasApplication() { couchDB = new CouchDB( - "local", + 'local', Capacitor.getPlatform() == 'ios' ? 'cordova-sqlite' : undefined ) @@ -22,19 +22,19 @@ export async function createShlokasApplication() { // @ts-ignore new PouchRepository( couchDB, - "verseStatus", + 'verseStatus', new VerseStatusSerializer(), new VerseStatusDeserializer() ), new SyncRepository(new PouchRepository( couchDB, - "inbox", + 'inbox', new InboxCardSerializer(), new InboxCardDeserializer() )), new SyncRepository(new PouchRepository( couchDB, - "review", + 'review', new ReviewCardSerializer(), new ReviewCardDeserializer() )), diff --git a/src/app/Events.ts b/src/app/Events.ts index 4c26a628..0e5561c1 100644 --- a/src/app/Events.ts +++ b/src/app/Events.ts @@ -3,5 +3,5 @@ import { Command, AnyResult } from '@akdasa-studios/framework' export type Events = { commandExecuted: Command syncCompleted: void, - appOpened: void + appStateChanged: { isActive: boolean } } \ No newline at end of file diff --git a/src/app/composables/useDialog.ts b/src/app/composables/useDialog.ts index d2439fb0..e34e527b 100644 --- a/src/app/composables/useDialog.ts +++ b/src/app/composables/useDialog.ts @@ -1,4 +1,4 @@ -import { Ref, ref } from "vue" +import { Ref, ref } from 'vue' export function useDialog(defaultData: any) { const isOpen = ref(false) diff --git a/src/app/composables/useToast.ts b/src/app/composables/useToast.ts index 509dde76..826755a0 100644 --- a/src/app/composables/useToast.ts +++ b/src/app/composables/useToast.ts @@ -1,4 +1,4 @@ -import { Ref, ref } from "vue" +import { Ref, ref } from 'vue' interface ToastArgs { message?: string @@ -7,11 +7,11 @@ interface ToastArgs { export function useToast() { const isOpen = ref(false) - const message = ref("") + const message = ref('') const data: Ref> = ref({}) function open(options?: ToastArgs) { - message.value = options?.message || "" + message.value = options?.message || '' data.value = options?.data isOpen.value = true } diff --git a/src/app/decks/UserMovesCards.ts b/src/app/decks/UserMovesCards.ts index 2d109005..61a79b10 100644 --- a/src/app/decks/UserMovesCards.ts +++ b/src/app/decks/UserMovesCards.ts @@ -1,5 +1,5 @@ -import { CardViewModel } from "./shared/CardViewModel" -import { Vector3d } from "./shared/Vector3d" +import { CardViewModel } from './shared/CardViewModel' +import { Vector3d } from './shared/Vector3d' export class DeckCardInteraction { constructor( diff --git a/src/app/decks/inbox/components/InboxDeckPage.vue b/src/app/decks/inbox/components/InboxDeckPage.vue index e66eaa3a..97538ed0 100644 --- a/src/app/decks/inbox/components/InboxDeckPage.vue +++ b/src/app/decks/inbox/components/InboxDeckPage.vue @@ -17,7 +17,7 @@ :scroll-x="false" > @@ -58,30 +58,43 @@ import { IonContent, IonHeader, IonPage, IonTitle, IonToast, IonToolbar } from '@ionic/vue' -import { computed, inject } from 'vue' +import { inject } from 'vue' +import { Application } from '@akdasa-studios/shlokas-core' import { - CardsDeck , StackedDeckBehaviour, Vector3d, VerseCardViewModel, - TutorialCard, TutorialCardViewModel + CardsDeck, Vector3d, VerseCardViewModel, + TutorialCard, TutorialCardViewModel, useStackedDeck } from '@/app/decks/shared' import { InboxCard, InboxCardViewModel, InboxDeckEmpty, - InboxDeckTutorialController, InboxVerseCardViewModel, - MemorizingStatus, InboxDeckCardsController + MemorizingStatus, useInboxDeck, useInboxDeckTutorial, } from '@/app/decks/inbox' import { testId } from '@/app/TestId' -const cardsMemorization = inject('inboxDeckCardsController') as InboxDeckCardsController -const inboxDeckTutorial = inject('inboxDeckTutorialController') as InboxDeckTutorialController +const app = inject('app') as Application + +const { + isEmpty, cardsToShow, cardMemorizedToast, + shiftTopCard, memorizeTopCard, topCard, revert +} = useInboxDeck(app) +const { + tutorialCardSwiped, addTutorialCards +} = useInboxDeckTutorial() +const { + updateInactiveCard, + updateMovingCard, + updateMovedCard, + swipeThreshold +} = useStackedDeck() + +addTutorialCards() + + -const deck = new StackedDeckBehaviour() -const cardsToShow = computed(() => - cardsMemorization.cards.filter(x => x.index < 3) -) function onCardPlaced(card: VerseCardViewModel) { - deck.updateInactiveCard(card) + updateInactiveCard(card) } function onCardMoving( @@ -89,10 +102,10 @@ function onCardMoving( deltaPos: Vector3d, deltaPosTotal: Vector3d ) { - deck.updateMovingCard(card, deltaPos) + updateMovingCard(card, deltaPos) - if (card.type === "tutorial") { return } - if (deltaPosTotal.length < deck.swipeThreshold) { + if (card instanceof TutorialCardViewModel) { return } + if (deltaPosTotal.length < swipeThreshold) { card.memorizingStatus = MemorizingStatus.Unknown } else { card.memorizingStatus = deltaPosTotal.isLeftOrRight @@ -102,29 +115,25 @@ function onCardMoving( } function onCardMoved(card: InboxCardViewModel, deltaPos: Vector3d) { - deck.updateMovedCard(card, deltaPos) - if (deltaPos.length < deck.swipeThreshold) { return } + updateMovedCard(card, deltaPos) + if (deltaPos.length < swipeThreshold) { return } setTimeout(() => { - if (card.type === "tutorial") { - inboxDeckTutorial.tutorialCardSwiped(card as TutorialCardViewModel) + if (card instanceof TutorialCardViewModel) { + tutorialCardSwiped(card as TutorialCardViewModel) return } if (deltaPos.isLeftOrRight) { - cardsMemorization.shiftTopCard() + shiftTopCard() } else { - cardsMemorization.memorizeTopCard() + memorizeTopCard() } card.memorizingStatus = MemorizingStatus.Unknown - if (cardsMemorization.cards.length === 1) { - onCardPlaced(cardsMemorization.topCard) - cardsMemorization.topCard.showFrontSide() + if (cardsToShow.value.length === 1) { + onCardPlaced(topCard.value) + topCard.value.showFrontSide() } }, 250) } - -async function onRevert() { - await cardsMemorization.revert() -} diff --git a/src/app/decks/inbox/components/cards/InboxCard.vue b/src/app/decks/inbox/components/cards/InboxCard.vue index 4022ecca..0bf20908 100644 --- a/src/app/decks/inbox/components/cards/InboxCard.vue +++ b/src/app/decks/inbox/components/cards/InboxCard.vue @@ -74,8 +74,8 @@ const appearance = useAppearanceStore() const { colorfulCards } = toRefs(appearance) const style = computed(() => { return colorfulCards.value - ? "side-color-" + (1+(hashString(props.card.verseNumber + props.card.type.toString()) % 8)).toString() - : "side-color-0" + ? 'side-color-' + (1+(hashString(props.card.verseNumber + props.card.type.toString()) % 8)).toString() + : 'side-color-0' }) diff --git a/src/app/decks/inbox/composables/useInboxDeck.ts b/src/app/decks/inbox/composables/useInboxDeck.ts new file mode 100644 index 00000000..3ba4857e --- /dev/null +++ b/src/app/decks/inbox/composables/useInboxDeck.ts @@ -0,0 +1,51 @@ +import { Application, InboxCardMemorized, UpdateVerseStatus } from '@akdasa-studios/shlokas-core' +import { useArrayFilter, useArrayFind } from '@vueuse/core' +import { computed } from 'vue' +import { InboxVerseCardViewModel, useInboxDeckStore } from '@/app/decks/inbox' +import { useToast } from '@/app/composables' + + +export function useInboxDeck(app: Application) { + const inboxDeckStore = useInboxDeckStore() + const cardMemorizedToast = useToast() + const cardsToShow = useArrayFilter(inboxDeckStore.cards, x => x.index < 3) + const topCard = useArrayFind(inboxDeckStore.cards, x => x.index === 0) + const isEmpty = computed(() => inboxDeckStore.cards.length === 0) + + /** + * Shifts the top card to the bottom of the deck. + */ + function shiftTopCard() { + inboxDeckStore.shiftTopCard() + } + + /** + * Removes the top card from the deck and marks it as memorized. + */ + async function memorizeTopCard() { + const cardViewModel = inboxDeckStore.removeTopCard() as InboxVerseCardViewModel + if (cardViewModel) { + const inboxCard = cardViewModel.card + await app.processor.execute(new InboxCardMemorized(inboxCard)) + await app.processor.execute(new UpdateVerseStatus(inboxCard.verseId)) + cardMemorizedToast.open() + } + } + + /** + * Reverts the last action. + */ + async function revert() { + await app.processor.revert() + } + + return { + cardsToShow, isEmpty, + cardMemorizedToast, + shiftTopCard, + memorizeTopCard, + revert, + topCard, + // addCardsToDeck + } +} \ No newline at end of file diff --git a/src/app/decks/inbox/composables/useInboxDeckTutorial.ts b/src/app/decks/inbox/composables/useInboxDeckTutorial.ts new file mode 100644 index 00000000..15caaba4 --- /dev/null +++ b/src/app/decks/inbox/composables/useInboxDeckTutorial.ts @@ -0,0 +1,51 @@ +import { useInboxDeckStore } from '@/app/decks/inbox' +import { TutorialCardViewModel, useTutorialStore } from '@/app/decks/shared' + + +export function useInboxDeckTutorial() { + const inboxDeckStore = useInboxDeckStore() + const tutorialStore = useTutorialStore() + const TUTORIAL_CARDS = [ + 'inbox.cards', + 'inbox.overall', + 'inbox.verse', + 'inbox.final' + ] + + /** + * Adds tutorial cards to the Inbox deck + */ + function addTutorialCards() { + if (!tutorialStore.enabled) { return } + TUTORIAL_CARDS.reverse().forEach( + cardId => addTutorialCard(cardId) + ) + } + + /** + * Tutorial card has been swiped. Mark it is as completed + * and remove it from the Inbox deck. + * @param card The card that has been swiped. + */ + function tutorialCardSwiped(card: TutorialCardViewModel) { + tutorialStore.completeStep(card.id) + inboxDeckStore.removeTopCard() + } + + /** + * Adds tutorial card to the Inbox deck. + * @param id Id of the tutorial card. + */ + function addTutorialCard(id: string) { + const isAlreadyInInbox = inboxDeckStore.hasCard(id) + const isCompleted = tutorialStore.isStepCompleted(id) + if (!isAlreadyInInbox && !isCompleted) { + inboxDeckStore.addCard(new TutorialCardViewModel(id)) + } + } + + return { + tutorialCardSwiped, + addTutorialCards + } +} \ No newline at end of file diff --git a/src/app/decks/inbox/controllers/InboxDeckCardsController.ts b/src/app/decks/inbox/controllers/InboxDeckCardsController.ts deleted file mode 100644 index 5b4dc1eb..00000000 --- a/src/app/decks/inbox/controllers/InboxDeckCardsController.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { AddVerseToInboxDeck, Application, InboxCardMemorized, UpdateVerseStatus } from "@akdasa-studios/shlokas-core" -import { Emitter } from 'mitt' -import { useToast } from '@/app/composables' -import { InboxVerseCardViewModel, useInboxDeckStore } from '@/app/decks/inbox' -import { Events } from '@/app/Events' -import { useLibraryStore } from '@/app/library' - - -export class InboxDeckCardsController { - private readonly _app: Application - private readonly _inboxDeckStore - private readonly _libraryStore - private readonly _cardMemorizedToast - - constructor(app: Application, emitter: Emitter) { - this._app = app - this._inboxDeckStore = useInboxDeckStore() - this._libraryStore = useLibraryStore(this._app) - this._cardMemorizedToast = useToast() - - emitter.on('commandExecuted', async (e) => { - if (e instanceof AddVerseToInboxDeck) { await this.addCardsToDeck() } - }) - emitter.on('syncCompleted', async () => { await this.addCardsToDeck() }) - emitter.on('appOpened', async () => { await this.addCardsToDeck() }) - } - - /** - * Adds all cards from the inbox deck to the inbox deck. - */ - async addCardsToDeck() { - const cards = await this._app.inboxDeck.cards() - console.log('addCardsToDeck', JSON.stringify(cards)) - - for (const card of cards) { - const isAlreadyInDeck = this._inboxDeckStore.hasCard(card.id.value) - if (!isAlreadyInDeck) { - const verse = await this._libraryStore.getVerse(card.verseId) - const newCard = new InboxVerseCardViewModel(card, verse) - this._inboxDeckStore.addCard(newCard) - } - } - - const cardsToDelete = [] - for (const vm of this._inboxDeckStore.cards) { - const index = cards.findIndex(x => x.id.value === vm.id) - if (index === -1 && vm.type !== "tutorial") { cardsToDelete.push(vm.id) } - } - cardsToDelete.forEach(x => this._inboxDeckStore.removeCardById(x)) - } - - /** - * Shifts the top card to the bottom of the deck. - */ - async shiftTopCard() { - this._inboxDeckStore.shiftTopCard() - } - - /** - * Removes the top card from the deck and marks it as memorized. - */ - async memorizeTopCard() { - const cardViewModel = this._inboxDeckStore.removeTopCard() as InboxVerseCardViewModel - if (cardViewModel) { - const inboxCard = cardViewModel.card - await this._app.processor.execute(new InboxCardMemorized(inboxCard)) - await this._app.processor.execute(new UpdateVerseStatus(inboxCard.verseId)) - this._cardMemorizedToast.open() - } - } - - /** - * Reverts the last action. - */ - async revert() { - await this._app.processor.revert() - } - - get cards() { return this._inboxDeckStore.cards } - get cardMemorizedToast() { return this._cardMemorizedToast } - get topCard() { return this._inboxDeckStore.cards[0] } -} \ No newline at end of file diff --git a/src/app/decks/inbox/controllers/InboxDeckTutorialController.ts b/src/app/decks/inbox/controllers/InboxDeckTutorialController.ts deleted file mode 100644 index 60e329e7..00000000 --- a/src/app/decks/inbox/controllers/InboxDeckTutorialController.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Emitter } from 'mitt' -import { useTutorialStore , TutorialCardViewModel } from '@/app/decks/shared' -import { useInboxDeckStore } from '@/app/decks/inbox' -import { Events } from '@/app/Events' - - -export class InboxDeckTutorialController { - private readonly _inboxDeckStore - private readonly _tutorialStore - private readonly TUTORIAL_CARDS = [ - "inbox.cards", - "inbox.overall", - "inbox.verse", - "inbox.final" - ] - - constructor(emitter: Emitter) { - this._inboxDeckStore = useInboxDeckStore() - this._tutorialStore = useTutorialStore() - emitter.on('appOpened', async () => await this.addTutorialCards()) - } - - /** - * Adds tutorial cards to the Inbox deck - */ - async addTutorialCards() { - if (!this._tutorialStore.enabled) { return } - this.TUTORIAL_CARDS.reverse().forEach( - cardId => this.addTutorialCard(cardId) - ) - } - - /** - * Tutorial card has been swiped. Mark it is as completed - * and remove it from the Inbox deck. - * @param card The card that has been swiped. - */ - tutorialCardSwiped(card: TutorialCardViewModel) { - this._tutorialStore.completeStep(card.id) - this._inboxDeckStore.removeTopCard() - } - - /** - * Adds tutorial card to the Inbox deck. - * @param id Id of the tutorial card. - */ - private addTutorialCard(id: string) { - const isAlreadyInInbox = this._inboxDeckStore.hasCard(id) - const isCompleted = this._tutorialStore.isStepCompleted(id) - if (!isAlreadyInInbox && !isCompleted) { - this._inboxDeckStore.addCard(new TutorialCardViewModel(id)) - } - } -} \ No newline at end of file diff --git a/src/app/decks/inbox/index.ts b/src/app/decks/inbox/index.ts index 8996140d..cee04809 100644 --- a/src/app/decks/inbox/index.ts +++ b/src/app/decks/inbox/index.ts @@ -11,5 +11,8 @@ export * from './viewModels/InboxCardViewModel' export * from './viewModels/InboxVerseCardViewModel' // useCases: -export * from './controllers/InboxDeckCardsController' -export * from './controllers/InboxDeckTutorialController' +export * from './composables/useInboxDeck' +export * from './composables/useInboxDeckTutorial' + +// tasks: +export * from './tasks/SyncInboxDeckTask' \ No newline at end of file diff --git a/src/app/decks/inbox/stores/useInboxStore.ts b/src/app/decks/inbox/stores/useInboxStore.ts index 6a9e37c6..6b3ae128 100644 --- a/src/app/decks/inbox/stores/useInboxStore.ts +++ b/src/app/decks/inbox/stores/useInboxStore.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { InboxCardViewModel } from "@/app/decks/inbox" +import { InboxCardViewModel } from '@/app/decks/inbox' import { Deck, CardViewModel } from '@/app/decks/shared' diff --git a/src/app/decks/inbox/tasks/SyncInboxDeckTask.ts b/src/app/decks/inbox/tasks/SyncInboxDeckTask.ts new file mode 100644 index 00000000..58b75c0e --- /dev/null +++ b/src/app/decks/inbox/tasks/SyncInboxDeckTask.ts @@ -0,0 +1,38 @@ +import { AddVerseToInboxDeck, Application } from '@akdasa-studios/shlokas-core' +import { Emitter } from 'mitt' +import { Events } from '@/app/Events' +import { InboxVerseCardViewModel, useInboxDeckStore } from '@/app/decks/inbox' +import { useLibraryStore } from '@/app/library' + + +export function runSyncInboxDeckTask(app: Application, emitter: Emitter) { + const inboxDeckStore = useInboxDeckStore() + const libraryStore = useLibraryStore(app) + + // Subscribe + emitter.on('commandExecuted', async (e) => { + if (e instanceof AddVerseToInboxDeck) { await sync() } + }) + emitter.on('syncCompleted', async () => { await sync() }) + emitter.on('appStateChanged', async () => { await sync() }) + + async function sync() { + const cards = await app.inboxDeck.cards() + + for (const card of cards) { + const isAlreadyInDeck = inboxDeckStore.hasCard(card.id.value) + if (!isAlreadyInDeck) { + const verse = await libraryStore.getVerse(card.verseId) + const newCard = new InboxVerseCardViewModel(card, verse) + inboxDeckStore.addCard(newCard) + } + } + + const cardsToDelete = [] + for (const vm of inboxDeckStore.cards) { + const index = cards.findIndex(x => x.id.value === vm.id) + if (index === -1 && vm.type !== 'tutorial') { cardsToDelete.push(vm.id) } + } + cardsToDelete.forEach(x => inboxDeckStore.removeCardById(x)) + } +} diff --git a/src/app/decks/review/components/ReviewDeckPage.vue b/src/app/decks/review/components/ReviewDeckPage.vue index d1666df0..1000dc8c 100644 --- a/src/app/decks/review/components/ReviewDeckPage.vue +++ b/src/app/decks/review/components/ReviewDeckPage.vue @@ -15,9 +15,9 @@ :scroll-x="false" > @@ -53,22 +53,31 @@ diff --git a/src/app/decks/review/composables/useReviewDeck.ts b/src/app/decks/review/composables/useReviewDeck.ts new file mode 100644 index 00000000..441b20b4 --- /dev/null +++ b/src/app/decks/review/composables/useReviewDeck.ts @@ -0,0 +1,30 @@ +import { Application, ReviewCard, ReviewCardReviewed, ReviewGrade } from '@akdasa-studios/shlokas-core' +import { useArrayFilter, useArrayFind } from '@vueuse/core' +import { computed } from 'vue' +import { ReviewVerseCardViewModel, useReviewDeckStore } from '@/app/decks/review' + + +export function useReviewDeck(app: Application) { + const reviewDeckStore = useReviewDeckStore() + const cards = useArrayFilter(reviewDeckStore.cards, x => x.index < 3) + const topCard = useArrayFind(reviewDeckStore.cards, x => x.index === 0) + const isEmpty = computed(() => reviewDeckStore.cards.length === 0) + + async function gradeTopCard(grade: ReviewGrade) { + const topCard1 = (topCard.value as ReviewVerseCardViewModel).card as ReviewCard + await app.processor.execute(new ReviewCardReviewed(topCard1, grade)) + + if (topCard1.dueTo.getTime() !== app.timeMachine.today.getTime()) { + reviewDeckStore.removeTopCard() + } else { + reviewDeckStore.shiftTopCard() + } + } + + return { + gradeTopCard, + isEmpty, + cards, + topCard, + } +} \ No newline at end of file diff --git a/src/app/decks/review/composables/useReviewDeckTutorial.ts b/src/app/decks/review/composables/useReviewDeckTutorial.ts new file mode 100644 index 00000000..8010f199 --- /dev/null +++ b/src/app/decks/review/composables/useReviewDeckTutorial.ts @@ -0,0 +1,49 @@ +import { useReviewDeckStore } from '@/app/decks/review' +import { TutorialCardViewModel, useTutorialStore } from '@/app/decks/shared' + + +export function useReviewDeckTutorial() { + const reviewDeckStore = useReviewDeckStore() + const tutorialStore = useTutorialStore() + const TUTORIAL_CARDS = [ + 'review.questionAnswer', + 'review.intervals', + ] + + /** + * Adds tutorial cards to the Inbox deck + */ + function addTutorialCards() { + if (!tutorialStore.enabled) { return } + TUTORIAL_CARDS.reverse().forEach( + cardId => addTutorialCard(cardId) + ) + } + + /** + * Tutorial card has been swiped. Mark it is as completed + * and remove it from the Review deck. + * @param card The card that has been swiped. + */ + function tutorialCardSwiped(card: TutorialCardViewModel) { + tutorialStore.completeStep(card.id) + reviewDeckStore.removeTopCard() + } + + /** + * Adds tutorial card to the Review deck. + * @param id Id of the tutorial card. + */ + function addTutorialCard(id: string) { + const isAlreadyInDeck = reviewDeckStore.hasCard(id) + const isCompleted = tutorialStore.isStepCompleted(id) + if (!isAlreadyInDeck && !isCompleted) { + reviewDeckStore.addCard(new TutorialCardViewModel(id)) + } + } + + return { + addTutorialCards, + tutorialCardSwiped, + } +} \ No newline at end of file diff --git a/src/app/decks/review/index.ts b/src/app/decks/review/index.ts index ef4b3718..6b964c72 100644 --- a/src/app/decks/review/index.ts +++ b/src/app/decks/review/index.ts @@ -3,6 +3,7 @@ export { default as ReviewDeckEmpty } from './components/ReviewDeckEmpty.vue' export * from './components/cards' export * from './viewModels/ReviewVerseCardViewModel' export * from './stores/useReviewStore' -export * from './useCases/ReviewDeckCardsController' -export * from './useCases/ReviewDeckTutorialController' -export * from './viewModels/ReviewCardViewModel' \ No newline at end of file +export * from './composables/useReviewDeck' +export * from './composables/useReviewDeckTutorial' +export * from './viewModels/ReviewCardViewModel' +export * from './tasks/SyncReviewDeckTask' diff --git a/src/app/decks/review/stores/useReviewStore.ts b/src/app/decks/review/stores/useReviewStore.ts index e512b1a7..a6b7c2e7 100644 --- a/src/app/decks/review/stores/useReviewStore.ts +++ b/src/app/decks/review/stores/useReviewStore.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { CardViewModel, Deck } from "@/app/decks/shared" +import { CardViewModel, Deck } from '@/app/decks/shared' import { ReviewCardViewModel } from '../viewModels/ReviewCardViewModel' diff --git a/src/app/decks/review/tasks/SyncReviewDeckTask.ts b/src/app/decks/review/tasks/SyncReviewDeckTask.ts new file mode 100644 index 00000000..51a47556 --- /dev/null +++ b/src/app/decks/review/tasks/SyncReviewDeckTask.ts @@ -0,0 +1,37 @@ +import { Application, InboxCardMemorized } from '@akdasa-studios/shlokas-core' +import { Emitter } from 'mitt' +import { ReviewVerseCardViewModel, useReviewDeckStore } from '@/app/decks/review' +import { Events } from '@/app/Events' +import { useLibraryStore } from '@/app/library' + +export function runSyncReviewDeckTask(app: Application, emitter: Emitter) { + const _reviewDeckStore = useReviewDeckStore() + const _libraryStore = useLibraryStore(app) + + emitter.on('commandExecuted', async (e) => { + if (e instanceof InboxCardMemorized) { await addCardsToDeck()} + }) + emitter.on('syncCompleted', async () => await addCardsToDeck()) + emitter.on('appStateChanged', async () => await addCardsToDeck()) + + async function addCardsToDeck() { + const cards = await app.reviewDeck.dueToCards(app.timeMachine.now) + const sorted = Array.from(cards).sort((a, b) => a.addedAt.getTime() - b.addedAt.getTime()) + + for (const card of sorted) { + const isAlreadyInDeck = _reviewDeckStore.hasCard(card.id.value) + if (!isAlreadyInDeck) { + const verse = await _libraryStore.getVerse(card.verseId) + const newCard = new ReviewVerseCardViewModel(card, verse) + _reviewDeckStore.addCard(newCard) + } + } + + const cardsToDelete = [] + for (const vm of _reviewDeckStore.cards) { + const index = cards.findIndex(x => x.id.value === vm.id) + if (index === -1 && vm.type !== 'tutorial') { cardsToDelete.push(vm.id) } + } + cardsToDelete.forEach(x => _reviewDeckStore.removeCardById(x)) + } +} \ No newline at end of file diff --git a/src/app/decks/review/useCases/ReviewDeckCardsController.ts b/src/app/decks/review/useCases/ReviewDeckCardsController.ts deleted file mode 100644 index 33975353..00000000 --- a/src/app/decks/review/useCases/ReviewDeckCardsController.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Application, InboxCardMemorized, ReviewCard, ReviewCardReviewed, ReviewGrade } from "@akdasa-studios/shlokas-core" -import { Emitter } from 'mitt' -import { ReviewVerseCardViewModel, useReviewDeckStore } from "@/app/decks/review" -import { Events } from '@/app/Events' -import { useLibraryStore } from "@/app/library" - -export class ReviewDeckCardsController { - private readonly _app: Application - private readonly _reviewDeckStore - private readonly _libraryStore - - constructor(app: Application, emitter: Emitter) { - this._app = app - this._reviewDeckStore = useReviewDeckStore() - this._libraryStore = useLibraryStore(this._app) - - - emitter.on('commandExecuted', async (e) => { - if (e instanceof InboxCardMemorized) { await this.addCardsToDeck()} - }) - emitter.on('syncCompleted', async () => await this.addCardsToDeck()) - emitter.on('appOpened', async () => await this.addCardsToDeck()) - } - - async addCardsToDeck() { - const cards = await this._app.reviewDeck.dueToCards(this._app.timeMachine.now) - const sorted = Array.from(cards).sort((a, b) => a.addedAt.getTime() - b.addedAt.getTime()) - - for (const card of sorted) { - const isAlreadyInDeck = this._reviewDeckStore.hasCard(card.id.value) - if (!isAlreadyInDeck) { - const verse = await this._libraryStore.getVerse(card.verseId) - const newCard = new ReviewVerseCardViewModel(card, verse) - this._reviewDeckStore.addCard(newCard) - } - } - - const cardsToDelete = [] - for (const vm of this._reviewDeckStore.cards) { - const index = cards.findIndex(x => x.id.value === vm.id) - if (index === -1 && vm.type !== "tutorial") { cardsToDelete.push(vm.id) } - } - cardsToDelete.forEach(x => this._reviewDeckStore.removeCardById(x)) - } - - async gradeTopCard(grade: ReviewGrade) { - const topCard = this.topCard - await this._app.processor.execute(new ReviewCardReviewed(topCard.card as ReviewCard, grade)) - - if (topCard.card.dueTo.getTime() !== this._app.timeMachine.today.getTime()) { - this._reviewDeckStore.removeTopCard() - } else { - this._reviewDeckStore.shiftTopCard() - } - } - - get count() { return this._reviewDeckStore.count } - get cards() { return this._reviewDeckStore.cards } - get topCard(): ReviewVerseCardViewModel { return this._reviewDeckStore.cards[0] as ReviewVerseCardViewModel } -} \ No newline at end of file diff --git a/src/app/decks/review/useCases/ReviewDeckTutorialController.ts b/src/app/decks/review/useCases/ReviewDeckTutorialController.ts deleted file mode 100644 index 673fe58c..00000000 --- a/src/app/decks/review/useCases/ReviewDeckTutorialController.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Emitter } from 'mitt' -import { useReviewDeckStore } from '@/app/decks/review' -import { useTutorialStore , TutorialCardViewModel } from '@/app/decks/shared' -import { Events } from '@/app/Events' - - -export class ReviewDeckTutorialController { - private readonly _reviewDeckStore - private readonly _tutorialStore - private readonly TUTORIAL_CARDS = [ - "review.questionAnswer", - "review.intervals", - ] - - constructor(emitter: Emitter) { - this._reviewDeckStore = useReviewDeckStore() - this._tutorialStore = useTutorialStore() - emitter.on('appOpened', async () => await this.addTutorialCards()) - } - - /** - * Adds tutorial cards to the Inbox deck - */ - async addTutorialCards() { - if (!this._tutorialStore.enabled) { return } - this.TUTORIAL_CARDS.reverse().forEach( - cardId => this.addTutorialCard(cardId) - ) - } - - /** - * Tutorial card has been swiped. Mark it is as completed - * and remove it from the Review deck. - * @param card The card that has been swiped. - */ - tutorialCardSwiped(card: TutorialCardViewModel) { - this._tutorialStore.completeStep(card.id) - this._reviewDeckStore.removeTopCard() - } - - /** - * Adds tutorial card to the Review deck. - * @param id Id of the tutorial card. - */ - private addTutorialCard(id: string) { - const isAlreadyInDeck = this._reviewDeckStore.hasCard(id) - const isCompleted = this._tutorialStore.isStepCompleted(id) - if (!isAlreadyInDeck && !isCompleted) { - this._reviewDeckStore.addCard(new TutorialCardViewModel(id)) - } - } -} \ No newline at end of file diff --git a/src/app/decks/shared/CardViewModel.ts b/src/app/decks/shared/CardViewModel.ts index 8e223352..7b41a7f2 100644 --- a/src/app/decks/shared/CardViewModel.ts +++ b/src/app/decks/shared/CardViewModel.ts @@ -13,7 +13,7 @@ export abstract class CardViewModel { angle = new Vector3d(0, 0, 0) state = CardState.Inactive opacity = 1 - style = "" + style = '' abstract get id(): string abstract get type(): string @@ -21,6 +21,6 @@ export abstract class CardViewModel { showFrontSide() { this.flipped = false } get isTutorialCard(): boolean { - return this.type === "tutorial" + return this.type === 'tutorial' } } diff --git a/src/app/decks/shared/CardsDeck.vue b/src/app/decks/shared/CardsDeck.vue index 636ef701..585a03cd 100644 --- a/src/app/decks/shared/CardsDeck.vue +++ b/src/app/decks/shared/CardsDeck.vue @@ -53,11 +53,11 @@ watch(topCardRef, (newTopCard, prevTopCard) => { }) watch(topCardObj, () => { - props.cards.forEach(x => emit("place", x)) + props.cards.forEach(x => emit('place', x)) }) watch(items, () => { - props.cards.forEach(x => emit("place", x)) + props.cards.forEach(x => emit('place', x)) }, {immediate: true}) @@ -70,7 +70,7 @@ function calculateStyle(card: any) { ` rotateX(${card.angle.x}deg)` + ` rotateY(${card.angle.y}deg)` + ` rotateZ(${card.angle.z}deg);` + - `transition: .1s linear;`+ + 'transition: .1s linear;'+ `opacity: ${card.opacity.value};`+ `z-index: ${10 - card.index.value}` } @@ -84,7 +84,7 @@ function enableInteraction(ref: any) { listeners: { move(event:any) { emit( - "moving", + 'moving', unref(topCardObj), new Vector3d(event.dx, event.dy, 0), new Vector3d(event.pageX - event.x0, event.pageY - event.y0, 0) @@ -92,7 +92,7 @@ function enableInteraction(ref: any) { }, end(event:any) { emit( - "moved", + 'moved', unref(topCardObj), new Vector3d(event.pageX - event.x0, event.pageY - event.y0, 0) ) diff --git a/src/app/decks/shared/Deck.ts b/src/app/decks/shared/Deck.ts index 320a4d04..f5d58eae 100644 --- a/src/app/decks/shared/Deck.ts +++ b/src/app/decks/shared/Deck.ts @@ -1,4 +1,4 @@ -import { computed, ref, Ref } from "vue" +import { computed, ref, Ref } from 'vue' import { CardViewModel } from './CardViewModel' type Maybe = T | undefined @@ -51,7 +51,7 @@ export function removeItem(items: Ref, index = 0): IndexedItem | items.value.filter(x => x.index > index).forEach(x => x.index--) return items.value.splice(topItemIndex, 1)[0] } else { - console.log("!!!") + console.log('!!!') } } diff --git a/src/app/decks/shared/OrderedCollection.ts b/src/app/decks/shared/OrderedCollection.ts index c5c815a2..1bbf59f3 100644 --- a/src/app/decks/shared/OrderedCollection.ts +++ b/src/app/decks/shared/OrderedCollection.ts @@ -1,4 +1,4 @@ -import { Ref } from "vue" +import { Ref } from 'vue' interface IndexedItem { index: number @@ -18,7 +18,7 @@ export function removeItem(items: Ref, index = 0): IndexedItem | items.value.filter(x => x.index > index).forEach(x => x.index--) return items.value.splice(topItemIndex, 1)[0] } else { - console.log("!!!") + console.log('!!!') } } diff --git a/src/app/decks/shared/Vector3d.ts b/src/app/decks/shared/Vector3d.ts index c3322f08..a51bf6b7 100644 --- a/src/app/decks/shared/Vector3d.ts +++ b/src/app/decks/shared/Vector3d.ts @@ -16,14 +16,14 @@ export class Vector3d { get direction() { const xaxis = Math.abs(this.x) > Math.abs(this.y) return ( - xaxis && this.x > 0 ? "right" : - xaxis && this.x < 0 ? "left" : - !xaxis && this.y > 0 ? "bottom" : - !xaxis && this.y < 0 ? "top" : "unknown" + xaxis && this.x > 0 ? 'right' : + xaxis && this.x < 0 ? 'left' : + !xaxis && this.y > 0 ? 'bottom' : + !xaxis && this.y < 0 ? 'top' : 'unknown' ) } get isLeftOrRight() { - return ["left", "right"].includes(this.direction) + return ['left', 'right'].includes(this.direction) } } \ No newline at end of file diff --git a/src/app/decks/shared/VerseTextLines.vue b/src/app/decks/shared/VerseTextLines.vue index 4fb361e8..185d59f0 100644 --- a/src/app/decks/shared/VerseTextLines.vue +++ b/src/app/decks/shared/VerseTextLines.vue @@ -21,7 +21,7 @@ diff --git a/src/app/library/composables/useAddVerse.ts b/src/app/library/composables/useAddVerse.ts new file mode 100644 index 00000000..e851c580 --- /dev/null +++ b/src/app/library/composables/useAddVerse.ts @@ -0,0 +1,32 @@ +import { Transaction } from '@akdasa-studios/framework' +import { AddVerseToInboxDeck, Application, UpdateVerseStatus, VerseId } from '@akdasa-studios/shlokas-core' +import { Ref, ref } from 'vue' +import { DummyLibraryVerse, LibraryVerse } from '@/app/library' +import { useDialog, useToast } from '@/app/composables' + + +export function useAddVerse(app: Application) { + const addedVerseId: Ref = ref(undefined) + const toastVerseAdded = useToast() + const dialogAddVerse = useDialog(DummyLibraryVerse) + + async function addVerseToInbox(verse: LibraryVerse) { + addedVerseId.value = verse.verseId + toastVerseAdded.open({data: { verseNumber: verse.number }}) + const transaction = new Transaction() + await app.processor.execute(new AddVerseToInboxDeck(verse.verseId), transaction) + await app.processor.execute(new UpdateVerseStatus(verse.verseId), transaction) + } + + async function revert() { + if (!addedVerseId.value) { return } + await app.processor.revert() + } + + return { + addVerseToInbox, + dialogAddVerse, + toastVerseAdded, + revert + } +} \ No newline at end of file diff --git a/src/app/library/composables/useLibrary.ts b/src/app/library/composables/useLibrary.ts new file mode 100644 index 00000000..34631686 --- /dev/null +++ b/src/app/library/composables/useLibrary.ts @@ -0,0 +1,50 @@ +import { Application, UpdateVerseStatus, VerseId } from '@akdasa-studios/shlokas-core' +import { Emitter } from 'mitt' +import { storeToRefs } from 'pinia' +import { ref, Ref, watch } from 'vue' +import { useLocaleStore } from '@/app/settings' +import { LibraryVerse, useLibraryStore } from '@/app/library' +import { Events } from '@/app/Events' + + +export function useLibrary(app: Application, emitter: Emitter) { + const filteredVerses: Ref = ref([]) + const searchQuery = ref('') + const localeStore = useLocaleStore() + const libraryStore = useLibraryStore(app) + const language = storeToRefs(localeStore).language + + // Subscribe + watch( + [language, searchQuery], + () => onQueryChanged(), + { immediate: true } + ) + emitter.on('commandExecuted', async (e) => { + if (e instanceof UpdateVerseStatus) { await refreshVerse(e.verseId)} + }) + emitter.on('syncCompleted', async () => { + await onQueryChanged() + }) + + function refreshVerse(verseId: VerseId) { + libraryStore.sync(verseId) + } + + async function onQueryChanged() { + const foundVerses = await libraryStore.getByContent(language.value, searchQuery.value) + const result = [] + + for (const verse of foundVerses) { + const status = await libraryStore.getStatus(verse.id) + const viewModel = new LibraryVerse(verse, status) + result.push(viewModel) + } + filteredVerses.value = result + } + + return { + searchQuery, + filteredVerses + } +} \ No newline at end of file diff --git a/src/app/library/controllers/LibraryAddVerseController.ts b/src/app/library/controllers/LibraryAddVerseController.ts deleted file mode 100644 index 2a341d3e..00000000 --- a/src/app/library/controllers/LibraryAddVerseController.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Transaction } from '@akdasa-studios/framework' -import { AddVerseToInboxDeck, Application, UpdateVerseStatus, VerseId } from "@akdasa-studios/shlokas-core" -import { useDialog, useToast } from "@/app/composables" -import { DummyLibraryVerse, LibraryVerse } from '@/app/library' - - -export class LibraryAddVerseController { - private _app: Application - private _addedVerseId: VerseId|undefined - private _toast - private _dialog - - constructor(app: Application) { - this._app = app - this._toast = useToast() - this._dialog = useDialog(DummyLibraryVerse) - } - - async addVerseToInbox(verse: LibraryVerse) { - this._addedVerseId = verse.verseId - - this._toast.open({data: { verseNumber: verse.number }}) - - const transaction = new Transaction() - await this._app.processor.execute(new AddVerseToInboxDeck(verse.verseId), transaction) - await this._app.processor.execute(new UpdateVerseStatus(verse.verseId), transaction) - } - - /* -------------------------------------------------------------------------- */ - /* User can revert changes */ - /* -------------------------------------------------------------------------- */ - - async revert() { - if (!this._addedVerseId) { return } - await this._app.processor.revert() - } - - /* -------------------------------------------------------------------------- */ - /* Properties */ - /* -------------------------------------------------------------------------- */ - - get dialog() { - return this._dialog - } - - get toast() { - return this._toast - } -} \ No newline at end of file diff --git a/src/app/library/controllers/LibraryVersesController.ts b/src/app/library/controllers/LibraryVersesController.ts deleted file mode 100644 index 4549a198..00000000 --- a/src/app/library/controllers/LibraryVersesController.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Application, UpdateVerseStatus, VerseId } from "@akdasa-studios/shlokas-core" -import { Emitter } from "mitt" -import { storeToRefs } from "pinia" -import { ref, Ref, watch } from "vue" -import { useLocaleStore } from "@/app/settings" -import { LibraryVerse, useLibraryStore } from '@/app/library' -import { Events } from "@/app/Events" - - -export class LibraryVersesController { - public verses: Ref = ref([]) - public query = ref("") - - private _language - private readonly _localeStore - private readonly _libraryStore - - - constructor(app: Application, emitter: Emitter) { - this._localeStore = useLocaleStore() - this._libraryStore = useLibraryStore(app) - this._language = storeToRefs(this._localeStore).language - - watch( - [this._language, this.query], - () => this.onQueryChanged(), - { immediate: true } - ) - - emitter.on('commandExecuted', async (e) => { - if (e instanceof UpdateVerseStatus) { await this.refreshVerse(e.verseId)} - }) - emitter.on('syncCompleted', async () => { await this.onQueryChanged() }) - } - - refreshVerse(verseId: VerseId) { this._libraryStore.sync(verseId) } - - private async onQueryChanged() { - const verses = await this._libraryStore.getByContent(this._language.value, this.query.value) - const result = [] - - for (const verse of verses) { - const status = await this._libraryStore.getStatus(verse.id) - const viewModel = new LibraryVerse(verse, status) - result.push(viewModel) - } - this.verses.value = result - } -} \ No newline at end of file diff --git a/src/app/library/index.ts b/src/app/library/index.ts index 15d71e8d..f3011f1a 100644 --- a/src/app/library/index.ts +++ b/src/app/library/index.ts @@ -6,9 +6,9 @@ export * from './models/DummyLibraryVerse' export { default as LibraryPage } from './components/LibraryPage.vue' export { default as AddVerseDialog } from './components/AddVerseDialog.vue' -// scenarios -export * from './controllers/LibraryAddVerseController' -export * from './controllers/LibraryVersesController' +// composables +export * from './composables/useAddVerse' +export * from './composables/useLibrary' // stores export * from './stores/useLibraryStore' \ No newline at end of file diff --git a/src/app/library/models/DummyLibraryVerse.ts b/src/app/library/models/DummyLibraryVerse.ts index 9f6dadac..1ab8ab5c 100644 --- a/src/app/library/models/DummyLibraryVerse.ts +++ b/src/app/library/models/DummyLibraryVerse.ts @@ -1,6 +1,6 @@ -import { Text, Translation, VerseBuilder, VerseId, VerseNumber, VerseStatus } from "@akdasa-studios/shlokas-core" -import { Ref, ref } from "vue" -import { LibraryVerse } from "./LibraryVerse" +import { Text, Translation, VerseBuilder, VerseId, VerseNumber, VerseStatus } from '@akdasa-studios/shlokas-core' +import { Ref, ref } from 'vue' +import { LibraryVerse } from './LibraryVerse' const dummyVerse = new VerseBuilder() .withId(new VerseId('00000000-0000-0000-0000-000000000000')) diff --git a/src/app/settings/components/GeneralSettingsPage.vue b/src/app/settings/components/GeneralSettingsPage.vue index 9d7a88ef..67d3f720 100644 --- a/src/app/settings/components/GeneralSettingsPage.vue +++ b/src/app/settings/components/GeneralSettingsPage.vue @@ -69,7 +69,7 @@ import { import { inject } from 'vue' import { useAppearanceStore, useLocaleStore } from '@/app/settings' -const i18n = inject("i18n") as any +const i18n = inject('i18n') as any const locale = useLocaleStore() const appearance = useAppearanceStore() diff --git a/src/app/settings/components/account/AccountPage.vue b/src/app/settings/components/account/AccountPage.vue index 50c29f69..c40a2bba 100644 --- a/src/app/settings/components/account/AccountPage.vue +++ b/src/app/settings/components/account/AccountPage.vue @@ -116,8 +116,8 @@ import LogInViaEmailPage from './email/LogInViaEmailPage.vue' import SignUpViaEmailPage from './email/SignUpViaEmailPage.vue' const inProgress = ref(false) -const emitter = inject("emitter") as Emitter -const app = inject("app") as Application +const emitter = inject('emitter') as Emitter +const app = inject('app') as Application const account = useAccountStore() const { isAuthenticated, syncHost, token, email } = storeToRefs(account) const { logOut } = account diff --git a/src/app/settings/stores/useAccountStore.ts b/src/app/settings/stores/useAccountStore.ts index 2d7a63a7..eda0fcdd 100644 --- a/src/app/settings/stores/useAccountStore.ts +++ b/src/app/settings/stores/useAccountStore.ts @@ -16,9 +16,9 @@ export const useAccountStore = defineStore('settings/account', () => { /* State */ /* -------------------------------------------------------------------------- */ - const name = ref("") - const email = ref("") - const password = ref("") + const name = ref('') + const email = ref('') + const password = ref('') const token: Ref = ref() const isAuthenticated = computed(() => !!token.value) @@ -50,9 +50,9 @@ export const useAccountStore = defineStore('settings/account', () => { } function logOut() { - name.value = "" - email.value = "" - password.value = "" + name.value = '' + email.value = '' + password.value = '' token.value = undefined } diff --git a/src/app/settings/stores/useLocaleStore.ts b/src/app/settings/stores/useLocaleStore.ts index 536cdb75..ef895d1a 100644 --- a/src/app/settings/stores/useLocaleStore.ts +++ b/src/app/settings/stores/useLocaleStore.ts @@ -12,11 +12,11 @@ export const useLocaleStore = defineStore('settings/locale', () => { /* State */ /* -------------------------------------------------------------------------- */ - const languageCode: Ref = ref("en") //i18n.global.locale.value) + const languageCode: Ref = ref('en') //i18n.global.locale.value) const language = computed(() => availableLanguages.find(x => x.code === languageCode.value) || availableLanguages[0]) const availableLanguages = [ - new Language("en", "English"), - new Language("ru", "Русский"), + new Language('en', 'English'), + new Language('ru', 'Русский'), // new Language("rs", "Српски") ] @@ -27,7 +27,7 @@ export const useLocaleStore = defineStore('settings/locale', () => { /* -------------------------------------------------------------------------- */ async function load() { - languageCode.value = (await storage.get(KEY_LANGUAGE)) || "en" + languageCode.value = (await storage.get(KEY_LANGUAGE)) || 'en' } /* -------------------------------------------------------------------------- */ @@ -35,7 +35,7 @@ export const useLocaleStore = defineStore('settings/locale', () => { /* -------------------------------------------------------------------------- */ async function onLanguageChanged(lang: string) { - await storage.set("language", lang) + await storage.set('language', lang) } /* -------------------------------------------------------------------------- */ diff --git a/src/app/shared/index.ts b/src/app/shared/index.ts index c851e183..74c07aec 100644 --- a/src/app/shared/index.ts +++ b/src/app/shared/index.ts @@ -1 +1,2 @@ -export { default as DarkImage } from './DarkImage.vue' \ No newline at end of file +export { default as DarkImage } from './DarkImage.vue' +export * from './tasks/RefreshTokenTask' \ No newline at end of file diff --git a/src/app/shared/tasks/RefreshTokenTask.ts b/src/app/shared/tasks/RefreshTokenTask.ts new file mode 100644 index 00000000..867deda5 --- /dev/null +++ b/src/app/shared/tasks/RefreshTokenTask.ts @@ -0,0 +1,41 @@ +import { Logger } from '@akdasa-studios/framework' +import { BackgroundTask } from '@capawesome/capacitor-background-task' +import { Emitter } from 'mitt' +import { Capacitor } from '@capacitor/core' +import { AUTH_HOST } from '@/app/Env' +import { Events } from '@/app/Events' +import { useAccountStore } from '@/app/settings' +import { AuthService } from '@/services/AuthService' + + +export function runRefreshTokenTask(emitter: Emitter) { + const service = new AuthService(AUTH_HOST) + const log = new Logger('auth') + + emitter.on('appStateChanged', async ({ isActive }) => { + if (isActive) { return } + if (Capacitor.getPlatform() !== 'ios') { return } + + const taskId = await BackgroundTask.beforeExit(async () => { + await refreshToken() + }) + BackgroundTask.finish({ taskId }) + }) + + async function refreshToken() { + const now = new Date().getTime() + const account = useAccountStore() + if (!account.token) { return } + + const expires = account.token?.expires ?? 0 + + if (now >= expires) { + const token = await service.refreshToken(account.token) + if (!token) { log.error('Failed to refresh token') } + account.token.expires = token.expires + log.debug('auth token refreshed', account.token) + } else { + log.debug('token still valid') + } + } +} diff --git a/src/app/statistics/controllers/StatisticsController.ts b/src/app/statistics/controllers/StatisticsController.ts deleted file mode 100644 index a0bec256..00000000 --- a/src/app/statistics/controllers/StatisticsController.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Application, ReviewCardReviewed } from '@akdasa-studios/shlokas-core' -import { Emitter } from 'mitt' -import { Events } from '@/app/Events' -import { useStatisticsStore } from '@/app/statistics' - - -export class StatisticsController { - private statisticsStore = useStatisticsStore() - - constructor( - private readonly app: Application, - private readonly emitter: Emitter - ) { - emitter.on('commandExecuted', async (e) => { - if (e instanceof ReviewCardReviewed) { await this.updateStatistics() } - }) - emitter.on('appOpened', async () => await this.updateStatistics()) - } - - private async updateStatistics() { - const cards = await this.app.reviewDeck.dueToCards(nextDays(1)) - this.statisticsStore.cardsCountDueToTomorrow = cards.length - } -} - -function nextDays(days: number) { - const result = new Date() - result.setDate(result.getDate()+days) - return result -} diff --git a/src/app/statistics/index.ts b/src/app/statistics/index.ts index b1962c15..1652f4a2 100644 --- a/src/app/statistics/index.ts +++ b/src/app/statistics/index.ts @@ -1,2 +1,2 @@ export * from './stores/useStatisticsStore' -export * from './controllers/StatisticsController' \ No newline at end of file +export * from './tasks/UpdateStatisticsTask' \ No newline at end of file diff --git a/src/app/statistics/stores/useStatisticsStore.ts b/src/app/statistics/stores/useStatisticsStore.ts index cf4143b6..169adcbd 100644 --- a/src/app/statistics/stores/useStatisticsStore.ts +++ b/src/app/statistics/stores/useStatisticsStore.ts @@ -1,5 +1,5 @@ -import { defineStore } from "pinia" -import { ref } from "vue" +import { defineStore } from 'pinia' +import { ref } from 'vue' diff --git a/src/app/statistics/tasks/UpdateStatisticsTask.ts b/src/app/statistics/tasks/UpdateStatisticsTask.ts new file mode 100644 index 00000000..82451a43 --- /dev/null +++ b/src/app/statistics/tasks/UpdateStatisticsTask.ts @@ -0,0 +1,25 @@ +import { Application, ReviewCardReviewed } from '@akdasa-studios/shlokas-core' +import { Emitter } from 'mitt' +import { Events } from '@/app/Events' +import { useStatisticsStore } from '@/app/statistics' + + +export function runUpdateStatisticsTask(app: Application, emitter: Emitter) { + const statisticsStore = useStatisticsStore() + + emitter.on('commandExecuted', async (e) => { + if (e instanceof ReviewCardReviewed) { await updateStatistics() } + }) + emitter.on('appStateChanged', async () => await updateStatistics()) + + async function updateStatistics() { + const cards = await app.reviewDeck.dueToCards(nextDays(1)) + statisticsStore.cardsCountDueToTomorrow = cards.length + } +} + +function nextDays(days: number) { + const result = new Date() + result.setDate(result.getDate()+days) + return result +} diff --git a/src/app/utils/sync.ts b/src/app/utils/sync.ts index 33f4d88f..06c34fc0 100644 --- a/src/app/utils/sync.ts +++ b/src/app/utils/sync.ts @@ -1,6 +1,6 @@ import { InMemoryRepository } from '@akdasa-studios/framework' import { SyncRepository } from '@akdasa-studios/framework-sync' -import { InboxCard, Repositories, ReviewCard, Verse, VerseStatus } from "@akdasa-studios/shlokas-core" +import { InboxCard, Repositories, ReviewCard, Verse, VerseStatus } from '@akdasa-studios/shlokas-core' import { CouchDB, InboxCardDeserializer, InboxCardSerializer, PouchRepository, ReviewCardDeserializer, @@ -14,19 +14,19 @@ export function createRepositories(remote: string) { new InMemoryRepository(), new SyncRepository(new PouchRepository( couchDB, - "verseStatus", + 'verseStatus', new VerseStatusSerializer(), new VerseStatusDeserializer() )), new SyncRepository(new PouchRepository( couchDB, - "inbox", + 'inbox', new InboxCardSerializer(), new InboxCardDeserializer() )), new SyncRepository(new PouchRepository( couchDB, - "review", + 'review', new ReviewCardSerializer(), new ReviewCardDeserializer() )), diff --git a/src/init/stage-1/initCommands.ts b/src/init/app/initCommands.ts similarity index 70% rename from src/init/stage-1/initCommands.ts rename to src/init/app/initCommands.ts index 3b04a82a..1534fd7d 100644 --- a/src/init/stage-1/initCommands.ts +++ b/src/init/app/initCommands.ts @@ -1,12 +1,12 @@ import { Emitter } from 'mitt' import { Events } from '@/app/Events' -import { InitArgs, InitStageResult } from '../initialization' +import { InitArgs, InitResult } from '../initialization' export async function initCommands( { shlokas, get }: InitArgs -): Promise { - const emitter = get>("emitter") +): Promise { + const emitter = get>('emitter') shlokas.processor.commandExecuted.subscribe((cmd: any) => emitter.emit('commandExecuted', cmd)) shlokas.processor.commandReverted.subscribe((cmd: any) => emitter.emit('commandExecuted', cmd)) return {} diff --git a/src/init/stage-1/initLocale.ts b/src/init/app/initLocale.ts similarity index 72% rename from src/init/stage-1/initLocale.ts rename to src/init/app/initLocale.ts index 981c78d7..f87a09b6 100644 --- a/src/init/stage-1/initLocale.ts +++ b/src/init/app/initLocale.ts @@ -9,14 +9,14 @@ export async function initLocale( { get }: InitArgs ) { // dependencies: - const deviceStorage = get("deviceStorage") as any - const i18n = get("i18n") as any + const deviceStorage = get('deviceStorage') as any + const i18n = get('i18n') as any const store = useLocaleStore() // calculate language - const savedLang = await deviceStorage.get("language") + const savedLang = await deviceStorage.get('language') const deviceLang = (await Device.getLanguageCode()).value - const fallbackLang = "en" + const fallbackLang = 'en' const finalLang = check(savedLang) || check(deviceLang) || fallbackLang // update state @@ -26,7 +26,7 @@ export async function initLocale( function check(lang: string) { // TODO: one place to store available languages - if (lang === "ru") return "ru" - if (lang === "en") return "en" + if (lang === 'ru') return 'ru' + if (lang === 'en') return 'en' return undefined } \ No newline at end of file diff --git a/src/init/stage-1/initParams.ts b/src/init/app/initParams.ts similarity index 84% rename from src/init/stage-1/initParams.ts rename to src/init/app/initParams.ts index c1e1dacd..3f16384a 100644 --- a/src/init/stage-1/initParams.ts +++ b/src/init/app/initParams.ts @@ -1,10 +1,10 @@ import { useTutorialStore } from '@/app/decks/shared' -import { InitArgs, InitStageResult } from '../initialization' +import { InitArgs } from '../initialization' export async function initParams( { shlokas }: InitArgs -): Promise { +) { const tutorialSore = useTutorialStore() const params = new URLSearchParams(window.location.search) const tutorialEnabled = params.get('tutorialEnabled') @@ -18,6 +18,4 @@ export async function initParams( console.debug('[params] date', date) shlokas.timeMachine.set(new Date(date)) } - - return {} } \ No newline at end of file diff --git a/src/init/stage-1/initStaticData.ts b/src/init/app/initStaticData.ts similarity index 95% rename from src/init/stage-1/initStaticData.ts rename to src/init/app/initStaticData.ts index b42d888a..213490a4 100644 --- a/src/init/stage-1/initStaticData.ts +++ b/src/init/app/initStaticData.ts @@ -12,7 +12,7 @@ export async function initStaticData( } function loadVerses( - { shlokas }: Pick, + { shlokas }: Pick, lang: Language, verses: any[] ) { diff --git a/src/init/stage-1/initStores.ts b/src/init/app/initStores.ts similarity index 92% rename from src/init/stage-1/initStores.ts rename to src/init/app/initStores.ts index 28f93106..84ef5c6f 100644 --- a/src/init/stage-1/initStores.ts +++ b/src/init/app/initStores.ts @@ -1,13 +1,13 @@ -import { useDeviceStore } from '@/app/useDeviceStorage' -import { useAccountStore, useAppearanceStore, useLocaleStore } from '@/app/settings' import { useTutorialStore } from '@/app/decks/shared' +import { useAccountStore, useAppearanceStore, useLocaleStore } from '@/app/settings' +import { useDeviceStore } from '@/app/useDeviceStorage' import { InitArgs } from '../initialization' export async function initStores( { get }: InitArgs ) { const storage = useDeviceStore() - storage.init(get("deviceStorage")) + storage.init(get('deviceStorage')) await Promise.all([ useLocaleStore().load(), diff --git a/src/init/app/initTasks.ts b/src/init/app/initTasks.ts new file mode 100644 index 00000000..b0357af5 --- /dev/null +++ b/src/init/app/initTasks.ts @@ -0,0 +1,18 @@ +import { Emitter } from 'mitt' +import { runSyncInboxDeckTask } from '@/app/decks/inbox' +import { runSyncReviewDeckTask } from '@/app/decks/review' +import { Events } from '@/app/Events' +import { runRefreshTokenTask } from '@/app/shared' +import { runUpdateStatisticsTask } from '@/app/statistics' +import { InitArgs } from '../initialization' + + +export async function initTasks( + { get, shlokas }: InitArgs +) { + const emitter = get>('emitter') + runRefreshTokenTask(emitter) + runUpdateStatisticsTask(shlokas, emitter) + runSyncInboxDeckTask(shlokas, emitter) + runSyncReviewDeckTask(shlokas, emitter) +} \ No newline at end of file diff --git a/src/init/index.ts b/src/init/index.ts index b602736c..f0776d6c 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -1,20 +1,19 @@ // stage 0: infrastructure -import { initSentry } from "./stage-0/initSentry" -import { initLogging } from "./stage-0/initLogging" -import { initDeviceStorage } from "./stage-0/initDeviceStorage" -import { initI18n } from "./stage-0/initI18n" -import { initPinia } from "./stage-0/initPinia" -import { initEmitter } from "./stage-0/initEmitter" -import { initAppStateChange } from "./stage-0/initAppStateChange" +import { initSentry } from './infrastructure/initSentry' +import { initLogging } from './infrastructure/initLogging' +import { initDeviceStorage } from './infrastructure/initDeviceStorage' +import { initI18n } from './infrastructure/initI18n' +import { initPinia } from './infrastructure/initPinia' +import { initEmitter } from './infrastructure/initEmitter' +import { initAppStateChange } from './infrastructure/initAppStateChange' // stage 1: logic -import { initCommands } from "./stage-1/initCommands" -import { initControllers } from "./stage-1/initControllers" -import { initParams } from "./stage-1/initParams" -import { initStores } from "./stage-1/initStores" -import { initStaticData } from "./stage-1/initStaticData" -import { initLocale } from "./stage-1/initLocale" -import { initSyncTask } from "./stage-1/initSyncTask" +import { initCommands } from './app/initCommands' +import { initTasks } from './app/initTasks' +import { initParams } from './app/initParams' +import { initStores } from './app/initStores' +import { initStaticData } from './app/initStaticData' +import { initLocale } from './app/initLocale' const initStages = [ @@ -31,8 +30,7 @@ const initStages = [ initParams, initCommands, initStaticData, - initControllers, - initSyncTask, + initTasks, ] export default initStages \ No newline at end of file diff --git a/src/init/stage-0/initAppStateChange.ts b/src/init/infrastructure/initAppStateChange.ts similarity index 72% rename from src/init/stage-0/initAppStateChange.ts rename to src/init/infrastructure/initAppStateChange.ts index 188fbd58..c5d3d155 100644 --- a/src/init/stage-0/initAppStateChange.ts +++ b/src/init/infrastructure/initAppStateChange.ts @@ -3,11 +3,12 @@ import { Emitter } from 'mitt' import { Events } from '@/app/Events' import { InitArgs } from '../initialization' + export async function initAppStateChange( { get }: InitArgs ) { - const emitter = get>("emitter") + const emitter = get>('emitter') App.addListener('appStateChange', async ({ isActive }) => { - if (isActive) { emitter.emit("appOpened") } + emitter.emit('appStateChanged', { isActive: isActive }) }) } diff --git a/src/init/infrastructure/initDeviceStorage.ts b/src/init/infrastructure/initDeviceStorage.ts new file mode 100644 index 00000000..5c30b9c3 --- /dev/null +++ b/src/init/infrastructure/initDeviceStorage.ts @@ -0,0 +1,9 @@ +import { Storage } from '@ionic/storage' +import { InitResult } from '../initialization' + +/** + * Initialize the device storage to keep user settings + */ +export async function initDeviceStorage(): Promise { + return { deviceStorage: await new Storage().create() } +} \ No newline at end of file diff --git a/src/init/infrastructure/initEmitter.ts b/src/init/infrastructure/initEmitter.ts new file mode 100644 index 00000000..a7214823 --- /dev/null +++ b/src/init/infrastructure/initEmitter.ts @@ -0,0 +1,11 @@ +import mitt from 'mitt' +import { Events } from '@/app/Events' +import { InitResult } from '../initialization' + + +/** + * Initialize event bus + */ +export async function initEmitter(): Promise { + return { 'emitter': mitt() } +} diff --git a/src/init/stage-0/initI18n.ts b/src/init/infrastructure/initI18n.ts similarity index 76% rename from src/init/stage-0/initI18n.ts rename to src/init/infrastructure/initI18n.ts index ee585ee2..1dc8ad44 100644 --- a/src/init/stage-0/initI18n.ts +++ b/src/init/infrastructure/initI18n.ts @@ -2,7 +2,7 @@ import { createI18n } from 'vue-i18n' import en from '@/locale/en.json' import rs from '@/locale/rs.json' import ru from '@/locale/ru.json' -import { InitArgs, InitStageResult } from '../initialization' +import { InitArgs, InitResult } from '../initialization' /** @@ -10,7 +10,7 @@ import { InitArgs, InitStageResult } from '../initialization' */ export async function initI18n( { vue }: InitArgs -): Promise { +): Promise { const i18n = createI18n({ legacy: false, locale: 'en', @@ -23,5 +23,5 @@ export async function initI18n( }) vue.use(i18n) - return { inject: { "i18n": i18n.global } } + return { 'i18n': i18n.global } } diff --git a/src/init/infrastructure/initLogging.ts b/src/init/infrastructure/initLogging.ts new file mode 100644 index 00000000..4ca80842 --- /dev/null +++ b/src/init/infrastructure/initLogging.ts @@ -0,0 +1,24 @@ +import { LogRecord, Logs, LogTransport, LogLevel } from '@akdasa-studios/framework' + + +/** + * Initialize logging system + */ +export async function initLogging() { + Logs.register(new ConsoleLogTransport()) +} + + +class ConsoleLogTransport implements LogTransport { + log(record: LogRecord): void { + if (record.level === LogLevel.DEBUG) { + console.debug(record.context, record.message, JSON.stringify(record.data)) + } else if (record.level === LogLevel.INFO) { + console.info(record.context, record.message, JSON.stringify(record.data)) + } else if (record.level === LogLevel.WARN) { + console.warn(record.context, record.message, JSON.stringify(record.data)) + } else if (record.level === LogLevel.ERROR || record.level === LogLevel.FATAL) { + console.error(record.context, record.message, JSON.stringify(record.data)) + } + } +} \ No newline at end of file diff --git a/src/init/stage-0/initPinia.ts b/src/init/infrastructure/initPinia.ts similarity index 100% rename from src/init/stage-0/initPinia.ts rename to src/init/infrastructure/initPinia.ts diff --git a/src/init/stage-0/initSentry.ts b/src/init/infrastructure/initSentry.ts similarity index 99% rename from src/init/stage-0/initSentry.ts rename to src/init/infrastructure/initSentry.ts index 8c0ab8f1..ab2df08b 100644 --- a/src/init/stage-0/initSentry.ts +++ b/src/init/infrastructure/initSentry.ts @@ -4,6 +4,7 @@ import * as SentrySibling from '@sentry/vue' import { App } from '@capacitor/app' import { InitArgs } from '../initialization' + /** * Initialize the sentry plugin to track errors */ diff --git a/src/init/initialization.ts b/src/init/initialization.ts index 6bbcde35..a37b5b17 100644 --- a/src/init/initialization.ts +++ b/src/init/initialization.ts @@ -7,6 +7,6 @@ export interface InitArgs { get: (name: string) => T } -export interface InitStageResult { - inject?: { [name:string]: any } +export interface InitResult { + [name:string]: unknown } \ No newline at end of file diff --git a/src/init/stage-0/initDeviceStorage.ts b/src/init/stage-0/initDeviceStorage.ts deleted file mode 100644 index 3f8f2ea2..00000000 --- a/src/init/stage-0/initDeviceStorage.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Storage } from '@ionic/storage' -import { InitStageResult } from '../initialization' - -/** - * Initialize the device storage to keep user settings - */ -export async function initDeviceStorage(): Promise { - const storage = new Storage() - await storage.create() - return { inject: { "deviceStorage": storage } } -} \ No newline at end of file diff --git a/src/init/stage-0/initEmitter.ts b/src/init/stage-0/initEmitter.ts deleted file mode 100644 index 97ee5c2b..00000000 --- a/src/init/stage-0/initEmitter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import mitt from 'mitt' -import { Events } from '@/app/Events' -import { InitStageResult } from '../initialization' - - -/** - * Initialize event bus - */ -export async function initEmitter(): Promise { - const emitter = mitt() - return { - inject: { - "emitter": emitter - } - } -} diff --git a/src/init/stage-0/initLogging.ts b/src/init/stage-0/initLogging.ts deleted file mode 100644 index 407cd068..00000000 --- a/src/init/stage-0/initLogging.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { LogRecord, Logs, LogTransport } from '@akdasa-studios/framework' - - -class ConsoleLogTransport implements LogTransport { - log(record: LogRecord): void { - console.log(record.context, record.message, JSON.stringify(record.data)) - } -} - -/** - * Initialize logging system - */ -export async function initLogging() { - Logs.register(new ConsoleLogTransport()) -} diff --git a/src/init/stage-1/initControllers.ts b/src/init/stage-1/initControllers.ts deleted file mode 100644 index a5c7f4d0..00000000 --- a/src/init/stage-1/initControllers.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Emitter } from 'mitt' -import { InboxDeckCardsController, InboxDeckTutorialController } from '@/app/decks/inbox' -import { ReviewDeckCardsController, ReviewDeckTutorialController } from '@/app/decks/review' -import { Events } from '@/app/Events' -import { LibraryAddVerseController, LibraryVersesController } from '@/app/library' -import { StatisticsController } from '@/app/statistics' -import { InitArgs, InitStageResult } from '../initialization' - - -export async function initControllers( - { get, shlokas }: InitArgs -): Promise { - const emitter = get>("emitter") - - return { - inject: { - "libraryAddVerseController": new LibraryAddVerseController(shlokas), - "libraryVersesController": new LibraryVersesController(shlokas, emitter), - "inboxDeckCardsController": new InboxDeckCardsController(shlokas, emitter), - "inboxDeckTutorialController": new InboxDeckTutorialController(emitter), - "reviewDeckCardsController": new ReviewDeckCardsController(shlokas, emitter), - "reviewDeckTutorialController": new ReviewDeckTutorialController(emitter), - "statisticsController": new StatisticsController(shlokas, emitter) - } - } -} \ No newline at end of file diff --git a/src/init/stage-1/initSyncTask.ts b/src/init/stage-1/initSyncTask.ts deleted file mode 100644 index a0bcde32..00000000 --- a/src/init/stage-1/initSyncTask.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { App } from '@capacitor/app' -import { Capacitor } from '@capacitor/core' -import { BackgroundTask } from '@capawesome/capacitor-background-task' -import { AUTH_HOST } from '@/app/Env' -import { useAccountStore } from '@/app/settings' -import { AuthService } from '@/services/AuthService' -import { InitArgs } from '../initialization' - -export async function initSyncTask( - { get }: InitArgs -) { - if (Capacitor.getPlatform() !== 'ios') { return } - - // beforeExit works on iOS only - App.addListener('appStateChange', async ({ isActive }) => { - if (isActive) { return } - const taskId = await BackgroundTask.beforeExit(async () => { - const account = useAccountStore() - if (account.token === undefined) { return } - - if (new Date().getTime() >= account.token.expires) { - // refresh token - const service = new AuthService(AUTH_HOST) - account.token.expires = (await service.refreshToken(account.token)).expires - } else if (account.syncHost) { - // sync db - await (get("couchDB") as any).sync(account.syncHost) - } - BackgroundTask.finish({ taskId }) - }) - }) -} diff --git a/src/main.ts b/src/main.ts index be390b4f..22addeba 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,17 +35,13 @@ async function initApp() { const aaa = await createShlokasApplication() const shlokas = aaa[0] - vue.provide("app", shlokas) + vue.provide('app', shlokas) const services: {[name: string]: any} = { - "couchDB": aaa[1] + 'couchDB': aaa[1] } - console.group("Init...") - for (const initStage of initStages) { - console.log(initStage.name) - const initResult = await initStage({ shlokas: shlokas, vue: vue, @@ -53,15 +49,14 @@ async function initApp() { } as InitArgs) - if (initResult?.inject) { - for (const [key, value] of Object.entries(initResult.inject)) { + if (initResult) { + for (const [key, value] of Object.entries(initResult)) { vue.provide(key, value) services[key] = value } } } - console.groupEnd() - services["emitter"].emit("appOpened") + services['emitter'].emit('appStateChanged', { isActive: true }) router.isReady().then(() => { vue.mount('#app') diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index cce05140..31345351 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -20,8 +20,8 @@ export class AuthService { email: string, password: string, ): Promise> { - const response = await this._post("register", { - name, email, password, "confirmPassword": password, + const response = await this._post('register', { + name, email, password, 'confirmPassword': password, }) if (response.error) { return Result.fail(response.error) } return Result.ok() @@ -31,7 +31,7 @@ export class AuthService { email: string, password: string, ): Promise> { - const response = await this._post("login", { "username": email, password }) + const response = await this._post('login', { 'username': email, password }) if (response.error) { return Result.fail(response.error) } return Result.ok({ issued: response.issued, @@ -43,8 +43,8 @@ export class AuthService { } async refreshToken(token: any): Promise { - const response = await this._post("refresh", token, { - "Authorization": `Bearer ${token.token}:${token.password}` + const response = await this._post('refresh', token, { + 'Authorization': `Bearer ${token.token}:${token.password}` }) return { expires: response.expires, diff --git a/src/services/persistence/InboxCardSerializer.ts b/src/services/persistence/InboxCardSerializer.ts index aa7686fa..2b7f9a0e 100644 --- a/src/services/persistence/InboxCardSerializer.ts +++ b/src/services/persistence/InboxCardSerializer.ts @@ -1,18 +1,18 @@ -import { Result } from "@akdasa-studios/framework" -import { InboxCard, InboxCardType, VerseId } from "@akdasa-studios/shlokas-core" -import { ObjectMapper } from "./ObjectMapper" +import { Result } from '@akdasa-studios/framework' +import { InboxCard, InboxCardType, VerseId } from '@akdasa-studios/shlokas-core' +import { ObjectMapper } from './ObjectMapper' export class InboxCardSerializer implements ObjectMapper { map(from: InboxCard): Result { return Result.ok({ - "_id": from.id.value, - "@type": "inbox", - "verseId": from.verseId.value, - "type": from.type, - "addedAt": from.addedAt, - "memorizedAt": from.memorizedAt, - "version": from.version, + '_id': from.id.value, + '@type': 'inbox', + 'verseId': from.verseId.value, + 'type': from.type, + 'addedAt': from.addedAt, + 'memorizedAt': from.memorizedAt, + 'version': from.version, }) } } @@ -20,7 +20,7 @@ export class InboxCardSerializer implements ObjectMapper { export class InboxCardDeserializer implements ObjectMapper { map(from: any): Result { const card = new InboxCard( - new VerseId(from["verseId"]), + new VerseId(from['verseId']), from['type'] as InboxCardType, new Date(from['addedAt']), from['memorizedAt'] ? new Date(from['memorizedAt']) : undefined diff --git a/src/services/persistence/PouchRepository.ts b/src/services/persistence/PouchRepository.ts index 59bcbee7..362bff4b 100644 --- a/src/services/persistence/PouchRepository.ts +++ b/src/services/persistence/PouchRepository.ts @@ -31,7 +31,7 @@ export class CouchDB { } async sync(to: string) { - console.log("syncing to", to) + console.log('syncing to', to) await this._db.sync(to) } @@ -70,7 +70,7 @@ export class PouchRepository< const allDocs = await this.find( new QueryBuilder() // @ts-ignore - .eq("@type", this._collectionName) // @ts-ignore + .eq('@type', this._collectionName) // @ts-ignore ) // .allDocs({ include_docs: true }) // const mappedDoc = allDocs.value.map(x => this._deserializer.map(x.doc).value) return Result.ok(allDocs.value) @@ -90,7 +90,7 @@ export class PouchRepository< const document = await this._db.db.get(id.value) return Result.ok(this._deserializer.map(document).value) } catch { - return Result.fail("404?") + return Result.fail('404?') } } @@ -101,7 +101,7 @@ export class PouchRepository< async find(query: Query): Promise> { const convertedQuery = new QueryConverter().convert(query) - convertedQuery.selector["@type"] = this._collectionName + convertedQuery.selector['@type'] = this._collectionName const items = await this._db.db.find(convertedQuery) const objs = items.docs.map(x => this._deserializer.map(x).value) return Result.ok(objs) @@ -127,13 +127,13 @@ class QueryConverter { } convert(query: Query): any { - return { "selector": this._visit(query) } + return { 'selector': this._visit(query) } } _visit(query: Query): any { if (query instanceof Predicate) { if (query.operator === Operators.Equal && query.value === undefined) { - return { [query.field]: { "$exists": false } } + return { [query.field]: { '$exists': false } } } return { @@ -143,7 +143,7 @@ class QueryConverter { } } else if (query instanceof Expression) { if (query.operator === LogicalOperators.Not) { - return { "$not": deepMerge({}, ...query.query.map(x => this._visit(x)) ) } + return { '$not': deepMerge({}, ...query.query.map(x => this._visit(x)) ) } } return deepMerge( diff --git a/src/services/persistence/ReviewCardSerializer.ts b/src/services/persistence/ReviewCardSerializer.ts index 6937e3b8..002db35a 100644 --- a/src/services/persistence/ReviewCardSerializer.ts +++ b/src/services/persistence/ReviewCardSerializer.ts @@ -1,18 +1,18 @@ -import { Result } from "@akdasa-studios/framework" -import { ReviewCard, ReviewCardType, VerseId } from "@akdasa-studios/shlokas-core" -import { ObjectMapper } from "./ObjectMapper" +import { Result } from '@akdasa-studios/framework' +import { ReviewCard, ReviewCardType, VerseId } from '@akdasa-studios/shlokas-core' +import { ObjectMapper } from './ObjectMapper' export class ReviewCardSerializer implements ObjectMapper { map(from: ReviewCard): Result { return Result.ok({ - "_id": from.id.value, - "@type": "review", - "verseId": from.verseId.value, - "type": from.type, - "addedAt": from.addedAt, - "dueTo": from.dueTo, - "version": from.version, + '_id': from.id.value, + '@type': 'review', + 'verseId': from.verseId.value, + 'type': from.type, + 'addedAt': from.addedAt, + 'dueTo': from.dueTo, + 'version': from.version, }) } } @@ -20,7 +20,7 @@ export class ReviewCardSerializer implements ObjectMapper { export class ReviewCardDeserializer implements ObjectMapper { map(from: any): Result { const ob = new ReviewCard( - new VerseId(from["verseId"]), + new VerseId(from['verseId']), from['type'] as ReviewCardType, new Date(from['addedAt']), new Date(from['dueTo']), diff --git a/src/services/persistence/VerseStatusSerializer.ts b/src/services/persistence/VerseStatusSerializer.ts index 3b809322..107261ee 100644 --- a/src/services/persistence/VerseStatusSerializer.ts +++ b/src/services/persistence/VerseStatusSerializer.ts @@ -1,16 +1,16 @@ -import { Result } from "@akdasa-studios/framework" -import { Decks, VerseId, VerseStatus } from "@akdasa-studios/shlokas-core" -import { ObjectMapper } from "./ObjectMapper" +import { Result } from '@akdasa-studios/framework' +import { Decks, VerseId, VerseStatus } from '@akdasa-studios/shlokas-core' +import { ObjectMapper } from './ObjectMapper' export class VerseStatusSerializer implements ObjectMapper { map(from: VerseStatus): Result { return Result.ok({ - "_id": from.id.value, - "@type": "verseStatus", - "verseId": from.verseId.value, - "inDeck": from.inDeck, - "version": from.version, + '_id': from.id.value, + '@type': 'verseStatus', + 'verseId': from.verseId.value, + 'inDeck': from.inDeck, + 'version': from.version, }) } } @@ -18,7 +18,7 @@ export class VerseStatusSerializer implements ObjectMapper { export class VerseStatusDeserializer implements ObjectMapper { map(from: any): Result { const ob = new VerseStatus( - new VerseId(from["verseId"]), + new VerseId(from['verseId']), from['inDeck'] as Decks ) ob.version = from['version'] diff --git a/tests/e2e/components/Account.ts b/tests/e2e/components/Account.ts index 1c666bca..5b1f2ef5 100644 --- a/tests/e2e/components/Account.ts +++ b/tests/e2e/components/Account.ts @@ -1,4 +1,4 @@ -import { Page } from "@playwright/test" +import { Page } from '@playwright/test' export class Account { constructor(private readonly page: Page) {} @@ -13,6 +13,6 @@ export class Account { get verifyEmail() { return this.page.getByTestId('verifyEmail') } async open() { - await this.page.goto("/home/settings/account?tutorialEnabled=false") + await this.page.goto('/home/settings/account?tutorialEnabled=false') } } \ No newline at end of file diff --git a/tests/e2e/components/Application.ts b/tests/e2e/components/Application.ts index 7373e338..ce11cf10 100644 --- a/tests/e2e/components/Application.ts +++ b/tests/e2e/components/Application.ts @@ -1,4 +1,4 @@ -import { Page } from "@playwright/test" +import { Page } from '@playwright/test' export interface ApplicationParams { tutorialEnabled?: boolean, diff --git a/tests/e2e/components/InboxDeckPage.ts b/tests/e2e/components/InboxDeckPage.ts index a5da110f..bd6ece64 100644 --- a/tests/e2e/components/InboxDeckPage.ts +++ b/tests/e2e/components/InboxDeckPage.ts @@ -1,17 +1,17 @@ -import { Locator, Page } from "@playwright/test" +import { Locator, Page } from '@playwright/test' export class InboxDeckPage { constructor(private readonly page: Page) {} get tutorialCardIds() { return [ - "tutorial.inbox.cards", - "tutorial.inbox.overall", - "tutorial.inbox.verse", - "tutorial.inbox.final" + 'tutorial.inbox.cards', + 'tutorial.inbox.overall', + 'tutorial.inbox.verse', + 'tutorial.inbox.final' ] } - get inboxEmpty() { return this.page.getByTestId("inboxEmpty") } + get inboxEmpty() { return this.page.getByTestId('inboxEmpty') } async swipeCardLeft(locator: Locator) { await locator.dragTo(locator, { diff --git a/tests/e2e/components/LibraryPage.ts b/tests/e2e/components/LibraryPage.ts index 88d057df..79af57a7 100644 --- a/tests/e2e/components/LibraryPage.ts +++ b/tests/e2e/components/LibraryPage.ts @@ -1,5 +1,5 @@ -import { Page } from "@playwright/test" -import { testId } from "@/app/TestId" +import { Page } from '@playwright/test' +import { testId } from '@/app/TestId' export class LibraryPage { constructor(private readonly page: Page) {} @@ -8,7 +8,7 @@ export class LibraryPage { get listItems() { return this.page.getByRole('listitem') } verse(title: string) { return this.page.getByRole('heading', { name: title }) } - verseBadge(number: string) { return this.page.getByTestId(testId(number, "badge")) } + verseBadge(number: string) { return this.page.getByTestId(testId(number, 'badge')) } verseAddedBadge(number: string) { return this.page.getByText(`Verse ${number} added to inbox`)} get revertButton() { return this.page.getByRole('button', { name: 'Revert' }) } diff --git a/tests/e2e/components/ReviewDeckPage.ts b/tests/e2e/components/ReviewDeckPage.ts index 71d8c68e..0a012f34 100644 --- a/tests/e2e/components/ReviewDeckPage.ts +++ b/tests/e2e/components/ReviewDeckPage.ts @@ -1,6 +1,6 @@ -import { ReviewCardType } from "@akdasa-studios/shlokas-core" -import { Locator, Page } from "@playwright/test" -import { testId } from "@/app/TestId" +import { ReviewCardType } from '@akdasa-studios/shlokas-core' +import { Locator, Page } from '@playwright/test' +import { testId } from '@/app/TestId' export class ReviewDeckPage { constructor(private readonly page: Page) {} @@ -14,12 +14,12 @@ export class ReviewDeckPage { get tutorialCardIds() { return [ - "tutorial.review.questionAnswer", - "tutorial.review.intervals", + 'tutorial.review.questionAnswer', + 'tutorial.review.intervals', ] } - get reviewEmpty() { return this.page.getByTestId("reviewEmpty") } - get cardsCountDueToTomorrow() { return this.page.getByTestId("cardsCountDueToTomorrow") } + get reviewEmpty() { return this.page.getByTestId('reviewEmpty') } + get cardsCountDueToTomorrow() { return this.page.getByTestId('cardsCountDueToTomorrow') } async swipeCardLeft(locator: Locator) { await locator.dragTo(locator, { diff --git a/tests/e2e/components/Settings.ts b/tests/e2e/components/Settings.ts index 070e0d45..632dbd98 100644 --- a/tests/e2e/components/Settings.ts +++ b/tests/e2e/components/Settings.ts @@ -1,7 +1,7 @@ -import { Page } from "@playwright/test" +import { Page } from '@playwright/test' export class Settings { constructor(private readonly page: Page) {} - get account() { return this.page.getByTestId("account") } + get account() { return this.page.getByTestId('account') } } \ No newline at end of file diff --git a/tests/e2e/components/TabsBar.ts b/tests/e2e/components/TabsBar.ts index 13c9953e..63c2df15 100644 --- a/tests/e2e/components/TabsBar.ts +++ b/tests/e2e/components/TabsBar.ts @@ -1,16 +1,16 @@ -import { Page } from "@playwright/test" +import { Page } from '@playwright/test' export class TabsBar { constructor(private readonly page: Page) {} get libraryTab() { return this.page.getByTestId('library-tab') } - get inboxEmpty() { return this.page.getByTestId("inboxEmpty") } + get inboxEmpty() { return this.page.getByTestId('inboxEmpty') } get inboxTab() { return this.page.getByTestId('inbox-tab') } get reviewTab() { return this.page.getByTestId('review-tab') } - get inboxBadge() { return this.page.getByTestId("inbox-tab-badge") } - get reviewBadge() { return this.page.getByTestId("review-tab-badge") } + get inboxBadge() { return this.page.getByTestId('inbox-tab-badge') } + get reviewBadge() { return this.page.getByTestId('review-tab-badge') } - get settingsTab() { return this.page.getByTestId("settings-tab") } + get settingsTab() { return this.page.getByTestId('settings-tab') } } \ No newline at end of file diff --git a/tests/e2e/fixtures/basic.ts b/tests/e2e/fixtures/basic.ts index b13897ca..c6a88db5 100644 --- a/tests/e2e/fixtures/basic.ts +++ b/tests/e2e/fixtures/basic.ts @@ -1,7 +1,7 @@ import { ReviewCardBuilder, ReviewCardType, VerseId } from '@akdasa-studios/shlokas-core' const b = new ReviewCardBuilder() - .ofVerse(new VerseId("ae78e9f1-e5e9-3650-a90e-e79ccd1eb86c")) + .ofVerse(new VerseId('ae78e9f1-e5e9-3650-a90e-e79ccd1eb86c')) .addedAt(new Date()) .dueTo(new Date) diff --git a/tests/e2e/inbox/InboxDeck.spec.ts b/tests/e2e/inbox/InboxDeck.spec.ts index 7bb4c483..24e7da8d 100644 --- a/tests/e2e/inbox/InboxDeck.spec.ts +++ b/tests/e2e/inbox/InboxDeck.spec.ts @@ -27,34 +27,34 @@ test.describe('Inbox Deck', () => { }) test('Swipe card right', async ({ page }) => { - const cardLocator = page.getByTestId("bg 1.1-card-translation") + const cardLocator = page.getByTestId('bg 1.1-card-translation') await cardLocator.dragTo(cardLocator, { sourcePosition: { x: 40, y: 60 }, targetPosition: { x: 0, y: 60 } }) - await expect(cardLocator).toHaveAttribute("data-index", "1") - await expect(page.getByTestId('inbox-tab-badge')).toContainText("2") + await expect(cardLocator).toHaveAttribute('data-index', '1') + await expect(page.getByTestId('inbox-tab-badge')).toContainText('2') }) test('Swipe card top', async ({ page }) => { - const cardLocator = page.getByTestId("bg 1.1-card-translation") + const cardLocator = page.getByTestId('bg 1.1-card-translation') await cardLocator.dragTo(cardLocator, { sourcePosition: { x: 0, y: 60 }, targetPosition: { x: 0, y: 0 } }) - await expect(page.getByTestId('inbox-tab-badge')).toContainText("1") + await expect(page.getByTestId('inbox-tab-badge')).toContainText('1') }) test('Review badge', async ({ page }) => { - const cardLocator = page.getByTestId("bg 1.1-card-translation") + const cardLocator = page.getByTestId('bg 1.1-card-translation') await cardLocator.dragTo(cardLocator, { sourcePosition: { x: 0, y: 60 }, targetPosition: { x: 0, y: 0 } }) await page.getByTestId('library-tab').click() - await expect(page.getByTestId('bg 1.1-badge')).toHaveText("REVIEW") + await expect(page.getByTestId('bg 1.1-badge')).toHaveText('REVIEW') }) test('Swipe all cards', async ({ page }) => { @@ -64,16 +64,16 @@ test.describe('Inbox Deck', () => { ] for (const cardTypeToSwipe of cardTypesToSwipe) { - const cardLocator = page.getByTestId("bg 1.1-card-" + cardTypeToSwipe.toLowerCase()) + const cardLocator = page.getByTestId('bg 1.1-card-' + cardTypeToSwipe.toLowerCase()) await cardLocator.dragTo(cardLocator, { sourcePosition: { x: 0, y: 60 }, targetPosition: { x: 0, y: 0 } }) } - await expect(page.getByTestId("inboxEmpty")).toBeVisible() + await expect(page.getByTestId('inboxEmpty')).toBeVisible() await expect(page.getByTestId('inbox-tab-badge')).toBeHidden() - await expect(page.getByTestId('review-tab-badge')).toHaveText("1") + await expect(page.getByTestId('review-tab-badge')).toHaveText('1') }) }) }) \ No newline at end of file diff --git a/tests/e2e/library/AddToInbox.spec.ts b/tests/e2e/library/AddToInbox.spec.ts index 1cf62f42..fe30c9bf 100644 --- a/tests/e2e/library/AddToInbox.spec.ts +++ b/tests/e2e/library/AddToInbox.spec.ts @@ -16,8 +16,8 @@ test.describe('Library › Add to Inbox', () => { await libraryPage.addVerseButton.click() await libraryPage.verseAddedBadge('BG 1.1').waitFor() - await expect(libraryPage.verseBadge('BG 1.1')).toContainText("INBOX") - await expect(tabsBar.inboxBadge).toContainText("2") + await expect(libraryPage.verseBadge('BG 1.1')).toContainText('INBOX') + await expect(tabsBar.inboxBadge).toContainText('2') }) test('Revert adding verse to the Inbox deck', async ({ page }) => { diff --git a/tests/e2e/library/Search.spec.ts b/tests/e2e/library/Search.spec.ts index 551b6990..258ac49e 100644 --- a/tests/e2e/library/Search.spec.ts +++ b/tests/e2e/library/Search.spec.ts @@ -4,7 +4,7 @@ import { Application, LibraryPage } from '../components' test.beforeEach(async ({ page }) => { await new Application(page) - .goto("/home/library", { tutorialEnabled: false }) + .goto('/home/library', { tutorialEnabled: false }) }) test.describe('Library › Search', () => { diff --git a/tests/e2e/review/GradeButtons.spec.ts b/tests/e2e/review/GradeButtons.spec.ts index 23f46714..c70d864b 100644 --- a/tests/e2e/review/GradeButtons.spec.ts +++ b/tests/e2e/review/GradeButtons.spec.ts @@ -6,7 +6,7 @@ import { addCardsToReview } from '../scenarios' test.describe('Review Deck › Grade Buttons', () => { test.beforeEach(async ({ page }) => { await new Application(page) - .goto("/home/library", { tutorialEnabled: false }) + .goto('/home/library', { tutorialEnabled: false }) await addCardsToReview(page, ['BG 1.1']) await new TabsBar(page).reviewTab.click() }) @@ -43,6 +43,6 @@ test.describe('Review Deck › Grade Buttons', () => { // 1. card is still in the review deck await expect(review.reviewEmpty).toBeHidden() await expect(card).toBeVisible() - await expect(card).toHaveAttribute('data-index', "0") + await expect(card).toHaveAttribute('data-index', '0') }) }) \ No newline at end of file diff --git a/tests/e2e/review/ReviewDeck.spec.ts b/tests/e2e/review/ReviewDeck.spec.ts index 53296f9b..57ab3a59 100644 --- a/tests/e2e/review/ReviewDeck.spec.ts +++ b/tests/e2e/review/ReviewDeck.spec.ts @@ -5,7 +5,7 @@ import { Application, ReviewDeckPage, TabsBar } from '../components' test.describe('Review Deck', () => { test.beforeEach(async ({ page }) => { await new Application(page) - .goto("/home/library", { tutorialEnabled: false }) + .goto('/home/library', { tutorialEnabled: false }) }) /** diff --git a/tests/e2e/review/Schedule.spec.ts b/tests/e2e/review/Schedule.spec.ts index c2c17a12..00ef0873 100644 --- a/tests/e2e/review/Schedule.spec.ts +++ b/tests/e2e/review/Schedule.spec.ts @@ -6,7 +6,7 @@ import { addCardsToReview, nextDays } from '../scenarios' test.describe('Review Deck › Schedule', () => { test.beforeEach(async ({ page }) => { await new Application(page) - .goto("/home/library", { tutorialEnabled: false }) + .goto('/home/library', { tutorialEnabled: false }) }) test.beforeEach(async ({ page }) => { @@ -26,7 +26,7 @@ test.describe('Review Deck › Schedule', () => { // assert: // 1. we create 6 cards per verse, so we should see 6 cards - await (expect(tabs.reviewBadge)).toHaveText("6") + await (expect(tabs.reviewBadge)).toHaveText('6') }) /** @@ -43,7 +43,7 @@ test.describe('Review Deck › Schedule', () => { // assert: // 1. one card scheduled for today and one card from yesterday - await expect(review.cardsCountDueToTomorrow).toHaveText("You have 2 cards scheduled for tomorrow.") + await expect(review.cardsCountDueToTomorrow).toHaveText('You have 2 cards scheduled for tomorrow.') }) /** @@ -62,6 +62,6 @@ test.describe('Review Deck › Schedule', () => { // assert: // 1. one card scheduled for today and one card from yesterday - await (expect(tabs.reviewBadge)).toHaveText("2") + await (expect(tabs.reviewBadge)).toHaveText('2') }) }) \ No newline at end of file diff --git a/tests/e2e/review/SwipeCards.spec.ts b/tests/e2e/review/SwipeCards.spec.ts index 5c00e798..925c0c3f 100644 --- a/tests/e2e/review/SwipeCards.spec.ts +++ b/tests/e2e/review/SwipeCards.spec.ts @@ -7,7 +7,7 @@ import { addCardsToReview } from '../scenarios' test.describe('Review Deck › Swipe Cards', () => { test.beforeEach(async ({ page }) => { await new Application(page) - .goto("/home/library", { tutorialEnabled: false }) + .goto('/home/library', { tutorialEnabled: false }) await addCardsToReview(page, ['BG 1.1', 'BG 2.13']) }) diff --git a/tests/e2e/review/TutorialCards.spec.ts b/tests/e2e/review/TutorialCards.spec.ts index 18057f15..a873f1fa 100644 --- a/tests/e2e/review/TutorialCards.spec.ts +++ b/tests/e2e/review/TutorialCards.spec.ts @@ -5,7 +5,7 @@ import { Application, ReviewDeckPage } from '../components' test.describe('Review Deck › Tutorial Cards', () => { test.beforeEach(async ({ page }) => { await new Application(page) - .goto("/home/review", { tutorialEnabled: true }) + .goto('/home/review', { tutorialEnabled: true }) }) /** diff --git a/tests/e2e/scenarios/accounts.ts b/tests/e2e/scenarios/accounts.ts index 1b77ec76..ec65019b 100644 --- a/tests/e2e/scenarios/accounts.ts +++ b/tests/e2e/scenarios/accounts.ts @@ -1,5 +1,5 @@ -import { Browser, BrowserContext, Page } from "@playwright/test" -import { Account } from "../components" +import { Browser, BrowserContext, Page } from '@playwright/test' +import { Account } from '../components' export async function signUp( context: BrowserContext, @@ -11,9 +11,9 @@ export async function signUp( // act await account.open() await account.signUpViaEmail.click() - await account.name.type("Ivan Petrović") + await account.name.type('Ivan Petrović') await account.email.type(email) - await account.password.type("12345678") + await account.password.type('12345678') await account.signUp.click() // 1. open mail client and confirm email @@ -44,7 +44,7 @@ export async function logIn( await account.email.clear() await account.password.clear() await account.email.type(email) - await account.password.type("12345678") + await account.password.type('12345678') await account.logIn.click() } @@ -55,7 +55,7 @@ export async function logInNewDevice( const context = await browser.newContext() const page = await context.newPage() - await page.goto("/home/settings?tutorialEnabled=false") + await page.goto('/home/settings?tutorialEnabled=false') await logIn(page, email) return [context, page] diff --git a/tests/e2e/scenarios/cards.ts b/tests/e2e/scenarios/cards.ts index e8d46650..51cdd02a 100644 --- a/tests/e2e/scenarios/cards.ts +++ b/tests/e2e/scenarios/cards.ts @@ -1,7 +1,7 @@ -import { InboxCardType } from "@akdasa-studios/shlokas-core" -import { Page } from "@playwright/test" -import { testId } from "@/app/TestId" -import { InboxDeckPage, LibraryPage, TabsBar } from "../components" +import { InboxCardType } from '@akdasa-studios/shlokas-core' +import { Page } from '@playwright/test' +import { testId } from '@/app/TestId' +import { InboxDeckPage, LibraryPage, TabsBar } from '../components' export async function addCardsToReview(page: Page, verses: string[]) { const library = new LibraryPage(page) diff --git a/tests/e2e/settings/Language.spec.ts b/tests/e2e/settings/Language.spec.ts index 4dbc9769..ea58916f 100644 --- a/tests/e2e/settings/Language.spec.ts +++ b/tests/e2e/settings/Language.spec.ts @@ -4,7 +4,7 @@ import { Application } from '../components' test.beforeEach(async ({ page }) => { await new Application(page) - .goto("/home/library", { tutorialEnabled: false }) + .goto('/home/library', { tutorialEnabled: false }) await page.getByTestId('settings-tab').click() }) @@ -12,13 +12,13 @@ test.describe('Settings › Language', () => { test('Change language', async ({ page }) => { await page.getByTestId('language').click() await page.getByRole('button', { name: 'Русский' }).click() - await expect(page.getByRole("banner")).toHaveText("Настройки") + await expect(page.getByRole('banner')).toHaveText('Настройки') }) test('Saves settings', async ({ page }) => { await page.getByTestId('language').click() await page.getByRole('button', { name: 'Русский' }).click() await page.reload() - await expect(page.getByRole("banner")).toHaveText("Настройки") + await expect(page.getByRole('banner')).toHaveText('Настройки') }) }) \ No newline at end of file diff --git a/tests/e2e/settings/Registration.spec.ts b/tests/e2e/settings/Registration.spec.ts index 87aa38a2..9c27d6c4 100644 --- a/tests/e2e/settings/Registration.spec.ts +++ b/tests/e2e/settings/Registration.spec.ts @@ -12,7 +12,7 @@ test.describe('Settings › Account › Email', () => { await signUp(context, page, email) await expect(accountPage.verifyEmail).toBeVisible() await logIn(page, email) - await expect(page.getByText("Welcome back!")).toBeVisible() + await expect(page.getByText('Welcome back!')).toBeVisible() }) test('Log In on another device', async ({ page, context, browser }) => { @@ -25,7 +25,7 @@ test.describe('Settings › Account › Email', () => { // device2: login const [context2, page2] = await logInNewDevice(browser, email) - await expect(page2.getByText("Welcome back!")).toBeVisible() + await expect(page2.getByText('Welcome back!')).toBeVisible() await page2.close() await context2.close() diff --git a/tests/e2e/settings/Sync.spec.ts b/tests/e2e/settings/Sync.spec.ts index f128f772..6eb22e54 100644 --- a/tests/e2e/settings/Sync.spec.ts +++ b/tests/e2e/settings/Sync.spec.ts @@ -6,7 +6,7 @@ import { logIn, logInNewDevice, signUp, sync } from '../scenarios/accounts' test.beforeEach(async ({ page }) => { await new Application(page) - .goto("/home/library", { tutorialEnabled: false }) + .goto('/home/library', { tutorialEnabled: false }) }) test.describe('Settings › Account › Sync', () => { @@ -17,7 +17,7 @@ test.describe('Settings › Account › Sync', () => { // device1: register and login const account1 = new Account(page) - await addCardsToReview(page, ["BG 1.1"]) + await addCardsToReview(page, ['BG 1.1']) await signUp(context, page, email) await logIn(page, email) await account1.sync.click() @@ -27,7 +27,7 @@ test.describe('Settings › Account › Sync', () => { const account2 = new Account(page2) const tabs2 = new TabsBar(page2) await account2.sync.click() - await expect(tabs2.reviewBadge).toHaveText("1") + await expect(tabs2.reviewBadge).toHaveText('1') await context2.close() }) @@ -38,7 +38,7 @@ test.describe('Settings › Account › Sync', () => { // device1: register and login const account1 = new Account(page) - await addCardsToReview(page, ["BG 1.1"]) + await addCardsToReview(page, ['BG 1.1']) await signUp(context, page, email) await logIn(page, email) await account1.sync.click() @@ -51,7 +51,7 @@ test.describe('Settings › Account › Sync', () => { const library2 = new LibraryPage(page2) await tabs2.libraryTab.click() - await expect(library2.verseBadge("BG 1.1")).toHaveText("REVIEW") + await expect(library2.verseBadge('BG 1.1')).toHaveText('REVIEW') await context2.close() }) @@ -61,7 +61,7 @@ test.describe('Settings › Account › Sync', () => { // device1: register and login const account1 = new Account(page) - await addCardsToReview(page, ["BG 1.1"]) + await addCardsToReview(page, ['BG 1.1']) await signUp(context, page, email) await logIn(page, email) await account1.sync.click() @@ -74,14 +74,14 @@ test.describe('Settings › Account › Sync', () => { // device2: add same verse await tabs2.libraryTab.click() - await addCardsToReview(page2, ["BG 1.1"]) + await addCardsToReview(page2, ['BG 1.1']) // device2: sync await tabs2.settingsTab.click() await settings2.account.click() await account2.sync.click() await page2.waitForTimeout(1000) // wait sync to complete - await expect(tabs2.reviewBadge).toHaveText("1") + await expect(tabs2.reviewBadge).toHaveText('1') await context2.close() }) @@ -91,7 +91,7 @@ test.describe('Settings › Account › Sync', () => { // device1: register and login const account1 = new Account(page) - await addCardsToReview(page, ["BG 1.1"]) + await addCardsToReview(page, ['BG 1.1']) await signUp(context, page, email) await logIn(page, email) await account1.sync.click() @@ -108,7 +108,7 @@ test.describe('Settings › Account › Sync', () => { await page2.waitForTimeout(2000) // wait sync to complete await expect(tabs2.inboxBadge).toBeHidden() // already removed on device1. But still on device2 - await expect(tabs2.reviewBadge).toHaveText("1") + await expect(tabs2.reviewBadge).toHaveText('1') await context2.close() })