initial start at porting ionic to angular material. this just deals with the website. i hate ionics new web components. despise isn't to strong a word.

this also introduces a redux pattern into the state management, which should significantly improve the complexity of the prev code.
This commit is contained in:
Jason Wall 2020-07-19 18:23:03 -04:00
parent 87cc470bfa
commit b283898f8c
1776 changed files with 53895 additions and 0 deletions

18
app/db/.browserslistrc Normal file
View File

@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

16
app/db/.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
app/db/.gitignore vendored Normal file
View File

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

27
app/db/README.md Normal file
View File

@ -0,0 +1,27 @@
# Db
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.0.3.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

128
app/db/angular.json Normal file
View File

@ -0,0 +1,128 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"db": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/db",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "db:build"
},
"configurations": {
"production": {
"browserTarget": "db:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "db:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "db:serve"
},
"configurations": {
"production": {
"devServerTarget": "db:serve:production"
}
}
}
}
}},
"defaultProject": "db"
}

View File

@ -0,0 +1,36 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

View File

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('db app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

11
app/db/e2e/src/app.po.ts Normal file
View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}

14
app/db/e2e/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

32
app/db/karma.conf.js Normal file
View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/db'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

15243
app/db/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
app/db/package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "db",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~10.0.4",
"@angular/cdk": "^10.0.2",
"@angular/common": "~10.0.4",
"@angular/compiler": "~10.0.4",
"@angular/core": "~10.0.4",
"@angular/forms": "~10.0.4",
"@angular/material": "^10.0.2",
"@angular/platform-browser": "~10.0.4",
"@angular/platform-browser-dynamic": "~10.0.4",
"@angular/router": "~10.0.4",
"@types/mathjs": "^6.0.5",
"component": "^1.1.0",
"mathjs": "^7.0.2",
"redux": "^4.0.5",
"reselect": "^4.0.0",
"rxjs": "~6.5.5",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1000.3",
"@angular/cli": "~10.0.3",
"@angular/compiler-cli": "~10.0.4",
"@types/node": "^12.11.1",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~3.3.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~3.9.5"
}
}

View File

@ -0,0 +1,25 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SearchPage } from './search/components/search-page/search.page';
const routes: Routes = [
{
path: '',
redirectTo: 'search/',
pathMatch: 'full',
},
{
path: 'search/',
component: SearchPage,
},
{
path: 'search/:term',
component: SearchPage,
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@ -0,0 +1,25 @@
<body>
<mat-sidenav-container class="sidenav-container">
<mat-sidenav
#drawer
class="sidenav"
fixedInViewport
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
[mode]="'over'"
[opened]="false"
>
<mat-toolbar>Pages</mat-toolbar>
<mat-nav-list>
<a
*ngFor="let p of mainPages$ | async"
mat-list-item
[routerLink]="['/dashboard']"
><mat-icon color="accent">{{ p.icon }}</mat-icon> {{ p.title }}</a
>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<router-outlet></router-outlet>
</mat-sidenav-content>
</mat-sidenav-container>
</body>

View File

@ -0,0 +1,9 @@
.sidenav {
max-width: 30rem;
min-width: 20rem;
mat-icon {
padding-right: 1rem;
color: var(--primary-color);
}
}

View File

@ -0,0 +1,35 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'db'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('db');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('db app is running!');
});
});

View File

@ -0,0 +1,38 @@
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { AppService } from './services/app.service';
import { NavService } from './services/nav.service';
import { Observable } from 'rxjs';
import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { map, shareReplay } from 'rxjs/operators';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements AfterViewInit {
public savedPages$ = this.appService.select((state) => state.savedPages);
public mainPages$ = this.appService.select((state) => state.mainPages);
public isHandset$: Observable<boolean> = this.breakpointObserver
.observe(Breakpoints.Handset)
.pipe(
map((result) => result.matches),
shareReplay()
);
@ViewChild(MatSidenav) public sidenav: MatSidenav;
constructor(
private appService: AppService,
private navService: NavService,
private breakpointObserver: BreakpointObserver
) {
this.appService.getSavedPages();
}
ngAfterViewInit(): void {
this.navService.setSidenav(this.sidenav);
}
}

View File

