Merge branch 'db-1-upgrades' of gitlab.com:walljm/dynamicbible into db-1-upgrades

This commit is contained in:
Jeremy Wall 2020-08-09 09:24:10 -04:00
commit b53ae39bb0
33 changed files with 688 additions and 1527 deletions

View File

@ -15,7 +15,7 @@ end_of_line = lf
# this file has a need for arbitrarily long lines, so we exempt this here. # this file has a need for arbitrarily long lines, so we exempt this here.
[bible-reference.ts] [bible-reference.ts]
max_line_length = off max_line_length = 9999
[*.md] [*.md]
max_line_length = off max_line_length = off

View File

@ -28,30 +28,36 @@ To get more help on the Angular CLI use `ng help` or go check out the [Angular C
# Punch List # Punch List
- Swipe to close
- Drop down menu items for move card up/down
- Options to merge references \*\* - Options to merge references \*\*
- Merge if overlap - Merge if overlap
- Merge if contains - Merge if contains
- Font Size - Merge if equals
- Don't merge
- Page Admin \*\* - Page Admin \*\*
- Delete Page - Delete Page
- Show page and list of card titles - Show pages and list of card titles for each page in expansion panel
- Remove card from page - Remove card from page
- Make page public (private edit only) - Make page public (private edit only) [available only when logged in]
- Notes Admin \*\* - Notes Admin \*\*
- Edit Note - List notes by title
- Delete Note - Edit Note
- Delete Note
- Create Note - Create Note
- Note: Add Cross References
- Note: Auto link passages using markdown link syntax - Note: Auto link passages using markdown link syntax
- Passage
- Show Note Cross References \*\*
- Login
- Help Page - Help Page
- Incorporate Jacob's Geo Work
- Android and IOS mobile apps with Ionic Capactor
- Test note persistence
- Test note search
- remove old ionic project
- setup CI/CD
- Edit card query
## Optionally for Future
- Swipe to close
- Change Card Qry option in drop down
- Settings for theme - Settings for theme
- Custom Colors for Light/Dark modes - Custom Colors for Light/Dark modes
- Dark / Light / NightLight Mode - Dark / Light / NightLight Mode
- Merge saved pages lists when unlogged in -> login - Make card icons configurable (for future)
- Incorporate Jacob's Geo Work
- Android and IOS mobile apps with Ionic Capactor

View File

@ -23,14 +23,15 @@
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"aot": true, "aot": true,
"assets": [ "assets": ["src/favicon.ico", "src/assets"],
"src/favicon.ico", "styles": ["src/styles.scss"],
"src/assets" "scripts": [],
], "allowedCommonJsDependencies": [
"styles": [ "firebase",
"src/styles.scss" "@firebase/app",
], "@firebase/auth",
"scripts": [] "@firebase/database"
]
}, },
"configurations": { "configurations": {
"production": { "production": {
@ -87,13 +88,8 @@
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json", "tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js", "karmaConfig": "karma.conf.js",
"assets": [ "assets": ["src/favicon.ico", "src/assets"],
"src/favicon.ico", "styles": ["src/styles.scss"],
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [] "scripts": []
} }
}, },
@ -105,9 +101,7 @@
"tsconfig.spec.json", "tsconfig.spec.json",
"e2e/tsconfig.json" "e2e/tsconfig.json"
], ],
"exclude": [ "exclude": ["**/node_modules/**"]
"**/node_modules/**"
]
} }
}, },
"e2e": { "e2e": {
@ -130,4 +124,4 @@
} }
}, },
"defaultProject": "db" "defaultProject": "db"
} }

View File

@ -4,7 +4,7 @@
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build --prod",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e" "e2e": "ng e2e"

View File

@ -40,13 +40,12 @@ export class AppComponent extends SubscriberComponent implements AfterViewInit {
this.storageService.initSavedPages(); this.storageService.initSavedPages();
this.storageService.initDisplaySettings(); this.storageService.initDisplaySettings();
this.storageService.initNotes();
this.addSubscription( this.addSubscription(
this.error$.subscribe((err) => { this.error$.subscribe((err) => {
if (err) { if (err) {
this.snackBar.open(`Oh no! ${err.msg}`, 'Error', { this.snackBar.open(`Oh no! ${err.msg}`, 'Dismiss Error');
duration: 4 * 1000,
});
} }
}) })
); );

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,8 @@ import { Observable } from 'rxjs';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AddToPageModalComponent } from '../../search/components/saved-page/add-to-page-modal/add-to-page-modal.component'; import { AddToPageModalComponent } from '../../search/components/saved-page/add-to-page-modal/add-to-page-modal.component';
import { SubscriberComponent } from './subscriber.component'; import { SubscriberComponent } from './subscriber.component';
import { AppService } from 'src/app/services/app.service';
import { ListDirection } from '../list-direction';
@Component({ @Component({
template: '', template: '',
@ -20,7 +22,7 @@ export class CardComponent extends SubscriberComponent {
icon$: Observable<string>; icon$: Observable<string>;
constructor(protected elementRef: ElementRef, protected dialog: MatDialog) { constructor(protected elementRef: ElementRef, protected dialog: MatDialog, protected appService: AppService) {
super(); super();
} }
@ -63,4 +65,12 @@ export class CardComponent extends SubscriberComponent {
data: this.cardItem, data: this.cardItem,
}); });
} }
moveCardDown() {
this.appService.moveCard(this.cardItem, ListDirection.Down);
}
moveCardUp() {
this.appService.moveCard(this.cardItem, ListDirection.Up);
}
} }

View File

