diff --git a/app/db/.editorconfig b/app/db/.editorconfig index 26d55d55..7e71164d 100644 --- a/app/db/.editorconfig +++ b/app/db/.editorconfig @@ -10,7 +10,7 @@ trim_trailing_whitespace = true [*.ts] quote_type = single -max_line_length = 140 +max_line_length = 120 end_of_line = lf # this file has a need for arbitrarily long lines, so we exempt this here. diff --git a/app/db/.firebaserc b/app/db/.firebaserc index f6a24644..c127c1d1 100644 --- a/app/db/.firebaserc +++ b/app/db/.firebaserc @@ -1,11 +1,11 @@ { "targets": { - "dynamicbible-7c6cf": { + "dynamic-bible-testing": { "hosting": { "db": [ - "dynamicbible-7c6cf" + "dynamic-bible-testing" ] } } } -} \ No newline at end of file +} diff --git a/app/db/src/app/app.component.ts b/app/db/src/app/app.component.ts index 1bf619cc..ad3440a2 100644 --- a/app/db/src/app/app.component.ts +++ b/app/db/src/app/app.component.ts @@ -1,6 +1,7 @@ import { Component, ViewChild, AfterViewInit } from '@angular/core'; import { AppService } from './services/app.service'; import { NavService } from './services/nav.service'; +import { StorageService } from './services/storage.service'; import { Observable } from 'rxjs'; import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout'; import { map, shareReplay } from 'rxjs/operators'; @@ -16,20 +17,15 @@ import { SubscriberComponent } from './common/components/subscriber.component'; styleUrls: ['./app.component.scss'], }) export class AppComponent extends SubscriberComponent implements AfterViewInit { - savedPages$ = this.appService.select((state) => state.savedPages); + savedPages$ = this.appService.select((state) => state.savedPages.value); mainPages$ = this.appService.select((state) => state.mainPages); - fontSize$ = this.appService.select( - (state) => state.displaySettings.fontSize + 'pt' - ); - cardFont$ = this.appService.select((state) => state.displaySettings.cardFont); - displaySettings$ = this.appService.select((state) => state.displaySettings); + fontSize$ = this.appService.select((state) => state.displaySettings.value.cardFontSize + 'pt'); + cardFont$ = this.appService.select((state) => state.displaySettings.value.cardFontFamily); - isHandset$: Observable = this.breakpointObserver - .observe(Breakpoints.Handset) - .pipe( - map((result) => result.matches), - shareReplay() - ); + isHandset$: Observable = this.breakpointObserver.observe(Breakpoints.Handset).pipe( + map((result) => result.matches), + shareReplay() + ); @ViewChild('drawer') public sidenav: MatSidenav; @ViewChild('settings') public settings: MatSidenav; @@ -37,26 +33,28 @@ export class AppComponent extends SubscriberComponent implements AfterViewInit { constructor( private appService: AppService, private navService: NavService, + private storageService: StorageService, private breakpointObserver: BreakpointObserver, private dialog: MatDialog ) { super(); - this.appService.initSavedPages(); - this.appService.initDisplaySettings(); + this.storageService.initSavedPages(); + this.storageService.initDisplaySettings(); - //#region handle local storage + //#region Handle recieving updates from firebase this.addSubscription( - this.displaySettings$.subscribe((settings) => { - this.appService.saveSettingsApi(settings); - }) - ); - - this.addSubscription( - this.savedPages$.subscribe((savedPages) => { - this.appService.savePagesApi(savedPages); - }) + // when the user object changes, respond + this.appService + // this should trigger only once, when the user logs in. + .select((state) => state.user) + .subscribe((user) => { + if (!user) { + return; // if the user is null, avoid this. + } + this.storageService.initRemote(user); + }) ); //#endregion @@ -73,10 +71,7 @@ export class AppComponent extends SubscriberComponent implements AfterViewInit { this.addSubscription( this.cardFont$.subscribe((family) => { if (family) { - document.documentElement.style.setProperty( - '--card-font-family', - family - ); + document.documentElement.style.setProperty('--card-font-family', family); } }) ); @@ -93,7 +88,7 @@ export class AppComponent extends SubscriberComponent implements AfterViewInit { } updatePage() { - this.appService.updatePage(); + this.appService.updateSavedPage(); } createNote() { diff --git a/app/db/src/app/app.module.ts b/app/db/src/app/app.module.ts index ea3f7211..df2d4371 100644 --- a/app/db/src/app/app.module.ts +++ b/app/db/src/app/app.module.ts @@ -1,29 +1,34 @@ +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { HttpClientModule } from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { AppRoutingModule } from './app-routing.module'; -import { AppComponent } from './app.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { HttpClientModule } from '@angular/common/http'; - import { NgxMdModule } from 'ngx-md'; +import { AngularFireModule } from '@angular/fire'; +import { FirebaseConfig } from './constants'; +import { AngularFireAuthModule } from '@angular/fire/auth'; +import { AngularFireDatabaseModule } from '@angular/fire/database'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; + import { SearchPage } from './search/components/search-page/search.page'; -import { PassageCardComponent } from './search/components/passage/passage-card.component'; -import { WordsCardComponent } from './search/components/words/words-card.component'; -import { NoteCardComponent } from './search/components/note/note-card.component'; -import { SettingsComponent } from './common/components/settings/settings.component'; - -import { StrongsComponent } from './search/components/strongs/strongs.component'; -import { StrongsCardComponent } from './search/components/strongs/card/strongs-card.component'; -import { StrongsModalComponent } from './search/components/strongs/modal/strongs-modal.component'; - import { AddToPageModalComponent } from './search/components/saved-page/add-to-page-modal/add-to-page-modal.component'; import { PageEditModalComponent } from './search/components/saved-page/page-edit-modal/page-edit-modal.component'; import { NoteEditModalComponent } from './search/components/note/edit-modal/note-edit-modal.component'; import { VersePickerModalComponent } from './search/components/verse-picker-modal/verse-picker-modal.component'; +import { SettingsComponent } from './common/components/settings/settings.component'; +import { PassageCardComponent } from './search/components/passage/passage-card.component'; +import { WordsCardComponent } from './search/components/words/words-card.component'; +import { NoteCardComponent } from './search/components/note/note-card.component'; + +import { StrongsComponent } from './search/components/strongs/strongs.component'; +import { StrongsCardComponent } from './search/components/strongs/card/strongs-card.component'; +import { StrongsModalComponent } from './search/components/strongs/modal/strongs-modal.component'; + import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; @@ -60,8 +65,6 @@ import { MatDividerModule } from '@angular/material/divider'; import { MatNativeDateModule, MatRippleModule } from '@angular/material/core'; import { MatTreeModule } from '@angular/material/tree'; import { ClipboardModule } from '@angular/cdk/clipboard'; -import { AngularFireModule } from '@angular/fire'; -import { FirebaseConfig } from './constants'; @NgModule({ declarations: [ @@ -91,6 +94,8 @@ import { FirebaseConfig } from './constants'; NgxMdModule.forRoot(), AngularFireModule.initializeApp(FirebaseConfig), + AngularFireAuthModule, + AngularFireDatabaseModule, MatSidenavModule, MatToolbarModule, diff --git a/app/db/src/app/common/components/card.component.ts b/app/db/src/app/common/components/card.component.ts index 1b5ceb47..1c1b8e94 100644 --- a/app/db/src/app/common/components/card.component.ts +++ b/app/db/src/app/common/components/card.component.ts @@ -1,10 +1,4 @@ -import { - EventEmitter, - Output, - Input, - ElementRef, - Component, -} from '@angular/core'; +import { EventEmitter, Output, Input, ElementRef, Component } from '@angular/core'; import { CardItem, OpenData } from '../../models/app-state'; import { Observable } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; diff --git a/app/db/src/app/common/components/settings/settings.component.html b/app/db/src/app/common/components/settings/settings.component.html index 3467429b..4811b3ab 100644 --- a/app/db/src/app/common/components/settings/settings.component.html +++ b/app/db/src/app/common/components/settings/settings.component.html @@ -75,7 +75,7 @@
Card Font Family
@@ -88,9 +88,9 @@
Card Font Size
state.user); @@ -26,25 +27,30 @@ export class SettingsComponent extends SubscriberComponent { this.fonts = CardFonts; this.addSubscription( this.appService.state$.subscribe((state) => { - this.displaySettings = state.displaySettings; - this.cardFont = state.displaySettings.cardFont; + this.displaySettings = state.displaySettings.value; + this.cardFontFamily = state.displaySettings.value.cardFontFamily; + this.cardFontSize = state.displaySettings.value.cardFontSize; }) ); this.addSubscription( this.authService.authState.subscribe((user) => { - this.appService.setUser({ - uid: user.uid, - displayName: user.displayName, - providerId: user.providerId, - email: user.email, - }); + if (user) { + this.appService.setUser({ + uid: user.uid, + displayName: user.displayName, + providerId: user.providerId, + email: user.email, + }); + } }) ); } login() { - this.authService.signInWithPopup(new auth.GoogleAuthProvider()); + this.authService.signInWithPopup(new auth.GoogleAuthProvider()).then((cred) => { + console.log('Authenticated.'); + }); } logout() { this.authService.signOut(); @@ -65,68 +71,86 @@ export class SettingsComponent extends SubscriberComponent { //#region Search Settings toggleStrongsAsModal(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - showStrongsAsModal: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + showStrongsAsModal: toggle.checked, + }) + ); } toggleAppendCardToBottom(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - appendCardToBottom: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + appendCardToBottom: toggle.checked, + }) + ); } toggleInsertCardNextToItem(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - insertCardNextToItem: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + insertCardNextToItem: toggle.checked, + }) + ); } toggleClearSearchAfterQuery(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - clearSearchAfterQuery: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + clearSearchAfterQuery: toggle.checked, + }) + ); } toggleSyncCardsAcrossDevices(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - syncCardsAcrossDevices: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + syncCardsAcrossDevices: toggle.checked, + }) + ); } //#endregion //#region Passage Settings toggleParagraphHeadings(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - showParagraphHeadings: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + showParagraphHeadings: toggle.checked, + }) + ); } toggleParagraphs(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - showParagraphs: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + showParagraphs: toggle.checked, + }) + ); } toggleVerseNumbers(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - showVerseNumbers: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + showVerseNumbers: toggle.checked, + }) + ); } toggleVersesOnNewLine(toggle: MatSlideToggleChange) { - this.appService.updateDisplaySettings({ - ...this.displaySettings, - showVersesOnNewLine: toggle.checked, - }); + this.appService.updateDisplaySettings( + new Storable({ + ...this.displaySettings, + showVersesOnNewLine: toggle.checked, + }) + ); } //#endregion diff --git a/app/db/src/app/constants.ts b/app/db/src/app/constants.ts index 0e4cc2d3..01ef32b8 100644 --- a/app/db/src/app/constants.ts +++ b/app/db/src/app/constants.ts @@ -14,13 +14,30 @@ export const CardIcons = { Strongs: 'article', }; -export const CardFonts = ['Merriweather', 'PT Sans', 'PT Serif', 'Open Sans', 'Roboto', 'Roboto Condensed', 'Inconsolata']; +export const CardFonts = [ + 'Merriweather', + 'PT Sans', + 'PT Serif', + 'Open Sans', + 'Roboto', + 'Roboto Condensed', + 'Inconsolata', +]; +// export const FirebaseConfig = { +// apiKey: 'AIzaSyA3UV4s56CV2EumgvZmyJBTyU-vhv0xhc8', +// authDomain: 'dynamicbible-7c6cf.firebaseapp.com', +// databaseURL: 'https://dynamicbible-7c6cf.firebaseio.com', +// projectId: 'dynamicbible-7c6cf', +// storageBucket: '', +// messagingSenderId: '200739882604', +// }; export const FirebaseConfig = { - apiKey: 'AIzaSyA3UV4s56CV2EumgvZmyJBTyU-vhv0xhc8', - authDomain: 'dynamicbible-7c6cf.firebaseapp.com', - databaseURL: 'https://dynamicbible-7c6cf.firebaseio.com', - projectId: 'dynamicbible-7c6cf', - storageBucket: '', - messagingSenderId: '200739882604', + apiKey: 'AIzaSyA4b587psiOnpjbzu0t6z75A_hFksPyQkI', + authDomain: 'dynamic-bible-testing.firebaseapp.com', + databaseURL: 'https://dynamic-bible-testing.firebaseio.com', + projectId: 'dynamic-bible-testing', + storageBucket: 'dynamic-bible-testing.appspot.com', + messagingSenderId: '813845246474', + appId: '1:813845246474:web:6dccfa057b6cb3067565f3', }; diff --git a/app/db/src/app/models/app-state.ts b/app/db/src/app/models/app-state.ts index 3790f814..23b5bd36 100644 --- a/app/db/src/app/models/app-state.ts +++ b/app/db/src/app/models/app-state.ts @@ -1,12 +1,14 @@ +import { IStorable } from './storable'; + export interface AppState { readonly currentPage: SavedPage; - readonly savedPages: readonly SavedPage[]; + readonly savedPages: IStorable; readonly savedPagesLoaded: boolean; readonly mainPages: readonly Page[]; readonly cards: readonly CardItem[]; readonly autocomplete: readonly string[]; readonly error: Error; - readonly displaySettings: DisplaySettings; + readonly displaySettings: IStorable; readonly cardIcons: CardIcons; readonly user: User; } @@ -33,12 +35,14 @@ export interface CardIcons { export interface DisplaySettings { readonly showStrongsAsModal: boolean; + readonly appendCardToBottom: boolean; readonly insertCardNextToItem: boolean; + readonly clearSearchAfterQuery: boolean; - readonly fontSize: number; - readonly cardFont: string; + readonly cardFontSize: number; + readonly cardFontFamily: string; readonly showVersesOnNewLine: boolean; readonly showVerseNumbers: boolean; diff --git a/app/db/src/app/models/storable.ts b/app/db/src/app/models/storable.ts new file mode 100644 index 00000000..e57c09c6 --- /dev/null +++ b/app/db/src/app/models/storable.ts @@ -0,0 +1,14 @@ +export interface IStorable { + readonly createdOn: string; + readonly value: T; +} + +export class Storable implements IStorable { + constructor(v: T) { + this.value = v; + this.createdOn = new Date().toISOString(); + } + + createdOn: string; + value: T; +} diff --git a/app/db/src/app/search/components/passage/passage-card.component.ts b/app/db/src/app/search/components/passage/passage-card.component.ts index c0db6769..82a62721 100644 --- a/app/db/src/app/search/components/passage/passage-card.component.ts +++ b/app/db/src/app/search/components/passage/passage-card.component.ts @@ -15,30 +15,18 @@ import { StrongsModalComponent } from '../strongs/modal/strongs-modal.component' export class PassageCardComponent extends CardComponent implements OnInit { ref: BibleReference; - showParagraphs$ = this.appService.select( - (state) => state.displaySettings.showParagraphs - ); + showParagraphs$ = this.appService.select((state) => state.displaySettings.value.showParagraphs); showParagraphHeadings$ = this.appService.select( - (state) => - state.displaySettings.showParagraphHeadings && - state.displaySettings.showParagraphs - ); - showVersesOnNewLine$ = this.appService.select( - (state) => state.displaySettings.showVersesOnNewLine - ); - showVerseNumbers$ = this.appService.select( - (state) => state.displaySettings.showVerseNumbers + (state) => state.displaySettings.value.showParagraphHeadings && state.displaySettings.value.showParagraphs ); + showVersesOnNewLine$ = this.appService.select((state) => state.displaySettings.value.showVersesOnNewLine); + showVerseNumbers$ = this.appService.select((state) => state.displaySettings.value.showVerseNumbers); - displaySettings$ = this.appService.select((state) => state.displaySettings); + displaySettings$ = this.appService.select((state) => state.displaySettings.value); @ViewChild('passage') passageElement: ElementRef; - constructor( - protected elementRef: ElementRef, - private appService: AppService, - public dialog: MatDialog - ) { + constructor(protected elementRef: ElementRef, private appService: AppService, public dialog: MatDialog) { super(elementRef, dialog); this.icon$ = appService.select((state) => state.cardIcons.passage); } @@ -54,14 +42,9 @@ export class PassageCardComponent extends CardComponent implements OnInit { } next() { - const lastVerseForEnd = this.ref.section.book.chapters[ - this.ref.section.end.chapter - ]; + const lastVerseForEnd = this.ref.section.book.chapters[this.ref.section.end.chapter]; - if ( - this.ref.section.end.verse !== 0 && - this.ref.section.end.verse !== lastVerseForEnd - ) { + if (this.ref.section.end.verse !== 0 && this.ref.section.end.verse !== lastVerseForEnd) { this.ref.section.end.chapter = this.ref.section.end.chapter; } else { this.ref.section.end.chapter = this.ref.section.end.chapter + 1; @@ -69,9 +52,7 @@ export class PassageCardComponent extends CardComponent implements OnInit { this.ref.section.start.chapter = this.ref.section.end.chapter; this.ref.section.start.verse = 1; - this.ref.section.end.verse = this.ref.section.book.chapters[ - this.ref.section.end.chapter - ]; + this.ref.section.end.verse = this.ref.section.book.chapters[this.ref.section.end.chapter]; this.appService.updatePassage(this.cardItem, this.ref); } @@ -85,24 +66,19 @@ export class PassageCardComponent extends CardComponent implements OnInit { this.ref.section.end.chapter = this.ref.section.start.chapter; this.ref.section.start.verse = 1; - this.ref.section.end.verse = this.ref.section.book.chapters[ - this.ref.section.end.chapter - ]; + this.ref.section.end.verse = this.ref.section.book.chapters[this.ref.section.end.chapter]; this.appService.updatePassage(this.cardItem, this.ref); } expand() { - const lastVerseForEnd = this.ref.section.book.chapters[ - this.ref.section.end.chapter - ]; + const lastVerseForEnd = this.ref.section.book.chapters[this.ref.section.end.chapter]; // if your verse is at the beginning, to go the prev chapter and add 3 verses from that if (this.ref.section.start.verse < 4) { this.ref.section.start.chapter = this.ref.section.start.chapter - 1; this.ref.section.start.verse = - this.ref.section.book.chapters[this.ref.section.start.chapter] - - (3 - this.ref.section.start.verse); + this.ref.section.book.chapters[this.ref.section.start.chapter] - (3 - this.ref.section.start.verse); if (this.ref.section.start.chapter === 0) { this.ref.section.start.chapter = 1; this.ref.section.start.verse = 1; @@ -113,22 +89,15 @@ export class PassageCardComponent extends CardComponent implements OnInit { } // if your verse is at the end, go to the next chapter - if ( - this.ref.section.end.verse === 0 || - this.ref.section.end.verse + 3 > lastVerseForEnd - ) { + if (this.ref.section.end.verse === 0 || this.ref.section.end.verse + 3 > lastVerseForEnd) { this.ref.section.end.chapter = this.ref.section.end.chapter + 1; if (this.ref.section.end.verse === 0) { this.ref.section.end.verse = 3; } else { - this.ref.section.end.verse = - this.ref.section.end.verse + 3 - lastVerseForEnd; + this.ref.section.end.verse = this.ref.section.end.verse + 3 - lastVerseForEnd; } - if ( - this.ref.section.end.chapter === - this.ref.section.book.lastChapter + 1 - ) { + if (this.ref.section.end.chapter === this.ref.section.book.lastChapter + 1) { this.ref.section.end.chapter = this.ref.section.book.lastChapter; this.ref.section.end.verse = lastVerseForEnd; } diff --git a/app/db/src/app/search/components/search-page/search.page.ts b/app/db/src/app/search/components/search-page/search.page.ts index cf869fcf..1bcff784 100644 --- a/app/db/src/app/search/components/search-page/search.page.ts +++ b/app/db/src/app/search/components/search-page/search.page.ts @@ -9,10 +9,7 @@ import { BibleReference } from '../../../common/bible-reference'; import { VersePickerModalComponent } from '../verse-picker-modal/verse-picker-modal.component'; import { SubscriberComponent } from '../../../common/components/subscriber.component'; -import { - MatAutocompleteTrigger, - MatAutocomplete, -} from '@angular/material/autocomplete'; +import { MatAutocompleteTrigger, MatAutocomplete } from '@angular/material/autocomplete'; @Component({ selector: 'app-search-page', @@ -46,8 +43,7 @@ export class SearchPage extends SubscriberComponent implements OnInit { this.addSubscription( this.appService.state$.subscribe((state) => { this.savedPagedLoaded = state.savedPagesLoaded; - this.clearSearchAfterQuery = - state.displaySettings.clearSearchAfterQuery; + this.clearSearchAfterQuery = state.displaySettings.value.clearSearchAfterQuery; }) ); } @@ -65,9 +61,7 @@ export class SearchPage extends SubscriberComponent implements OnInit { this.init(); // subscribe to autocomplete input control's changes this.addSubscription( - this.searchControl.valueChanges.subscribe((value: string) => - this.appService.getAutoComplete(value.toLowerCase()) - ) + this.searchControl.valueChanges.subscribe((value: string) => this.appService.getAutoComplete(value.toLowerCase())) ); } diff --git a/app/db/src/app/search/components/strongs/card/strongs-card.component.ts b/app/db/src/app/search/components/strongs/card/strongs-card.component.ts index b6600b37..31143110 100644 --- a/app/db/src/app/search/components/strongs/card/strongs-card.component.ts +++ b/app/db/src/app/search/components/strongs/card/strongs-card.component.ts @@ -15,16 +15,12 @@ export class StrongsCardComponent extends CardComponent { asModal = false; @ViewChild('strongs') strongsElement: ElementRef; - constructor( - protected elementRef: ElementRef, - protected appService: AppService, - protected dialog: MatDialog - ) { + constructor(protected elementRef: ElementRef, protected appService: AppService, protected dialog: MatDialog) { super(elementRef, dialog); this.icon$ = appService.select((state) => state.cardIcons.strongs); this.addSubscription( this.appService.state$.subscribe((state) => { - this.asModal = state.displaySettings.showStrongsAsModal; + this.asModal = state.displaySettings.value.showStrongsAsModal; }) ); } diff --git a/app/db/src/app/services/app.service.ts b/app/db/src/app/services/app.service.ts index f973637b..5994a224 100644 --- a/app/db/src/app/services/app.service.ts +++ b/app/db/src/app/services/app.service.ts @@ -28,6 +28,8 @@ import { PageTitles, PageIcons } from '../constants'; import { createStateService } from '../common/state-service'; import { UUID } from 'angular2-uuid'; import { StorageMap } from '@ngx-pwa/local-storage'; +import { AngularFireDatabase } from '@angular/fire/database'; +import { IStorable, Storable } from '../models/storable'; const initialState: AppState = { user: null, @@ -46,7 +48,10 @@ const initialState: AppState = { ], autocomplete: [], currentPage: null, - savedPages: [], + savedPages: { + createdOn: new Date(0).toISOString(), + value: [], + }, savedPagesLoaded: false, mainPages: [ { title: PageTitles.Search, icon: PageIcons.Search, route: 'search' }, @@ -54,17 +59,20 @@ const initialState: AppState = { ], error: null, displaySettings: { - showStrongsAsModal: false, - appendCardToBottom: true, - insertCardNextToItem: true, - clearSearchAfterQuery: true, - fontSize: 12, - cardFont: 'PT Serif', - showVersesOnNewLine: false, - showVerseNumbers: false, - showParagraphs: true, - showParagraphHeadings: true, - syncCardsAcrossDevices: false, + createdOn: new Date(0).toISOString(), + value: { + showStrongsAsModal: false, + appendCardToBottom: true, + insertCardNextToItem: true, + clearSearchAfterQuery: true, + cardFontSize: 12, + cardFontFamily: 'PT Serif', + showVersesOnNewLine: false, + showVerseNumbers: false, + showParagraphs: true, + showParagraphHeadings: true, + syncCardsAcrossDevices: false, + }, }, cardIcons: { words: 'font_download', @@ -88,7 +96,7 @@ type AppAction = } | { type: 'UPDATE_SAVED_PAGES'; - savedPages: SavedPage[]; + savedPages: IStorable; } | { type: 'ADD_CARD_TO_SAVED_PAGE'; @@ -115,11 +123,11 @@ type AppAction = } | { type: 'UPDATE_FONT_SIZE'; - size: number; + cardFontSize: number; } | { type: 'UPDATE_FONT_FAMILY'; - cardFont: string; + cardFontFamily: string; } | { type: 'UPDATE_AUTOCOMPLETE'; @@ -127,13 +135,28 @@ type AppAction = } | { type: 'UPDATE_DISPLAY_SETTINGS'; - settings: DisplaySettings; + settings: IStorable; } | { type: 'SET_USER'; user: User; }; +function maybeMutateStorable( + state: AppState, + candidate: IStorable, + incumbant: IStorable, + composeState: (item: IStorable) => AppState +): AppState { + // only update if the settings are newer. + if (new Date(candidate.createdOn) > new Date(incumbant.createdOn)) { + return composeState(candidate); + } + + // candidate didn't win. return state untouched. + return state; +} + function reducer(state: AppState, action: AppAction): AppState { // somtimes the state is null. lets not complain if that happens. if (state === undefined) { @@ -141,12 +164,41 @@ function reducer(state: AppState, action: AppAction): AppState { } switch (action.type) { + //#region Display Settings + case 'UPDATE_DISPLAY_SETTINGS': { - return { - ...state, - displaySettings: action.settings, - }; + return maybeMutateStorable(state, action.settings, state.displaySettings, (item) => { + return { + ...state, + displaySettings: item, + }; + }); } + case 'UPDATE_FONT_SIZE': { + const settings = new Storable({ + ...state.displaySettings.value, + cardFontSize: action.cardFontSize, + }); + + return reducer(state, { + type: 'UPDATE_DISPLAY_SETTINGS', + settings, + }); + } + case 'UPDATE_FONT_FAMILY': { + const settings = new Storable({ + ...state.displaySettings.value, + cardFontFamily: action.cardFontFamily, + }); + + return reducer(state, { + type: 'UPDATE_DISPLAY_SETTINGS', + settings, + }); + } + + //#endregion + case 'UPDATE_AUTOCOMPLETE': { return { ...state, @@ -154,49 +206,50 @@ function reducer(state: AppState, action: AppAction): AppState { }; } case 'UPDATE_SAVED_PAGES': { - return { - ...state, - savedPagesLoaded: true, - savedPages: action.savedPages, - }; + return maybeMutateStorable(state, action.savedPages, state.savedPages, (item) => { + return { + ...state, + savedPagesLoaded: true, + savedPages: item, + }; + }); } case 'UPDATE_CURRENT_PAGE': { - const savedPages = [ - ...state.savedPages.filter((o) => o.id === state.currentPage.id), + const savedPages = new Storable([ + ...state.savedPages.value.filter((o) => o.id === state.currentPage.id), { id: state.currentPage.id, title: state.currentPage.title, queries: [...state.cards], }, - ]; + ]); - return { - ...state, - savedPagesLoaded: true, + return reducer(state, { + type: 'UPDATE_SAVED_PAGES', savedPages, - }; + }); } case 'SAVE_PAGE': { - const savedPages = [ - ...state.savedPages, + const savedPages = new Storable([ + ...state.savedPages.value, { // create a new saved page object title: action.title, id: UUID.UUID().toString(), queries: [...state.cards], }, - ]; - return { - ...state, - savedPagesLoaded: true, + ]); + + return reducer(state, { + type: 'UPDATE_SAVED_PAGES', savedPages, - }; + }); } case 'GET_SAVED_PAGE': { - const page = state.savedPages.find((o) => o.id.toString() === action.pageId); + const page = state.savedPages.value.find((o) => o.id.toString() === action.pageId); if (!page) { - return; + return state; } return { @@ -206,34 +259,36 @@ function reducer(state: AppState, action: AppAction): AppState { }; } case 'ADD_CARD_TO_SAVED_PAGE': { - return { - ...state, - savedPages: [ - ...state.savedPages.map((o) => { - if (o.id.toString() === action.pageId) { - let cards = []; - if (state.displaySettings.appendCardToBottom) { - cards = [...o.queries, action.card]; - } else { - cards = [action.card, ...o.queries]; - } - return { - ...o, - queries: cards, - }; + const savedPages = new Storable([ + ...state.savedPages.value.map((o) => { + if (o.id.toString() === action.pageId) { + let cards = []; + if (state.displaySettings.value.appendCardToBottom) { + cards = [...o.queries, action.card]; + } else { + cards = [action.card, ...o.queries]; } - return o; - }), - ], - }; + return { + ...o, + queries: cards, + }; + } + return o; + }), + ]); + + return reducer(state, { + type: 'UPDATE_SAVED_PAGES', + savedPages, + }); } case 'ADD_CARD': { let cards = []; - if (action.nextToItem && state.displaySettings.insertCardNextToItem) { + if (action.nextToItem && state.displaySettings.value.insertCardNextToItem) { const idx = state.cards.indexOf(action.nextToItem); - if (state.displaySettings.appendCardToBottom) { + if (state.displaySettings.value.appendCardToBottom) { const before = state.cards.slice(0, idx + 1); const after = state.cards.slice(idx + 1); cards = [...before, action.card, ...after]; @@ -243,7 +298,7 @@ function reducer(state: AppState, action: AppAction): AppState { cards = [...before, action.card, ...after]; } } else { - if (state.displaySettings.appendCardToBottom) { + if (state.displaySettings.value.appendCardToBottom) { cards = [...state.cards, action.card]; } else { cards = [action.card, ...state.cards]; @@ -271,24 +326,6 @@ function reducer(state: AppState, action: AppAction): AppState { cards: [...state.cards.filter((c) => c !== action.card)], }; } - case 'UPDATE_FONT_SIZE': { - return { - ...state, - displaySettings: { - ...state.displaySettings, - fontSize: action.size, - }, - }; - } - case 'UPDATE_FONT_FAMILY': { - return { - ...state, - displaySettings: { - ...state.displaySettings, - cardFont: action.cardFont, - }, - }; - } case 'SET_USER': { return { ...state, @@ -309,7 +346,7 @@ export class AppService extends createStateService(reducer, initialState) { private readonly dataPath = 'assets/data'; - constructor(private http: HttpClient, private localStorageService: StorageMap) { + constructor(private http: HttpClient, private localStorageService: StorageMap, private db: AngularFireDatabase) { super(); this.searchIndexArray = this.buildIndexArray().sort(); @@ -322,7 +359,7 @@ export class AppService extends createStateService(reducer, initialState) { }); } - private dispatchError(msg: string) { + dispatchError(msg: string) { this.dispatch({ type: 'UPDATE_ERROR', error: { @@ -341,19 +378,6 @@ export class AppService extends createStateService(reducer, initialState) { //#region Saved Pages - async initSavedPages() { - const exists = await this.localStorageService.has('savedPages').toPromise(); - - if (exists) { - const savedPages = (await this.localStorageService.get('savedPages').toPromise()) as SavedPage[]; - - this.dispatch({ - type: 'UPDATE_SAVED_PAGES', - savedPages, - }); - } - } - getSavedPage(pageid: string) { this.dispatch({ type: 'GET_SAVED_PAGE', @@ -368,30 +392,19 @@ export class AppService extends createStateService(reducer, initialState) { }); } - savePagesApi(savedPages: readonly SavedPage[]) { - this.localStorageService.set('savedPages', savedPages).subscribe( - () => { - // nop - }, - // error - () => { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - // tslint:disable-next-line: quotemark - msg: "Something went wrong and the page wasn't saved. :(", - }, - }); - } - ); - } - - updatePage() { + updateSavedPage() { this.dispatch({ type: 'UPDATE_CURRENT_PAGE', }); } + updateSavedPages(savedPages: IStorable) { + this.dispatch({ + type: 'UPDATE_SAVED_PAGES', + savedPages, + }); + } + addCardToSavedPage(pageId: string, card: CardItem) { this.dispatch({ type: 'ADD_CARD_TO_SAVED_PAGE', @@ -407,45 +420,24 @@ export class AppService extends createStateService(reducer, initialState) { changeCardFontFamily(cardFont: string) { this.dispatch({ type: 'UPDATE_FONT_FAMILY', - cardFont, + cardFontFamily: cardFont, }); } changeCardFontSize(size: number) { this.dispatch({ type: 'UPDATE_FONT_SIZE', - size, + cardFontSize: size, }); } - updateDisplaySettings(settings: DisplaySettings) { + updateDisplaySettings(settings: IStorable) { this.dispatch({ type: 'UPDATE_DISPLAY_SETTINGS', settings, }); } - async initDisplaySettings() { - const hasDisplaySettings = await this.localStorageService.has('displaySettings').toPromise(); - - if (hasDisplaySettings) { - const settings = await this.getSettingsApi(); - - this.dispatch({ - type: 'UPDATE_DISPLAY_SETTINGS', - settings, - }); - } - } - - saveSettingsApi(settings: DisplaySettings) { - return this.localStorageService.set('displaySettings', settings); - } - - private async getSettingsApi() { - return (await this.localStorageService.get('displaySettings').toPromise()) as DisplaySettings; - } - //#endregion //#region Notes @@ -581,13 +573,17 @@ export class AppService extends createStateService(reducer, initialState) { if (dict === 'grk') { result.prefix = 'G'; if (sn > 5624 || sn < 1) { - this.dispatchError(`Strong's Number G${sn} is out of range. Strong's numbers range from 1 - 5624 in the New Testament.`); + this.dispatchError( + `Strong's Number G${sn} is out of range. Strong's numbers range from 1 - 5624 in the New Testament.` + ); return; } } else { result.prefix = 'H'; if (sn > 8674 || sn < 1) { - this.dispatchError(`Strong's Number H${sn} is out of range. Strong's numbers range from 1 - 8674 in the Old Testament.`); + this.dispatchError( + `Strong's Number H${sn} is out of range. Strong's numbers range from 1 - 8674 in the Old Testament.` + ); return; } } @@ -788,7 +784,11 @@ export class AppService extends createStateService(reducer, initialState) { return passages; } - private convertToParagraphs(ch: BiblePassage, section: Section, paragraphMarkers: HashTable): BibleParagraph[] { + private convertToParagraphs( + ch: BiblePassage, + section: Section, + paragraphMarkers: HashTable + ): BibleParagraph[] { // group the verses into paragraphs. // create an initial paragraph to hold verses that might come before a paragraph. @@ -875,7 +875,9 @@ export class AppService extends createStateService(reducer, initialState) { // handle the first case. if (stem <= this.searchIndexArray[0]) { - results.unshift(await this.getSearchReferences(`${this.dataPath}/index/${this.searchIndexArray[0]}idx.json`, stem)); + results.unshift( + await this.getSearchReferences(`${this.dataPath}/index/${this.searchIndexArray[0]}idx.json`, stem) + ); break; } @@ -883,7 +885,9 @@ export class AppService extends createStateService(reducer, initialState) { for (let w = 1; w < this.searchIndexArray.length; w++) { // If we are at the end of the array, we want to use a different test. if (stem <= this.searchIndexArray[w] && stem > this.searchIndexArray[w - 1]) { - results.unshift(await this.getSearchReferences(`${this.dataPath}/index/${this.searchIndexArray[w]}idx.json`, stem)); + results.unshift( + await this.getSearchReferences(`${this.dataPath}/index/${this.searchIndexArray[w]}idx.json`, stem) + ); } } } // End of loop through query terms @@ -1290,7 +1294,8 @@ export class AppService extends createStateService(reducer, initialState) { for (const item of BibleReference.Books) { if ( item.name !== 'Unknown' && - (item.name.toLowerCase().indexOf(qry.toLowerCase()) > -1 || item.abbreviation.toLowerCase().indexOf(qry.toLowerCase()) > -1) + (item.name.toLowerCase().indexOf(qry.toLowerCase()) > -1 || + item.abbreviation.toLowerCase().indexOf(qry.toLowerCase()) > -1) ) { words.push(prefix + item.name); if (words.length > 2) { diff --git a/app/db/src/app/services/storage.service.ts b/app/db/src/app/services/storage.service.ts new file mode 100644 index 00000000..5c2ce5ef --- /dev/null +++ b/app/db/src/app/services/storage.service.ts @@ -0,0 +1,131 @@ +import { Injectable } from '@angular/core'; +import { StorageMap } from '@ngx-pwa/local-storage'; +import { AngularFireDatabase, AngularFireObject } from '@angular/fire/database'; +import { IStorable } from '../models/storable'; +import { AppService } from './app.service'; +import { DisplaySettings, SavedPage, User } from '../models/app-state'; +import { SubscriberComponent } from '../common/components/subscriber.component'; + +@Injectable({ + providedIn: 'root', +}) +export class StorageService extends SubscriberComponent { + private displaySettingsState$ = this.appService.select((state) => state.displaySettings); + private displaySettingsPath = 'displaySettings'; + private displaySettingsRemoteObject: AngularFireObject>; + + private savedPagesState$ = this.appService.select((state) => state.savedPages); + private savedPagesPath = 'savedPaged'; + private savedPagesRemoteObject: AngularFireObject>; + + constructor(private local: StorageMap, private remote: AngularFireDatabase, private appService: AppService) { + super(); + + //#region handle remote and local storage + // when the specific items change in the state, + // store them locally and remotely, if you're logged in. + + this.addSubscription( + this.displaySettingsState$.subscribe((settings) => { + if (!settings) { + return; + } + + // update local + this.local.set('displaySettings', settings).subscribe( + () => { + // nop + }, + // error + () => { + // tslint:disable-next-line: quotemark + this.appService.dispatchError("Something went wrong and the display settings weren't saved. :("); + } + ); + + // update remote + if (this.displaySettingsRemoteObject) { + this.displaySettingsRemoteObject.set(settings); + } + }) + ); + + this.addSubscription( + this.savedPagesState$.subscribe((savedPages) => { + if (!savedPages) { + return; + } + + // update local + this.local.set('savedPages', savedPages).subscribe( + () => { + // nop + }, + // error + () => { + // tslint:disable-next-line: quotemark + this.appService.dispatchError("Something went wrong and the page wasn't saved. :("); + } + ); + + // update remote + if (this.savedPagesRemoteObject) { + this.savedPagesRemoteObject.set(savedPages); + } + }) + ); + + //#endregion + } + + initRemote(user: User) { + this.displaySettingsRemoteObject = this.remote.object>( + `/${this.displaySettingsPath}/${user.uid}` + ); + this.savedPagesRemoteObject = this.remote.object>(`/${this.savedPagesPath}/${user.uid}`); + + // display settings + this.addSubscription( + this.displaySettingsRemoteObject + .valueChanges() // when the value changes + .subscribe((remoteDisplaySettings) => { + if (remoteDisplaySettings) { + // update the display settings locally from remote if it isn't null + this.appService.updateDisplaySettings(remoteDisplaySettings); + } + }) + ); + + // saved pages + this.addSubscription( + this.savedPagesRemoteObject + .valueChanges() // when the saved pages have changed + .subscribe((remoteSavedPages) => { + if (remoteSavedPages) { + // update the saved pages locally from remote if it isn't null + this.appService.updateSavedPages(remoteSavedPages); + } + }) + ); + } + + async initDisplaySettings() { + const hasDisplaySettings = await this.local.has(this.displaySettingsPath).toPromise(); + + if (hasDisplaySettings) { + const settings = (await this.local.get(this.displaySettingsPath).toPromise()) as IStorable; + + this.appService.updateDisplaySettings(settings); + } + } + + async initSavedPages() { + const exists = await this.local.has(this.savedPagesPath).toPromise(); + + if (exists) { + const savedPages = (await this.local.get(this.savedPagesPath).toPromise()) as IStorable; + + this.appService.updateSavedPages(savedPages); + } + } +}