@ -0,0 +1,100 @@
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, HTTP_INTERCEPTORS } from '@angular/common/http';
import { SearchPage } from './search/components/search-page/search.page';
import { PassageComponent } from './search/components/passage/passage';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSliderModule } from '@angular/material/slider';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatListModule } from '@angular/material/list';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatCardModule } from '@angular/material/card';
import { MatStepperModule } from '@angular/material/stepper';
import { MatTabsModule } from '@angular/material/tabs';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatBadgeModule } from '@angular/material/badge';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatDividerModule } from '@angular/material/divider';
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
import { MatTreeModule } from '@angular/material/tree';
@NgModule({
declarations: [AppComponent, SearchPage, PassageComponent],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
ReactiveFormsModule,
FormsModule,
AppRoutingModule,
BrowserAnimationsModule,
MatSidenavModule,
MatToolbarModule,
MatIconModule,
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatStepperModule,
MatDatepickerModule,
MatDialogModule,
MatDividerModule,
MatExpansionModule,
MatGridListModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatTooltipModule,
MatTreeModule,
MatFormFieldModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@ -0,0 +1,295 @@
import { BibleReference } from './bible-reference';
describe('Reference', () => {
it('Should parse the reference: Gen 1:1', () => {
const ref = new BibleReference('Gen 1:1').toString();
expect(ref).toBe('Genesis 1:1');
});
it('Should parse the reference: Gen 1:1-11', () => {
const ref = new BibleReference('Gen 1:1-11').toString();
expect(ref).toBe('Genesis 1:1 - 11');
});
it('Should parse the reference: Gen 1-2', () => {
const ref = new BibleReference('Gen 1-2').toString();
expect(ref).toBe('Genesis 1:1 - 2:*');
});
it('Should parse the reference: John 3:16', () => {
const ref = new BibleReference('John 3:16').toString();
expect(ref).toBe('John 3:16');
});
it('Should parse the reference: John 3-6', () => {
const ref = new BibleReference('Jn 3-6').toString();
expect(ref).toBe('John 3:1 - 6:*');
});
it('Should parse the reference: 1 Corinthians 3:5-6:5', () => {
const ref = new BibleReference('1 Corinthians 3:5-6:5').toString();
expect(ref).toBe('1 Corinthians 3:5 - 6:5');
});
it('Should parse the reference: 2 Samuel 5:5-6:5', () => {
expect(new BibleReference('II sam 5:5-6:5').toString()).toBe('2 Samuel 5:5 - 6:5');
});
const booknames = [
{ abbr: 'gen', actual: 'Genesis' },
{ abbr: 'ge', actual: 'Genesis' },
{ abbr: 'genesis', actual: 'Genesis' },
{ abbr: 'gn', actual: 'Genesis' },
{ abbr: 'exodus', actual: 'Exodus' },
{ abbr: 'ex', actual: 'Exodus' },
{ abbr: 'exo', actual: 'Exodus' },
{ abbr: 'exod', actual: 'Exodus' },
{ abbr: 'exd', actual: 'Exodus' },
{ abbr: 'leviticus', actual: 'Leviticus' },
{ abbr: 'lev', actual: 'Leviticus' },
{ abbr: 'le', actual: 'Leviticus' },
{ abbr: 'levi', actual: 'Leviticus' },
{ abbr: 'lv', actual: 'Leviticus' },
{ abbr: 'numbers', actual: 'Numbers' },
{ abbr: 'num', actual: 'Numbers' },
{ abbr: 'nu', actual: 'Numbers' },
{ abbr: 'numb', actual: 'Numbers' },
{ abbr: 'number', actual: 'Numbers' },
{ abbr: 'deuteronomy', actual: 'Deuteronomy' },
{ abbr: 'deut', actual: 'Deuteronomy' },
{ abbr: 'de', actual: 'Deuteronomy' },
{ abbr: 'dt', actual: 'Deuteronomy' },
{ abbr: 'deu', actual: 'Deuteronomy' },
{ abbr: 'joshua', actual: 'Joshua' },
{ abbr: 'josh', actual: 'Joshua' },
{ abbr: 'jos', actual: 'Joshua' },
{ abbr: 'judges', actual: 'Judges' },
{ abbr: 'jud', actual: 'Judges' },
{ abbr: 'jdg', actual: 'Judges' },
{ abbr: 'judg', actual: 'Judges' },
{ abbr: 'ruth', actual: 'Ruth' },
{ abbr: 'ru', actual: 'Ruth' },
{ abbr: '1 samuel', actual: '1 Samuel' },
{ abbr: '1 sa', actual: '1 Samuel' },
{ abbr: '1 sam', actual: '1 Samuel' },
{ abbr: '1 sml', actual: '1 Samuel' },
{ abbr: 'i samuel', actual: '1 Samuel' },
{ abbr: 'i sa', actual: '1 Samuel' },
{ abbr: 'i sam', actual: '1 Samuel' },
{ abbr: 'i sml', actual: '1 Samuel' },
{ abbr: '1st samuel', actual: '1 Samuel' },
{ abbr: '1st sa', actual: '1 Samuel' },
{ abbr: '1st sam', actual: '1 Samuel' },
{ abbr: '1st sml', actual: '1 Samuel' },
{ abbr: 'first samuel', actual: '1 Samuel' },
{ abbr: 'first sa', actual: '1 Samuel' },
{ abbr: 'first sam', actual: '1 Samuel' },
{ abbr: 'first sml', actual: '1 Samuel' },
{ abbr: '2 samuel', actual: '2 Samuel' },
{ abbr: '2 sa', actual: '2 Samuel' },
{ abbr: '2 sam', actual: '2 Samuel' },
{ abbr: '2 sml', actual: '2 Samuel' },
{ abbr: 'ii samuel', actual: '2 Samuel' },
{ abbr: 'ii sa', actual: '2 Samuel' },
{ abbr: 'ii sam', actual: '2 Samuel' },
{ abbr: 'ii sml', actual: '2 Samuel' },
{ abbr: '2nd samuel', actual: '2 Samuel' },
{ abbr: '2nd sa', actual: '2 Samuel' },
{ abbr: '2nd sam', actual: '2 Samuel' },
{ abbr: '2nd sml', actual: '2 Samuel' },
{ abbr: 'second samuel', actual: '2 Samuel' },
{ abbr: 'second sa', actual: '2 Samuel' },
{ abbr: 'second sam', actual: '2 Samuel' },
{ abbr: 'second sml', actual: '2 Samuel' },
{ abbr: 'sec samuel', actual: '2 Samuel' },
{ abbr: 'sec sa', actual: '2 Samuel' },
{ abbr: 'sec sam', actual: '2 Samuel' },
{ abbr: 'sec sml', actual: '2 Samuel' },
{ abbr: '1 kings', actual: '1 Kings' },
{ abbr: '1 king', actual: '1 Kings' },
{ abbr: '1 kgs', actual: '1 Kings' },
{ abbr: '1 kn', actual: '1 Kings' },
{ abbr: '1 k', actual: '1 Kings' },
{ abbr: '1 ki', actual: '1 Kings' },
{ abbr: 'i kings', actual: '1 Kings' },
{ abbr: 'i king', actual: '1 Kings' },
{ abbr: 'i kgs', actual: '1 Kings' },
{ abbr: 'i kn', actual: '1 Kings' },
{ abbr: 'i k', actual: '1 Kings' },
{ abbr: 'i ki', actual: '1 Kings' },
{ abbr: '1st kings', actual: '1 Kings' },
{ abbr: '1st king', actual: '1 Kings' },
{ abbr: '1st kgs', actual: '1 Kings' },
{ abbr: '1st kn', actual: '1 Kings' },
{ abbr: '1st k', actual: '1 Kings' },
{ abbr: '1st ki', actual: '1 Kings' },
{ abbr: 'first kings', actual: '1 Kings' },
{ abbr: 'first king', actual: '1 Kings' },
{ abbr: 'first kgs', actual: '1 Kings' },
{ abbr: 'first kn', actual: '1 Kings' },
{ abbr: 'first k', actual: '1 Kings' },
{ abbr: 'first ki', actual: '1 Kings' },
{ abbr: '2 Kings', actual: '2 Kings' },
{ abbr: '2 king', actual: '2 Kings' },
{ abbr: '2 kgs', actual: '2 Kings' },
{ abbr: '2 kn', actual: '2 Kings' },
{ abbr: '2 k', actual: '2 Kings' },
{ abbr: '2 ki', actual: '2 Kings' },
{ abbr: 'ii kings', actual: '2 Kings' },
{ abbr: 'ii king', actual: '2 Kings' },
{ abbr: 'ii kgs', actual: '2 Kings' },
{ abbr: 'ii kn', actual: '2 Kings' },
{ abbr: 'ii k', actual: '2 Kings' },
{ abbr: 'ii ki', actual: '2 Kings' },
{ abbr: '2nd kings', actual: '2 Kings' },
{ abbr: '2nd king', actual: '2 Kings' },
{ abbr: '2nd kgs', actual: '2 Kings' },
{ abbr: '2nd kn', actual: '2 Kings' },
{ abbr: '2nd k', actual: '2 Kings' },
{ abbr: '2nd ki', actual: '2 Kings' },
{ abbr: 'second kings', actual: '2 Kings' },
{ abbr: 'second king', actual: '2 Kings' },
{ abbr: 'second kgs', actual: '2 Kings' },
{ abbr: 'second kn', actual: '2 Kings' },
{ abbr: 'second k', actual: '2 Kings' },
{ abbr: 'second ki', actual: '2 Kings' },
{ abbr: 'sec kings', actual: '2 Kings' },
{ abbr: 'sec king', actual: '2 Kings' },
{ abbr: 'sec kgs', actual: '2 Kings' },
{ abbr: 'sec kn', actual: '2 Kings' },
{ abbr: 'sec k', actual: '2 Kings' },
{ abbr: 'sec ki', actual: '2 Kings' },
{ abbr: '2 Chronicles', actual: '2 Chronicles' },
{ abbr: '2 chronicles', actual: '2 Chronicles' },
{ abbr: '2 chron', actual: '2 Chronicles' },
{ abbr: '2 ch', actual: '2 Chronicles' },
{ abbr: '2 chr', actual: '2 Chronicles' },
{ abbr: 'ii Chronicles', actual: '2 Chronicles' },
{ abbr: 'ii chronicles', actual: '2 Chronicles' },
{ abbr: 'ii chron', actual: '2 Chronicles' },
{ abbr: 'ii ch', actual: '2 Chronicles' },
{ abbr: 'ii chr', actual: '2 Chronicles' },
{ abbr: '2nd Chronicles', actual: '2 Chronicles' },
{ abbr: '2nd chronicles', actual: '2 Chronicles' },
{ abbr: '2nd chron', actual: '2 Chronicles' },
{ abbr: '2nd ch', actual: '2 Chronicles' },
{ abbr: '2nd chr', actual: '2 Chronicles' },
{ abbr: 'second Chronicles', actual: '2 Chronicles' },
{ abbr: 'second chronicles', actual: '2 Chronicles' },
{ abbr: 'second chron', actual: '2 Chronicles' },
{ abbr: 'second ch', actual: '2 Chronicles' },
{ abbr: 'second chr', actual: '2 Chronicles' },
{ abbr: 'sec Chronicles', actual: '2 Chronicles' },
{ abbr: 'sec chronicles', actual: '2 Chronicles' },
{ abbr: 'sec chron', actual: '2 Chronicles' },
{ abbr: 'sec ch', actual: '2 Chronicles' },
{ abbr: 'sec chr', actual: '2 Chronicles' },
{ abbr: '1 Chronicles', actual: '1 Chronicles' },
{ abbr: '1 chronicles', actual: '1 Chronicles' },
{ abbr: '1 chron', actual: '1 Chronicles' },
{ abbr: '1 ch', actual: '1 Chronicles' },
{ abbr: '1 chr', actual: '1 Chronicles' },
{ abbr: 'i Chronicles', actual: '1 Chronicles' },
{ abbr: 'i chronicles', actual: '1 Chronicles' },
{ abbr: 'i chron', actual: '1 Chronicles' },
{ abbr: 'i ch', actual: '1 Chronicles' },
{ abbr: 'i chr', actual: '1 Chronicles' },
{ abbr: '1st Chronicles', actual: '1 Chronicles' },
{ abbr: '1st chronicles', actual: '1 Chronicles' },
{ abbr: '1st chron', actual: '1 Chronicles' },
{ abbr: '1st ch', actual: '1 Chronicles' },
{ abbr: '1st chr', actual: '1 Chronicles' },
{ abbr: 'first Chronicles', actual: '1 Chronicles' },
{ abbr: 'first chronicles', actual: '1 Chronicles' },
{ abbr: 'first chron', actual: '1 Chronicles' },
{ abbr: 'first ch', actual: '1 Chronicles' },
{ abbr: 'first chr', actual: '1 Chronicles' },
{ abbr: 'ezra', actual: 'Ezra' },
{ abbr: 'ezr', actual: 'Ezra' },
{ abbr: 'nehemiah', actual: 'Nehemiah' },
{ abbr: 'neh', actual: 'Nehemiah' },
{ abbr: 'ne', actual: 'Nehemiah' },
{ abbr: 'nehamiah', actual: 'Nehemiah' },
{ abbr: 'esther', actual: 'Esther' },
{ abbr: 'est', actual: 'Esther' },
{ abbr: 'es', actual: 'Esther' },
{ abbr: 'esth', actual: 'Esther' },
{ abbr: 'job', actual: 'Job' },
{ abbr: 'jo', actual: 'Job' },
{ abbr: 'jb', actual: 'Job' },
{ abbr: 'psalms', actual: 'Psalm' },
{ abbr: 'ps', actual: 'Psalm' },
{ abbr: 'psa', actual: 'Psalm' },
{ abbr: 'psalm', actual: 'Psalm' },
{ abbr: 'psm', actual: 'Psalm' },
{ abbr: 'proverbs', actual: 'Proverbs' },
{ abbr: 'prov', actual: 'Proverbs' },
{ abbr: 'pr', actual: 'Proverbs' },
{ abbr: 'pro', actual: 'Proverbs' },
{ abbr: 'proverb', actual: 'Proverbs' },
{ abbr: 'prv', actual: 'Proverbs' },
{ abbr: 'prvbs', actual: 'Proverbs' },
{ abbr: 'ecclesiastes', actual: 'Ecclesiastes' },
{ abbr: 'eccl', actual: 'Ecclesiastes' },
{ abbr: 'ecc', actual: 'Ecclesiastes' },
{ abbr: 'eccles', actual: 'Ecclesiastes' },
{ abbr: 'ec', actual: 'Ecclesiastes' },
{ abbr: 'ecl', actual: 'Ecclesiastes' },
{ abbr: 'ecclesiaste', actual: 'Ecclesiastes' },
{ abbr: 'song of solomon', actual: 'Song of Solomon' },
{ abbr: 'song of songs', actual: 'Song of Solomon' },
{ abbr: 'sos', actual: 'Song of Solomon' },
{ abbr: 'ss', actual: 'Song of Solomon' },
{ abbr: 'son', actual: 'Song of Solomon' },
{ abbr: 'so', actual: 'Song of Solomon' },
{ abbr: 'song', actual: 'Song of Solomon' },
{ abbr: 'songs', actual: 'Song of Solomon' },
{ abbr: 'isaiah', actual: 'Isaiah' },
{ abbr: 'is', actual: 'Isaiah' },
{ abbr: 'isah', actual: 'Isaiah' },
{ abbr: 'isai', actual: 'Isaiah' },
{ abbr: 'ia', actual: 'Isaiah' },
{ abbr: 'jerimiah', actual: 'Jeremiah' },
{ abbr: 'jeremiah', actual: 'Jeremiah' },
{ abbr: 'jer', actual: 'Jeremiah' },
{ abbr: 'je', actual: 'Jeremiah' },
{ abbr: 'jere', actual: 'Jeremiah' },
{ abbr: 'lamentations', actual: 'Lamentations' },
{ abbr: 'lam', actual: 'Lamentations' },
{ abbr: 'la', actual: 'Lamentations' },
{ abbr: 'lamentation', actual: 'Lamentations' },
];
for (const bk of booknames) {
it('Should parse the references: ' + bk.abbr, () => {
const book = BibleReference.parseBook(bk.abbr);
expect(book.name).toBe(bk.actual);
for (let i = 1; i <= book.last_chapter; i++) {
expect(new BibleReference(bk.abbr + ' ' + i).toString()).toBe(bk.actual + ' ' + i + ':1 - *');
}
for (let i = 1; i < book.last_chapter; i++) {
expect(new BibleReference(bk.abbr + ' ' + i + '-' + (i + 1)).toString()).toBe(
bk.actual + ' ' + i + ':1 - ' + (i + 1) + ':*'
);
expect(new BibleReference(bk.abbr + ' ' + i + ':3-' + (i + 1) + ':6').toString()).toBe(
bk.actual + ' ' + i + ':3 - ' + (i + 1) + ':6'
);
}
expect(new BibleReference(bk.abbr + ' 1:4-2:5').toString()).toBe(bk.actual + ' 1:4 - 2:5');
expect(new BibleReference(bk.abbr + ' 1:1 - 5').toString()).toBe(bk.actual + ' 1:1 - 5');
expect(new BibleReference(bk.abbr + ' 1:4 - 5').toString()).toBe(bk.actual + ' 1:4 - 5');
});
}
const refs = [
{ src: '2 sam 3:4-6:8', actual: '2 Samuel 3:4 - 6:8' },
// { "src": "gen 50 - ex 2", "actual": "Genesis 50:1 - Exodus 2:*" },
];
for (const ref of refs) {
it('Should parse the reference: ' + ref.src, () => {
expect(new BibleReference(ref.src).toString()).toBe(ref.actual);
});
}
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
import {
EventEmitter,
Output,
Input,
ElementRef,
Component,
} from '@angular/core';
import { CardItem, OpenData } from '../models/app-state';
@Component({
template: '',
})
export class CardComponent {
@Output()
onItemClicked = new EventEmitter<OpenData>();
@Output()
onClose = new EventEmitter<CardItem>();
@Input()
cardItem: CardItem;
constructor(protected elementRef: ElementRef) {}
close(ev) {
let translate = 'translate3d(200%, 0, 0)';
if (ev != null && ev.direction === 2) {
translate = 'translate3d(-200%, 0, 0)';
}
const d = 250;
this.elementRef.nativeElement.parentElement.animate(
{
transform: ['none', translate],
},
{
fill: 'forwards',
duration: d,
iterations: 1,
easing: 'ease-in-out',
}
);
setTimeout(() => {
this.onClose.emit(this.cardItem);
}, d);
}
}

View File

@ -0,0 +1,130 @@
import { PreloadedState, Store, createStore } from 'redux';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
/**
* A base class from which to extend a service to provide predictable state to components. You should not extend this
* class directly; instead, use the {@link createStateService} function to create a base class specific to your service.
* @see {@link createStateService}
*/
class StateService<TState, TAction extends { type: string }> {
/** An observable that provides the entire state managed by the service. */
public readonly state$: Observable<TState>;
private readonly store: Store<TState, TAction>;
private readonly internalState$: BehaviorSubject<TState>;
protected constructor(reducer: (state: TState, action: TAction) => TState, initialState: TState) {
this.store = createStore(
reducer,
initialState as PreloadedState<TState>, // this cast is required by Redux's typings, it should have no impact
undefined // in the future, we may want to add some middleware to the Redux stores. that goes here!
);
this.internalState$ = new BehaviorSubject<TState>(initialState as TState);
// BehaviorSubject.asObservable returns a new object, so hold onto it to avoid unnecessary allocations
this.state$ = this.internalState$.asObservable();
this.store.subscribe(() => this.internalState$.next(this.store.getState()));
}
/**
* Creates an observable that provides data derived from the state managed by the service.
* @param selector A selector that maps the state to the derived data.
*/
public select<TDerived>(selector: (state: TState) => TDerived): Observable<TDerived> {
return this.state$.pipe(map(selector), distinctUntilChanged());
}
/**
* Gets the current state managed by the service. **You should only use the current state to validate operations or
* help to construct actions to be dispatched.** Try not to expose any state retrieved using this method outside the
* derived service class.
*/
protected getState(): TState {
return this.store.getState();
}
/**
* Dispatches an action to the underlying Redux store.
* @param action The action to dispatch.
*/
protected dispatch(action: TAction) {
this.store.dispatch(action);
}
}
/**
* Creates a deeply-immutable type from the provided type. Objects' properties will be marked as `readonly`, array types
* will be replaced with {@link ReadonlyArray}, {@link Map} types will be replaced with {@link ReadonlyMap}, and
* {@link Set} types will be replaced with {@link ReadonlySet}.
*/
export type Immutable<T> = T extends undefined | null | boolean | string | number
? T
: T extends Array<infer U>
? ImmutableArray<U>
: T extends Map<infer K, infer V>
? ImmutableMap<K, V>
: T extends Set<infer M>
? ImmutableSet<M>
: { readonly [N in keyof T]: Immutable<T[N]> };
interface ImmutableArray<T> extends ReadonlyArray<Immutable<T>> {}
interface ImmutableMap<K, V> extends ReadonlyMap<Immutable<K>, Immutable<V>> {}
interface ImmutableSet<T> extends ReadonlySet<Immutable<T>> {}
// The below type definition is simpler, but it only works with TypeScript 3.7 and above. Swap this in when we upgrade!
// export type Immutable<T> = T extends undefined | null | boolean | string | number
// ? T
// : T extends Array<infer U>
// ? ReadonlyArray<Immutable<U>>
// : T extends Map<infer K, infer V>
// ? ReadonlyMap<Immutable<K>, Immutable<V>>
// : T extends Set<infer M>
// ? ReadonlySet<Immutable<M>>
// : { readonly [N in keyof T]: Immutable<T[N]> };
type IfEquals<X, Y, A = X, B = never> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? A : B;
type IfImmutable<TState extends {}, TImmutable = TState, TMutable = never> = IfEquals<
TState,
{ readonly [K in keyof TState]: Immutable<TState[K]> },
TImmutable,
TMutable
>;
// tslint:disable-next-line
type YourStateTypeNeedsToBeImmutable<TState> = {};
/**
* Creates a base class from which to extend a service to provide predictable state to components.
*
* **If you're looking at this function because calling it yields an error that looks like this:**
*
* ```text
* Type 'YourStateTypeNeedsToBeImmutable<State>' is not a constructor function type.
* ```
*
* **That error means that the type you specified as `TState` (most likely the first parameter and return type of your
* reducer) isn't fully immutable.** The Redux pattern requires that you treat your state as completely immutablethat
* means that every property in your state hierarchy must be marked as `readonly` and you must use immutable collection
* types. You can either do this by hand, or use the {@link Immutable} helper type also exported by this module to
* quickly make an entire type hierarchy immutable.
*
* @param reducer A function that takes the previous state and the dispatched action and returns the new state.
* @param initialState The initial state of the service.
*/
export function createStateService<TState, TAction extends { type: string }>(
reducer: (state: TState, action: TAction) => TState,
initialState: TState
) {
const stateServiceClass = class extends StateService<TState, TAction> {
constructor() {
super(reducer, initialState);
}
};
return stateServiceClass as IfImmutable<TState, typeof stateServiceClass, YourStateTypeNeedsToBeImmutable<TState>>;
}

View File

@ -0,0 +1,5 @@
export const PageTitles = {
Search: 'Search',
Help: 'Help',
Settings: 'Settings',
};

View File

@ -0,0 +1,91 @@
export interface AppState {
readonly savedPages: readonly SavedPage[];
readonly mainPages: readonly Page[];
readonly cards: readonly CardItem[];
readonly error: Error;
readonly paragraphs: HashTable<Paragraph>;
readonly displaySettings: DisplaySettings;
}
export interface Error {
readonly msg: string;
}
export type Data = BiblePassageResult;
export interface DisplaySettings {
readonly showStrongsAsModal: boolean;
readonly appendCardToBottom: boolean;
readonly insertCardNextToItem: boolean;
readonly fontSize: number;
readonly fontFamily: string;
readonly showVersesOnNewLine: boolean;
readonly showVerseNumbers: boolean;
readonly showParagraphs: boolean;
readonly showParagraphHeadings: boolean;
}
export interface SavedPage {
readonly queries: readonly CardItem[];
readonly title: string;
}
export interface CardItem {
readonly qry: string;
readonly data: Data;
readonly type: string;
readonly dict: string;
}
export type OpenData = {
card: CardItem;
qry: string;
from_search_bar: boolean;
};
export class Page {
readonly title: string;
// readonly component: any;
// readonly params: any;
readonly icon?: string;
}
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;
}
export interface HashTable<T> {
readonly [key: string]: T;
}

View File

@ -0,0 +1,41 @@
<div class="card-title passage-title">
<mat-icon aria-hidden="false" aria-label="Bible Passage Icon"
>menu_book</mat-icon
>
<span *ngIf="ref">{{ ref }}</span>
<button
mat-icon
class="card-close-button"
aria-label="Remove the passage card from the list"
(click)="close($event)"
>
<mat-icon>cancel</mat-icon>
</button>
</div>
<div class="card-content" *ngIf="cardItem">
<div class="passage-text" *ngFor="let ch of cardItem.data.cs">
<h2>Chapter {{ ch.ch }}</h2>
<ng-template ngFor let-para [ngForOf]="ch.paras">
<span [ngClass]="{'as-paragraph': (showParagraphs$ | async)}">
<h3
class="paragraph-heading"
*ngIf="(showParagraphHeadings$ | async) && hasHeader(para.p)"
>
{{ para.p.h }}
</h3>
<ng-template ngFor let-vs [ngForOf]="para.vss">
<strong class="verse-number" *ngIf="(showVerseNumbers$ | async)"
>{{ vs.v }}.</strong
>
<ng-template ngFor let-w [ngForOf]="vs.w">
<ng-template [ngIf]="!isPunct(w.t)"> </ng-template
><a *ngIf="w.s != null" (click)="openStrongs(w.s)">{{ w.t }}</a
><ng-template [ngIf]="w.s == null"
>{{ w.t }}</ng-template
> </ng-template
><br *ngIf="(showVersesOnNewLine$ | async)" />
</ng-template>
</span>
</ng-template>
</div>
</div>

View File

@ -0,0 +1,17 @@
.passage-title {
background-color: var(--passage-color-primary);
}
.as-paragraph {
display: block;
margin-bottom: 1rem;
}
.paragraph-heading {
font-family: var(--passage-heading-font-family);
font-weight: 600;
}
.card-close-button {
color: var(--passage-color-accent);
}

View File

@ -0,0 +1,165 @@
import { Component, OnInit, ElementRef } from '@angular/core';
import { BibleReference } from '../../../common/bible-reference';
import { AppService } from '../../../services/app.service';
import { CardComponent } from '../../../common/card.component';
import { Paragraph } from '../../../models/app-state';
@Component({
selector: 'app-passage',
templateUrl: 'passage.html',
styleUrls: ['./passage.scss'],
preserveWhitespaces: true,
})
export class PassageComponent extends CardComponent implements OnInit {
ref: BibleReference;
showParagraphs$ = this.appService.select(
(state) => state.displaySettings.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
);
constructor(
protected elementRef: ElementRef,
private appService: AppService
) {
super(elementRef);
}
ngOnInit(): void {
this.ref = new BibleReference(this.cardItem.qry);
}
contextMenu() {
// cardContextMenu(this.profileService, this._actionSheet, this._pagesSvc, this._alertCtrl, this.cardItem);
}
next() {
const lastVerseForEnd = this.ref.Section.end.book.chapters[
parseInt(this.ref.Section.end.chapter, 10)
].toString();
if (
this.ref.Section.end.verse !== '*' &&
this.ref.Section.end.verse !== lastVerseForEnd
) {
this.ref.Section.end.chapter = this.ref.Section.end.chapter;
} else {
this.ref.Section.end.chapter = (
parseInt(this.ref.Section.end.chapter, 10) + 1
).toString();
}
this.ref.Section.start.chapter = this.ref.Section.end.chapter;
this.ref.Section.start.verse = '1';
this.ref.Section.end.verse = '*';
this.appService.updatePassage(this.cardItem, this.ref);
}
prev() {
if (this.ref.Section.start.verse !== '1') {
this.ref.Section.start.chapter = this.ref.Section.start.chapter;
} else {
this.ref.Section.start.chapter = (
parseInt(this.ref.Section.start.chapter, 10) - 1
).toString();
}
this.ref.Section.end.chapter = this.ref.Section.start.chapter;
this.ref.Section.start.verse = '1';
this.ref.Section.end.verse = '*';
this.appService.updatePassage(this.cardItem, this.ref);
}
expand() {
const lastVerseForEnd = this.ref.Section.end.book.chapters[
parseInt(this.ref.Section.end.chapter, 10)
];
// if your verse is at the beginning, to go the prev chapter and add 3 verses from that
if (parseInt(this.ref.Section.start.verse, 10) < 4) {
this.ref.Section.start.chapter = (
parseInt(this.ref.Section.start.chapter, 10) - 1
).toString();
this.ref.Section.start.verse =
'*-' + (3 - parseInt(this.ref.Section.start.verse, 10));
if (this.ref.Section.start.chapter === '0') {
this.ref.Section.start.chapter = '1';
this.ref.Section.start.verse = '1';
}
} else {
// or go back 3 verses
this.ref.Section.start.verse = (
parseInt(this.ref.Section.start.verse, 10) - 3
).toString();
}
// if your verse is at the end, go to the next chapter
if (
this.ref.Section.end.verse === '*' ||
parseInt(this.ref.Section.end.verse, 10) + 3 > lastVerseForEnd
) {
this.ref.Section.end.chapter = (
parseInt(this.ref.Section.end.chapter, 10) + 1
).toString();
if (this.ref.Section.end.verse === '*') {
this.ref.Section.end.verse = '3';
} else {
this.ref.Section.end.verse = (
parseInt(this.ref.Section.end.verse, 10) +
3 -
lastVerseForEnd
).toString();
}
if (
this.ref.Section.end.chapter ===
(this.ref.Section.end.book.last_chapter + 1).toString()
) {
this.ref.Section.end.chapter = this.ref.Section.end.book.last_chapter.toString();
this.ref.Section.end.verse = lastVerseForEnd.toString();
}
} else {
// or add 3 verses
this.ref.Section.end.verse = (
parseInt(this.ref.Section.end.verse, 10) + 3
).toString();
}
if (this.ref.Section.start.verse === '0') {
this.ref.Section.start.verse = '1';
}
this.appService.updatePassage(this.cardItem, this.ref);
}
openStrongs(strongs: string) {
this.onItemClicked.emit({
card: this.cardItem,
qry: this.cardItem.dict + strongs,
from_search_bar: false,
});
}
isPunct(c: string) {
return new RegExp('^[.,;:?!]$').test(c);
}
hasHeader(p: Paragraph) {
if (p === undefined) {
return false;
}
return p.h.length > 0;
}
}

View File

@ -0,0 +1,41 @@
<mat-toolbar>
<button type="button" mat-icon-button (click)="navService.toggle()">
<mat-icon md-48 aria-hidden="false" aria-label="Menu Toggle">menu</mat-icon>
</button>
<div class="search-bar">
<div class="searchbar-search-icon"></div>
<input
class="search-bar-input"
type="text"
aria-label="Search"
placeholder="Search"
type="search"
autocomplete="off"
matInput
[formControl]="searchControl"
[matAutocomplete]="auto"
(keyup.enter)="search($event.target.value)"
/>
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let option of suggestions$ | async" [value]="option">
{{option}}
</mat-option>
</mat-autocomplete>
</div>
</mat-toolbar>
<ng-template [ngIf]="(cards$ | async).length > 0" [ngIfElse]="nocards">
<div *ngFor="let item of cards$ | async">
<mat-card>
<app-passage
*ngIf="isPassage(item)"
[cardItem]="item"
(onClose)="removeCard(item)"
(onItemClicked)="getItemsNextToCard($event)"
></app-passage>
</mat-card>
</div>
</ng-template>
<ng-template #nocards>
<span>&nbsp;</span>
</ng-template>

View File

@ -0,0 +1,44 @@
.full-width {
width: 100%;
}
mat-card {
max-width: 400px;
margin: 1em auto;
padding: 0;
}
mat-toolbar {
padding-left: 12px;
}
.search-bar {
width: 100%;
position: relative;
margin-left: 12px;
}
.search-bar-input {
width: 100%;
border: 0;
padding: 6px 55px;
border-radius: 0.2rem;
height: auto;
font-size: 1.2rem;
font-family: var(--font-family);
line-height: 2.2rem;
-webkit-box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
touch-action: manipulation;
}
.searchbar-search-icon {
position: absolute;
background-repeat: no-repeat;
background-size: 20px;
left: 16px;
top: 11px;
background-image: url("data:image/svg+xml;charset=utf-8,<svg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20512%20512'><path%20fill='%235b5b5b'%20d='M337.509,305.372h-17.501l-6.571-5.486c20.791-25.232,33.922-57.054,33.922-93.257C347.358,127.632,283.896,64,205.135,64C127.452,64,64,127.632,64,206.629s63.452,142.628,142.225,142.628c35.011,0,67.831-13.167,92.991-34.008l6.561,5.487v17.551L415.18,448L448,415.086L337.509,305.372z%20M206.225,305.372c-54.702,0-98.463-43.887-98.463-98.743c0-54.858,43.761-98.742,98.463-98.742c54.7,0,98.462,43.884,98.462,98.742C304.687,261.485,260.925,305.372,206.225,305.372z'/></svg>");
width: 21px;
height: 21px;
}

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchPage } from './search.page';
describe('SearchComponent', () => {
let component: SearchPage;
let fixture: ComponentFixture<SearchPage>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SearchPage],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchPage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,139 @@
import { Component, OnInit, HostListener } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { FormControl } from '@angular/forms';
import { map, startWith } from 'rxjs/operators';
import { AppService } from 'src/app/services/app.service';
import { NavService } from 'src/app/services/nav.service';
import { OpenData, CardItem } from 'src/app/models/app-state';
import { BibleReference } from 'src/app/common/bible-reference';
@Component({
selector: 'app-search-page',
templateUrl: './search.page.html',
styleUrls: ['./search.page.scss'],
})
export class SearchPage implements OnInit {
cards$ = this.appService.select((state) => state.cards);
suggestions$: Observable<string[]>;
searchControl = new FormControl();
constructor(
private activatedRoute: ActivatedRoute,
private appService: AppService,
public navService: NavService
) {}
ngOnInit() {
// if a route was passed in, perform a search.
if (this.activatedRoute.snapshot.paramMap.has('term')) {
const term = this.activatedRoute.snapshot.paramMap.get('term');
this.search(term);
}
this.suggestions$ = this.searchControl.valueChanges.pipe(
startWith(''),
map((value) => this.getSearchItems(value))
);
this.appService.state$.subscribe((state) => {
console.log(state);
});
}
//#region Search
getItemsNextToCard(data: OpenData) {}
removeCard(card: CardItem) {
this.appService.removeCard(card);
}
async search(search: string) {
// clear search box.
this.searchControl.setValue('');
try {
const terms = search.split(';');
for (const term of terms) {
const q = term.trim();
if (q !== '') {
if (q.startsWith('note:')) {
// // It's a note lookup
// list.push({
// qry: q.replace('note:', ''),
// dict: '',
// type: 'Note',
// });
} else if (q.search(/[0-9]/i) === -1) {
// // its a search term.
// list.push({ qry: q, dict: 'na', type: 'Words' });
} else if (q.search(/(H|G)[0-9]/i) !== -1) {
// // its a strongs lookup
// let dict = q.substring(0, 1);
// if (dict.search(/h/i) !== -1) {
// dict = 'heb';
// } else {
// dict = 'grk';
// }
// q = q.substring(1, q.length);
// list.push({ qry: q, dict, type: 'Strongs' });
} else {
// its a verse reference.
if (q !== '') {
const myref = new BibleReference(q.trim());
this.appService.getNewPassage(myref);
}
}
}
}
} catch (error) {
console.log(error);
}
}
isError(t: CardItem) {
return t.type === 'Error';
}
isPassage(t: CardItem) {
return t.type === 'Passage';
}
isNote(t: CardItem) {
return t.type === 'Note';
}
isStrongs(t: CardItem) {
return t.type === 'Strongs';
}
isWords(t: CardItem) {
return t.type === 'Words';
}
//#endregion
//#region Typeahead/Autocomplete
private getSearchItems(value: string): string[] {
const filterValue = value.toLowerCase();
return ['One', 'Two', 'Three'].filter((option) =>
option.toLowerCase().includes(filterValue)
);
}
select(selection: any): void {
this.search(selection);
}
@HostListener('document:click', ['$event'])
private documentClickHandler(event) {
// if (this.searchbarElem) {
// this.searchbarElem.getInputElement().then((el) => {
// if (el.contains(event.target)) {
// this.showList = false;
// }
// });
// }
}
//#endregion
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AppService } from './app.service';
describe('AppService', () => {
let service: AppService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AppService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,380 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {
AppState,
SavedPage,
HashTable,
Paragraph,
BiblePassage,
BibleVerse,
Data,
Error,
BibleParagraph,
BibleParagraphPassage,
CardItem,
} from '../models/app-state';
import { Section, BibleReference } from '../common/bible-reference';
import { PageTitles } from '../constants';
import { createStateService } from '../common/state-service';
import * as math from 'mathjs';
const initialState: AppState = {
cards: [],
savedPages: [],
mainPages: [
{ title: PageTitles.Search, icon: 'search' },
{ title: PageTitles.Settings, icon: 'settings' },
{ title: PageTitles.Help, icon: 'help' },
],
error: null,
paragraphs: null,
displaySettings: {
showStrongsAsModal: false,
appendCardToBottom: true,
insertCardNextToItem: true,
fontSize: 11,
fontFamily: 'PT Serif',
showVersesOnNewLine: false,
showVerseNumbers: false,
showParagraphs: true,
showParagraphHeadings: true,
},
};
type AppAction =
| {
type: 'UPDATE_PAGES';
pages: SavedPage[];
}
| {
type: 'ADD_CARD';
card: CardItem;
}
| {
type: 'UPDATE_CARD';
newCard: CardItem;
oldCard: CardItem;
}
| {
type: 'REMOVE_CARD';
card: CardItem;
}
| {
type: 'UPDATE_ERROR';
error: Error;
}
| {
type: 'UPDATE_PARAGRAPHS';
paragraphs: HashTable<Paragraph>;
};
function reducer(state: AppState, action: AppAction): AppState {
// somtimes the state is null. lets not complain if that happens.
if (state === undefined) {
state = initialState;
}
switch (action.type) {
case 'UPDATE_PAGES': {
return {
...state,
savedPages: [...action.pages],
};
}
case 'ADD_CARD': {
return {
...state,
cards: [...state.cards, action.card],
};
}
case 'UPDATE_CARD': {
return {
...state,
cards: [
...state.cards.map((c) => {
if (c === action.oldCard) {
return action.newCard;
}
return c;
}),
],
};
}
case 'REMOVE_CARD': {
return {
...state,
cards: [...state.cards.filter((c) => c !== action.card)],
};
}
case 'UPDATE_PARAGRAPHS': {
return {
...state,
paragraphs: action.paragraphs,
};
}
}
}
@Injectable({
providedIn: 'root',
})
export class AppService extends createStateService(reducer, initialState) {
constructor(private http: HttpClient) {
super();
}
async getSavedPages() {
this.dispatch({
type: 'UPDATE_PAGES',
pages: [
{
queries: [],
title: 'My Page',
},
],
});
}
async getParagraphMarkers(): Promise<HashTable<Paragraph>> {
const paras = await this.http
.get<HashTable<Paragraph>>('assets/data/bibles/paras.json')
.toPromise();
this.dispatch({
type: 'UPDATE_PARAGRAPHS',
paragraphs: paras,
});
return paras;
}
removeCard(card: CardItem) {
this.dispatch({
type: 'REMOVE_CARD',
card,
});
}
//#region Bible Passages
async getNewPassage(ref: BibleReference) {
const card = await this.composeBiblePassageCardItem(ref);
this.dispatch({
type: 'ADD_CARD',
card,
});
}
async updatePassage(oldCard: CardItem, ref: BibleReference) {
const newCard = await this.composeBiblePassageCardItem(ref);
this.dispatch({
type: 'UPDATE_CARD',
oldCard,
newCard,
});
}
private async composeBiblePassageCardItem(ref: BibleReference) {
const result = await this.getPassageFromApi(ref.Section);
return {
qry: ref.toString(),
dict: ref.Section.start.book.book_number > 39 ? 'G' : 'H',
type: 'Passage',
data: result,
};
}
private async getPassageFromApi(section: Section) {
try {
const chapters = []; // the verses from the chapter.
const result = {
cs: [],
testament: '',
ref: BibleReference.toString(section),
};
if (Number(section.start.chapter) > section.start.book.last_chapter) {
this.dispatch({
type: 'UPDATE_ERROR',
error: {
msg: `The requested chapter ${section.start.book.name} is out of range. Please pick a chapter between 1 and ${section.end.book.last_chapter}.`,
},
});
return;
}
if (Number(section.end.chapter) > section.end.book.last_chapter) {
this.dispatch({
type: 'UPDATE_ERROR',
error: {
msg: `The requested chapter ${section.end.book.name} is out of range. Please pick a chapter between 1 and ${section.end.book.last_chapter}.`,
},
});
return;
}
for (
let i = Number(section.start.chapter);
i <= Number(section.end.chapter);
i++
) {
try {
const d = await this.http
.get<BiblePassage>(
`assets/data/bibles/kjv_strongs/${section.start.book.book_number}-${i}.json`
)
.toPromise();
chapters.push(d);
} catch (err) {
this.dispatch({
type: 'UPDATE_ERROR',
error: {
msg: `Unable to retrieve bible passage ${result.ref}.`,
},
});
return;
}
}
const passages: BiblePassage[] = [];
// prep the passages
for (let j = 0; j < chapters.length; j++) {
const vss: BibleVerse[] = [];
let start: number;
let end: string | number;
// figure out the start verse.
if (j === 0) {
if (section.start.verse.indexOf('*') !== -1) {
// you sometimes use this as a shortcut to the last verse
// replace the * with the last verse, then eval the expression.
section.start.verse = section.start.verse.replace(
'*',
chapters[j].vss.length.toString()
);
start = math.evaluate(section.start.verse);
// update the section and the ref.
section.start.verse = start.toString();
result.ref = BibleReference.toString(section);
} else {
start = parseInt(section.start.verse, 10);
}
} else {
start = 1;
}
// figure out the end verse
if (j + 1 === chapters.length) {
end = section.end.verse;
} else {
end = '*';
}
// get the verses requested.
const tvs = chapters[j].vss.length;
if (end === '*' || parseInt(end, 10) > tvs) {
end = tvs;
}
// we're using c based indexes here, so the index is 1 less than the verse #.
for (let i = start; i <= end; i++) {
vss.push(chapters[j].vss[i - 1]);
}
passages.push({
ch: chapters[j].ch,
vss,
});
}
// convert into paragraphs.
result.cs = await this.convertToParagraphPassages(passages, section);
if (section.start.book.book_number >= 40) {
result.testament = 'new';
} else {
result.testament = 'old';
}
return result;
} catch (error) {
this.dispatch({
type: 'UPDATE_ERROR',
error: {
msg: `An unknown error occurred: ${error}.`,
},
});
console.log(error);
}
}
private async convertToParagraphPassages(
chapters: BiblePassage[],
section: Section
) {
let paragraphMarkers: HashTable<Paragraph> = null;
if (this.getState().paragraphs === null) {
paragraphMarkers = await this.getParagraphMarkers();
} else {
paragraphMarkers = this.getState().paragraphs;
}
const passages: BibleParagraphPassage[] = [];
for (const ch of chapters) {
const passage = {
ch: ch.ch,
paras: this.convertToParagraphs(ch, section, paragraphMarkers),
};
passages.push(passage);
}
return passages;
}
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.
let para = { p: { h: '', p: 0 }, vss: [] };
const paras = [];
const vss: BibleVerse[] = [];
// for each verse in the chapter, break them into paragraphs.
for (const v of ch.vss) {
if (this.getRefKey(v, section) in paragraphMarkers) {
paras.push(para);
para = {
p: paragraphMarkers[this.getRefKey(v, section)],
vss: [v],
};
if (para.p === undefined) {
para.p = { h: '', p: 0 };
} // just in case you can't find a paragraph.
} else {
para.vss.push(v);
}
}
// add the final paragraph if it has verses.
if (para.vss.length > 0) {
paras.push(para);
}
return paras;
}
private getRefKey(vs: BibleVerse, section: Section) {
return (
section.start.book.book_number + ';' + section.start.chapter + ';' + vs.v
);
}
//#endregion
}

View File

@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
@Injectable({
providedIn: 'root',
})
export class NavService {
private sidenav: MatSidenav;
public setSidenav(sidenav: MatSidenav) {
this.sidenav = sidenav;
}
public open() {
return this.sidenav.open();
}
public close() {
return this.sidenav.close();
}
public toggle(): void {
this.sidenav.toggle();
}
}

View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More