From 3dd690922b44df065ee73dd1491ca2ba6489dccb Mon Sep 17 00:00:00 2001 From: "Darryl L. Pierce" Date: Sun, 15 Nov 2020 17:18:05 -0500 Subject: [PATCH] Added the ComicImport page [#539] * Added routing to show the page. * Added user preference loading. * Added a menu item to go to the page. --- comixed-web/angular.json | 7 +- comixed-web/src/app/app.component.spec.ts | 15 +- comixed-web/src/app/app.component.ts | 8 +- comixed-web/src/app/app.module.ts | 24 +- .../comic-import-routing.module.ts | 34 ++ .../import-toolbar.component.html | 44 ++- .../import-comics.component.html | 29 ++ .../import-comics.component.scss | 0 .../import-comics.component.spec.ts | 373 ++++++++++++++++++ .../import-comics/import-comics.component.ts | 156 ++++++++ .../pipes/comic-file-cover-url.pipe.spec.ts | 2 +- .../pipes/comic-file-cover-url.pipe.ts | 4 +- .../navigation-bar.component.html | 24 +- .../navigation-bar.component.spec.ts | 48 ++- .../navigation-bar.component.ts | 20 +- .../app/interceptors/http.interceptor.spec.ts | 15 +- .../src/app/user/effects/user.effects.spec.ts | 32 +- comixed-web/src/app/user/index.ts | 2 + comixed-web/src/app/user/user.constants.ts | 12 + comixed-web/src/app/user/user.fixtures.ts | 11 +- comixed-web/src/app/user/user.functions.ts | 32 ++ comixed-web/src/assets/i18n/en/app.json | 8 + .../src/assets/i18n/en/comic-import.json | 8 + comixed-web/src/assets/i18n/es/app.json | 8 + .../src/assets/i18n/es/comic-import.json | 8 + comixed-web/src/assets/i18n/fr/app.json | 8 + .../src/assets/i18n/fr/comic-import.json | 8 + comixed-web/src/assets/i18n/pt/app.json | 8 + .../src/assets/i18n/pt/comic-import.json | 8 + comixed-web/src/styles.scss | 39 ++ comixed-web/tslint.json | 7 +- 31 files changed, 923 insertions(+), 79 deletions(-) create mode 100644 comixed-web/src/app/comic-import/comic-import-routing.module.ts create mode 100644 comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.html create mode 100644 comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.scss create mode 100644 comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.spec.ts create mode 100644 comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.ts create mode 100644 comixed-web/src/app/user/user.functions.ts diff --git a/comixed-web/angular.json b/comixed-web/angular.json index 0a8a806cf..2f11e37fe 100644 --- a/comixed-web/angular.json +++ b/comixed-web/angular.json @@ -17,6 +17,7 @@ "build": { "builder": "@angular-devkit/build-angular:browser", "options": { + "allowedCommonJsDependencies": ["lodash"], "outputPath": "target/classes/static", "index": "src/index.html", "main": "src/main.ts", @@ -49,13 +50,13 @@ "budgets": [ { "type": "initial", - "maximumWarning": "2mb", + "maximumWarning": "4mb", "maximumError": "5mb" }, { "type": "anyComponentStyle", - "maximumWarning": "6kb", - "maximumError": "10kb" + "maximumWarning": "150kb", + "maximumError": "150kb" } ] } diff --git a/comixed-web/src/app/app.component.spec.ts b/comixed-web/src/app/app.component.spec.ts index a874ea0fa..48aec6aba 100644 --- a/comixed-web/src/app/app.component.spec.ts +++ b/comixed-web/src/app/app.component.spec.ts @@ -23,17 +23,18 @@ import { LoggerModule } from '@angular-ru/logger'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { initialState as initialUserState, - USER_FEATURE_KEY, + USER_FEATURE_KEY } from '@app/user/reducers/user.reducer'; import { BUSY_FEATURE_KEY, - initialState as initialBusyState, + initialState as initialBusyState } from '@app/core/reducers/busy.reducer'; +import { TranslateModule } from '@ngx-translate/core'; describe('AppComponent', () => { const initialState = { [USER_FEATURE_KEY]: initialUserState, - [BUSY_FEATURE_KEY]: initialBusyState, + [BUSY_FEATURE_KEY]: initialBusyState }; let component: AppComponent; @@ -42,9 +43,13 @@ describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [RouterTestingModule, LoggerModule.forRoot()], + imports: [ + RouterTestingModule, + TranslateModule.forRoot(), + LoggerModule.forRoot() + ], declarations: [AppComponent], - providers: [provideMockStore({ initialState })], + providers: [provideMockStore({ initialState })] }).compileComponents(); fixture = TestBed.createComponent(AppComponent); diff --git a/comixed-web/src/app/app.component.ts b/comixed-web/src/app/app.component.ts index 4a8c19cf3..2e39db6ce 100644 --- a/comixed-web/src/app/app.component.ts +++ b/comixed-web/src/app/app.component.ts @@ -23,6 +23,7 @@ import { selectUser } from '@app/user/selectors/user.selectors'; import { User } from '@app/user/models/user'; import { loadCurrentUser } from '@app/user/actions/user.actions'; import { selectBusyState } from '@app/core/selectors/busy.selectors'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'cx-root', @@ -33,7 +34,12 @@ export class AppComponent implements OnInit { user: User = null; busy = false; - constructor(private logger: LoggerService, private store: Store) { + constructor( + private logger: LoggerService, + private translateService: TranslateService, + private store: Store + ) { + this.translateService.use('en'); this.logger.trace('Subscribing to user changes'); this.store.select(selectUser).subscribe(user => { this.logger.debug('User updated:', user); diff --git a/comixed-web/src/app/app.module.ts b/comixed-web/src/app/app.module.ts index f66e95ff0..45369cf17 100644 --- a/comixed-web/src/app/app.module.ts +++ b/comixed-web/src/app/app.module.ts @@ -34,7 +34,7 @@ import { StoreRouterConnectingModule } from '@ngrx/router-store'; import { TranslateCompiler, TranslateLoader, - TranslateModule + TranslateModule, } from '@ngx-translate/core'; import { HttpLoaderFactory } from '@app/app.translate'; import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler'; @@ -43,11 +43,18 @@ import { LoggerLevel, LoggerModule } from '@angular-ru/logger'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatTooltipModule } from '@angular/material/tooltip'; +import { ComicImportModule } from '@app/comic-import/comic-import.module'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { + _MatMenuDirectivesModule, + MatMenuModule, +} from '@angular/material/menu'; @NgModule({ declarations: [AppComponent, HomeComponent, NavigationBarComponent], imports: [ UserModule, + ComicImportModule, BrowserModule, AppRoutingModule, BrowserAnimationsModule, @@ -61,21 +68,24 @@ import { MatTooltipModule } from '@angular/material/tooltip'; loader: { provide: TranslateLoader, useFactory: HttpLoaderFactory, - deps: [HttpClient] + deps: [HttpClient], }, compiler: { provide: TranslateCompiler, - useClass: TranslateMessageFormatCompiler + useClass: TranslateMessageFormatCompiler, }, - defaultLanguage: 'en' + defaultLanguage: 'en', }), MatButtonModule, MatFormFieldModule, - MatTooltipModule + MatTooltipModule, + MatProgressSpinnerModule, + _MatMenuDirectivesModule, + MatMenuModule, ], providers: [ - [{ provide: HTTP_INTERCEPTORS, useClass: HttpInterceptor, multi: true }] + [{ provide: HTTP_INTERCEPTORS, useClass: HttpInterceptor, multi: true }], ], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) export class AppModule {} diff --git a/comixed-web/src/app/comic-import/comic-import-routing.module.ts b/comixed-web/src/app/comic-import/comic-import-routing.module.ts new file mode 100644 index 000000000..288d2601e --- /dev/null +++ b/comixed-web/src/app/comic-import/comic-import-routing.module.ts @@ -0,0 +1,34 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { RouterModule, Routes } from '@angular/router'; +import { NgModule } from '@angular/core'; +import { ImportComicsComponent } from './pages/import-comics/import-comics.component'; + +const routes: Routes = [ + { + path: 'admin/import', + component: ImportComicsComponent + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ComicImportRoutingModule {} diff --git a/comixed-web/src/app/comic-import/components/import-toolbar/import-toolbar.component.html b/comixed-web/src/app/comic-import/components/import-toolbar/import-toolbar.component.html index 55bf65cb3..76ad38a75 100644 --- a/comixed-web/src/app/comic-import/components/import-toolbar/import-toolbar.component.html +++ b/comixed-web/src/app/comic-import/components/import-toolbar/import-toolbar.component.html @@ -1,25 +1,29 @@
-
- +
+
+ +
+
+ + {{option.label|translate}} + +
+
+ +
-
- - {{option.label|translate}} - -
- - -
diff --git a/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.html b/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.html new file mode 100644 index 000000000..6791764e7 --- /dev/null +++ b/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.html @@ -0,0 +1,29 @@ +
+
+ +
+
+

{{"import-comic-files.page-title"|translate:{count: files.length, selected: selectedFiles.length} }}

+
+
+
+ +
+
+
+ {{"import-comic-files.label.ignore-metadata"|translate}} + {{"import-comic-files.label.delete-blocked-pages"|translate}} + + +
+
+
+
diff --git a/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.scss b/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.spec.ts b/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.spec.ts new file mode 100644 index 000000000..42528156d --- /dev/null +++ b/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.spec.ts @@ -0,0 +1,373 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ImportComicsComponent } from './import-comics.component'; +import { LoggerModule } from '@angular-ru/logger'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + COMIC_IMPORT_FEATURE_KEY, + initialState as initialComicImportState, +} from '@app/comic-import/reducers/comic-import.reducer'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { + initialState as initialUserState, + USER_FEATURE_KEY, +} from '@app/user/reducers/user.reducer'; +import { setBusyState } from '@app/core/actions/busy.actions'; +import { + COMIC_FILE_1, + COMIC_FILE_2, + COMIC_FILE_3, + COMIC_FILE_4, +} from '@app/comic-import/comic-import.fixtures'; +import { ConfirmationService } from '@app/core'; +import { Confirmation } from '@app/core/models/confirmation'; +import { sendComicFiles } from '@app/comic-import/actions/comic-import.actions'; +import { USER_ADMIN } from '@app/user/user.fixtures'; +import { User } from '@app/user/models/user'; +import { + USER_PREFERENCE_DELETE_BLOCKED_PAGES, + USER_PREFERENCE_IGNORE_METADATA, +} from '@app/user/user.constants'; +import { MatIconModule } from '@angular/material/icon'; +import { ImportToolbarComponent } from '@app/comic-import/components/import-toolbar/import-toolbar.component'; +import { ComicFileListComponent } from '@app/comic-import/components/comic-file-list/comic-file-list.component'; +import { ComicFileDetailsComponent } from '@app/comic-import/components/comic-file-details/comic-file-details.component'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTableModule } from '@angular/material/table'; +import { ComicFileCoverUrlPipe } from '@app/comic-import/pipes/comic-file-cover-url.pipe'; + +describe('ImportComicsComponent', () => { + const initialState = { + [COMIC_IMPORT_FEATURE_KEY]: initialComicImportState, + [USER_FEATURE_KEY]: initialUserState, + }; + const FILES = [COMIC_FILE_1, COMIC_FILE_2, COMIC_FILE_3, COMIC_FILE_4]; + const FILE = COMIC_FILE_3; + + let component: ImportComicsComponent; + let fixture: ComponentFixture; + let store: MockStore; + let confirmationService: ConfirmationService; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + FormsModule, + LoggerModule.forRoot(), + TranslateModule.forRoot(), + MatDialogModule, + MatButtonModule, + MatCheckboxModule, + MatIconModule, + MatInputModule, + MatSelectModule, + MatTableModule, + ], + declarations: [ + ImportComicsComponent, + ImportToolbarComponent, + ComicFileListComponent, + ComicFileDetailsComponent, + ComicFileCoverUrlPipe, + ], + providers: [provideMockStore({ initialState }), ConfirmationService], + }).compileComponents(); + + fixture = TestBed.createComponent(ImportComicsComponent); + component = fixture.componentInstance; + store = TestBed.inject(MockStore); + spyOn(store, 'dispatch'); + confirmationService = TestBed.inject(ConfirmationService); + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('loading user preferences', () => { + const IGNORE_METADATA = Math.random() > 0.5; + const DELETE_BLOCKED_PAGES = Math.random() > 0.5; + + beforeEach(() => { + const user = { + ...USER_ADMIN, + preferences: [ + { + name: USER_PREFERENCE_IGNORE_METADATA, + value: `${IGNORE_METADATA}`, + }, + { + name: USER_PREFERENCE_DELETE_BLOCKED_PAGES, + value: `${DELETE_BLOCKED_PAGES}`, + }, + ], + } as User; + store.setState({ + ...initialState, + [USER_FEATURE_KEY]: { + ...initialUserState, + user, + }, + }); + }); + + it('sets the ignore metadata flag', () => { + expect(component.ignoreMetadata).toEqual(IGNORE_METADATA); + }); + + it('sets the delete blocked pages flag', () => { + expect(component.deleteBlockedPages).toEqual(DELETE_BLOCKED_PAGES); + }); + }); + + describe('when loading files', () => { + describe('when loading starts', () => { + beforeEach(() => { + component.busy = false; + store.setState({ + ...initialState, + [COMIC_IMPORT_FEATURE_KEY]: { + ...initialComicImportState, + loading: true, + }, + }); + }); + + it('sets the busy flag', () => { + expect(component.busy).toBeTruthy(); + }); + + it('fires an action', () => { + expect(store.dispatch).toHaveBeenCalledWith( + setBusyState({ enabled: true }) + ); + }); + }); + + describe('when loading stops', () => { + beforeEach(() => { + component.busy = true; + store.setState({ + ...initialState, + [COMIC_IMPORT_FEATURE_KEY]: { + ...initialComicImportState, + loading: false, + }, + }); + }); + + it('clears the busy flag', () => { + expect(component.busy).toBeFalsy(); + }); + + it('fires an action', () => { + expect(store.dispatch).toHaveBeenCalledWith( + setBusyState({ enabled: false }) + ); + }); + }); + }); + + describe('when sending files', () => { + describe('when sending starts', () => { + beforeEach(() => { + component.busy = false; + store.setState({ + ...initialState, + [COMIC_IMPORT_FEATURE_KEY]: { + ...initialComicImportState, + sending: true, + }, + }); + }); + + it('sets the busy flag', () => { + expect(component.busy).toBeTruthy(); + }); + + it('fires an action', () => { + expect(store.dispatch).toHaveBeenCalledWith( + setBusyState({ enabled: true }) + ); + }); + }); + + describe('when sending stops', () => { + beforeEach(() => { + component.busy = true; + store.setState({ + ...initialState, + [COMIC_IMPORT_FEATURE_KEY]: { + ...initialComicImportState, + sending: false, + }, + }); + }); + + it('clears the busy flag', () => { + expect(component.busy).toBeFalsy(); + }); + + it('fires an action', () => { + expect(store.dispatch).toHaveBeenCalledWith( + setBusyState({ enabled: false }) + ); + }); + }); + }); + + describe('setting the current file', () => { + beforeEach(() => { + component.currentFile = null; + component.currentFileSelected = false; + }); + + describe('and the file is not selected', () => { + beforeEach(() => { + component.selectedFiles = []; + component.onCurrentFile(FILE); + }); + + it('sets the current file', () => { + expect(component.currentFile).toEqual(FILE); + }); + + it('clears the current file selected flag', () => { + expect(component.currentFileSelected).toBeFalsy(); + }); + }); + + describe('and the file is selected', () => { + beforeEach(() => { + component.selectedFiles = [FILE]; + component.onCurrentFile(FILE); + }); + + it('sets the current file', () => { + expect(component.currentFile).toEqual(FILE); + }); + + it('sets the current file selected flag', () => { + expect(component.currentFileSelected).toBeTruthy(); + }); + }); + }); + + describe('file selection updates', () => { + const SELECTED_FILE = COMIC_FILE_1; + const CURRENT_FILE = COMIC_FILE_2; + + describe('when there is no current file', () => { + beforeEach(() => { + component.currentFile = null; + component.currentFileSelected = true; + store.setState({ + ...initialState, + [COMIC_IMPORT_FEATURE_KEY]: { + ...initialComicImportState, + selections: [SELECTED_FILE], + }, + }); + fixture.detectChanges(); + }); + + it('clears the current file selected flag', () => { + expect(component.currentFileSelected).toBeFalsy(); + }); + }); + + describe('when the current file is not selected', () => { + beforeEach(() => { + component.currentFile = CURRENT_FILE; + component.currentFileSelected = true; + store.setState({ + ...initialState, + [COMIC_IMPORT_FEATURE_KEY]: { + ...initialComicImportState, + selections: [SELECTED_FILE], + }, + }); + fixture.detectChanges(); + }); + + it('clears the current file selected flag', () => { + expect(component.currentFileSelected).toBeFalsy(); + }); + }); + + describe('when the current file is selected', () => { + beforeEach(() => { + component.currentFile = SELECTED_FILE; + component.currentFileSelected = false; + store.setState({ + ...initialState, + [COMIC_IMPORT_FEATURE_KEY]: { + ...initialComicImportState, + selections: [SELECTED_FILE], + }, + }); + fixture.detectChanges(); + }); + + it('clears the current file selected flag', () => { + expect(component.currentFileSelected).toBeTruthy(); + }); + }); + }); + + describe('starting the import process', () => { + const IGNORE_METADATA = Math.random() > 0.5; + const DELETE_BLOCKED_PAGES = Math.random() > 0.5; + + beforeEach(() => { + component.files = FILES; + component.selectedFiles = FILES; + component.ignoreMetadata = IGNORE_METADATA; + component.deleteBlockedPages = DELETE_BLOCKED_PAGES; + + spyOn( + confirmationService, + 'confirm' + ).and.callFake((confirm: Confirmation) => confirm.confirm()); + component.onStartImport(); + }); + + it('confirms with the user', () => { + expect(confirmationService.confirm).toHaveBeenCalled(); + }); + + it('fires an action', () => { + expect(store.dispatch).toHaveBeenCalledWith( + sendComicFiles({ + files: FILES, + ignoreMetadata: IGNORE_METADATA, + deleteBlockedPages: DELETE_BLOCKED_PAGES, + }) + ); + }); + }); +}); diff --git a/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.ts b/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.ts new file mode 100644 index 000000000..0511066f4 --- /dev/null +++ b/comixed-web/src/app/comic-import/pages/import-comics/import-comics.component.ts @@ -0,0 +1,156 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { ComicFile } from '@app/comic-import/models/comic-file'; +import { LoggerService } from '@angular-ru/logger'; +import { Store } from '@ngrx/store'; +import { + selectComicFiles, + selectComicFileSelections, + selectComicImportState, +} from '@app/comic-import/selectors/comic-import.selectors'; +import { setBusyState } from '@app/core/actions/busy.actions'; +import { ConfirmationService } from '@app/core'; +import { TranslateService } from '@ngx-translate/core'; +import { sendComicFiles } from '@app/comic-import/actions/comic-import.actions'; +import { selectUser } from '@app/user/selectors/user.selectors'; +import { filter } from 'rxjs/operators'; +import { getUserPreference } from '@app/user'; +import { + USER_PREFERENCE_DELETE_BLOCKED_PAGES, + USER_PREFERENCE_IGNORE_METADATA, +} from '@app/user/user.constants'; +import { Title } from '@angular/platform-browser'; + +@Component({ + selector: 'cx-import-comics', + templateUrl: './import-comics.component.html', + styleUrls: ['./import-comics.component.scss'], +}) +export class ImportComicsComponent implements OnInit, OnDestroy { + filesSubscription: Subscription; + files: ComicFile[]; + translateSubscription: Subscription; + userSubscription: Subscription; + comicImportStateSubscription: Subscription; + selectedFilesSubscription: Subscription; + selectedFiles: ComicFile[]; + currentFile: ComicFile; + currentFileSelected: boolean; + busy = false; + ignoreMetadata = false; + deleteBlockedPages = false; + + constructor( + private logger: LoggerService, + private title: Title, + private store: Store, + private confirmationService: ConfirmationService, + private translateService: TranslateService + ) { + this.translateSubscription = this.translateService.onLangChange.subscribe( + () => this.loadTranslations() + ); + this.userSubscription = this.store + .select(selectUser) + .pipe(filter((user) => !!user)) + .subscribe((user) => { + this.ignoreMetadata = + getUserPreference( + user.preferences, + USER_PREFERENCE_IGNORE_METADATA, + 'false' + ) === 'true'; + this.deleteBlockedPages = + getUserPreference( + user.preferences, + USER_PREFERENCE_DELETE_BLOCKED_PAGES, + 'false' + ) === 'true'; + }); + this.filesSubscription = this.store + .select(selectComicFiles) + .subscribe((files) => (this.files = files)); + this.selectedFilesSubscription = this.store + .select(selectComicFileSelections) + .subscribe((selectedFiles) => { + this.selectedFiles = selectedFiles; + this.currentFileSelected = + !!this.currentFile && this.selectedFiles.includes(this.currentFile); + }); + this.comicImportStateSubscription = this.store + .select(selectComicImportState) + .subscribe((state) => { + const busy = state.sending || state.loading; + if (this.busy !== busy) { + this.logger.debug('Setting busy state:', busy); + this.busy = busy; + this.store.dispatch(setBusyState({ enabled: busy })); + } + }); + } + + ngOnInit(): void { + this.loadTranslations(); + } + + ngOnDestroy(): void { + this.translateSubscription.unsubscribe(); + this.userSubscription.unsubscribe(); + this.filesSubscription.unsubscribe(); + this.selectedFilesSubscription.unsubscribe(); + this.comicImportStateSubscription.unsubscribe(); + } + + onCurrentFile(file: ComicFile): void { + this.logger.debug('Current file changed:', file); + this.currentFile = file; + this.currentFileSelected = + !!this.currentFile && this.selectedFiles.includes(this.currentFile); + } + + onStartImport(): void { + this.confirmationService.confirm({ + title: this.translateService.instant( + 'import-comic-files.confirm-start-title' + ), + message: this.translateService.instant( + 'import-comic-files.confirm-start-message', + { count: this.selectedFiles.length } + ), + confirm: () => { + this.logger.debug('Starting import'); + this.store.dispatch( + sendComicFiles({ + files: this.selectedFiles, + ignoreMetadata: this.ignoreMetadata, + deleteBlockedPages: this.deleteBlockedPages, + }) + ); + }, + }); + } + + private loadTranslations(): void { + this.title.setTitle( + this.translateService.instant('import-comic-files.tab-title') + ); + } +} diff --git a/comixed-web/src/app/comic-import/pipes/comic-file-cover-url.pipe.spec.ts b/comixed-web/src/app/comic-import/pipes/comic-file-cover-url.pipe.spec.ts index 0a4eb08e0..c41db244a 100644 --- a/comixed-web/src/app/comic-import/pipes/comic-file-cover-url.pipe.spec.ts +++ b/comixed-web/src/app/comic-import/pipes/comic-file-cover-url.pipe.spec.ts @@ -21,7 +21,7 @@ import { COMIC_FILE_1 } from '@app/comic-import/comic-import.fixtures'; import { API_ROOT_URL } from '@app/core'; describe('ComicFileCoverUrlPipe', () => { - let pipe = new ComicFileCoverUrlPipe(); + const pipe = new ComicFileCoverUrlPipe(); it('returns the url for given page', () => { expect(pipe.transform(COMIC_FILE_1)).toEqual( diff --git a/comixed-web/src/app/comic-import/pipes/comic-file-cover-url.pipe.ts b/comixed-web/src/app/comic-import/pipes/comic-file-cover-url.pipe.ts index 318148094..34aeb2e63 100644 --- a/comixed-web/src/app/comic-import/pipes/comic-file-cover-url.pipe.ts +++ b/comixed-web/src/app/comic-import/pipes/comic-file-cover-url.pipe.ts @@ -24,9 +24,9 @@ import { API_ROOT_URL } from '@app/core'; name: 'comicFileCoverUrl', }) export class ComicFileCoverUrlPipe implements PipeTransform { - transform(comic_file: ComicFile): string { + transform(comicFile: ComicFile): string { return `${API_ROOT_URL}/files/import/cover?filename=${encodeURIComponent( - comic_file.filename + comicFile.filename )}`; } } diff --git a/comixed-web/src/app/components/navigation-bar/navigation-bar.component.html b/comixed-web/src/app/components/navigation-bar/navigation-bar.component.html index 90fc2b956..9a40f3ce4 100644 --- a/comixed-web/src/app/components/navigation-bar/navigation-bar.component.html +++ b/comixed-web/src/app/components/navigation-bar/navigation-bar.component.html @@ -1,10 +1,11 @@ - ComiXed - {{"app.logged-in"|translate:{email:user.email} }} + {{"app.logged-in"|translate:{email: user.email} }} + + + + + + + + diff --git a/comixed-web/src/app/components/navigation-bar/navigation-bar.component.spec.ts b/comixed-web/src/app/components/navigation-bar/navigation-bar.component.spec.ts index 06d902a6e..9b272b51b 100644 --- a/comixed-web/src/app/components/navigation-bar/navigation-bar.component.spec.ts +++ b/comixed-web/src/app/components/navigation-bar/navigation-bar.component.spec.ts @@ -23,12 +23,22 @@ import { LoggerModule } from '@angular-ru/logger'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { Router } from '@angular/router'; -import { CoreModule } from '@app/core/core.module'; import { ConfirmationService } from '@app/core'; import { Confirmation } from '@app/core/models/confirmation'; import { logoutUser } from '@app/user/actions/user.actions'; +import { + initialState as initialUserState, + USER_FEATURE_KEY, +} from '@app/user/reducers/user.reducer'; +import { USER_ADMIN, USER_READER } from '@app/user/user.fixtures'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatMenuModule } from '@angular/material/menu'; describe('NavigationBarComponent', () => { + const initialState = { + [USER_FEATURE_KEY]: { ...initialUserState, user: USER_ADMIN }, + }; + let component: NavigationBarComponent; let fixture: ComponentFixture; let store: MockStore; @@ -38,13 +48,14 @@ describe('NavigationBarComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ - CoreModule, RouterTestingModule.withRoutes([{ path: '*', redirectTo: '' }]), TranslateModule.forRoot(), - LoggerModule.forRoot() + LoggerModule.forRoot(), + MatDialogModule, + MatMenuModule, ], declarations: [NavigationBarComponent], - providers: [provideMockStore({})] + providers: [provideMockStore({ initialState })], }).compileComponents(); fixture = TestBed.createComponent(NavigationBarComponent); @@ -73,9 +84,10 @@ describe('NavigationBarComponent', () => { describe('on logout clicked', () => { beforeEach(() => { - spyOn(confirmationService, 'confirm').and.callFake( - (confirm: Confirmation) => confirm.confirm() - ); + spyOn( + confirmationService, + 'confirm' + ).and.callFake((confirm: Confirmation) => confirm.confirm()); component.onLogout(); }); @@ -87,4 +99,26 @@ describe('NavigationBarComponent', () => { expect(router.navigate).toHaveBeenCalledWith(['login']); }); }); + + describe('when an admin user is logged in', () => { + beforeEach(() => { + component.isAdmin = false; + component.user = USER_ADMIN; + }); + + it('sets the admin flag', () => { + expect(component.isAdmin).toBeTruthy(); + }); + }); + + describe('when an admin reader is logged in', () => { + beforeEach(() => { + component.isAdmin = true; + component.user = USER_READER; + }); + + it('clears the admin flag', () => { + expect(component.isAdmin).toBeFalsy(); + }); + }); }); diff --git a/comixed-web/src/app/components/navigation-bar/navigation-bar.component.ts b/comixed-web/src/app/components/navigation-bar/navigation-bar.component.ts index cc63a1561..a9fed1679 100644 --- a/comixed-web/src/app/components/navigation-bar/navigation-bar.component.ts +++ b/comixed-web/src/app/components/navigation-bar/navigation-bar.component.ts @@ -24,14 +24,17 @@ import { ConfirmationService } from '@app/core'; import { TranslateService } from '@ngx-translate/core'; import { logoutUser } from '@app/user/actions/user.actions'; import { Store } from '@ngrx/store'; +import { ROLE_NAME_ADMIN } from '@app/user/user.constants'; @Component({ selector: 'cx-navigation-bar', templateUrl: './navigation-bar.component.html', - styleUrls: ['./navigation-bar.component.scss'] + styleUrls: ['./navigation-bar.component.scss'], }) export class NavigationBarComponent { - @Input() user: User; + private _user: User; + + isAdmin = false; constructor( private logger: LoggerService, @@ -41,6 +44,17 @@ export class NavigationBarComponent { private translateService: TranslateService ) {} + @Input() + set user(user: User) { + this._user = user; + this.isAdmin = + !!user && user.roles.some((role) => role.name === ROLE_NAME_ADMIN); + } + + get user(): User { + return this._user; + } + onLogin(): void { this.logger.trace('Navigating to the login page'); this.router.navigate(['login']); @@ -57,7 +71,7 @@ export class NavigationBarComponent { this.logger.debug('User logged out'); this.store.dispatch(logoutUser()); this.router.navigate(['login']); - } + }, }); } } diff --git a/comixed-web/src/app/interceptors/http.interceptor.spec.ts b/comixed-web/src/app/interceptors/http.interceptor.spec.ts index 29f20f3f0..07f8b992b 100644 --- a/comixed-web/src/app/interceptors/http.interceptor.spec.ts +++ b/comixed-web/src/app/interceptors/http.interceptor.spec.ts @@ -20,12 +20,11 @@ import { TestBed } from '@angular/core/testing'; import { HttpInterceptor } from './http.interceptor'; import { LoggerModule } from '@angular-ru/logger'; import { TokenService } from '@app/core'; -import { CoreModule } from '@app/core/core.module'; import { AUTHENTICATION_TOKEN } from '@app/core/core.fixtures'; import { HttpClientTestingModule, HttpTestingController, - TestRequest + TestRequest, } from '@angular/common/http/testing'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; @@ -33,7 +32,7 @@ import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; import { HTTP_AUTHORIZATION_HEADER, HTTP_REQUESTED_WITH_HEADER, - HTTP_XML_REQUEST + HTTP_XML_REQUEST, } from '@app/app.constants'; const TEST_REQUEST_URL = 'http://localhost'; @@ -54,7 +53,7 @@ describe('HttpInterceptor', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CoreModule, HttpClientTestingModule, LoggerModule.forRoot()], + imports: [HttpClientTestingModule, LoggerModule.forRoot()], providers: [ { provide: HTTP_INTERCEPTORS, useClass: HttpInterceptor, multi: true }, TestService, @@ -62,10 +61,10 @@ describe('HttpInterceptor', () => { provide: TokenService, useValue: { hasAuthToken: jasmine.createSpy('TokenService.hasAuthToken()'), - getAuthToken: jasmine.createSpy('TokenService.getAuthToken()') - } - } - ] + getAuthToken: jasmine.createSpy('TokenService.getAuthToken()'), + }, + }, + ], }); tokenService = TestBed.inject(TokenService) as jasmine.SpyObj; diff --git a/comixed-web/src/app/user/effects/user.effects.spec.ts b/comixed-web/src/app/user/effects/user.effects.spec.ts index db9628bb4..c9e9e8d8e 100644 --- a/comixed-web/src/app/user/effects/user.effects.spec.ts +++ b/comixed-web/src/app/user/effects/user.effects.spec.ts @@ -28,16 +28,18 @@ import { loadCurrentUser, loadCurrentUserFailed, loginUser, - loginUserFailed, logoutUser, - userLoggedIn, userLoggedOut + loginUserFailed, + logoutUser, + userLoggedIn, + userLoggedOut, } from '@app/user/actions/user.actions'; import { hot } from 'jasmine-marbles'; import { LoggerModule } from '@angular-ru/logger'; import { AlertService, ApiResponse, TokenService } from '@app/core'; -import { CoreModule } from '@app/core/core.module'; import { TranslateModule } from '@ngx-translate/core'; import { HttpErrorResponse } from '@angular/common/http'; import { LoginResponse } from '@app/user/models/net/login-response'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; describe('UserEffects', () => { const USER = USER_READER; @@ -52,7 +54,11 @@ describe('UserEffects', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CoreModule, LoggerModule.forRoot(), TranslateModule.forRoot()], + imports: [ + LoggerModule.forRoot(), + TranslateModule.forRoot(), + MatSnackBarModule, + ], providers: [ UserEffects, provideMockActions(() => actions$), @@ -60,10 +66,10 @@ describe('UserEffects', () => { provide: UserService, useValue: { loadCurrentUser: jasmine.createSpy('UserService.loadCurrentUser()'), - loginUser: jasmine.createSpy('userService.loginUser()') - } - } - ] + loginUser: jasmine.createSpy('userService.loginUser()'), + }, + }, + ], }); effects = TestBed.inject(UserEffects); @@ -83,7 +89,7 @@ describe('UserEffects', () => { it('fires an action on success', () => { const serviceResponse: ApiResponse = { success: true, - result: USER + result: USER, }; const action = loadCurrentUser(); const outcome = currentUserLoaded({ user: USER }); @@ -97,7 +103,7 @@ describe('UserEffects', () => { it('fires an action on failure', () => { const serviceResponse: ApiResponse = { - success: false + success: false, }; const action = loadCurrentUser(); const outcome = loadCurrentUserFailed(); @@ -139,7 +145,7 @@ describe('UserEffects', () => { it('fires an action on success', () => { const serviceResponse = { success: true, - result: { email: USER.email, token: AUTH_TOKEN } + result: { email: USER.email, token: AUTH_TOKEN }, } as ApiResponse; const action = loginUser({ email: USER.email, password: PASSWORD }); const outcome1 = userLoggedIn({ token: AUTH_TOKEN }); @@ -199,9 +205,9 @@ describe('UserEffects', () => { const action = logoutUser(); const outcome = userLoggedOut(); - actions$ = hot('-a', {a: action}); + actions$ = hot('-a', { a: action }); - const expected = hot('-b', {b: outcome}); + const expected = hot('-b', { b: outcome }); expect(effects.logoutUser$).toBeObservable(expected); expect(tokenService.clearAuthToken).toHaveBeenCalled(); }); diff --git a/comixed-web/src/app/user/index.ts b/comixed-web/src/app/user/index.ts index 469aecb7b..1195fd896 100644 --- a/comixed-web/src/app/user/index.ts +++ b/comixed-web/src/app/user/index.ts @@ -25,6 +25,8 @@ import { UserState } from './reducers/user.reducer'; +export * from '@app/user/user.functions'; + interface RouterStateUrl { url: string; params: Params; diff --git a/comixed-web/src/app/user/user.constants.ts b/comixed-web/src/app/user/user.constants.ts index 064e2c86e..9b57b2a5b 100644 --- a/comixed-web/src/app/user/user.constants.ts +++ b/comixed-web/src/app/user/user.constants.ts @@ -18,5 +18,17 @@ import { API_ROOT_URL } from '@app/core'; +export const ROLE_NAME_READER = 'reader'; +export const ROLE_NAME_ADMIN = 'admin'; + export const LOAD_CURRENT_USER_URL = `${API_ROOT_URL}/user`; export const LOGIN_USER_URL = `${API_ROOT_URL}/token/generate-token`; + +// user preferences +export const USER_PREFERENCE_IMPORT_ROOT_DIRECTORY = + 'comic-files.import.root-directory'; +export const USER_PREFERENCE_IMPORT_MAXIMUM = 'comic-files.import.maximum'; +export const USER_PREFERENCE_IGNORE_METADATA = + 'comic-files.import.ignore-metadata'; +export const USER_PREFERENCE_DELETE_BLOCKED_PAGES = + 'comic-files.import.delete-blocked-pages'; diff --git a/comixed-web/src/app/user/user.fixtures.ts b/comixed-web/src/app/user/user.fixtures.ts index bf5f88bb5..9b180bf7c 100644 --- a/comixed-web/src/app/user/user.fixtures.ts +++ b/comixed-web/src/app/user/user.fixtures.ts @@ -18,13 +18,14 @@ import { User } from './models/user'; import { Role } from '@app/user/models/role'; +import { ROLE_NAME_ADMIN, ROLE_NAME_READER } from '@app/user/user.constants'; export const ROLE_READER: Role = { - name: 'READER' + name: ROLE_NAME_READER, }; export const ROLE_ADMIN: Role = { - name: 'ADMIN' + name: ROLE_NAME_ADMIN, }; export const USER_READER: User = { @@ -35,7 +36,7 @@ export const USER_READER: User = { ).getTime(), last_login_date: new Date().getTime(), roles: [ROLE_READER], - preferences: [] + preferences: [], }; export const USER_ADMIN: User = { @@ -46,7 +47,7 @@ export const USER_ADMIN: User = { ).getTime(), last_login_date: new Date().getTime(), roles: [ROLE_ADMIN], - preferences: [] + preferences: [], }; export const USER_BLOCKED: User = { @@ -57,5 +58,5 @@ export const USER_BLOCKED: User = { ).getTime(), last_login_date: new Date().getTime(), roles: [], - preferences: [] + preferences: [], }; diff --git a/comixed-web/src/app/user/user.functions.ts b/comixed-web/src/app/user/user.functions.ts new file mode 100644 index 000000000..b1bec66bb --- /dev/null +++ b/comixed-web/src/app/user/user.functions.ts @@ -0,0 +1,32 @@ +/* + * ComiXed - A digital comic book library management application. + * Copyright (C) 2020, The ComiXed Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +import { Preference } from '@app/user/models/preference'; + +/** Find a specific user preference. */ +export function getUserPreference( + preferences: Preference[], + name: string, + defaultValue: string +): string { + const found = preferences.find(preference => preference.name === name); + if (!found) { + return defaultValue; + } + return found.value; +} diff --git a/comixed-web/src/assets/i18n/en/app.json b/comixed-web/src/assets/i18n/en/app.json index 22413ff5f..81e108d46 100644 --- a/comixed-web/src/assets/i18n/en/app.json +++ b/comixed-web/src/assets/i18n/en/app.json @@ -5,11 +5,19 @@ }, "button": { "deselect": "Deselect", + "load": "Load", "login": "Login", "logout": "Logout", "no": "No", "select": "Select", + "start": "Start", "submit": "Submit", "yes": "Yes" + }, + "menu": { + "admin-menu": { + "root": "Admin", + "import-comic-files": "Import Comics" + } } } diff --git a/comixed-web/src/assets/i18n/en/comic-import.json b/comixed-web/src/assets/i18n/en/comic-import.json index ba472fe74..18ebd41ab 100644 --- a/comixed-web/src/assets/i18n/en/comic-import.json +++ b/comixed-web/src/assets/i18n/en/comic-import.json @@ -12,6 +12,8 @@ } }, "import-comic-files": { + "tab-title": "Import Comics", + "page-title": "Import Comics: {selected, plural, =1{One} other{#}} Of {count, plural, =1{One Comic} other{# Comics}} Selected", "header": { "cover": "Cover Image", "filename": "Filename", @@ -21,6 +23,12 @@ "selected-for-import": "This file will be imported.", "file-size": "{size} bytes" }, + "label": { + "ignore-metadata": "Ignore any metadata", + "delete-blocked-pages": "Mark blocked pages as delete" + }, + "confirm-start-title": "Import Comic Files?", + "confirm-start-message": "Are you sure you want to import the {count, plural, =1{one comic} other{# comic files}} into the library?" }, "send-comic-files": { "effect-success-detail": "Submitted {count, plural, =1{one comic file} other{# comic files}} for import.", diff --git a/comixed-web/src/assets/i18n/es/app.json b/comixed-web/src/assets/i18n/es/app.json index 22413ff5f..81e108d46 100644 --- a/comixed-web/src/assets/i18n/es/app.json +++ b/comixed-web/src/assets/i18n/es/app.json @@ -5,11 +5,19 @@ }, "button": { "deselect": "Deselect", + "load": "Load", "login": "Login", "logout": "Logout", "no": "No", "select": "Select", + "start": "Start", "submit": "Submit", "yes": "Yes" + }, + "menu": { + "admin-menu": { + "root": "Admin", + "import-comic-files": "Import Comics" + } } } diff --git a/comixed-web/src/assets/i18n/es/comic-import.json b/comixed-web/src/assets/i18n/es/comic-import.json index ba472fe74..18ebd41ab 100644 --- a/comixed-web/src/assets/i18n/es/comic-import.json +++ b/comixed-web/src/assets/i18n/es/comic-import.json @@ -12,6 +12,8 @@ } }, "import-comic-files": { + "tab-title": "Import Comics", + "page-title": "Import Comics: {selected, plural, =1{One} other{#}} Of {count, plural, =1{One Comic} other{# Comics}} Selected", "header": { "cover": "Cover Image", "filename": "Filename", @@ -21,6 +23,12 @@ "selected-for-import": "This file will be imported.", "file-size": "{size} bytes" }, + "label": { + "ignore-metadata": "Ignore any metadata", + "delete-blocked-pages": "Mark blocked pages as delete" + }, + "confirm-start-title": "Import Comic Files?", + "confirm-start-message": "Are you sure you want to import the {count, plural, =1{one comic} other{# comic files}} into the library?" }, "send-comic-files": { "effect-success-detail": "Submitted {count, plural, =1{one comic file} other{# comic files}} for import.", diff --git a/comixed-web/src/assets/i18n/fr/app.json b/comixed-web/src/assets/i18n/fr/app.json index 22413ff5f..81e108d46 100644 --- a/comixed-web/src/assets/i18n/fr/app.json +++ b/comixed-web/src/assets/i18n/fr/app.json @@ -5,11 +5,19 @@ }, "button": { "deselect": "Deselect", + "load": "Load", "login": "Login", "logout": "Logout", "no": "No", "select": "Select", + "start": "Start", "submit": "Submit", "yes": "Yes" + }, + "menu": { + "admin-menu": { + "root": "Admin", + "import-comic-files": "Import Comics" + } } } diff --git a/comixed-web/src/assets/i18n/fr/comic-import.json b/comixed-web/src/assets/i18n/fr/comic-import.json index ba472fe74..18ebd41ab 100644 --- a/comixed-web/src/assets/i18n/fr/comic-import.json +++ b/comixed-web/src/assets/i18n/fr/comic-import.json @@ -12,6 +12,8 @@ } }, "import-comic-files": { + "tab-title": "Import Comics", + "page-title": "Import Comics: {selected, plural, =1{One} other{#}} Of {count, plural, =1{One Comic} other{# Comics}} Selected", "header": { "cover": "Cover Image", "filename": "Filename", @@ -21,6 +23,12 @@ "selected-for-import": "This file will be imported.", "file-size": "{size} bytes" }, + "label": { + "ignore-metadata": "Ignore any metadata", + "delete-blocked-pages": "Mark blocked pages as delete" + }, + "confirm-start-title": "Import Comic Files?", + "confirm-start-message": "Are you sure you want to import the {count, plural, =1{one comic} other{# comic files}} into the library?" }, "send-comic-files": { "effect-success-detail": "Submitted {count, plural, =1{one comic file} other{# comic files}} for import.", diff --git a/comixed-web/src/assets/i18n/pt/app.json b/comixed-web/src/assets/i18n/pt/app.json index 22413ff5f..81e108d46 100644 --- a/comixed-web/src/assets/i18n/pt/app.json +++ b/comixed-web/src/assets/i18n/pt/app.json @@ -5,11 +5,19 @@ }, "button": { "deselect": "Deselect", + "load": "Load", "login": "Login", "logout": "Logout", "no": "No", "select": "Select", + "start": "Start", "submit": "Submit", "yes": "Yes" + }, + "menu": { + "admin-menu": { + "root": "Admin", + "import-comic-files": "Import Comics" + } } } diff --git a/comixed-web/src/assets/i18n/pt/comic-import.json b/comixed-web/src/assets/i18n/pt/comic-import.json index ba472fe74..18ebd41ab 100644 --- a/comixed-web/src/assets/i18n/pt/comic-import.json +++ b/comixed-web/src/assets/i18n/pt/comic-import.json @@ -12,6 +12,8 @@ } }, "import-comic-files": { + "tab-title": "Import Comics", + "page-title": "Import Comics: {selected, plural, =1{One} other{#}} Of {count, plural, =1{One Comic} other{# Comics}} Selected", "header": { "cover": "Cover Image", "filename": "Filename", @@ -21,6 +23,12 @@ "selected-for-import": "This file will be imported.", "file-size": "{size} bytes" }, + "label": { + "ignore-metadata": "Ignore any metadata", + "delete-blocked-pages": "Mark blocked pages as delete" + }, + "confirm-start-title": "Import Comic Files?", + "confirm-start-message": "Are you sure you want to import the {count, plural, =1{one comic} other{# comic files}} into the library?" }, "send-comic-files": { "effect-success-detail": "Submitted {count, plural, =1{one comic file} other{# comic files}} for import.", diff --git a/comixed-web/src/styles.scss b/comixed-web/src/styles.scss index 7730e02ae..2bf41c241 100644 --- a/comixed-web/src/styles.scss +++ b/comixed-web/src/styles.scss @@ -1,3 +1,5 @@ +@import "~@angular/material/prebuilt-themes/indigo-pink.css"; + html, body { height: 100%; @@ -9,15 +11,52 @@ body { } // component sizes +.cx-width-20 { + width: 20%; +} + .cx-width-50 { width: 50%; } +.cx-width-80 { + width: 80%; +} + .cx-width-100 { width: 100%; } +.cx-height-100 { + height: 100vh; +} + +// padding + +.cx-padding-left-5 { + padding-left: 5px; +} + +.cx-padding-right-15 { + padding-right: 15px; +} + +// table column widths + +$cx-column-small: 50px; +$cx-column-medium: 150px; +$cx-column-large: 325px; + +// text layout + +.cx-text-nowrap { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + // component position + .cx-centered { position: absolute !important; top: 50%; diff --git a/comixed-web/tslint.json b/comixed-web/tslint.json index 7ca464ba7..7b957950d 100644 --- a/comixed-web/tslint.json +++ b/comixed-web/tslint.json @@ -75,7 +75,12 @@ ] }, "variable-name": { - "options": ["ban-keywords", "check-format", "allow-pascal-case"] + "options": [ + "ban-keywords", + "check-format", + "allow-pascal-case", + "allow-leading-underscore" + ] }, "whitespace": { "options": [