diff --git a/src/src/app/common/state-service.ts b/src/src/app/common/state-service.ts index 9d46f5ed..cc95da97 100644 --- a/src/src/app/common/state-service.ts +++ b/src/src/app/common/state-service.ts @@ -128,3 +128,9 @@ export function createStateService( return stateServiceClass as IfImmutable>; } + + +export interface IStateAction { + type: TAction; + handle: (state: TState) => TState; +} diff --git a/src/src/app/common/storable.ts b/src/src/app/common/storable.ts index 27d3528c..a6c66a8b 100644 --- a/src/src/app/common/storable.ts +++ b/src/src/app/common/storable.ts @@ -22,5 +22,5 @@ export interface UserVersion { export enum StorableType { initial, - modified + modified, } diff --git a/src/src/app/services/app-state-actions.ts b/src/src/app/services/app-state-actions.ts deleted file mode 100644 index 91b89303..00000000 --- a/src/src/app/services/app-state-actions.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { Error, User, Settings } from '../models/app-state'; -import { IStorable } from '../common/storable'; -import { NoteItem } from '../models/note-state'; -import { MoveDirection } from '../common/move-direction'; -import { SavedPage } from '../models/page-state'; -import { CardItem } from '../models/card-state'; -import { Overlap } from '../common/bible-reference'; - -export class AppActionFactory { - static newSavePage(title: string): AppAction { - return { - type: 'SAVE_PAGE', - title, - } as AppAction; - } - - static newUpdateCurrentPage(): AppAction { - return { - type: 'UPDATE_CURRENT_PAGE', - } as AppAction; - } - - static newUpdateSavedPages(savedPages: IStorable): AppAction { - return { - type: 'UPDATE_SAVED_PAGES', - savedPages, - } as AppAction; - } - - static newUpdateSavedPage(savedPage: SavedPage): AppAction { - return { - type: 'UPDATE_SAVED_PAGE', - savedPage, - } as AppAction; - } - - static newMoveSavedPageCard(savedPage: SavedPage, fromIndex: number, toIndex: number): AppAction { - return { - type: 'MOVE_SAVED_PAGE_CARD', - savedPage, - fromIndex, - toIndex, - } as AppAction; - } - - static newRemoveSavedPage(savedPage: SavedPage): AppAction { - return { - type: 'REMOVE_SAVED_PAGE', - savedPage, - } as AppAction; - } - - static newAddCardToSavedPage(card: CardItem, pageId: string): AppAction { - return { - type: 'ADD_CARD_TO_SAVED_PAGE', - card, - pageId, - } as AppAction; - } - - static newAddCard(card: CardItem, nextToItem: CardItem): AppAction { - return { - type: 'ADD_CARD', - card, - nextToItem, - } as AppAction; - } - - static newUpdateCard(newCard: CardItem, oldCard: CardItem): AppAction { - return { - type: 'UPDATE_CARD', - newCard, - oldCard, - } as AppAction; - } - - static newRemoveCard(card: CardItem): AppAction { - return { - type: 'REMOVE_CARD', - card, - } as AppAction; - } - - static newMoveCard(card: CardItem, direction: MoveDirection): AppAction { - return { - type: 'MOVE_CARD', - card, - direction, - } as AppAction; - } - - static newUpdateCards(cards: IStorable): AppAction { - return { - type: 'UPDATE_CARDS', - cards, - } as AppAction; - } - - static newClearCards(): AppAction { - return { - type: 'CLEAR_CARDS', - } as AppAction; - } - - static newUpdateError(error: Error): AppAction { - return { - type: 'UPDATE_ERROR', - error, - } as AppAction; - } - - static newUpdateCardMergeStrategy(strategy: Overlap): AppAction { - return { - type: 'UPDATE_CARD_MERGE_STRATEGY', - cardMergeStrategy: strategy, - }; - } - - static newUpdateCardFontSize(cardFontSize: number): AppAction { - return { - type: 'UPDATE_CARD_FONT_SIZE', - cardFontSize, - } as AppAction; - } - - static newUpdateCardFontFamily(cardFontFamily: string): AppAction { - return { - type: 'UPDATE_CARD_FONT_FAMILY', - cardFontFamily, - } as AppAction; - } - - static newUpdateAutocomplete(words: string[]): AppAction { - return { - type: 'UPDATE_AUTOCOMPLETE', - words, - } as AppAction; - } - - static newUpdateSettings(settings: IStorable): AppAction { - return { - type: 'UPDATE_SETTINGS', - settings, - } as AppAction; - } - static newUser(user: User): AppAction { - return { - type: 'SET_USER', - user, - } as AppAction; - } - - static newFindNotes(qry: string, nextToItem: CardItem): AppAction { - return { - type: 'FIND_NOTES', - qry, - nextToItem, - } as AppAction; - } - - static newGetNote(noteId: string, nextToItem: CardItem): AppAction { - return { - type: 'GET_NOTE', - noteId, - nextToItem, - } as AppAction; - } - - static newUpdateNotes(notes: IStorable): AppAction { - return { - type: 'UPDATE_NOTES', - notes, - } as AppAction; - } - - static newSaveNote(note: NoteItem): AppAction { - return { - type: 'SAVE_NOTE', - note, - } as AppAction; - } - - static newDeleteNote(note: NoteItem): AppAction { - return { - type: 'DELETE_NOTE', - note, - } as AppAction; - } -} - -export type AppAction = - | { - type: 'SAVE_PAGE'; - title: string; - } - | { - type: 'UPDATE_CURRENT_PAGE'; - } - | { - type: 'UPDATE_SAVED_PAGES'; - savedPages: IStorable; - } - | { - type: 'UPDATE_SAVED_PAGE'; - savedPage: SavedPage; - } - | { - type: 'REMOVE_SAVED_PAGE'; - savedPage: SavedPage; - } - | { - type: 'MOVE_SAVED_PAGE_CARD'; - fromIndex: number; - toIndex: number; - savedPage: SavedPage; - } - | { - type: 'ADD_CARD_TO_SAVED_PAGE'; - card: CardItem; - pageId: string; - } - | { - type: 'ADD_CARD'; - card: CardItem; - nextToItem: CardItem; - } - | { - type: 'UPDATE_CARD'; - newCard: CardItem; - oldCard: CardItem; - } - | { - type: 'REMOVE_CARD'; - card: CardItem; - } - | { - type: 'CLEAR_CARDS'; - } - | { - type: 'MOVE_CARD'; - card: CardItem; - direction: MoveDirection; - } - | { - type: 'UPDATE_CARDS'; - cards: IStorable; - } - | { - type: 'UPDATE_ERROR'; - error: Error; - } - | { - type: 'UPDATE_CARD_MERGE_STRATEGY'; - cardMergeStrategy: Overlap; - } - | { - type: 'UPDATE_CARD_FONT_SIZE'; - cardFontSize: number; - } - | { - type: 'UPDATE_CARD_FONT_FAMILY'; - cardFontFamily: string; - } - | { - type: 'UPDATE_AUTOCOMPLETE'; - words: string[]; - } - | { - type: 'UPDATE_SETTINGS'; - settings: IStorable; - } - | { - type: 'SET_USER'; - user: User; - } - | { - type: 'FIND_NOTES'; - qry: string; - nextToItem: CardItem; - } - | { - type: 'GET_NOTE'; - noteId: string; - nextToItem: CardItem; - } - | { - type: 'UPDATE_NOTES'; - notes: IStorable; - } - | { - type: 'SAVE_NOTE'; - note: NoteItem; - } - | { - type: 'DELETE_NOTE'; - note: NoteItem; - }; diff --git a/src/src/app/services/app-state-initial-state.ts b/src/src/app/services/app-state-initial-state.ts deleted file mode 100644 index 6a660317..00000000 --- a/src/src/app/services/app-state-initial-state.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { AppState } from '../models/app-state'; -import { Overlap } from '../common/bible-reference'; -import { StorableType } from '../common/storable'; - -export const initialState: AppState = { - user: null, - currentCards: { - type: StorableType.initial, - createdOn: new Date(0).toISOString(), - value: [], - }, - cardCache: {}, - autocomplete: [], - currentSavedPage: null, - savedPages: null, - notes: { - type: StorableType.initial, - createdOn: new Date(0).toISOString(), - value: [], - }, - savedPagesLoaded: false, - error: null, - settings: { - type: StorableType.initial, - createdOn: new Date(0).toISOString(), - value: { - displaySettings: { - showStrongsAsModal: false, - appendCardToBottom: true, - insertCardNextToItem: true, - clearSearchAfterQuery: true, - cardFontSize: 12, - cardFontFamily: 'PT Serif', - showVersesOnNewLine: false, - showVerseNumbers: false, - showParagraphs: true, - showParagraphHeadings: true, - syncCardsAcrossDevices: false, - }, - pageSettings: { - mergeStrategy: Overlap.Equal, - }, - cardIcons: { - words: 'font_download', - passage: 'menu_book', - strongs: 'speaker_notes', - note: 'note', - savedPage: 'inbox', - }, - }, - }, -}; diff --git a/src/src/app/services/app-state-reducer.spec.ts b/src/src/app/services/app-state-reducer.spec.ts index 23a5c385..4aec8096 100644 --- a/src/src/app/services/app-state-reducer.spec.ts +++ b/src/src/app/services/app-state-reducer.spec.ts @@ -1,6 +1,4 @@ import { TestBed } from '@angular/core/testing'; -import { reducer, getNewestStorable } from './app-state-reducer'; -import { AppActionFactory } from './app-state-actions'; import { Overlap } from '../common/bible-reference'; import { Storable, StorableType } from '../common/storable'; import { CardType, CardItem } from '../models/card-state'; @@ -9,6 +7,30 @@ import { AppState, User } from '../models/app-state'; import { getCardCacheKey } from '../common/card-cache-operations'; import { MoveDirection } from '../common/move-direction'; import { NoteItem } from '../models/note-state'; +import { + addCardAction, + addCardToSavedPageAction, + deleteNoteAction, + findNotesAction, + getNewestStorable, + getNotesAction, + moveCardAction, + moveSavedPageCardAction, + removeCardAction, + removeSavedPageAction, + saveNoteAction, + savePageAction, + updateAutoCompleteAction, + updateCardAction, + updateCardFontFamilyAction, + updateCardFontSizeAction, + updateCardMergeStrategyAction, + updateCurrentPageAction, + updateNotesAction, + updateSavedPageAction, + updateSavedPagesAction, + updateSettingsAction, +} from './app.service'; describe('getNewestStorable', () => { it('maybeMutateStorable', () => { @@ -126,8 +148,7 @@ describe('AppService Reducer', () => { it('UPDATE_CARD_MERGE_STRATEGY', () => { for (const strategy of [Overlap.None, Overlap.Equal, Overlap.Subset, Overlap.Intersect]) { - const action = AppActionFactory.newUpdateCardMergeStrategy(strategy); - const testState = reducer(preState, action); + const testState = updateCardMergeStrategyAction(strategy).handle(preState); expect(testState.settings.value.pageSettings.mergeStrategy).toEqual(strategy); } }); @@ -163,20 +184,17 @@ describe('AppService Reducer', () => { }, }; - const action = AppActionFactory.newUpdateSettings(settings); - const testState = reducer(preState, action); + const testState = updateSettingsAction(settings).handle(preState); expect(testState.settings).toBe(settings, 'Failed to update the display settings'); }); it('UPDATE_CARD_FONT_SIZE', () => { - const action = AppActionFactory.newUpdateCardFontSize(32); - const testState = reducer(preState, action); + const testState = updateCardFontSizeAction(32).handle(preState); expect(testState.settings.value.displaySettings.cardFontSize).toBe(32, 'Failed to change card font size to 32'); }); it('UPDATE_CARD_FONT_FAMILY', () => { - const action = AppActionFactory.newUpdateCardFontFamily('Jason'); - const testState = reducer(preState, action); + const testState = updateCardFontFamilyAction('Jason').handle(preState); expect(testState.settings.value.displaySettings.cardFontFamily).toBe( 'Jason', 'Failed to change card font family to "Jason"' @@ -188,8 +206,7 @@ describe('AppService Reducer', () => { it('UPDATE_AUTOCOMPLETE', () => { const words = ['word1', 'word2', 'word3']; - const action = AppActionFactory.newUpdateAutocomplete(words); - const testState = reducer(preState, action); + const testState = updateAutoCompleteAction(words).handle(preState); expect(testState.autocomplete).toEqual(words, 'Failed to update the autocomplete array'); }); @@ -210,8 +227,7 @@ describe('AppService Reducer', () => { }, ]); - const action = AppActionFactory.newUpdateSavedPages(savedPages); - const testState = reducer(preState, action); + const testState = updateSavedPagesAction(savedPages).handle(preState); expect(testState.savedPages).toBe(savedPages, 'Failed to update the savedPages array'); expect(testState.savedPages.value.length).toBe(1, 'Updated savedPages is the wrong length'); expect(testState.savedPages.value[0].queries.length).toBe( @@ -227,8 +243,7 @@ describe('AppService Reducer', () => { id: 'myid2', }; - const action = AppActionFactory.newUpdateSavedPage(sp); - const testState = reducer(preState, action); + const testState = updateSavedPageAction(sp).handle(preState); expect(testState.savedPages.value[0].queries.length).toBe( 1, @@ -244,8 +259,7 @@ describe('AppService Reducer', () => { it('REMOVE_SAVED_PAGE', () => { const sp = preState.savedPages.value[0]; - const action = AppActionFactory.newRemoveSavedPage(sp); - const testState = reducer(preState, action); + const testState = removeSavedPageAction(sp).handle(preState); expect(testState.savedPages.value.length).toBe(1, 'Updated savedPages should be 1'); expect(testState.savedPages.value[0].title).toBe('page2'); @@ -258,12 +272,10 @@ describe('AppService Reducer', () => { type: CardType.Strongs, }; - const action1 = AppActionFactory.newAddCard(card1, null); - const testState = reducer(preState, action1); + const testState = addCardAction(card1, null).handle(preState); expect(testState.currentCards.value[0]).toBe(card1, 'Failed to add first card to empty list'); - const action = AppActionFactory.newUpdateCurrentPage(); - const testState2 = reducer(testState, action); + const testState2 = updateCurrentPageAction().handle(testState); expect(testState2.currentSavedPage.queries.length).toBe(1); expect(preState.currentSavedPage.queries.length).toBe(0); @@ -276,11 +288,9 @@ describe('AppService Reducer', () => { type: CardType.Passage, }; - const action1 = AppActionFactory.newAddCard(card1, null); - const preState2 = reducer(preState, action1); + const preState2 = addCardAction(card1, null).handle(preState); - const action = AppActionFactory.newSavePage('my saved page'); - const testState = reducer(preState2, action); + const testState = savePageAction('my saved page').handle(preState2); expect(testState.savedPages.value[2].queries.length).toBe( 1, @@ -292,8 +302,7 @@ describe('AppService Reducer', () => { it('MOVE_SAVED_PAGE_CARD', () => { const page = preState.savedPages.value[1]; - const action1 = AppActionFactory.newMoveSavedPageCard(page, 1, 0); - const testState = reducer(preState, action1); + const testState = moveSavedPageCardAction(page, 1, 0).handle(preState); expect(testState.savedPages.value[1].queries[0].qry).toBe('G1', 'Failed to move card in saved page'); }); @@ -303,8 +312,7 @@ describe('AppService Reducer', () => { data: null, type: CardType.Passage, }; - const action1 = AppActionFactory.newAddCardToSavedPage(card1, 'myid1'); - const testState = reducer(preState, action1); + const testState = addCardToSavedPageAction(card1, 'myid1').handle(preState); expect(testState.savedPages.value[0].queries[1].qry).toBe('jn 3:16', 'Failed to add card to saved page'); }); @@ -319,8 +327,7 @@ describe('AppService Reducer', () => { type: CardType.Strongs, }; - const action1 = AppActionFactory.newAddCard(card1, null); - const testState = reducer(preState, action1); + const testState = addCardAction(card1, null).handle(preState); expect(testState.currentCards.value[0]).toBe(card1, 'Failed to add first card to empty list'); const card2: CardItem = { @@ -329,8 +336,7 @@ describe('AppService Reducer', () => { type: CardType.Strongs, }; - const action2 = AppActionFactory.newAddCard(card2, null); - const testState2 = reducer(testState, action2); + const testState2 = addCardAction(card2, null).handle(testState); expect(testState2.currentCards.value.length).toBe(2, 'Failed to add second card to list with 1 item'); expect(testState2.currentCards.value[1]).toBe(card2); @@ -342,125 +348,109 @@ describe('AppService Reducer', () => { // append to top, insert next to card - let setState = reducer( - testState2, - AppActionFactory.newUpdateSettings({ - createdOn: new Date(2020, 1, 1, 0, 0, 0, 0).toISOString(), - type: StorableType.initial, - value: { - ...testState2.settings.value, - displaySettings: { - showStrongsAsModal: true, - appendCardToBottom: false, - insertCardNextToItem: true, - clearSearchAfterQuery: false, - cardFontSize: 100, - cardFontFamily: 'Jason', - showVersesOnNewLine: true, - showVerseNumbers: true, - showParagraphs: false, - showParagraphHeadings: false, - syncCardsAcrossDevices: true, - }, + let setState = updateSettingsAction({ + createdOn: new Date(2020, 1, 1, 0, 0, 0, 0).toISOString(), + type: StorableType.initial, + value: { + ...testState2.settings.value, + displaySettings: { + showStrongsAsModal: true, + appendCardToBottom: false, + insertCardNextToItem: true, + clearSearchAfterQuery: false, + cardFontSize: 100, + cardFontFamily: 'Jason', + showVersesOnNewLine: true, + showVerseNumbers: true, + showParagraphs: false, + showParagraphHeadings: false, + syncCardsAcrossDevices: true, }, - }) - ); + }, + }).handle(testState2); - const action3 = AppActionFactory.newAddCard(card3, card2); - const testState3 = reducer(setState, action3); + const testState3 = addCardAction(card3, card2).handle(setState); expect(testState3.currentCards.value.length).toBe(3, 'Failed to add third card'); expect(testState3.currentCards.value[1]).toBe(card3, 'Failed to insert card above the second card'); // append to bottom, insert next to card - setState = reducer( - testState2, - AppActionFactory.newUpdateSettings({ - createdOn: new Date(2020, 1, 1, 0, 0, 0, 0).toISOString(), - type: StorableType.initial, - value: { - ...testState2.settings.value, - displaySettings: { - showStrongsAsModal: true, - appendCardToBottom: true, - insertCardNextToItem: true, - clearSearchAfterQuery: false, - cardFontSize: 100, - cardFontFamily: 'Jason', - showVersesOnNewLine: true, - showVerseNumbers: true, - showParagraphs: false, - showParagraphHeadings: false, - syncCardsAcrossDevices: true, - }, + setState = updateSettingsAction({ + createdOn: new Date(2020, 1, 1, 0, 0, 0, 0).toISOString(), + type: StorableType.initial, + value: { + ...testState2.settings.value, + displaySettings: { + showStrongsAsModal: true, + appendCardToBottom: true, + insertCardNextToItem: true, + clearSearchAfterQuery: false, + cardFontSize: 100, + cardFontFamily: 'Jason', + showVersesOnNewLine: true, + showVerseNumbers: true, + showParagraphs: false, + showParagraphHeadings: false, + syncCardsAcrossDevices: true, }, - }) - ); + }, + }).handle(testState2); - const action4 = AppActionFactory.newAddCard(card3, card1); - const testState4 = reducer(setState, action4); + const testState4 = addCardAction(card3, card1).handle(setState); expect(testState4.currentCards.value.length).toBe(3, 'Failed to add third card'); expect(testState4.currentCards.value[1]).toBe(card3, 'Failed to insert card below the first card'); // append to bottom, do not insert next to card - setState = reducer( - testState2, - AppActionFactory.newUpdateSettings({ - createdOn: new Date(2020, 1, 1, 0, 0, 0, 0).toISOString(), - type: StorableType.initial, - value: { - ...testState2.settings.value, - displaySettings: { - showStrongsAsModal: true, - appendCardToBottom: true, - insertCardNextToItem: false, - clearSearchAfterQuery: false, - cardFontSize: 100, - cardFontFamily: 'Jason', - showVersesOnNewLine: true, - showVerseNumbers: true, - showParagraphs: false, - showParagraphHeadings: false, - syncCardsAcrossDevices: true, - }, + setState = updateSettingsAction({ + createdOn: new Date(2020, 1, 1, 0, 0, 0, 0).toISOString(), + type: StorableType.initial, + value: { + ...testState2.settings.value, + displaySettings: { + showStrongsAsModal: true, + appendCardToBottom: true, + insertCardNextToItem: false, + clearSearchAfterQuery: false, + cardFontSize: 100, + cardFontFamily: 'Jason', + showVersesOnNewLine: true, + showVerseNumbers: true, + showParagraphs: false, + showParagraphHeadings: false, + syncCardsAcrossDevices: true, }, - }) - ); + }, + }).handle(testState2); - const action5 = AppActionFactory.newAddCard(card3, card1); - const testState5 = reducer(setState, action5); + const testState5 = addCardAction(card3, card1).handle(setState); expect(testState5.currentCards.value.length).toBe(3, 'Failed to add third card'); expect(testState5.currentCards.value[2]).toBe(card3, 'Failed to insert card at end of the list'); // append to top, do not insert next to card - setState = reducer( - testState2, - AppActionFactory.newUpdateSettings({ - createdOn: new Date(2020, 1, 1, 0, 0, 0, 0).toISOString(), - type: StorableType.initial, - value: { - ...testState2.settings.value, - displaySettings: { - showStrongsAsModal: true, - appendCardToBottom: false, - insertCardNextToItem: false, - clearSearchAfterQuery: false, - cardFontSize: 100, - cardFontFamily: 'Jason', - showVersesOnNewLine: true, - showVerseNumbers: true, - showParagraphs: false, - showParagraphHeadings: false, - syncCardsAcrossDevices: true, - }, + setState = updateSettingsAction({ + createdOn: new Date(2020, 1, 1, 0, 0, 0, 0).toISOString(), + type: StorableType.initial, + value: { + ...testState2.settings.value, + displaySettings: { + showStrongsAsModal: true, + appendCardToBottom: false, + insertCardNextToItem: false, + clearSearchAfterQuery: false, + cardFontSize: 100, + cardFontFamily: 'Jason', + showVersesOnNewLine: true, + showVerseNumbers: true, + showParagraphs: false, + showParagraphHeadings: false, + syncCardsAcrossDevices: true, }, - }) - ); + }, + }).handle(testState2); - const action6 = AppActionFactory.newAddCard(card3, card1); - const testState6 = reducer(setState, action6); + const testState6 = addCardAction(card3, card1).handle(setState); expect(testState6.currentCards.value.length).toBe(3, 'Failed to add third card'); expect(testState6.currentCards.value[0]).toBe(card3, 'Failed to insert card at start of the list'); }); @@ -472,8 +462,7 @@ describe('AppService Reducer', () => { type: CardType.Strongs, }; - const action1 = AppActionFactory.newAddCard(oldCard, null); - const preState1 = reducer(preState, action1); + const preState1 = addCardAction(oldCard, null).handle(preState); const newCard: CardItem = { qry: 'H88', @@ -481,8 +470,7 @@ describe('AppService Reducer', () => { type: CardType.Strongs, }; - const action2 = AppActionFactory.newUpdateCard(newCard, oldCard); - const testState = reducer(preState1, action2); + const testState = updateCardAction(newCard, oldCard).handle(preState1); expect(testState.currentCards.value[0].qry).toBe('H88', 'Should update the card'); expect(testState.cardCache[getCardCacheKey(newCard)].qry).toBe('H88', 'Should exist in card cache'); @@ -498,13 +486,14 @@ describe('AppService Reducer', () => { type: CardType.Strongs, }; - const action1 = AppActionFactory.newAddCard(card, null); - const preState1 = reducer(preState, action1); + const preState1 = addCardAction(card, null).handle(preState); - const action2 = AppActionFactory.newRemoveCard(card); - const testState = reducer(preState1, action2); + const testState = removeCardAction(card).handle(preState1); - expect(preState1.currentCards.value.length).toBe(1, 'Should have added the card in preparation for removing the card'); + expect(preState1.currentCards.value.length).toBe( + 1, + 'Should have added the card in preparation for removing the card' + ); expect(testState.currentCards.value.length).toBe(0, 'Should have removed the card'); expect(testState.cardCache[getCardCacheKey(card)]).toBeUndefined( 'Should be undefined, having been removed from card cache' @@ -518,33 +507,30 @@ describe('AppService Reducer', () => { type: CardType.Strongs, }; - const action1 = AppActionFactory.newAddCard(card1, null); - const preState1 = reducer(preState, action1); + const preState1 = addCardAction(card1, null).handle(preState); const card2: CardItem = { qry: 'H88', data: null, type: CardType.Strongs, }; - const action2 = AppActionFactory.newAddCard(card2, null); - const preState2 = reducer(preState1, action2); + + const preState2 = addCardAction(card2, null).handle(preState1); expect(preState2.currentCards.value.length).toBe(2, 'Should have two cards'); expect(preState2.currentCards.value[0].qry).toBe('G123'); expect(preState2.currentCards.value[1].qry).toBe('H88'); - const action3 = AppActionFactory.newMoveCard(card2, MoveDirection.Up); - const testState1 = reducer(preState2, action3); + const testState1 = moveCardAction(card2, MoveDirection.Up).handle(preState2); expect(testState1.currentCards.value[0].qry).toBe('H88'); expect(testState1.currentCards.value[1].qry).toBe('G123'); - const testState4 = reducer(preState2, action3); + const testState4 = moveCardAction(card2, MoveDirection.Up).handle(preState2); expect(testState4.currentCards.value[0].qry).toBe('H88'); expect(testState4.currentCards.value[1].qry).toBe('G123'); - const action4 = AppActionFactory.newMoveCard(card1, MoveDirection.Down); - const testState2 = reducer(preState2, action4); + const testState2 = moveCardAction(card1, MoveDirection.Down).handle(preState2); expect(testState2.currentCards.value[0].qry).toBe('H88'); expect(testState2.currentCards.value[1].qry).toBe('G123'); - const testState3 = reducer(preState2, action4); + const testState3 = moveCardAction(card1, MoveDirection.Down).handle(preState2); expect(testState3.currentCards.value[0].qry).toBe('H88'); expect(testState3.currentCards.value[1].qry).toBe('G123'); }); @@ -559,8 +545,7 @@ describe('AppService Reducer', () => { providerId: 'asdfasf', }; - const action1 = AppActionFactory.newUser(user); - const testState = reducer(preState, action1); + const testState = newUserAction(user).handle(preState); expect(testState.user).toBe(user, 'Should set the user'); }); @@ -581,18 +566,14 @@ describe('AppService Reducer', () => { xref: 'jn 3:16', }; - const action1 = AppActionFactory.newSaveNote(note1); - const preState1 = reducer(preState, action1); - const action2 = AppActionFactory.newSaveNote(note2); - const preState2 = reducer(preState1, action2); + const preState1 = saveNoteAction(note1).handle(preState); + const preState2 = saveNoteAction(note2).handle(preState1); expect(preState2.notes.value.length).toBe(2, 'Should have two notes'); - const action3 = AppActionFactory.newGetNote('123456789', null); - const preState3 = reducer(preState2, action3); + const preState3 = getNotesAction('123456789', null).handle(preState2); expect(preState3.currentCards.value.length).toBe(1, 'Should have added the note card'); - const action4 = AppActionFactory.newGetNote('1234567890', null); - const preState4 = reducer(preState3, action4); + const preState4 = getNotesAction('1234567890', null).handle(preState3); expect(preState4.currentCards.value.length).toBe(2, 'Should have added the note card'); }); @@ -610,19 +591,20 @@ describe('AppService Reducer', () => { xref: 'jn 3:16', }; - const action1 = AppActionFactory.newUpdateNotes(new Storable([note1, note2])); - const preState1 = reducer(preState, action1); + const preState1 = updateNotesAction(new Storable([note1, note2])).handle(preState); expect(preState1.notes.value.length).toBe(2, 'Should have added the notes'); - const action2 = AppActionFactory.newFindNotes('note', null); - const preState2 = reducer(preState1, action2); + const preState2 = findNotesAction('note', null).handle(preState1); expect(preState2.currentCards.value.length).toBe(2, 'Should have found two notes card'); - const action3 = AppActionFactory.newDeleteNote(note1); - const preState3 = reducer(preState2, action3); + const preState3 = deleteNoteAction(note1).handle(preState2); expect(preState3.currentCards.value.length).toBe(1, 'Should have deleted the note card'); expect(preState3.notes.value.length).toBe(1, 'Should have added the notes'); }); //#endregion }); +function newUserAction(preState: AppState, action1: any) { + throw new Error('Function not implemented.'); +} + diff --git a/src/src/app/services/app-state-reducer.ts b/src/src/app/services/app-state-reducer.ts deleted file mode 100644 index c1458d9e..00000000 --- a/src/src/app/services/app-state-reducer.ts +++ /dev/null @@ -1,489 +0,0 @@ -import { UUID } from 'angular2-uuid'; - -import { AppState, Settings } from '../models/app-state'; -import { IStorable, Storable } from '../common/storable'; -import { NoteItem } from '../models/note-state'; - -import { mergeCardList } from '../common/card-operations'; -import { updateInCardCache, removeFromCardCache, getFromCardCache } from '../common/card-cache-operations'; - -import { AppAction, AppActionFactory } from './app-state-actions'; -import { initialState } from './app-state-initial-state'; -import { SavedPage } from '../models/page-state'; -import { CardType, CardItem, DataReference } from '../models/card-state'; -import { moveItem, moveItemUpOrDown } from '../common/array-operations'; - -export function getNewestStorable(candidate: IStorable, incumbant: IStorable): IStorable { - // if the candidate is null, then return the state. - if (!candidate) { - return incumbant; - } - - // only update if the settings are newer. - if (!incumbant || new Date(candidate.createdOn) > new Date(incumbant.createdOn)) { - return candidate; - } - - // candidate didn't win. return state untouched. - return incumbant; -} - -export function reducer(state: AppState, action: AppAction): AppState { - // somtimes the state is null. lets not complain if that happens. - if (state === undefined) { - state = initialState; - } - - switch (action.type) { - case 'UPDATE_ERROR': { - return { - ...state, - error: action.error, - }; - } - - //#region Settings - - case 'UPDATE_CARD_MERGE_STRATEGY': { - const settings = new Storable({ - ...state.settings.value, - pageSettings: { - ...state.settings.value.pageSettings, - mergeStrategy: action.cardMergeStrategy, - }, - }); - return reducer(state, { - type: 'UPDATE_SETTINGS', - settings, - }); - } - - case 'UPDATE_SETTINGS': { - const item = getNewestStorable(action.settings, state.settings); - - return { - ...state, - settings: item, - }; - } - - case 'UPDATE_CARD_FONT_SIZE': { - const settings = new Storable({ - ...state.settings.value, - displaySettings: { - ...state.settings.value.displaySettings, - cardFontSize: action.cardFontSize, - }, - }); - - return reducer(state, { - type: 'UPDATE_SETTINGS', - settings, - }); - } - - case 'UPDATE_CARD_FONT_FAMILY': { - const settings = new Storable({ - ...state.settings.value, - displaySettings: { - ...state.settings.value.displaySettings, - cardFontFamily: action.cardFontFamily, - }, - }); - - return reducer(state, { - type: 'UPDATE_SETTINGS', - settings, - }); - } - - //#endregion - - case 'UPDATE_AUTOCOMPLETE': { - return { - ...state, - autocomplete: [...action.words], - }; - } - - //#region Saved Pages - - case 'UPDATE_SAVED_PAGES': { - const savedPages = getNewestStorable(action.savedPages, state.savedPages); - - // only true if a currentSavedPage was set, indicating that the user - // is currently looking at a saved page. - const hasCurrentSavedPage = - state.currentSavedPage !== null && - state.currentSavedPage !== undefined && - action.savedPages.value.some((o) => o.id === state.currentSavedPage.id); - - const currentSavedPage = hasCurrentSavedPage - ? action.savedPages.value.find((o) => o.id === state.currentSavedPage.id) - : null; - - return { - ...state, - // if the currentSavedPage was loaded, replace it with the info from the - // new savedPages array, as it might have changed. - currentCards: hasCurrentSavedPage ? new Storable(currentSavedPage.queries) : state.currentCards, - currentSavedPage: hasCurrentSavedPage ? currentSavedPage : state.currentSavedPage, - savedPagesLoaded: true, - savedPages, // update the savedPages - }; - } - - case 'UPDATE_SAVED_PAGE': { - const newSavedPages = new Storable( - state.savedPages.value.map((o) => { - if (o.id === action.savedPage.id) { - return action.savedPage; - } - return o; - }) - ); - - const savedPages = getNewestStorable(newSavedPages, state.savedPages); - return reducer(state, AppActionFactory.newUpdateSavedPages(savedPages)); - } - - case 'REMOVE_SAVED_PAGE': { - const savedPages = new Storable(state.savedPages.value.filter((o) => o.id !== action.savedPage.id)); - const item = getNewestStorable(savedPages, state.savedPages); - - return { - ...state, - savedPagesLoaded: true, - savedPages: item, - }; - } - - case 'UPDATE_CURRENT_PAGE': { - const current = { - id: state.currentSavedPage.id, - title: state.currentSavedPage.title, - queries: [...mergeCardList(state.currentCards.value, state.settings.value.pageSettings.mergeStrategy)], - }; - - const savedPages = new Storable([ - ...state.savedPages.value.filter((o) => o.id !== state.currentSavedPage.id), - current, - ]); - - const item = getNewestStorable(savedPages, state.savedPages); - return { - ...state, - currentSavedPage: current, - savedPagesLoaded: true, - savedPages: item, - }; - } - - case 'SAVE_PAGE': { - const savedPages = new Storable([ - ...(state.savedPages ? state.savedPages.value : []), - { - // create a new saved page object - title: action.title, - id: UUID.UUID().toString(), - queries: [...mergeCardList(state.currentCards.value, state.settings.value.pageSettings.mergeStrategy)], - }, - ]); - - return reducer(state, { - type: 'UPDATE_SAVED_PAGES', - savedPages, - }); - } - - case 'MOVE_SAVED_PAGE_CARD': { - const queries = moveItem(action.savedPage.queries, action.fromIndex, action.toIndex); - const savedPage = { - ...action.savedPage, - queries, // update the queries. - }; - - return reducer(state, AppActionFactory.newUpdateSavedPage(savedPage)); - } - - case 'ADD_CARD_TO_SAVED_PAGE': { - const savedPages = new Storable([ - ...(state.savedPages ? state.savedPages.value : []).map((o) => { - if (o.id.toString() === action.pageId) { - let references = [] as DataReference[]; - if (state.settings.value.displaySettings.appendCardToBottom) { - references = [...o.queries, action.card]; - } else { - references = [action.card, ...o.queries]; - } - return { - ...o, - queries: mergeCardList(references, state.settings.value.pageSettings.mergeStrategy), - }; - } - return o; - }), - ]); - - return reducer(state, { - type: 'UPDATE_SAVED_PAGES', - savedPages, - }); - } - - //#endregion - - //#region Cards - - case 'ADD_CARD': { - let cards = []; - - if (action.nextToItem && state.settings.value.displaySettings.insertCardNextToItem) { - const idx = state.currentCards.value.indexOf(action.nextToItem); - - if (state.settings.value.displaySettings.appendCardToBottom) { - const before = state.currentCards.value.slice(0, idx + 1); - const after = state.currentCards.value.slice(idx + 1); - cards = [...before, action.card, ...after]; - } else { - const before = state.currentCards.value.slice(0, idx); - const after = state.currentCards.value.slice(idx); - cards = [...before, action.card, ...after]; - } - } else { - if (state.settings.value.displaySettings.appendCardToBottom) { - cards = [...state.currentCards.value, action.card]; - } else { - cards = [action.card, ...state.currentCards.value]; - } - } - - return { - ...state, - currentCards: new Storable(cards), - cardCache: updateInCardCache(action.card, state.cardCache), - }; - } - - case 'UPDATE_CARD': { - return { - ...state, - currentCards: new Storable( - state.currentCards.value.map((c) => { - if (c === action.oldCard) { - return action.newCard; - } - return c; - }) - ), - cardCache: updateInCardCache(action.newCard, removeFromCardCache(action.oldCard, state.cardCache)), - }; - } - - case 'REMOVE_CARD': { - // potentially remove card from a saved page. - const currentSavedPage = - state.currentSavedPage !== null - ? { - ...state.currentSavedPage, - queries: state.currentSavedPage.queries.filter((q) => q !== action.card), - } - : null; - - const savedPages = - state.currentSavedPage !== null - ? new Storable( - (state.savedPages ? state.savedPages.value : []).map((o) => { - if (o === state.currentSavedPage) { - return currentSavedPage; - } - return o; - }) - ) - : state.savedPages; - - return { - ...state, - currentSavedPage, - savedPages, - currentCards: new Storable([...state.currentCards.value.filter((c) => c !== action.card)]), - cardCache: removeFromCardCache(action.card, state.cardCache), - }; - } - - case 'CLEAR_CARDS': { - // potentially remove card from a saved page. - - return { - ...state, - currentCards: new Storable([]), - }; - } - - case 'MOVE_CARD': { - const cards = moveItemUpOrDown(state.currentCards.value, action.card, action.direction); - - return { - ...state, - currentCards: new Storable(cards), - }; - } - - case 'UPDATE_CARDS': { - let cardCache = { ...state.cardCache }; - for (const card of action.cards.value) { - cardCache = updateInCardCache(card, cardCache); - } - return { - ...state, - currentCards: action.cards, - cardCache, - }; - } - - //#endregion - - case 'SET_USER': { - return { - ...state, - user: action.user, - }; - } - - //#region Notes - - case 'FIND_NOTES': { - const notes = state.notes.value - .filter((o) => o.title.search(action.qry) > -1) - .map((o) => { - return { - qry: o.id, - type: CardType.Note, - data: o, - } as CardItem; - }); - - let cards = [] as DataReference[]; - - if (action.nextToItem && state.settings.value.displaySettings.insertCardNextToItem) { - const idx = state.currentCards.value.indexOf(action.nextToItem); - - if (state.settings.value.displaySettings.appendCardToBottom) { - const before = state.currentCards.value.slice(0, idx + 1); - const after = state.currentCards.value.slice(idx + 1); - cards = [...before, ...notes, ...after]; - } else { - const before = state.currentCards.value.slice(0, idx); - const after = state.currentCards.value.slice(idx); - cards = [...before, ...notes, ...after]; - } - } else { - if (state.settings.value.displaySettings.appendCardToBottom) { - cards = [...state.currentCards.value, ...notes]; - } else { - cards = [...notes, ...state.currentCards.value]; - } - } - - let cache = { ...state.cardCache }; - for (const n of notes) { - cache = updateInCardCache(n, cache); - } - - return { - ...state, - currentCards: new Storable(cards), - cardCache: cache, - }; - } - - case 'GET_NOTE': { - const note = state.notes.value.find((o) => o.id === action.noteId); - const card: CardItem = { - qry: note.id, - type: CardType.Note, - data: note, - }; - - return reducer(state, { - type: 'ADD_CARD', - card, - nextToItem: action.nextToItem, - }); - } - - case 'UPDATE_NOTES': { - return { - ...state, - notes: action.notes ? action.notes : new Storable([]), - }; - } - - case 'SAVE_NOTE': { - // you may be creating a new note or updating an existing. - // if its an update, you need to update the note in the following: - // * card list could have it. - // * notes list could have it. - // * it could be in any of the saved pages lists... - // so iterate through all of them and if you find the note - // in any of them, swap it out - - const notes = new Storable([ - ...state.notes.value.filter((o) => o.id !== action.note.id), - action.note, - ]); - - const newState = { - ...state, - currentCards: new Storable([...state.currentCards.value]), // you want to trigger an update to the cards if a card update is different. - notes, - }; - - return newState; - } - - case 'DELETE_NOTE': { - // the note may be in any of the following: - // * card list could have it. - // * notes list could have it. - // * it could be in any of the saved pages lists... - // so iterate through all of them and if you find the note - // in any of them, remove it - - const card = state.currentCards.value.find((o) => o.qry === action.note.id); - - const cards = card - ? [ - ...state.currentCards.value.filter((o) => { - return o.type !== CardType.Note || o.qry !== action.note.id; - }), - ] - : state.currentCards.value; // if card is undefined, then it wasn't in the current card list. - - const notes = new Storable([...state.notes.value.filter((o) => o.id !== action.note.id)]); - - const savedPages = new Storable([ - ...(state.savedPages ? state.savedPages.value : []).map((sp) => { - return { - ...sp, - queries: sp.queries.filter((o) => { - return o.type !== CardType.Note || o.qry !== action.note.id; - }), - }; - }), - ]); - - return { - ...state, - currentCards: new Storable(cards), - notes, - savedPages, - cardCache: card - ? removeFromCardCache(getFromCardCache(card, state.cardCache), state.cardCache) - : state.cardCache, - }; - } - - //#endregion - } -} diff --git a/src/src/app/services/app.service.ts b/src/src/app/services/app.service.ts index 497322fc..842d151a 100644 --- a/src/src/app/services/app.service.ts +++ b/src/src/app/services/app.service.ts @@ -1,11 +1,24 @@ +import { AngularFireDatabase } from '@angular/fire/compat/database'; import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { User, Settings, DisplaySettings, PageSettings } from '../models/app-state'; + +import { StorageMap } from '@ngx-pwa/local-storage'; + +import { UUID } from 'angular2-uuid'; + +import { HashTable } from '../common/hashtable'; +import { MoveDirection } from '../common/move-direction'; +import { IStateAction } from '../common/state-service'; +import { IStorable, Storable, StorableType } from '../common/storable'; +import { mergeCardList } from '../common/card-operations'; +import { updateInCardCache, removeFromCardCache, getFromCardCache } from '../common/card-cache-operations'; +import { moveItem, moveItemUpOrDown } from '../common/array-operations'; import { Section, BibleReference, Overlap } from '../common/bible-reference'; import { createStateService } from '../common/state-service'; -import { StorageMap } from '@ngx-pwa/local-storage'; -import { AngularFireDatabase } from '@angular/fire/compat/database'; -import { IStorable, Storable } from '../common/storable'; + +import { SavedPage } from '../models/page-state'; +import { CardType, CardItem, DataReference } from '../models/card-state'; +import { AppState, User, Settings, DisplaySettings, PageSettings, Error } from '../models/app-state'; import { NoteItem } from '../models/note-state'; import { Paragraph, @@ -24,18 +37,696 @@ import { StrongsResult, } from '../models/strongs-state'; import { WordToStem, IndexResult, WordLookupResult } from '../models/words-state'; -import { HashTable } from '../common/hashtable'; -import { MoveDirection } from '../common/move-direction'; -import { reducer } from './app-state-reducer'; -import { initialState } from './app-state-initial-state'; -import { CardItem, CardType, DataReference } from '../models/card-state'; -import { SavedPage } from '../models/page-state'; -import { AppActionFactory } from './app-state-actions'; + +const initialState: AppState = { + user: null, + currentCards: { + type: StorableType.initial, + createdOn: new Date(0).toISOString(), + value: [], + }, + cardCache: {}, + autocomplete: [], + currentSavedPage: null, + savedPages: null, + notes: { + type: StorableType.initial, + createdOn: new Date(0).toISOString(), + value: [], + }, + savedPagesLoaded: false, + error: null, + settings: { + type: StorableType.initial, + createdOn: new Date(0).toISOString(), + value: { + displaySettings: { + showStrongsAsModal: false, + appendCardToBottom: true, + insertCardNextToItem: true, + clearSearchAfterQuery: true, + cardFontSize: 12, + cardFontFamily: 'PT Serif', + showVersesOnNewLine: false, + showVerseNumbers: false, + showParagraphs: true, + showParagraphHeadings: true, + syncCardsAcrossDevices: false, + }, + pageSettings: { + mergeStrategy: Overlap.Equal, + }, + cardIcons: { + words: 'font_download', + passage: 'menu_book', + strongs: 'speaker_notes', + note: 'note', + savedPage: 'inbox', + }, + }, + }, +}; + +interface AppAction extends IStateAction {} + +export function getNewestStorable(candidate: IStorable, incumbant: IStorable): IStorable { + // if the candidate is null, then return the state. + if (!candidate) { + return incumbant; + } + + // only update if the settings are newer. + if (!incumbant || new Date(candidate.createdOn) > new Date(incumbant.createdOn)) { + return candidate; + } + + // candidate didn't win. return state untouched. + return incumbant; +} + +function appReducer(state: AppState, action: IStateAction): AppState { + if (state === undefined || action.handle === undefined) { + return initialState; + } + return action.handle(state); +} + +//#region Saved Pages + +const savePageActionType = 'SAVE_PAGE'; +export const savePageAction = (title: string): AppAction => { + return { + type: savePageActionType, + handle(state: AppState) { + const savedPages = new Storable([ + ...(state.savedPages ? state.savedPages.value : []), + { + // create a new saved page object + title: title, + id: UUID.UUID().toString(), + queries: [...mergeCardList(state.currentCards.value, state.settings.value.pageSettings.mergeStrategy)], + }, + ]); + + return updateSavedPagesAction(savedPages).handle(state); + }, + }; +}; + +const updateCurrentPageActionType = 'UPDATE_CURRENT_PAGE'; +export const updateCurrentPageAction = (): AppAction => { + return { + type: updateCurrentPageActionType, + handle(state: AppState) { + const current = { + id: state.currentSavedPage.id, + title: state.currentSavedPage.title, + queries: [...mergeCardList(state.currentCards.value, state.settings.value.pageSettings.mergeStrategy)], + }; + + const savedPages = new Storable([ + ...state.savedPages.value.filter((o) => o.id !== state.currentSavedPage.id), + current, + ]); + + const item = getNewestStorable(savedPages, state.savedPages); + return { + ...state, + currentSavedPage: current, + savedPagesLoaded: true, + savedPages: item, + }; + }, + }; +}; + +const updateSavedPagesActionType = 'UPDATE_SAVED_PAGES'; +export const updateSavedPagesAction = (oldSavedPages: IStorable): AppAction => { + return { + type: updateSavedPagesActionType, + handle(state: AppState) { + const newSavedPages = getNewestStorable(oldSavedPages, state.savedPages); + + // only true if a currentSavedPage was set, indicating that the user + // is currently looking at a saved page. + const hasCurrentSavedPage = + state.currentSavedPage !== null && + state.currentSavedPage !== undefined && + oldSavedPages.value.some((o) => o.id === state.currentSavedPage.id); + + const currentSavedPage = hasCurrentSavedPage + ? oldSavedPages.value.find((o) => o.id === state.currentSavedPage.id) + : null; + + return { + ...state, + // if the currentSavedPage was loaded, replace it with the info from the + // new savedPages array, as it might have changed. + currentCards: hasCurrentSavedPage ? new Storable(currentSavedPage.queries) : state.currentCards, + currentSavedPage: hasCurrentSavedPage ? currentSavedPage : state.currentSavedPage, + savedPagesLoaded: true, + savedPages: newSavedPages, // update the savedPages + }; + }, + }; +}; + +const updateSavedPageActionType = 'UPDATE_SAVED_PAGES'; +export const updateSavedPageAction = (savedPage: SavedPage): AppAction => { + return { + type: updateSavedPageActionType, + handle(state: AppState) { + const newSavedPages = new Storable( + state.savedPages.value.map((o) => { + if (o.id === savedPage.id) { + return savedPage; + } + return o; + }) + ); + + const savedPages = getNewestStorable(newSavedPages, state.savedPages); + return updateSavedPagesAction(savedPages).handle(state); + }, + }; +}; + +const removeSavedPageActionType = 'REMOVE_SAVED_PAGE'; +export const removeSavedPageAction = (savedPage: SavedPage): AppAction => { + return { + type: removeSavedPageActionType, + handle(state: AppState) { + const savedPages = new Storable(state.savedPages.value.filter((o) => o.id !== savedPage.id)); + const item = getNewestStorable(savedPages, state.savedPages); + + return { + ...state, + savedPagesLoaded: true, + savedPages: item, + }; + }, + }; +}; + +const moveSavedPageCardActionType = 'MOVE_SAVED_PAGE_CARD'; +export const moveSavedPageCardAction = (oldSavedPage: SavedPage, fromIndex: number, toIndex: number): AppAction => { + return { + type: moveSavedPageCardActionType, + handle(state: AppState) { + const queries = moveItem(oldSavedPage.queries, fromIndex, toIndex); + const savedPage = { + ...oldSavedPage, + queries, // update the queries. + }; + + return updateSavedPageAction(savedPage).handle(state); + }, + }; +}; + +const addCardToSavedPageActionType = 'ADD_CARD_TO_SAVED_PAGE'; +export const addCardToSavedPageAction = (card: CardItem, pageId: string): AppAction => { + return { + type: addCardToSavedPageActionType, + handle(state: AppState) { + const savedPages = new Storable([ + ...(state.savedPages ? state.savedPages.value : []).map((o) => { + if (o.id.toString() === pageId) { + let references = [] as DataReference[]; + if (state.settings.value.displaySettings.appendCardToBottom) { + references = [...o.queries, card]; + } else { + references = [card, ...o.queries]; + } + return { + ...o, + queries: mergeCardList(references, state.settings.value.pageSettings.mergeStrategy), + }; + } + return o; + }), + ]); + + return updateSavedPagesAction(savedPages).handle(state); + }, + }; +}; + +//#endregion + +//#region Cards + +const addCardActionType = 'ADD_CARD'; +export const addCardAction = (card: CardItem, nextToItem: CardItem): AppAction => { + return { + type: addCardActionType, + handle(state: AppState) { + let cards = []; + + if (nextToItem && state.settings.value.displaySettings.insertCardNextToItem) { + const idx = state.currentCards.value.indexOf(nextToItem); + + if (state.settings.value.displaySettings.appendCardToBottom) { + const before = state.currentCards.value.slice(0, idx + 1); + const after = state.currentCards.value.slice(idx + 1); + cards = [...before, card, ...after]; + } else { + const before = state.currentCards.value.slice(0, idx); + const after = state.currentCards.value.slice(idx); + cards = [...before, card, ...after]; + } + } else { + if (state.settings.value.displaySettings.appendCardToBottom) { + cards = [...state.currentCards.value, card]; + } else { + cards = [card, ...state.currentCards.value]; + } + } + + return { + ...state, + currentCards: new Storable(cards), + cardCache: updateInCardCache(card, state.cardCache), + }; + }, + }; +}; + +const updateCardActionType = 'UPDATE_CARD'; +export const updateCardAction = (newCard: CardItem, oldCard: CardItem): AppAction => { + return { + type: updateCardActionType, + handle(state: AppState) { + return { + ...state, + currentCards: new Storable( + state.currentCards.value.map((c) => { + if (c === oldCard) { + return newCard; + } + return c; + }) + ), + cardCache: updateInCardCache(newCard, removeFromCardCache(oldCard, state.cardCache)), + }; + }, + }; +}; + +const removeCardActionType = 'REMOVE_CARD'; +export const removeCardAction = (card: CardItem): AppAction => { + return { + type: removeCardActionType, + handle(state: AppState) { + // potentially remove card from a saved page. + const currentSavedPage = + state.currentSavedPage !== null + ? { + ...state.currentSavedPage, + queries: state.currentSavedPage.queries.filter((q) => q !== card), + } + : null; + + const savedPages = + state.currentSavedPage !== null + ? new Storable( + (state.savedPages ? state.savedPages.value : []).map((o) => { + if (o === state.currentSavedPage) { + return currentSavedPage; + } + return o; + }) + ) + : state.savedPages; + + return { + ...state, + currentSavedPage, + savedPages, + currentCards: new Storable([...state.currentCards.value.filter((c) => c !== card)]), + cardCache: removeFromCardCache(card, state.cardCache), + }; + }, + }; +}; + +const moveCardActionType = 'MOVE_CARD'; +export const moveCardAction = (card: CardItem, direction: MoveDirection): AppAction => { + return { + type: moveCardActionType, + handle(state: AppState) { + const cards = moveItemUpOrDown(state.currentCards.value, card, direction); + + return { + ...state, + currentCards: new Storable(cards), + }; + }, + }; +}; + +const updateCardsActionType = 'UPDATE_CARDS'; +export const updateCardsAction = (cards: IStorable): AppAction => { + return { + type: savePageActionType, + handle(state: AppState) { + let cardCache = { ...state.cardCache }; + for (const card of cards.value) { + cardCache = updateInCardCache(card, cardCache); + } + return { + ...state, + currentCards: cards, + cardCache, + }; + }, + }; +}; + +const clearCardsActionType = 'CLEAR_CARDS'; +export const clearCardsAction = (): AppAction => { + return { + type: clearCardsActionType, + handle(state: AppState) { + return { + ...state, + currentCards: new Storable([]), + }; + }, + }; +}; + +//#endregion + +//#region General + +const updateErrorActionType = 'UPDATE_ERROR'; +export const updateErrorAction = (error: Error): AppAction => { + return { + type: updateErrorActionType, + handle(state: AppState) { + return { + ...state, + error: error, + }; + }, + }; +}; + +const setUserActionType = 'SET_USER'; +export const setUserAction = (user: User): AppAction => { + return { + type: setUserActionType, + handle(state: AppState) { + return { + ...state, + user: user, + }; + }, + }; +}; + +const updateAutoCompleteActionType = 'UPDATE_AUTOCOMPLETE'; +export const updateAutoCompleteAction = (words: string[]): AppAction => { + return { + type: updateAutoCompleteActionType, + handle(state: AppState) { + return { + ...state, + autocomplete: [...words], + }; + }, + }; +}; + +//#endregion + +//#region Settings + +const updateSettingsActionType = 'UPDATE_SETTINGS'; +export const updateSettingsAction = (settings: IStorable): AppAction => { + return { + type: updateSettingsActionType, + handle(state: AppState) { + const item = getNewestStorable(settings, state.settings); + + return { + ...state, + settings: item, + }; + }, + }; +}; + +const updateCardMergeStrategyActionType = 'UPDATE_CARD_MERGE_STRATEGY'; +export const updateCardMergeStrategyAction = (mergeStrategy: Overlap): AppAction => { + return { + type: updateCardMergeStrategyActionType, + handle(state: AppState) { + const settings = new Storable({ + ...state.settings.value, + pageSettings: { + ...state.settings.value.pageSettings, + mergeStrategy: mergeStrategy, + }, + }); + return updateSettingsAction(settings).handle(state); + }, + }; +}; + +const updateCardFontSizeActionType = 'UPDATE_CARD_FONT_SIZE'; +export const updateCardFontSizeAction = (cardFontSize: number): AppAction => { + return { + type: updateCardFontSizeActionType, + handle(state: AppState) { + const settings = new Storable({ + ...state.settings.value, + displaySettings: { + ...state.settings.value.displaySettings, + cardFontSize: cardFontSize, + }, + }); + + return updateSettingsAction(settings).handle(state); + }, + }; +}; + +const updateCardFontFamilyActionType = 'UPDATE_CARD_FONT_FAMILY'; +export const updateCardFontFamilyAction = (cardFontFamily: string): AppAction => { + return { + type: updateCardFontFamilyActionType, + handle(state: AppState) { + const settings = new Storable({ + ...state.settings.value, + displaySettings: { + ...state.settings.value.displaySettings, + cardFontFamily: cardFontFamily, + }, + }); + + return updateSettingsAction(settings).handle(state); + }, + }; +}; + +//#endregion + +//#region Notes + +const findNotesActionType = 'FIND_NOTES'; +export const findNotesAction = (qry: string, nextToItem: CardItem): AppAction => { + return { + type: findNotesActionType, + handle(state: AppState) { + const notes = state.notes.value + .filter((o) => o.title.search(qry) > -1) + .map((o) => { + return { + qry: o.id, + type: CardType.Note, + data: o, + } as CardItem; + }); + + let cards = [] as DataReference[]; + + if (nextToItem && state.settings.value.displaySettings.insertCardNextToItem) { + const idx = state.currentCards.value.indexOf(nextToItem); + + if (state.settings.value.displaySettings.appendCardToBottom) { + const before = state.currentCards.value.slice(0, idx + 1); + const after = state.currentCards.value.slice(idx + 1); + cards = [...before, ...notes, ...after]; + } else { + const before = state.currentCards.value.slice(0, idx); + const after = state.currentCards.value.slice(idx); + cards = [...before, ...notes, ...after]; + } + } else { + if (state.settings.value.displaySettings.appendCardToBottom) { + cards = [...state.currentCards.value, ...notes]; + } else { + cards = [...notes, ...state.currentCards.value]; + } + } + + let cache = { ...state.cardCache }; + for (const n of notes) { + cache = updateInCardCache(n, cache); + } + + return { + ...state, + currentCards: new Storable(cards), + cardCache: cache, + }; + }, + }; +}; + +const getNotesActionType = 'GET_NOTE'; +export const getNotesAction = (noteId: string, nextToItem: CardItem): AppAction => { + return { + type: getNotesActionType, + handle(state: AppState) { + const note = state.notes.value.find((o) => o.id === noteId); + const card: CardItem = { + qry: note.id, + type: CardType.Note, + data: note, + }; + + return addCardAction(card, nextToItem).handle(state); + }, + }; +}; + +const updateNotesActionType = 'UPDATE_NOTES'; +export const updateNotesAction = (notes: IStorable): AppAction => { + return { + type: updateNotesActionType, + handle(state: AppState) { + return { + ...state, + notes: notes ? notes : new Storable([]), + }; + }, + }; +}; + +const saveNoteActionType = 'SAVE_NOTE'; +export const saveNoteAction = (note: NoteItem): AppAction => { + return { + type: saveNoteActionType, + handle(state: AppState) { + // you may be creating a new note or updating an existing. + // if its an update, you need to update the note in the following: + // * card list could have it. + // * notes list could have it. + // * it could be in any of the saved pages lists... + // so iterate through all of them and if you find the note + // in any of them, swap it out + + const notes = new Storable([...state.notes.value.filter((o) => o.id !== note.id), note]); + + return { + ...state, + // you want to trigger an update to the cards if a card update is different. + currentCards: new Storable([...state.currentCards.value]), + notes, + }; + }, + }; +}; + +const deleteNoteActionType = 'DELETE_NOTE'; +export const deleteNoteAction = (note: NoteItem): AppAction => { + return { + type: deleteNoteActionType, + handle(state: AppState) { + // the note may be in any of the following: + // * card list could have it. + // * notes list could have it. + // * it could be in any of the saved pages lists... + // so iterate through all of them and if you find the note + // in any of them, remove it + + const card = state.currentCards.value.find((o) => o.qry === note.id); + + const cards = card + ? [ + ...state.currentCards.value.filter((o) => { + return o.type !== CardType.Note || o.qry !== note.id; + }), + ] + : state.currentCards.value; // if card is undefined, then it wasn't in the current card list. + + const notes = new Storable([...state.notes.value.filter((o) => o.id !== note.id)]); + + const savedPages = new Storable([ + ...(state.savedPages ? state.savedPages.value : []).map((sp) => { + return { + ...sp, + queries: sp.queries.filter((o) => { + return o.type !== CardType.Note || o.qry !== note.id; + }), + }; + }), + ]); + + return { + ...state, + currentCards: new Storable(cards), + notes, + savedPages, + cardCache: card + ? removeFromCardCache(getFromCardCache(card, state.cardCache), state.cardCache) + : state.cardCache, + }; + }, + }; +}; + +//#endregion + +type AppActions = + // Saved Page Actions + | typeof savePageActionType + | typeof updateCurrentPageActionType + | typeof updateSavedPagesActionType + | typeof updateSavedPageActionType + | typeof removeSavedPageActionType + | typeof moveSavedPageCardActionType + | typeof addCardToSavedPageActionType + // Card Actions + | typeof addCardActionType + | typeof updateCardActionType + | typeof removeCardActionType + | typeof moveCardActionType + | typeof updateCardsActionType + | typeof clearCardsActionType + // General Actions + | typeof updateErrorActionType + | typeof setUserActionType + | typeof updateAutoCompleteActionType + // Settings Actions + | typeof updateSettingsActionType + | typeof updateCardMergeStrategyActionType + | typeof updateCardFontSizeActionType + | typeof updateCardFontFamilyActionType + // Note Actions + | typeof findNotesActionType + | typeof getNotesActionType + | typeof updateNotesActionType + | typeof saveNoteActionType + | typeof deleteNoteActionType; @Injectable({ providedIn: 'root', }) -export class AppService extends createStateService(reducer, initialState) { +export class AppService extends createStateService(appReducer, initialState) { private wordToStem: Map; private paragraphs: HashTable; private searchIndexArray: string[]; @@ -118,20 +809,74 @@ export class AppService extends createStateService(reducer, initialState) { //#region General dispatchError(msg: string) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg, - }, - }); + this.dispatch(updateErrorAction({ msg })); console.log(msg); } setUser(user: User) { - this.dispatch({ - type: 'SET_USER', - user, - }); + this.dispatch(setUserAction(user)); + } + + async getAutoComplete(keyword: string) { + if (!this.autocomplete) { + // if you have't populated the word list yet, do so... + const data = await this.http.get(`${this.dataPath}/index/word_to_stem_idx.json`).toPromise(); + this.autocomplete = data.map((o) => o.w); + } + + let qry = keyword; + let prefix = ''; + const idx = qry.lastIndexOf(';'); + const words: string[] = []; + + if (idx > -1) { + qry = keyword.substring(idx + 1).trim(); + prefix = keyword.substring(0, idx).trim() + '; '; + } + + qry = qry.split('"').join(''); // remove any quotes, just in case + + if (qry.search(/[0-9]/i) === -1) { + // its a word + for (const item of BibleReference.Books) { + if ( + item.name !== 'Unknown' && + (item.name.toLowerCase().indexOf(qry.toLowerCase()) > -1 || + item.abbreviation.toLowerCase().indexOf(qry.toLowerCase()) > -1) + ) { + words.push(prefix + item.name); + if (words.length > 2) { + break; + } + } + } + + for (const item of this.autocomplete) { + if (item.toLowerCase().indexOf(qry.toLowerCase()) > -1) { + words.push(prefix + item); + if (words.length > 6) { + break; + } + } + } + + this.dispatch(updateAutoCompleteAction(words)); + } else if (qry.search(/(H|G)[0-9]/i) !== -1) { + // its a strongs lookup + if (qry.substring(0, 1).toUpperCase() === 'H') { + const num = parseInt(qry.substr(1), 10); + for (let x = num; x < num + 10 && x < 8675; x++) { + words.push('H' + x); + } + } else if (qry.substring(0, 1).toUpperCase() === 'G') { + const num = parseInt(qry.substring(1), 10); + for (let x = num; x < num + 10 && x < 5625; x++) { + words.push('G' + x); + } + } + + this.dispatch(updateAutoCompleteAction(words)); + } } //#endregion @@ -139,11 +884,11 @@ export class AppService extends createStateService(reducer, initialState) { //#region Cards removeCard(card: CardItem) { - this.dispatch(AppActionFactory.newRemoveCard(card)); + this.dispatch(removeCardAction(card)); } moveCard(card: CardItem, direction: MoveDirection) { - this.dispatch(AppActionFactory.newMoveCard(card, direction)); + this.dispatch(moveCardAction(card, direction)); } async addCard(qry: string, nextToItem: CardItem = null) { @@ -151,7 +896,7 @@ export class AppService extends createStateService(reducer, initialState) { if (!card) { return; } - this.dispatch(AppActionFactory.newAddCard(card, nextToItem)); + this.dispatch(addCardAction(card, nextToItem)); } async updateCards(queries: IStorable) { @@ -162,7 +907,7 @@ export class AppService extends createStateService(reducer, initialState) { cards.push(card); } this.dispatch( - AppActionFactory.newUpdateCards({ + updateCardsAction({ type: queries.type, createdOn: queries.createdOn, value: cards, @@ -171,9 +916,7 @@ export class AppService extends createStateService(reducer, initialState) { } async clearCards() { - this.dispatch( - AppActionFactory.newClearCards() - ); + this.dispatch(clearCardsAction()); } private async getCardByQuery(qry: string, type?: CardType): Promise { @@ -243,62 +986,62 @@ export class AppService extends createStateService(reducer, initialState) { cards.push(await this.getCardByQuery(ref.qry, ref.type)); } - this.dispatch(AppActionFactory.newUpdateCards(new Storable(cards))); + this.dispatch(updateCardsAction(new Storable(cards))); } savePage(title: string) { - this.dispatch(AppActionFactory.newSavePage(title)); + this.dispatch(savePageAction(title)); } updateCurrentSavedPage() { - this.dispatch(AppActionFactory.newUpdateCurrentPage()); + this.dispatch(updateCurrentPageAction()); } updateSavedPages(savedPages: IStorable) { - this.dispatch(AppActionFactory.newUpdateSavedPages(savedPages)); + this.dispatch(updateSavedPagesAction(savedPages)); } updateSavedPage(savedPage: SavedPage) { - this.dispatch(AppActionFactory.newUpdateSavedPage(savedPage)); + this.dispatch(updateSavedPageAction(savedPage)); } removeSavedPage(savedPage: SavedPage) { - this.dispatch(AppActionFactory.newRemoveSavedPage(savedPage)); + this.dispatch(removeSavedPageAction(savedPage)); } moveSavedPageCard(savedPage: SavedPage, fromIndex: number, toIndex: number) { - this.dispatch(AppActionFactory.newMoveSavedPageCard(savedPage, fromIndex, toIndex)); + this.dispatch(moveSavedPageCardAction(savedPage, fromIndex, toIndex)); } addCardToSavedPage(pageId: string, card: CardItem) { - this.dispatch(AppActionFactory.newAddCardToSavedPage(card, pageId)); + this.dispatch(addCardToSavedPageAction(card, pageId)); } //#endregion //#region Settings - updateCardMergeStrategy(strategy: Overlap) { - this.dispatch(AppActionFactory.newUpdateCardMergeStrategy(strategy)); + updateSettings(settings: IStorable) { + this.dispatch(updateSettingsAction(settings)); } - changeCardFontFamily(cardFont: string) { - this.dispatch(AppActionFactory.newUpdateCardFontFamily(cardFont)); + updateCardMergeStrategy(strategy: Overlap) { + this.dispatch(updateCardMergeStrategyAction(strategy)); } changeCardFontSize(size: number) { - this.dispatch(AppActionFactory.newUpdateCardFontSize(size)); + this.dispatch(updateCardFontSizeAction(size)); } - updateSettings(settings: IStorable) { - this.dispatch(AppActionFactory.newUpdateSettings(settings)); + changeCardFontFamily(cardFont: string) { + this.dispatch(updateCardFontFamilyAction(cardFont)); } updateDisplaySettings(displaySettings: DisplaySettings) { const state = this.getState(); this.dispatch( - AppActionFactory.newUpdateSettings( + updateSettingsAction( new Storable({ ...state.settings.value, displaySettings, @@ -311,7 +1054,7 @@ export class AppService extends createStateService(reducer, initialState) { const state = this.getState(); this.dispatch( - AppActionFactory.newUpdateSettings( + updateSettingsAction( new Storable({ ...state.settings.value, pageSettings, @@ -325,23 +1068,23 @@ export class AppService extends createStateService(reducer, initialState) { //#region Notes findNotes(qry: string, nextToItem: CardItem = null) { - this.dispatch(AppActionFactory.newFindNotes(qry, nextToItem)); + this.dispatch(findNotesAction(qry, nextToItem)); } async getNote(noteId: string, nextToItem: CardItem = null) { - this.dispatch(AppActionFactory.newGetNote(noteId, nextToItem)); + this.dispatch(getNotesAction(noteId, nextToItem)); } updateNotes(notes: IStorable) { - this.dispatch(AppActionFactory.newUpdateNotes(notes)); + this.dispatch(updateNotesAction(notes)); } saveNote(note: NoteItem) { - this.dispatch(AppActionFactory.newSaveNote(note)); + this.dispatch(saveNoteAction(note)); } deleteNote(note: NoteItem) { - this.dispatch(AppActionFactory.newDeleteNote(note)); + this.dispatch(deleteNoteAction(note)); } //#endregion @@ -354,7 +1097,7 @@ export class AppService extends createStateService(reducer, initialState) { return; // nothing was returned. so an error occurred. } - this.dispatch(AppActionFactory.newAddCard(card, nextToItem)); + this.dispatch(addCardAction(card, nextToItem)); } async getStrongsCard(strongsNumber: string, dict: StrongsDictionary) { @@ -458,6 +1201,7 @@ export class AppService extends createStateService(reducer, initialState) { } return result as StrongsResult; } + //#endregion //#region Bible Passages @@ -468,13 +1212,13 @@ export class AppService extends createStateService(reducer, initialState) { return; // nothing was returned. so an error occurred. } - this.dispatch(AppActionFactory.newAddCard(card, nextToItem)); + this.dispatch(addCardAction(card, nextToItem)); } async updatePassage(oldCard: CardItem, ref: BibleReference) { const newCard = await this.composeBiblePassageCardItem(ref); - this.dispatch(AppActionFactory.newUpdateCard(newCard, oldCard)); + this.dispatch(updateCardAction(newCard, oldCard)); } private async composeBiblePassageCardItem(ref: BibleReference) { @@ -657,7 +1401,7 @@ export class AppService extends createStateService(reducer, initialState) { data: result, } as CardItem; - this.dispatch(AppActionFactory.newAddCard(card, nextToItem)); + this.dispatch(addCardAction(card, nextToItem)); } private async getWordsFromApi(qry: string): Promise { @@ -680,12 +1424,11 @@ export class AppService extends createStateService(reducer, initialState) { } if (!this.wordToStem.has(q)) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { + this.dispatch( + updateErrorAction({ msg: `Unable to find the word "${q}" in any passages.`, - }, - }); + }) + ); return; } @@ -717,23 +1460,21 @@ export class AppService extends createStateService(reducer, initialState) { // that is shared by all of them. IF not, we can just return those refs. if (results.length === 0) { if (excluded.length > 0) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { + this.dispatch( + updateErrorAction({ msg: `The ${excluded.length > 1 ? 'words' : 'word'} "${excluded.reduce((prev, curr, idx, arr) => { return `${prev}, ${curr}`; })}" ${excluded.length > 1 ? 'have' : 'has'} been excluded from search because it is too common.`, - }, - }); + }) + ); return; } - this.dispatch({ - type: 'UPDATE_ERROR', - error: { + this.dispatch( + updateErrorAction({ msg: `No passages found for query: ${qry}.`, - }, - }); + }) + ); return; } @@ -780,12 +1521,11 @@ export class AppService extends createStateService(reducer, initialState) { this.wordToStem.set(i.w, i.s); } } catch (err) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { + this.dispatch( + updateErrorAction({ msg: err, - }, - }); + }) + ); return; } } @@ -803,12 +1543,11 @@ export class AppService extends createStateService(reducer, initialState) { try { r = await this.http.get(url).toPromise(); } catch (err) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { + this.dispatch( + updateErrorAction({ msg: err, - }, - }); + }) + ); return; } // find the right word @@ -1112,70 +1851,4 @@ export class AppService extends createStateService(reducer, initialState) { } //#endregion - - //#region Auto Complete - - async getAutoComplete(keyword: string) { - if (!this.autocomplete) { - // if you have't populated the word list yet, do so... - const data = await this.http.get(`${this.dataPath}/index/word_to_stem_idx.json`).toPromise(); - this.autocomplete = data.map((o) => o.w); - } - - let qry = keyword; - let prefix = ''; - const idx = qry.lastIndexOf(';'); - const words: string[] = []; - - if (idx > -1) { - qry = keyword.substr(idx + 1).trim(); - prefix = keyword.substr(0, idx).trim() + '; '; - } - - qry = qry.split('"').join(''); // remove any quotes, just in case - - if (qry.search(/[0-9]/i) === -1) { - // its a word - for (const item of BibleReference.Books) { - if ( - item.name !== 'Unknown' && - (item.name.toLowerCase().indexOf(qry.toLowerCase()) > -1 || - item.abbreviation.toLowerCase().indexOf(qry.toLowerCase()) > -1) - ) { - words.push(prefix + item.name); - if (words.length > 2) { - break; - } - } - } - - for (const item of this.autocomplete) { - if (item.toLowerCase().indexOf(qry.toLowerCase()) > -1) { - words.push(prefix + item); - if (words.length > 6) { - break; - } - } - } - - this.dispatch(AppActionFactory.newUpdateAutocomplete(words)); - } else if (qry.search(/(H|G)[0-9]/i) !== -1) { - // its a strongs lookup - if (qry.substr(0, 1).toUpperCase() === 'H') { - const num = parseInt(qry.substr(1), 10); - for (let x = num; x < num + 10 && x < 8675; x++) { - words.push('H' + x); - } - } else if (qry.substr(0, 1).toUpperCase() === 'G') { - const num = parseInt(qry.substr(1), 10); - for (let x = num; x < num + 10 && x < 5625; x++) { - words.push('G' + x); - } - } - - this.dispatch(AppActionFactory.newUpdateAutocomplete(words)); - } - } - - //#endregion } diff --git a/src/tsconfig.json b/src/tsconfig.json index f81bf877..3d1165e2 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,4 +1,3 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": {