@ -91,7 +91,7 @@ export class SettingsComponent extends SubscriberComponent {
.subscribe((ds: OkCancelResult) => { .subscribe((ds: OkCancelResult) => {
console.log(ds); console.log(ds);
if (ds.ok) { if (ds.ok) {
this.appService.updateSavedPage(); this.appService.updateCurrentSavedPage();
this.navService.closeSettings(); this.navService.closeSettings();
this.snackBar.open(`${page.title} has been updated!`, '', { this.snackBar.open(`${page.title} has been updated!`, '', {
duration: 3 * 1000, duration: 3 * 1000,

View File

@ -0,0 +1,3 @@
export interface HashTable<T> {
readonly [key: string]: T;
}

View File

@ -0,0 +1,4 @@
export enum ListDirection {
Up,
Down,
}

View File

@ -1,14 +1,19 @@
import { IStorable } from './storable'; import { IStorable } from './storable';
import { NoteItem } from './note-state';
import { BiblePassageResult } from './passage-state';
import { StrongsResult } from './strongs-state';
import { WordLookupResult } from './words-state';
export interface AppState { export interface AppState {
readonly currentSavedPage: SavedPage; readonly currentSavedPage: SavedPage;
readonly savedPages: IStorable<readonly SavedPage[]>; readonly savedPages: IStorable<readonly SavedPage[]>;
readonly notes: IStorable<readonly NoteItem[]>;
readonly displaySettings: IStorable<DisplaySettings>;
readonly savedPagesLoaded: boolean; readonly savedPagesLoaded: boolean;
readonly mainPages: readonly Page[]; readonly mainPages: readonly Page[];
readonly cards: readonly CardItem[]; readonly cards: readonly CardItem[];
readonly autocomplete: readonly string[]; readonly autocomplete: readonly string[];
readonly error: Error; readonly error: Error;
readonly displaySettings: IStorable<DisplaySettings>;
readonly cardIcons: CardIcons; readonly cardIcons: CardIcons;
readonly user: User; readonly user: User;
} }
@ -58,12 +63,6 @@ export type OpenData = {
from_search_bar: boolean; from_search_bar: boolean;
}; };
export interface HashTable<T> {
readonly [key: string]: T;
}
//#region Pages and Cards
export interface SavedPage { export interface SavedPage {
readonly queries: readonly CardItem[]; readonly queries: readonly CardItem[];
readonly title: string; readonly title: string;
@ -84,133 +83,3 @@ export class Page {
readonly icon?: string; readonly icon?: string;
readonly route: string; readonly route: string;
} }
//#endregion
//#region Passage
export interface BiblePassageResult {
readonly cs: readonly BibleParagraphPassage[];
readonly testament: string;
readonly ref: string;
}
export interface BibleParagraph {
readonly p: Paragraph;
readonly vss: readonly BibleVerse[];
}
export interface BibleParagraphPassage {
readonly ch: number;
readonly paras: readonly BibleParagraph[];
}
export interface BiblePassage {
readonly ch: number;
readonly vss: readonly BibleVerse[];
}
export interface BibleVerse {
readonly v: number;
readonly w: readonly [
{
readonly t: string;
readonly s: string;
}
];
}
export interface Paragraph {
readonly h: string;
readonly p: number;
}
//#endregion
//#region Strongs
export type DictionaryType = 'heb' | 'grk';
export interface StrongsResult {
readonly prefix: string;
readonly sn: number;
readonly def: StrongsDefinition;
readonly rmac: RMACDefinition;
readonly crossrefs: StrongsCrossReference;
readonly rmaccode: string;
}
export interface StrongsDefinition {
readonly n: number;
readonly i: string;
readonly tr: string;
readonly de: readonly StrongsDefinitionPart[];
readonly lemma: string;
readonly p: string;
}
export interface StrongsDefinitionPart {
readonly sn: string;
readonly w: string;
}
export interface StrongsCrossReference {
readonly id: string; // strongs id H1|G1
readonly t: string; // strongs testament grk|heb
readonly d: string; // strongs word/data definition Aaron {ah-ar-ohn'}
readonly ss: readonly [
{
readonly w: string; // translated word
readonly rs: readonly [
{
readonly r: string; // reference
}
];
}
];
}
export interface RMACDefinition {
readonly id: string;
readonly d: readonly string[];
}
export interface RMACCrossReference {
readonly i: string;
readonly r: string;
}
//#endregion
//#region Word Search
export interface WordLookupResult {
readonly refs: readonly string[];
readonly word: string;
}
export interface IndexResult {
readonly r: readonly string[];
readonly w: string;
}
export interface WordToStem {
readonly w: string;
readonly s: string;
}
//#endregion
//#region Notes
export type NoteItem = {
// The Note id
readonly id: string;
// A note title.
readonly title: string;
// An optional cross reference to a bible passage.
readonly xref: string | null;
// The content of the note styled as markdown.
readonly content: string;
};
//#endregion

View File

@ -0,0 +1,10 @@
export type NoteItem = {
// The Note id
readonly id: string;
// A note title.
readonly title: string;
// An optional cross reference to a bible passage.
readonly xref: string | null;
// The content of the note styled as markdown.
readonly content: string;
};

View File

@ -0,0 +1,35 @@
export interface BiblePassageResult {
readonly cs: readonly BibleParagraphPassage[];
readonly testament: string;
readonly ref: string;
}
export interface BibleParagraph {
readonly p: Paragraph;
readonly vss: readonly BibleVerse[];
}
export interface BibleParagraphPassage {
readonly ch: number;
readonly paras: readonly BibleParagraph[];
}
export interface BiblePassage {
readonly ch: number;
readonly vss: readonly BibleVerse[];
}
export interface BibleVerse {
readonly v: number;
readonly w: readonly [
{
readonly t: string;
readonly s: string;
}
];
}
export interface Paragraph {
readonly h: string;
readonly p: number;
}

View File

@ -0,0 +1,49 @@
export type StrongsDictionary = 'heb' | 'grk';
export interface StrongsResult {
readonly prefix: string;
readonly sn: number;
readonly def: StrongsDefinition;
readonly rmac: RMACDefinition;
readonly crossrefs: StrongsCrossReference;
readonly rmaccode: string;
}
export interface StrongsDefinition {
readonly n: number;
readonly i: string;
readonly tr: string;
readonly de: readonly StrongsDefinitionPart[];
readonly lemma: string;
readonly p: string;
}
export interface StrongsDefinitionPart {
readonly sn: string;
readonly w: string;
}
export interface StrongsCrossReference {
readonly id: string; // strongs id H1|G1
readonly t: string; // strongs testament grk|heb
readonly d: string; // strongs word/data definition Aaron {ah-ar-ohn'}
readonly ss: readonly [
{
readonly w: string; // translated word
readonly rs: readonly [
{
readonly r: string; // reference
}
];
}
];
}
export interface RMACDefinition {
readonly id: string;
readonly d: readonly string[];
}
export interface RMACCrossReference {
readonly i: string;
readonly r: string;
}

View File

@ -0,0 +1,14 @@
export interface WordLookupResult {
readonly refs: readonly string[];
readonly word: string;
}
export interface IndexResult {
readonly r: readonly string[];
readonly w: string;
}
export interface WordToStem {
readonly w: string;
readonly s: string;
}

View File

@ -10,6 +10,10 @@
<mat-label>Title</mat-label> <mat-label>Title</mat-label>
<input formControlName="title" matInput /> <input formControlName="title" matInput />
</mat-form-field> </mat-form-field>
<mat-form-field class="note-xrefs">
<mat-label>References</mat-label>
<input formControlName="xref" matInput />
</mat-form-field>
<mat-form-field class="note-content"> <mat-form-field class="note-content">
<mat-label>Content</mat-label> <mat-label>Content</mat-label>
<textarea formControlName="content" matInput></textarea> <textarea formControlName="content" matInput></textarea>

View File

@ -1,7 +1,8 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms'; import { FormGroup, FormBuilder } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CardItem, NoteItem } from '../../../../models/app-state'; import { CardItem } from '../../../../models/app-state';
import { NoteItem } from '../../../../models/note-state';
import { AppService } from '../../../../services/app.service'; import { AppService } from '../../../../services/app.service';
import { UUID } from 'angular2-uuid'; import { UUID } from 'angular2-uuid';
@ -40,30 +41,12 @@ export class NoteEditModalComponent {
} }
save() { save() {
if (this.isNew) { this.appService.saveNote({
this.appService.createNote({ ...this.data,
qry: '', title: this.noteForm.get('title').value,
dict: 'n/a', xref: this.noteForm.get('xref').value,
type: 'Note', content: this.noteForm.get('content').value,
data: { });
...this.data,
title: this.noteForm.get('title').value,
content: this.noteForm.get('content').value,
},
});
} else {
this.appService.editNote(
{
...this.cardItem,
data: {
...this.cardItem.data,
title: this.noteForm.get('title').value,
content: this.noteForm.get('content').value,
},
},
this.cardItem
);
}
this.dialogRef.close(); this.dialogRef.close();
} }

View File

@ -16,6 +16,20 @@
<ngx-md class="markdown" *ngIf="cardItem.data">{{ <ngx-md class="markdown" *ngIf="cardItem.data">{{
cardItem.data.content cardItem.data.content
}}</ngx-md> }}</ngx-md>
<mat-expansion-panel *ngIf="cardItem && cardItem.data.xref !== ''">
<mat-expansion-panel-header>
<mat-panel-title>
Cross References
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container *ngIf="prepXref(cardItem.data.xref) as refs">
<div *ngFor="let ref of refs">
<button mat-raised-button class="reference" (click)="openPassage(ref)">
{{ ref.toString() }}
</button>
</div>
</ng-container>
</mat-expansion-panel>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<span class="card-actions-left"> <span class="card-actions-left">
@ -50,6 +64,14 @@
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
<span>Add Card to Saved Page</span> <span>Add Card to Saved Page</span>
</button> </button>
<button mat-menu-item (click)="moveCardUp()">
<mat-icon>arrow_upward</mat-icon>
<span>Move Card Up</span>
</button>
<button mat-menu-item (click)="moveCardDown()">
<mat-icon>arrow_downward</mat-icon>
<span>Move Card Down</span>
</button>
</mat-menu> </mat-menu>
</span> </span>
</div> </div>

View File

@ -9,3 +9,8 @@
.card-actions { .card-actions {
color: var(--note-color-primary); color: var(--note-color-primary);
} }
.reference {
width: 100%;
margin: 3px;
}

View File

@ -1,8 +1,10 @@
import { Component, ViewChild, ElementRef } from '@angular/core'; import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { NoteEditModalComponent } from './edit-modal/note-edit-modal.component'; import { NoteEditModalComponent } from './edit-modal/note-edit-modal.component';
import { CardComponent } from '../../../common/components/card.component'; import { CardComponent } from '../../../common/components/card.component';
import { AppService } from '../../../services/app.service'; import { AppService } from '../../../services/app.service';
import { NoteItem } from '../../../models/note-state';
import { BibleReference } from 'src/app/common/bible-reference';
@Component({ @Component({
selector: 'app-note-card', selector: 'app-note-card',
@ -12,12 +14,8 @@ import { AppService } from '../../../services/app.service';
export class NoteCardComponent extends CardComponent { export class NoteCardComponent extends CardComponent {
@ViewChild('note') noteElement: ElementRef; @ViewChild('note') noteElement: ElementRef;
constructor( constructor(protected elementRef: ElementRef, protected appService: AppService, public dialog: MatDialog) {
protected elementRef: ElementRef, super(elementRef, dialog, appService);
private appService: AppService,
public dialog: MatDialog
) {
super(elementRef, dialog);
this.icon$ = appService.select((state) => state.cardIcons.note); this.icon$ = appService.select((state) => state.cardIcons.note);
} }
@ -28,6 +26,14 @@ export class NoteCardComponent extends CardComponent {
this.copyToClip(text, html); this.copyToClip(text, html);
} }
prepXref(xref: string) {
return xref.split(';').map((o) => new BibleReference(o));
}
openPassage(ref: BibleReference) {
this.appService.getPassage(ref, this.cardItem);
}
edit() { edit() {
this.dialog.open(NoteEditModalComponent, { this.dialog.open(NoteEditModalComponent, {
data: this.cardItem, data: this.cardItem,
@ -35,6 +41,6 @@ export class NoteCardComponent extends CardComponent {
} }
delete() { delete() {
this.appService.deleteNote(this.cardItem); this.appService.deleteNote(this.cardItem.data as NoteItem);
} }
} }

View File

@ -24,7 +24,12 @@
{{ para.p.h }} {{ para.p.h }}
</h3> </h3>
<p [ngClass]="{ 'as-inline': !(showParagraphs$ | async) }"> <p
[ngClass]="{
'as-inline':
(showParagraphs$ | async) === (false | null | undefined)
}"
>
<ng-container *ngFor="let vs of para.vss"> <ng-container *ngFor="let vs of para.vss">
<strong class="verse-number" *ngIf="showVerseNumbers$ | async" <strong class="verse-number" *ngIf="showVerseNumbers$ | async"
>{{ vs.v }}.</strong >{{ vs.v }}.</strong
@ -45,13 +50,17 @@
</ng-container> </ng-container>
</div> </div>
</ng-container> </ng-container>
<mat-expansion-panel *ngIf="hasNotes"> <mat-expansion-panel *ngIf="notes$ | async as notes">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>
Note References Note References
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<p>note link, note link, note link...</p> <div *ngFor="let note of notes">
<button mat-raised-button class="reference" (click)="openNote(note)">
{{ note.title }}
</button>
</div>
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
<div class="card-actions"> <div class="card-actions">
@ -98,6 +107,14 @@
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
<span>Add Card to Saved Page</span> <span>Add Card to Saved Page</span>
</button> </button>
<button mat-menu-item (click)="moveCardUp()">
<mat-icon>arrow_upward</mat-icon>
<span>Move Card Up</span>
</button>
<button mat-menu-item (click)="moveCardDown()">
<mat-icon>arrow_downward</mat-icon>
<span>Move Card Down</span>
</button>
</mat-menu> </mat-menu>
</span> </span>
</div> </div>

View File

@ -18,3 +18,8 @@
font-family: var(--card-heading-font-family); font-family: var(--card-heading-font-family);
font-weight: 600; font-weight: 600;
} }
.reference {
width: 100%;
margin: 3px;
}

View File

@ -1,10 +1,11 @@
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core'; import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { BibleReference } from '../../../common/bible-reference'; import { BibleReference, Overlap } from '../../../common/bible-reference';
import { AppService } from '../../../services/app.service'; import { AppService } from '../../../services/app.service';
import { CardComponent } from '../../../common/components/card.component'; import { CardComponent } from '../../../common/components/card.component';
import { Paragraph } from '../../../models/app-state'; import { Paragraph } from '../../../models/passage-state';
import { StrongsModalComponent } from '../strongs/modal/strongs-modal.component'; import { StrongsModalComponent } from '../strongs/modal/strongs-modal.component';
import { NoteItem } from 'src/app/models/note-state';
@Component({ @Component({
selector: 'app-passage-card', selector: 'app-passage-card',
@ -24,12 +25,19 @@ export class PassageCardComponent extends CardComponent implements OnInit {
displaySettings$ = this.appService.select((state) => state.displaySettings.value); displaySettings$ = this.appService.select((state) => state.displaySettings.value);
// whenever the notes changes, look for any notes that reference this passage.
notes$ = this.appService.select((state) =>
state.notes.value.filter((o) => {
const refs = o.xref.split(';').map((r) => new BibleReference(r));
return refs.filter((r) => BibleReference.overlap(this.ref, r) !== Overlap.None).length > 0;
})
);
hasNotes = false; hasNotes = false;
@ViewChild('passage') passageElement: ElementRef; @ViewChild('passage') passageElement: ElementRef;
constructor(protected elementRef: ElementRef, private appService: AppService, public dialog: MatDialog) { constructor(protected elementRef: ElementRef, protected appService: AppService, public dialog: MatDialog) {
super(elementRef, dialog); super(elementRef, dialog, appService);
this.icon$ = appService.select((state) => state.cardIcons.passage); this.icon$ = appService.select((state) => state.cardIcons.passage);
} }
@ -115,6 +123,10 @@ export class PassageCardComponent extends CardComponent implements OnInit {
this.appService.updatePassage(this.cardItem, this.ref); this.appService.updatePassage(this.cardItem, this.ref);
} }
openNote(note: NoteItem) {
this.appService.getNote(note.id, this.cardItem);
}
async openStrongs(q: string, asModal = false) { async openStrongs(q: string, asModal = false) {
const dict = this.cardItem.dict === 'H' ? 'heb' : 'grk'; const dict = this.cardItem.dict === 'H' ? 'heb' : 'grk';
const numbers = q.split(' '); const numbers = q.split(' ');

View File

@ -1,5 +1,5 @@
<mat-toolbar> <mat-toolbar>
<button type="button" mat-icon-button (click)="navService.toggle()"> <button type="button" mat-icon-button (click)="navService.toggleNav()">
<mat-icon md-48 aria-hidden="false" aria-label="Menu Toggle">menu</mat-icon> <mat-icon md-48 aria-hidden="false" aria-label="Menu Toggle">menu</mat-icon>
</button> </button>
<div class="search-bar"> <div class="search-bar">

View File

@ -119,12 +119,7 @@ export class SearchPage extends SubscriberComponent implements OnInit {
const q = term.trim(); const q = term.trim();
if (q !== '') { if (q !== '') {
if (q.startsWith('note:')) { if (q.startsWith('note:')) {
// // It's a note lookup await this.appService.findNotes(q.replace('note:', ''));
// list.push({
// qry: q.replace('note:', ''),
// dict: '',
// type: 'Note',
// });
} else if (q.search(/[0-9]/i) === -1) { } else if (q.search(/[0-9]/i) === -1) {
// // its a search term. // // its a search term.
await this.appService.getWords(q); await this.appService.getWords(q);

View File

@ -46,6 +46,14 @@
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
<span>Add Card to Saved Page</span> <span>Add Card to Saved Page</span>
</button> </button>
<button mat-menu-item (click)="moveCardUp()">
<mat-icon>arrow_upward</mat-icon>
<span>Move Card Up</span>
</button>
<button mat-menu-item (click)="moveCardDown()">
<mat-icon>arrow_downward</mat-icon>
<span>Move Card Down</span>
</button>
</mat-menu> </mat-menu>
</span> </span>
</div> </div>

View File

@ -16,7 +16,7 @@ export class StrongsCardComponent extends CardComponent {
@ViewChild('strongs') strongsElement: ElementRef; @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); super(elementRef, dialog, appService);
this.icon$ = appService.select((state) => state.cardIcons.strongs); this.icon$ = appService.select((state) => state.cardIcons.strongs);
this.addSubscription( this.addSubscription(
this.appService.state$.subscribe((state) => { this.appService.state$.subscribe((state) => {

View File

@ -1,5 +1,5 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import { StrongsResult } from 'src/app/models/app-state'; import { StrongsResult } from '../../../models/strongs-state';
import { BibleReference } from 'src/app/common/bible-reference'; import { BibleReference } from 'src/app/common/bible-reference';
@Component({ @Component({

View File

@ -42,11 +42,18 @@
<mat-icon>content_copy</mat-icon> <mat-icon>content_copy</mat-icon>
<span>Copy References</span> <span>Copy References</span>
</button> </button>
<button mat-menu-item (click)="addToSavedPage()"> <button mat-menu-item (click)="addToSavedPage()">
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
<span>Add Card to Saved Page</span> <span>Add Card to Saved Page</span>
</button> </button>
<button mat-menu-item (click)="moveCardUp()">
<mat-icon>arrow_upward</mat-icon>
<span>Move Card Up</span>
</button>
<button mat-menu-item (click)="moveCardDown()">
<mat-icon>arrow_downward</mat-icon>
<span>Move Card Down</span>
</button>
</mat-menu> </mat-menu>
</span> </span>
</div> </div>

View File

@ -1,7 +1,7 @@
import { Component, ElementRef, ViewChild } from '@angular/core'; import { Component, ElementRef, ViewChild } from '@angular/core';
import { AppService } from '../../../services/app.service'; import { AppService } from '../../../services/app.service';
import { CardComponent } from '../../../common/components/card.component'; import { CardComponent } from '../../../common/components/card.component';
import { WordLookupResult } from 'src/app/models/app-state'; import { WordLookupResult } from '../../../models/words-state';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { BibleReference } from 'src/app/common/bible-reference'; import { BibleReference } from 'src/app/common/bible-reference';
@ -14,12 +14,8 @@ import { BibleReference } from 'src/app/common/bible-reference';
export class WordsCardComponent extends CardComponent { export class WordsCardComponent extends CardComponent {
@ViewChild('words') wordsElement: ElementRef; @ViewChild('words') wordsElement: ElementRef;
constructor( constructor(protected elementRef: ElementRef, protected appService: AppService, public dialog: MatDialog) {
protected elementRef: ElementRef, super(elementRef, dialog, appService);
private appService: AppService,
public dialog: MatDialog
) {
super(elementRef, dialog);
this.icon$ = appService.select((state) => state.cardIcons.words); this.icon$ = appService.select((state) => state.cardIcons.words);
} }
@ -28,11 +24,7 @@ export class WordsCardComponent extends CardComponent {
BibleReference.makePassageFromReferenceKey(ref) BibleReference.makePassageFromReferenceKey(ref)
); );
const html = refs const html = refs.map((ref) => `<a href='http://dynamicbible.com/search/${ref}'>${ref}</a>`).join(', ');
.map(
(ref) => `<a href='http://dynamicbible.com/search/${ref}'>${ref}</a>`
)
.join(', ');
const text = refs.join(', '); const text = refs.join(', ');
this.copyToClip(text, html); this.copyToClip(text, html);
} }

View File

@ -1,28 +1,6 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import { AppState, SavedPage, Error, CardItem, DisplaySettings, User } from '../models/app-state';
AppState,
SavedPage,
HashTable,
Paragraph,
BiblePassage,
BibleVerse,
Error,
BibleParagraph,
BibleParagraphPassage,
CardItem,
DictionaryType,
StrongsDefinition,
StrongsCrossReference,
RMACCrossReference,
RMACDefinition,
WordLookupResult,
WordToStem,
IndexResult,
NoteItem,
DisplaySettings,
User,
} from '../models/app-state';
import { Section, BibleReference } from '../common/bible-reference'; import { Section, BibleReference } from '../common/bible-reference';
import { PageTitles, PageIcons } from '../constants'; import { PageTitles, PageIcons } from '../constants';
import { createStateService } from '../common/state-service'; import { createStateService } from '../common/state-service';
@ -30,6 +8,18 @@ import { UUID } from 'angular2-uuid';
import { StorageMap } from '@ngx-pwa/local-storage'; import { StorageMap } from '@ngx-pwa/local-storage';
import { AngularFireDatabase } from '@angular/fire/database'; import { AngularFireDatabase } from '@angular/fire/database';
import { IStorable, Storable } from '../models/storable'; import { IStorable, Storable } from '../models/storable';
import { NoteItem } from '../models/note-state';
import { Paragraph, BiblePassage, BibleVerse, BibleParagraphPassage, BibleParagraph } from '../models/passage-state';
import {
StrongsDefinition,
StrongsCrossReference,
RMACCrossReference,
RMACDefinition,
StrongsDictionary,
} from '../models/strongs-state';
import { WordToStem, IndexResult, WordLookupResult } from '../models/words-state';
import { HashTable } from '../common/hashtable';
import { ListDirection } from '../common/list-direction';
const initialState: AppState = { const initialState: AppState = {
user: null, user: null,
@ -40,7 +30,7 @@ const initialState: AppState = {
type: 'Note', type: 'Note',
data: { data: {
id: UUID.UUID(), id: UUID.UUID(),
xref: null, xref: '1 pe 2:16; jn 3:16',
title: 'Title Here', title: 'Title Here',
content: '# Content Here\nIn Markdown format.', content: '# Content Here\nIn Markdown format.',
}, },
@ -52,6 +42,10 @@ const initialState: AppState = {
createdOn: new Date(0).toISOString(), createdOn: new Date(0).toISOString(),
value: [], value: [],
}, },
notes: {
createdOn: new Date(0).toISOString(),
value: [],
},
savedPagesLoaded: false, savedPagesLoaded: false,
mainPages: [ mainPages: [
{ title: PageTitles.Search, icon: PageIcons.Search, route: 'search' }, { title: PageTitles.Search, icon: PageIcons.Search, route: 'search' },
@ -82,6 +76,8 @@ const initialState: AppState = {
}, },
}; };
//#region
type AppAction = type AppAction =
| { | {
type: 'GET_SAVED_PAGE'; type: 'GET_SAVED_PAGE';
@ -117,6 +113,11 @@ type AppAction =
type: 'REMOVE_CARD'; type: 'REMOVE_CARD';
card: CardItem; card: CardItem;
} }
| {
type: 'MOVE_CARD';
card: CardItem;
direction: ListDirection;
}
| { | {
type: 'UPDATE_ERROR'; type: 'UPDATE_ERROR';
error: Error; error: Error;
@ -140,8 +141,32 @@ type AppAction =
| { | {
type: 'SET_USER'; type: 'SET_USER';
user: User; user: User;
}
| {
type: 'FIND_NOTES';
qry: string;
nextToItem: CardItem;
}
| {
type: 'GET_NOTE';
noteId: string;
nextToItem: CardItem;
}
| {
type: 'UPDATE_NOTES';
notes: IStorable<readonly NoteItem[]>;
}
| {
type: 'SAVE_NOTE';
note: NoteItem;
}
| {
type: 'DELETE_NOTE';
note: NoteItem;
}; };
//#endregion Actions
function maybeMutateStorable<T>( function maybeMutateStorable<T>(
state: AppState, state: AppState,
candidate: IStorable<T>, candidate: IStorable<T>,
@ -216,7 +241,7 @@ function reducer(state: AppState, action: AppAction): AppState {
} }
case 'UPDATE_CURRENT_PAGE': { case 'UPDATE_CURRENT_PAGE': {
const savedPages = new Storable<SavedPage[]>([ const savedPages = new Storable<SavedPage[]>([
...state.savedPages.value.filter((o) => o.id === state.currentSavedPage.id), ...state.savedPages.value.filter((o) => o.id !== state.currentSavedPage.id),
{ {
id: state.currentSavedPage.id, id: state.currentSavedPage.id,
title: state.currentSavedPage.title, title: state.currentSavedPage.title,
@ -326,12 +351,184 @@ function reducer(state: AppState, action: AppAction): AppState {
cards: [...state.cards.filter((c) => c !== action.card)], cards: [...state.cards.filter((c) => c !== action.card)],
}; };
} }
case 'MOVE_CARD': {
let cards = [];
const idx = state.cards.indexOf(action.card);
if (
(idx === 0 && action.direction === ListDirection.Up) || // can't go up if you're at the top
(idx === state.cards.length - 1 && action.direction === ListDirection.Down) // can't go down if you're at the bottom
) {
// you can't go up.
return state;
}
const before = state.cards.slice(0, idx);
const after = state.cards.slice(idx + 1);
if (action.direction === ListDirection.Down) {
cards = [...before, after[0], action.card, ...after.slice(1)];
} else {
cards = [...before.slice(0, before.length - 1), action.card, before[before.length - 1], ...after];
}
return {
...state,
cards,
};
}
case 'SET_USER': { case 'SET_USER': {
return { return {
...state, ...state,
user: action.user, user: action.user,
}; };
} }
case 'FIND_NOTES': {
const notes = state.notes.value
.filter((o) => o.title.search(action.qry) > -1)
.map((o) => {
return {
qry: o.id,
dict: 'n/a',
type: 'Note',
data: o,
};
});
let cards = [];
if (action.nextToItem && state.displaySettings.value.insertCardNextToItem) {
const idx = state.cards.indexOf(action.nextToItem);
if (state.displaySettings.value.appendCardToBottom) {
const before = state.cards.slice(0, idx + 1);
const after = state.cards.slice(idx + 1);
cards = [...before, ...notes, ...after];
} else {
const before = state.cards.slice(0, idx);
const after = state.cards.slice(idx);
cards = [...before, ...notes, ...after];
}
} else {
if (state.displaySettings.value.appendCardToBottom) {
cards = [...state.cards, ...notes];
} else {
cards = [...notes, ...state.cards];
}
}
return {
...state,
cards,
};
}
case 'GET_NOTE': {
const note = state.notes.value.find((o) => o.id === action.noteId);
const card = {
qry: note.id,
dict: 'n/a',
type: 'Note',
data: note,
};
return reducer(state, {
type: 'ADD_CARD',
card,
nextToItem: action.nextToItem,
});
}
case 'UPDATE_NOTES': {
return {
...state,
notes: action.notes,
};
}
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 cards = [
...state.cards.map((o) => {
const n = o.data as NoteItem;
if (n && n.id === action.note.id) {
return {
...o,
data: action.note,
};
}
return o;
}),
];
const notes = new Storable<NoteItem[]>([
...state.notes.value.filter((o) => o.id !== action.note.id),
action.note,
]);
const savedPages = new Storable<SavedPage[]>([
...state.savedPages.value.map((sp) => {
return {
...sp,
queries: sp.queries.map((o) => {
const n = o.data as NoteItem;
if (n && n.id === action.note.id) {
return {
...o,
data: action.note,
};
}
return o;
}),
};
}),
]);
const newState = {
...state,
cards,
notes,
savedPages,
};
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 cards = [
...state.cards.filter((o) => {
const n = o.data as NoteItem;
return !n || n.id !== action.note.id;
}),
];
const notes = new Storable<NoteItem[]>([...state.notes.value.filter((o) => o.id !== action.note.id)]);
const savedPages = new Storable<SavedPage[]>([
...state.savedPages.value.map((sp) => {
return {
...sp,
queries: sp.queries.filter((o) => {
const n = o.data as NoteItem;
return !n || n.id !== action.note.id;
}),
};
}),
]);
return {
...state,
cards,
notes,
savedPages,
};
}
} }
} }
@ -352,13 +549,21 @@ export class AppService extends createStateService(reducer, initialState) {
this.searchIndexArray = this.buildIndexArray().sort(); this.searchIndexArray = this.buildIndexArray().sort();
} }
//#region General
removeCard(card: CardItem) { removeCard(card: CardItem) {
this.dispatch({ this.dispatch({
type: 'REMOVE_CARD', type: 'REMOVE_CARD',
card, card,
}); });
} }
moveCard(card: CardItem, direction: ListDirection) {
this.dispatch({
type: 'MOVE_CARD',
card,
direction,
});
}
dispatchError(msg: string) { dispatchError(msg: string) {
this.dispatch({ this.dispatch({
type: 'UPDATE_ERROR', type: 'UPDATE_ERROR',
@ -376,6 +581,8 @@ export class AppService extends createStateService(reducer, initialState) {
}); });
} }
//#endregion
//#region Saved Pages //#region Saved Pages
getSavedPage(pageid: string) { getSavedPage(pageid: string) {
@ -392,7 +599,7 @@ export class AppService extends createStateService(reducer, initialState) {
}); });
} }
updateSavedPage() { updateCurrentSavedPage() {
this.dispatch({ this.dispatch({
type: 'UPDATE_CURRENT_PAGE', type: 'UPDATE_CURRENT_PAGE',
}); });
@ -442,101 +649,48 @@ export class AppService extends createStateService(reducer, initialState) {
//#region Notes //#region Notes
async getNote(qry: string, nextToItem: CardItem = null) { findNotes(qry: string, nextToItem: CardItem = null) {
const note = (await this.localStorageService.get('notes/' + qry).toPromise()) as NoteItem;
const card = {
qry,
dict: 'n/a',
type: 'Note',
data: note,
};
this.dispatch({ this.dispatch({
type: 'ADD_CARD', type: 'FIND_NOTES',
card, qry,
nextToItem, nextToItem,
}); });
} }
async createNote(card: CardItem, nextToItem: CardItem = null) { async getNote(noteId: string, nextToItem: CardItem = null) {
this.saveNoteApi(card.data as NoteItem).subscribe( this.dispatch({
// success type: 'GET_NOTE',
() => { noteId,
this.dispatch({ nextToItem,
type: 'ADD_CARD', });
card,
nextToItem,
});
},
// error
() => {
this.dispatch({
type: 'UPDATE_ERROR',
error: {
// tslint:disable-next-line: quotemark
msg: "Something went wrong and the note wasn't saved. :(",
},
});
}
);
} }
async editNote(newCard: CardItem, oldCard: CardItem) { updateNotes(notes: IStorable<readonly NoteItem[]>) {
this.saveNoteApi(newCard.data as NoteItem).subscribe( this.dispatch({
// success type: 'UPDATE_NOTES',
() => { notes,
this.dispatch({ });
type: 'UPDATE_CARD',
newCard,
oldCard,
});
},
// error
() => {
this.dispatch({
type: 'UPDATE_ERROR',
error: {
// tslint:disable-next-line: quotemark
msg: "Something went wrong and the note wasn't saved. :(",
},
});
}
);
} }
async deleteNote(noteCard: CardItem) { saveNote(note: NoteItem, nextToItem: CardItem = null) {
this.deleteNoteApi(noteCard.data as NoteItem).subscribe( this.dispatch({
// success type: 'SAVE_NOTE',
() => { note,
this.removeCard(noteCard); });
},
// error
() => {
this.dispatch({
type: 'UPDATE_ERROR',
error: {
// tslint:disable-next-line: quotemark
msg: "Something went wrong and the note wasn't saved. :(",
},
});
}
);
} }
private deleteNoteApi(note: NoteItem) { deleteNote(note: NoteItem) {
return this.localStorageService.delete('notes/' + note.id); this.dispatch({
} type: 'DELETE_NOTE',
note,
private saveNoteApi(note: NoteItem) { });
return this.localStorageService.set('notes/' + note.id, note);
} }
//#endregion //#endregion
//#region Strongs //#region Strongs
async getStrongs(strongsNumber: string, dict: DictionaryType, nextToItem: CardItem = null) { async getStrongs(strongsNumber: string, dict: StrongsDictionary, nextToItem: CardItem = null) {
const card = await this.getStrongsCard(strongsNumber, dict); const card = await this.getStrongsCard(strongsNumber, dict);
this.dispatch({ this.dispatch({

View File

@ -4,7 +4,9 @@ import { AngularFireDatabase, AngularFireObject } from '@angular/fire/database';
import { IStorable } from '../models/storable'; import { IStorable } from '../models/storable';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { DisplaySettings, SavedPage, User } from '../models/app-state'; import { DisplaySettings, SavedPage, User } from '../models/app-state';
import { SubscriberComponent } from '../common/components/subscriber.component'; import { SubscriberComponent } from '../common/components/subscriber.component';
import { NoteItem } from '../models/note-state';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@ -18,6 +20,10 @@ export class StorageService extends SubscriberComponent {
private savedPagesPath = 'savedPaged'; private savedPagesPath = 'savedPaged';
private savedPagesRemoteObject: AngularFireObject<IStorable<readonly SavedPage[]>>; private savedPagesRemoteObject: AngularFireObject<IStorable<readonly SavedPage[]>>;
private noteItemsState$ = this.appService.select((state) => state.notes);
private noteItemsPath = 'noteItems';
private noteItemsRemoteObject: AngularFireObject<IStorable<readonly NoteItem[]>>;
constructor(private local: StorageMap, private remote: AngularFireDatabase, private appService: AppService) { constructor(private local: StorageMap, private remote: AngularFireDatabase, private appService: AppService) {
super(); super();
@ -32,7 +38,7 @@ export class StorageService extends SubscriberComponent {
} }
// update local // update local
this.local.set('displaySettings', settings).subscribe( this.local.set(this.displaySettingsPath, settings).subscribe(
() => { () => {
// nop // nop
}, },
@ -57,7 +63,7 @@ export class StorageService extends SubscriberComponent {
} }
// update local // update local
this.local.set('savedPages', savedPages).subscribe( this.local.set(this.savedPagesPath, savedPages).subscribe(
() => { () => {
// nop // nop
}, },
@ -75,6 +81,31 @@ export class StorageService extends SubscriberComponent {
}) })
); );
this.addSubscription(
this.noteItemsState$.subscribe((notes) => {
if (!notes) {
return;
}
// update local
this.local.set(this.noteItemsPath, notes).subscribe(
() => {
// nop
},
// error
() => {
// tslint:disable-next-line: quotemark
this.appService.dispatchError("Something went wrong and the note wasn't saved. :(");
}
);
// update remote
if (this.noteItemsRemoteObject) {
this.noteItemsRemoteObject.set(notes);
}
})
);
//#endregion //#endregion
} }
@ -83,6 +114,7 @@ export class StorageService extends SubscriberComponent {
`/${this.displaySettingsPath}/${user.uid}` `/${this.displaySettingsPath}/${user.uid}`
); );
this.savedPagesRemoteObject = this.remote.object<IStorable<SavedPage[]>>(`/${this.savedPagesPath}/${user.uid}`); this.savedPagesRemoteObject = this.remote.object<IStorable<SavedPage[]>>(`/${this.savedPagesPath}/${user.uid}`);
this.noteItemsRemoteObject = this.remote.object<IStorable<NoteItem[]>>(`/${this.noteItemsPath}/${user.uid}`);
// display settings // display settings
this.addSubscription( this.addSubscription(
@ -107,6 +139,18 @@ export class StorageService extends SubscriberComponent {
} }
}) })
); );
// note items
this.addSubscription(
this.noteItemsRemoteObject
.valueChanges() // when the saved pages have changed
.subscribe((remoteNoteItems) => {
if (remoteNoteItems) {
// update the saved pages locally from remote if it isn't null
this.appService.updateNotes(remoteNoteItems);
}
})
);
} }
async initDisplaySettings() { async initDisplaySettings() {
@ -128,4 +172,14 @@ export class StorageService extends SubscriberComponent {
this.appService.updateSavedPages(savedPages); this.appService.updateSavedPages(savedPages);
} }
} }
async initNotes() {
const exists = await this.local.has(this.noteItemsPath).toPromise();
if (exists) {
const notes = (await this.local.get(this.noteItemsPath).toPromise()) as IStorable<NoteItem[]>;
this.appService.updateNotes(notes);
}
}
} }

View File

@ -137,3 +137,8 @@ a {
font-size: var(--card-font-size) !important; font-size: var(--card-font-size) !important;
line-height: calc(var(--card-font-size) * 1.1) !important; line-height: calc(var(--card-font-size) * 1.1) !important;
} }
.mat-expansion-panel:not([class*="mat-elevation-z"]) {
box-shadow: none !important;
border: 1px solid #eee;
}