diff --git a/src/.eslintrc.json b/src/.eslintrc.json index e6a80eef..eaf27277 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -1,23 +1,23 @@ { "root": true, - "ignorePatterns": [ - "projects/**/*" - ], + "ignorePatterns": ["projects/**/*"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module", + "project": "./tsconfig.json" // <-- Point to your project's "tsconfig.json" or create a new one. + }, "overrides": [ { - "files": [ - "*.ts" - ], + "files": ["*.ts"], "parserOptions": { - "project": [ - "tsconfig.json", - "e2e/tsconfig.json" - ], + "project": ["tsconfig.json", "e2e/tsconfig.json"], "createDefaultProgram": true }, "extends": [ "plugin:@angular-eslint/recommended", - "plugin:@angular-eslint/template/process-inline-templates" + "plugin:@angular-eslint/template/process-inline-templates", + "plugin:deprecation/recommended" ], "rules": { "@angular-eslint/directive-selector": [ @@ -39,14 +39,9 @@ } }, { - "files": [ - "*.html" - ], - "extends": [ - "plugin:@angular-eslint/template/recommended" - ], - "rules": { - } + "files": ["*.html"], + "extends": ["plugin:@angular-eslint/template/recommended"], + "rules": {} } ] } diff --git a/src/package-lock.json b/src/package-lock.json index 42370b68..ba1a685a 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -24,6 +24,7 @@ "@capacitor/ios": "^5.7.0", "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.0", "@ngx-pwa/local-storage": "^17.0.0", + "@reduxjs/toolkit": "^2.2.1", "angular2-uuid": "^1.1.1", "component": "^1.1.0", "firebase": "^10.8.0", @@ -51,9 +52,10 @@ "@types/jasminewd2": "~2.0.13", "@types/node": "^20.11.21", "@typescript-eslint/eslint-plugin": "7.1.0", - "@typescript-eslint/parser": "7.1.0", + "@typescript-eslint/parser": "^7.1.0", "cypress": "latest", "eslint": "^8.57.0", + "eslint-plugin-deprecation": "^2.0.0", "firebase-tools": "^13.4.0", "fuzzy": "^0.1.3", "inquirer": "^9.2.15", @@ -6378,6 +6380,29 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.1.tgz", + "integrity": "sha512-8CREoqJovQW/5I4yvvijm/emUiCCmcs4Ev4XPWd4mizSO+dD3g5G6w34QK5AGeNrSH7qM8Fl66j4vuV7dpOdkw==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", @@ -12616,6 +12641,21 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-deprecation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-deprecation/-/eslint-plugin-deprecation-2.0.0.tgz", + "integrity": "sha512-OAm9Ohzbj11/ZFyICyR5N6LbOIvQMp7ZU2zI7Ej0jIc8kiGUERXPNMfw2QqqHD1ZHtjMub3yPZILovYEYucgoQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^6.0.0", + "tslib": "^2.3.1", + "tsutils": "^3.21.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "typescript": "^4.2.4 || ^5.0.0" + } + }, "node_modules/eslint-scope": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.0.tgz", @@ -15602,6 +15642,15 @@ "node": ">=0.10.0" } }, + "node_modules/immer": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", @@ -21754,6 +21803,14 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", @@ -24197,6 +24254,27 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/tuf-js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz", diff --git a/src/package.json b/src/package.json index 3842c889..63ee4ece 100644 --- a/src/package.json +++ b/src/package.json @@ -32,6 +32,7 @@ "@capacitor/ios": "^5.7.0", "@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.0", "@ngx-pwa/local-storage": "^17.0.0", + "@reduxjs/toolkit": "^2.2.1", "angular2-uuid": "^1.1.1", "component": "^1.1.0", "firebase": "^10.8.0", @@ -59,8 +60,10 @@ "@types/jasminewd2": "~2.0.13", "@types/node": "^20.11.21", "@typescript-eslint/eslint-plugin": "7.1.0", - "@typescript-eslint/parser": "7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "cypress": "latest", "eslint": "^8.57.0", + "eslint-plugin-deprecation": "^2.0.0", "firebase-tools": "^13.4.0", "fuzzy": "^0.1.3", "inquirer": "^9.2.15", @@ -74,8 +77,7 @@ "karma-jasmine-html-reporter": "^2.1.0", "open": "^10.0.4", "ts-node": "~10.9.2", - "typescript": "~5.3.3", - "cypress": "latest" + "typescript": "~5.3.3" }, "browserslist": [ "last 1 Chrome version", diff --git a/src/src/app/app.module.ts b/src/src/app/app.module.ts index 8c4a30f0..023e4874 100644 --- a/src/src/app/app.module.ts +++ b/src/src/app/app.module.ts @@ -1,7 +1,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HttpClientModule } from '@angular/common/http'; import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; +import { APP_ID, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MARKED_OPTIONS, MarkdownModule } from 'ngx-markdown'; @@ -66,7 +66,6 @@ import { NoteEditModalComponent } from './components/note/edit-modal/note-edit-m import { VersePickerModalComponent } from './components/verse-picker-modal/verse-picker-modal.component'; import { AddToPageModalComponent } from './components/add-to-page-modal/add-to-page-modal.component'; - import { MarkedOptions, MarkedRenderer } from 'ngx-markdown'; // function that returns `MarkedOptions` with renderer override @@ -107,7 +106,7 @@ export function markedOptionsFactory(): MarkedOptions { OkCancelModalComponent, ], imports: [ - BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), + BrowserModule, HttpClientModule, ReactiveFormsModule, @@ -156,7 +155,7 @@ export function markedOptionsFactory(): MarkedOptions { MatFormFieldModule, ClipboardModule, ], - providers: [], + providers: [{ provide: APP_ID, useValue: 'ng-cli-universal' }], bootstrap: [AppComponent], }) export class AppModule {} diff --git a/src/src/app/common/state-service.ts b/src/src/app/common/state-service.ts index cc95da97..3fac9256 100644 --- a/src/src/app/common/state-service.ts +++ b/src/src/app/common/state-service.ts @@ -1,4 +1,5 @@ -import { Store, createStore } from 'redux'; +import { Store } from 'redux'; +import { configureStore } from '@reduxjs/toolkit'; import { BehaviorSubject, Observable } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; @@ -16,11 +17,10 @@ class StateService { private readonly internalState$: BehaviorSubject; protected constructor(reducer: (state: TState, action: TAction) => TState, initialState: TState) { - this.store = createStore( - reducer, - initialState as any, // 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.store = configureStore({ + reducer: reducer, + preloadedState: initialState, + }); this.internalState$ = new BehaviorSubject(initialState as TState); @@ -129,7 +129,6 @@ export function createStateService( return stateServiceClass as IfImmutable>; } - export interface IStateAction { type: TAction; handle: (state: TState) => TState; diff --git a/src/src/app/components/card.component.ts b/src/src/app/components/card.component.ts index 24223b4b..cf5673b2 100644 --- a/src/src/app/components/card.component.ts +++ b/src/src/app/components/card.component.ts @@ -6,6 +6,7 @@ import { MoveDirection } from '../common/move-direction'; import { AddToPageModalComponent } from '../components/add-to-page-modal/add-to-page-modal.component'; import { CardItem } from '../models/card-state'; import { AppService } from '../services/app.service'; +import { Clipboard } from '@angular/cdk/clipboard'; @Component({ template: '', @@ -19,19 +20,29 @@ export class CardComponent extends SubscriberBase { icon$: Observable; - constructor(protected elementRef: ElementRef, protected dialog: MatDialog, protected appService: AppService) { + constructor( + protected elementRef: ElementRef, + protected dialog: MatDialog, + protected appService: AppService, + protected clipboard: Clipboard + ) { super(); } - protected copyToClip(text: string, html: string) { - function listener(e: ClipboardEvent) { - e.clipboardData.setData('text/html', html); - e.clipboardData.setData('text/plain', text); - e.preventDefault(); - } - document.addEventListener('copy', listener); - document.execCommand('copy'); - document.removeEventListener('copy', listener); + protected copyToClip(text: string) { + this.clipboard.copy(text); + const pending = this.clipboard.beginCopy(text); + let remainingAttempts = 3; + const attempt = () => { + const result = pending.copy(); + if (!result && --remainingAttempts) { + setTimeout(attempt); + } else { + // Remember to destroy when you're done! + pending.destroy(); + } + }; + attempt(); } close(ev) { diff --git a/src/src/app/components/note/edit-modal/note-edit-modal.component.ts b/src/src/app/components/note/edit-modal/note-edit-modal.component.ts index 7cb751ee..c71317d5 100644 --- a/src/src/app/components/note/edit-modal/note-edit-modal.component.ts +++ b/src/src/app/components/note/edit-modal/note-edit-modal.component.ts @@ -53,16 +53,11 @@ export class NoteEditModalComponent { //#region cross refs add(event: MatChipInputEvent): void { - const input = event.input; const value = event.value; if ((value || '').trim()) { this.references.push(value.trim()); } - - if (input) { - input.value = ''; - } } remove(reference: string): void { diff --git a/src/src/app/components/note/note-card.component.ts b/src/src/app/components/note/note-card.component.ts index a4d083ea..2789ca5b 100644 --- a/src/src/app/components/note/note-card.component.ts +++ b/src/src/app/components/note/note-card.component.ts @@ -1,5 +1,6 @@ import { Component, ViewChild, ElementRef, Input } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { Clipboard } from '@angular/cdk/clipboard'; import { CardComponent } from '../card.component'; import { BibleReference } from 'src/app/common/bible-reference'; import { NoteItem } from 'src/app/models/note-state'; @@ -22,16 +23,20 @@ export class NoteCardComponent extends CardComponent { return this.cardItem.data as NoteItem; } - constructor(protected elementRef: ElementRef, protected appService: AppService, public dialog: MatDialog) { - super(elementRef, dialog, appService); + constructor( + protected elementRef: ElementRef, + protected appService: AppService, + protected clipboard: Clipboard, + public dialog: MatDialog + ) { + super(elementRef, dialog, appService, clipboard); this.icon$ = appService.select((state) => state.settings.value.cardIcons.note); } copy() { - const html = this.noteElement.nativeElement.innerHTML; const text = this.noteElement.nativeElement.innerText; - this.copyToClip(text, html); + this.copyToClip(text); } private xrefs: BibleReference[]; diff --git a/src/src/app/components/passage/passage-card.component.ts b/src/src/app/components/passage/passage-card.component.ts index a6d54deb..5bfa1aca 100644 --- a/src/src/app/components/passage/passage-card.component.ts +++ b/src/src/app/components/passage/passage-card.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit, ElementRef, ViewChild, ChangeDetectionStrategy } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { Clipboard } from '@angular/cdk/clipboard'; import { NoteItem } from 'src/app/models/note-state'; import { CardComponent } from 'src/app/components/card.component'; import { BibleReference, Overlap } from 'src/app/common/bible-reference'; @@ -40,8 +41,13 @@ export class PassageCardComponent extends CardComponent implements OnInit { return this.cardItem.data as BiblePassageResult; } - constructor(protected elementRef: ElementRef, protected appService: AppService, public dialog: MatDialog) { - super(elementRef, dialog, appService); + constructor( + protected elementRef: ElementRef, + protected appService: AppService, + protected clipboard: Clipboard, + public dialog: MatDialog + ) { + super(elementRef, dialog, appService, clipboard); this.icon$ = appService.select((state) => state.settings.value.cardIcons.passage); } @@ -54,9 +60,8 @@ export class PassageCardComponent extends CardComponent implements OnInit { } copy() { - const html = this.passageElement.nativeElement.innerHTML + ` - ${this.ref.toString()}`; const text = this.passageElement.nativeElement.innerText + ` - ${this.ref.toString()}`; - this.copyToClip(text, html); + this.copyToClip(text); } next() { diff --git a/src/src/app/components/strongs/card/strongs-card.component.ts b/src/src/app/components/strongs/card/strongs-card.component.ts index fed30a64..57ef73ec 100644 --- a/src/src/app/components/strongs/card/strongs-card.component.ts +++ b/src/src/app/components/strongs/card/strongs-card.component.ts @@ -1,5 +1,6 @@ -import { Component, ElementRef, ViewChild, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { Component, ElementRef, ViewChild, ChangeDetectionStrategy } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { Clipboard } from '@angular/cdk/clipboard'; import { StrongsModalComponent } from '../modal/strongs-modal.component'; import { CardComponent } from 'src/app/components/card.component'; import { BibleReference } from 'src/app/common/bible-reference'; @@ -17,12 +18,17 @@ export class StrongsCardComponent extends CardComponent { asModal = false; @ViewChild('strongs') strongsElement: ElementRef; - get strongsResult(){ + get strongsResult() { return this.cardItem.data as StrongsResult; } - constructor(protected elementRef: ElementRef, protected appService: AppService, protected dialog: MatDialog) { - super(elementRef, dialog, appService); + constructor( + protected elementRef: ElementRef, + protected appService: AppService, + protected clipboard: Clipboard, + protected dialog: MatDialog + ) { + super(elementRef, dialog, appService, clipboard); this.icon$ = appService.select((state) => state.settings.value.cardIcons.strongs); this.addSubscription( this.appService.state$.subscribe((state) => { @@ -32,9 +38,8 @@ export class StrongsCardComponent extends CardComponent { } copy() { - const html = this.strongsElement.nativeElement.innerHTML; const text = this.strongsElement.nativeElement.innerText; - this.copyToClip(text, html); + this.copyToClip(text); } async openStrongs(q: string) { diff --git a/src/src/app/components/words/words-card.component.ts b/src/src/app/components/words/words-card.component.ts index f6b738e9..aa3e21b6 100644 --- a/src/src/app/components/words/words-card.component.ts +++ b/src/src/app/components/words/words-card.component.ts @@ -1,5 +1,6 @@ import { Component, ElementRef, ViewChild, ChangeDetectionStrategy } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; +import { Clipboard } from '@angular/cdk/clipboard'; import { BibleReference } from 'src/app/common/bible-reference'; import { CardComponent } from 'src/app/components/card.component'; import { WordLookupResult } from 'src/app/models/words-state'; @@ -19,8 +20,13 @@ export class WordsCardComponent extends CardComponent { return this.cardItem.data as WordLookupResult; } - constructor(protected elementRef: ElementRef, protected appService: AppService, public dialog: MatDialog) { - super(elementRef, dialog, appService); + constructor( + protected elementRef: ElementRef, + protected appService: AppService, + protected clipboard: Clipboard, + public dialog: MatDialog + ) { + super(elementRef, dialog, appService, clipboard); this.icon$ = appService.select((state) => state.settings.value.cardIcons.words); } @@ -29,9 +35,8 @@ export class WordsCardComponent extends CardComponent { BibleReference.makePassageFromReferenceKey(ref) ); - const html = refs.map((ref) => `${ref}`).join(', '); const text = refs.join(', '); - this.copyToClip(text, html); + this.copyToClip(text); } makePassage(p: string) { diff --git a/src/src/app/services/app.service.ts b/src/src/app/services/app.service.ts index dbf3b202..4006055b 100644 --- a/src/src/app/services/app.service.ts +++ b/src/src/app/services/app.service.ts @@ -865,7 +865,7 @@ export class AppService extends createStateService(appReducer, initialState) { } else if (qry.search(/(H|G)[0-9]/i) !== -1) { // its a strongs lookup if (qry.substring(0, 1).toUpperCase() === 'H') { - const num = parseInt(qry.substr(1), 10); + const num = parseInt(qry.substring(1), 10); for (let x = num; x < num + 10 && x < 8675; x++) { words.push('H' + x); } diff --git a/src/src/app/services/storage.service.ts b/src/src/app/services/storage.service.ts index be4150de..d5178aea 100644 --- a/src/src/app/services/storage.service.ts +++ b/src/src/app/services/storage.service.ts @@ -207,14 +207,14 @@ export class StorageService extends SubscriberBase { // console.log('Data saved to local store', data); // update local - this.local.set(this.settingsPath, data).subscribe( - () => { - // nop - }, - // error - () => { - this.appService.dispatchError(`Something went wrong and the Settings weren't saved. :(`); - } + this.addSubscription( + this.local.set(this.settingsPath, data).subscribe({ + next: () => {}, + complete: () => {}, + error: () => { + this.appService.dispatchError(`Something went wrong and the Settings weren't saved. :(`); + }, + }) ); // update remote