sync local and remote display settings

This commit is contained in:
Jason Wall 2020-08-07 20:19:39 -04:00
parent 573b9dc29d
commit 25ed414aba
15 changed files with 471 additions and 323 deletions

View File

@ -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.

View File

@ -1,9 +1,9 @@
{
"targets": {
"dynamicbible-7c6cf": {
"dynamic-bible-testing": {
"hosting": {
"db": [
"dynamicbible-7c6cf"
"dynamic-bible-testing"
]
}
}

View File

@ -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<boolean> = this.breakpointObserver
.observe(Breakpoints.Handset)
.pipe(
map((result) => result.matches),
shareReplay()
);
isHandset$: Observable<boolean> = 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() {

View File

@ -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,

View File

@ -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';

View File

@ -75,7 +75,7 @@
<div class="settings-h2">Card Font Family</div>
<mat-form-field appearance="fill" class="card-fonts">
<mat-select
[(value)]="cardFont"
[(value)]="cardFontFamily"
(selectionChange)="cardFontSelected($event)"
>
<mat-option *ngFor="let font of fonts" [value]="font">
@ -88,9 +88,9 @@
<div class="settings-h2">Card Font Size</div>
<mat-slider
class="font-size-slider"
max="32"
min="10"
step="1"
max="24"
min="12"
step="2"
thumbLabel="true"
(change)="cardFontSizeChanged($event)"
[(ngModel)]="cardFontSize"

View File

@ -8,6 +8,7 @@ import { MatSelectChange } from '@angular/material/select';
import { MatSliderChange } from '@angular/material/slider';
import { AngularFireAuth } from '@angular/fire/auth';
import { auth } from 'firebase/app';
import { Storable } from 'src/app/models/storable';
@Component({
selector: 'app-settings',
@ -17,7 +18,7 @@ import { auth } from 'firebase/app';
export class SettingsComponent extends SubscriberComponent {
displaySettings: DisplaySettings;
fonts: string[];
cardFont = '';
cardFontFamily = '';
cardFontSize = 10;
user$ = this.appService.select((state) => 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

View File

@ -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',
};

View File

@ -1,12 +1,14 @@
import { IStorable } from './storable';
export interface AppState {
readonly currentPage: SavedPage;
readonly savedPages: readonly SavedPage[];
readonly savedPages: IStorable<readonly SavedPage[]>;
readonly savedPagesLoaded: boolean;
readonly mainPages: readonly Page[];
readonly cards: readonly CardItem[];
readonly autocomplete: readonly string[];
readonly error: Error;
readonly displaySettings: DisplaySettings;
readonly displaySettings: IStorable<DisplaySettings>;
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;

View File

@ -0,0 +1,14 @@
export interface IStorable<T> {
readonly createdOn: string;
readonly value: T;
}
export class Storable<T> implements IStorable<T> {
constructor(v: T) {
this.value = v;
this.createdOn = new Date().toISOString();
}
createdOn: string;
value: T;
}

View File

@ -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;
}

View File

@ -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()))
);
}

View File

@ -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;
})
);
}

View File

@ -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<readonly SavedPage[]>;
}
| {
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<DisplaySettings>;
}
| {
type: 'SET_USER';
user: User;
};
function maybeMutateStorable<T>(
state: AppState,
candidate: IStorable<T>,
incumbant: IStorable<T>,
composeState: (item: IStorable<T>) => 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<DisplaySettings>({
...state.displaySettings.value,
cardFontSize: action.cardFontSize,
});
return reducer(state, {
type: 'UPDATE_DISPLAY_SETTINGS',
settings,
});
}
case 'UPDATE_FONT_FAMILY': {
const settings = new Storable<DisplaySettings>({
...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<SavedPage[]>([
...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<readonly SavedPage[]>) {
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<DisplaySettings>) {
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<Paragraph>): BibleParagraph[] {
private convertToParagraphs(
ch: BiblePassage,
section: Section,
paragraphMarkers: HashTable<Paragraph>
): 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) {

View File

@ -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<IStorable<DisplaySettings>>;
private savedPagesState$ = this.appService.select((state) => state.savedPages);
private savedPagesPath = 'savedPaged';
private savedPagesRemoteObject: AngularFireObject<IStorable<readonly SavedPage[]>>;
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<IStorable<DisplaySettings>>(
`/${this.displaySettingsPath}/${user.uid}`
);
this.savedPagesRemoteObject = this.remote.object<IStorable<SavedPage[]>>(`/${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<DisplaySettings>;
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<SavedPage[]>;
this.appService.updateSavedPages(savedPages);
}
}
}