Misc Polishing (#413)

* Ensure that after we assign a role to a user, we show it immediately

* Cached libraryType api as that is not going to change in a viewing session. Moved some components around to tighten bundles.

* Cleaned up more TODOs
* Refactored Configuration to use getter and setters so that the interface is a lot cleaner. Updated HashUtil to use JWT Secret instead of Machine name (as docker machine name is random each boot).
This commit is contained in:
Joseph Milazzo 2021-07-20 21:39:44 -05:00 committed by GitHub
parent ef5b22b585
commit b8165b311c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 408 additions and 307 deletions

View file

@ -1,15 +0,0 @@
<ng-container *ngIf="data !== undefined">
<div class="card" style="width: 18rem;">
<div class="overlay" (click)="handleClick()">
<i class="fa {{icon}} card-img-top text-center" aria-hidden="true"></i>
<div class="card-actions">
<app-card-actionables [actions]="actions" [labelBy]="data.name" iconClass="fa-ellipsis-v" (actionHandler)="performAction($event)"></app-card-actionables>
</div>
</div>
<div class="card-body text-center" *ngIf="data.name.length > 0 || actions.length > 0">
<span class="card-data.name" (click)="handleClick()">
{{data.name}}
</span>
</div>
</div>
</ng-container>

View file

@ -1,58 +0,0 @@
$primary-color: #cc7b19;
.card {
margin: 10px;
max-width: 160px;
cursor: pointer;
}
.card-title {
margin-top: 5px;
line-height: 20px;
font-size: 13px;
text-overflow: ellipsis;
white-space: normal;
max-width: 150px;
height: 52px;
padding-top: 10px;
}
.card-img-top {
height: 160px;
margin-top: 40% !important;
margin: auto;
font-size: 52px;
}
.overlay {
height: 160px;
&:hover {
background-color: rgba(0, 0, 0, 0.4);
visibility: visible;
.overlay-item {
visibility: visible;
}
}
.overlay-item {
visibility: hidden;
}
z-index: 10;
}
.card-actions {
position: absolute;
top: 125px;
right: -5px;
}
.card-body {
padding: 5px !important;
}
.dropdown-toggle:after {
content: none !important;
}

View file

@ -1,77 +0,0 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Router } from '@angular/router';
import { take } from 'rxjs/operators';
import { Library } from 'src/app/_models/library';
import { AccountService } from 'src/app/_services/account.service';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ActionService } from 'src/app/_services/action.service';
// Represents a library type card.
@Component({
selector: 'app-library-card',
templateUrl: './library-card.component.html',
styleUrls: ['./library-card.component.scss']
})
export class LibraryCardComponent implements OnInit, OnChanges {
@Input() data!: Library;
@Output() clicked = new EventEmitter<Library>();
isAdmin = false;
actions: ActionItem<Library>[] = [];
icon = 'fa-book-open';
constructor(private accountService: AccountService, private router: Router,
private actionFactoryService: ActionFactoryService, private actionService: ActionService) {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.isAdmin = this.accountService.hasAdminRole(user);
}
});
}
ngOnInit(): void {
}
ngOnChanges(changes: any) {
if (this.data) {
if (this.data.type === 0 || this.data.type === 1) {
this.icon = 'fa-book-open';
} else {
this.icon = 'fa-book';
}
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
}
}
handleAction(action: Action, library: Library) {
switch (action) {
case(Action.ScanLibrary):
this.actionService.scanLibrary(library);
break;
case(Action.RefreshMetadata):
this.actionService.refreshMetadata(library);
break;
default:
break;
}
}
performAction(action: ActionItem<Library>) {
if (typeof action.callback === 'function') {
action.callback(action.action, this.data);
}
}
handleClick() {
this.clicked.emit(this.data);
this.router.navigate(['library', this.data?.id]);
}
preventClick(event: any) {
event.stopPropagation();
event.preventDefault();
}
}

View file

@ -1,27 +0,0 @@
<div class="text-danger" *ngIf="errors.length > 0">
<p>Errors:</p>
<ul>
<li *ngFor="let error of errors">{{error}}</li>
</ul>
</div>
<form [formGroup]="registerForm" (ngSubmit)="register()">
<div class="form-group">
<label for="username">Username</label>
<input id="username" class="form-control" formControlName="username" type="text">
</div>
<div class="form-group">
<label for="password">Password</label>
<input id="password" class="form-control" formControlName="password" type="password">
</div>
<div class="form-check" *ngIf="!firstTimeFlow">
<input id="admin" type="checkbox" aria-label="Admin" class="form-check-input" formControlName="isAdmin">
<label for="admin" class="form-check-label">Admin</label>
</div>
<div class="float-right">
<button class="btn btn-secondary mr-2" type="button" (click)="cancel()">Cancel</button>
<button class="btn btn-primary" type="submit">Register</button>
</div>
</form>

View file

@ -1,44 +0,0 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { AccountService } from 'src/app/_services/account.service';
@Component({
selector: 'app-register-member',
templateUrl: './register-member.component.html',
styleUrls: ['./register-member.component.scss']
})
export class RegisterMemberComponent implements OnInit {
@Input() firstTimeFlow = false;
@Output() created = new EventEmitter<boolean>();
adminExists = false;
registerForm: FormGroup = new FormGroup({
username: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required]),
isAdmin: new FormControl(false, [])
});
errors: string[] = [];
constructor(private accountService: AccountService) {
}
ngOnInit(): void {
if (this.firstTimeFlow) {
this.registerForm.get('isAdmin')?.setValue(true);
}
}
register() {
this.accountService.register(this.registerForm.value).subscribe(resp => {
this.created.emit(true);
}, err => {
this.errors = err;
});
}
cancel() {
this.created.emit(false);
}
}

View file

@ -1,3 +0,0 @@
<ng-container *ngIf="data !== undefined">
<app-card-item [title]="data.name" [actions]="actions" [supressLibraryLink]="suppressLibraryLink" [imageUrl]="imageService.getSeriesCoverImage(data.id)" [entity]="data" [total]="data.pages" [read]="data.pagesRead" (clicked)="handleClick()"></app-card-item>
</ng-container>

View file

@ -1,146 +0,0 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators';
import { EditSeriesModalComponent } from 'src/app/_modals/edit-series-modal/edit-series-modal.component';
import { Series } from 'src/app/_models/series';
import { AccountService } from 'src/app/_services/account.service';
import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service';
import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service';
import { SeriesService } from 'src/app/_services/series.service';
import { ConfirmService } from '../confirm.service';
@Component({
selector: 'app-series-card',
templateUrl: './series-card.component.html',
styleUrls: ['./series-card.component.scss']
})
export class SeriesCardComponent implements OnInit, OnChanges {
@Input() data: Series | undefined;
@Input() libraryId = 0;
@Input() suppressLibraryLink = false;
@Output() clicked = new EventEmitter<Series>();
@Output() reload = new EventEmitter<boolean>();
@Output() dataChanged = new EventEmitter<Series>();
isAdmin = false;
actions: ActionItem<Series>[] = [];
constructor(private accountService: AccountService, private router: Router,
private seriesService: SeriesService, private toastr: ToastrService,
private libraryService: LibraryService, private modalService: NgbModal,
private confirmService: ConfirmService, public imageService: ImageService,
private actionFactoryService: ActionFactoryService) {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.isAdmin = this.accountService.hasAdminRole(user);
}
});
}
ngOnInit(): void {
}
ngOnChanges(changes: any) {
if (this.data) {
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
}
}
handleSeriesActionCallback(action: Action, series: Series) {
switch (action) {
case(Action.MarkAsRead):
this.markAsRead(series);
break;
case(Action.MarkAsUnread):
this.markAsUnread(series);
break;
case(Action.ScanLibrary):
this.scanLibrary(series);
break;
case(Action.RefreshMetadata):
this.refreshMetdata(series);
break;
case(Action.Delete):
this.deleteSeries(series);
break;
case(Action.Edit):
this.openEditModal(series);
break;
default:
break;
}
}
openEditModal(data: Series) {
const modalRef = this.modalService.open(EditSeriesModalComponent, { size: 'lg', scrollable: true });
modalRef.componentInstance.series = data;
modalRef.closed.subscribe((closeResult: {success: boolean, series: Series}) => {
window.scrollTo(0, 0);
if (closeResult.success) {
this.seriesService.getSeries(data.id).subscribe(series => {
this.data = series;
this.reload.emit(true);
this.dataChanged.emit(series);
});
}
});
}
refreshMetdata(series: Series) {
this.seriesService.refreshMetadata(series).subscribe((res: any) => {
this.toastr.success('Refresh started for ' + series.name);
});
}
scanLibrary(series: Series) {
this.seriesService.scan(series.libraryId, series.id).subscribe((res: any) => {
this.toastr.success('Scan started for ' + series.name);
});
}
async deleteSeries(series: Series) {
if (!await this.confirmService.confirm('Are you sure you want to delete this series? It will not modify files on disk.')) {
return;
}
this.seriesService.delete(series.id).subscribe((res: boolean) => {
if (res) {
this.toastr.success('Series deleted');
this.reload.emit(true);
}
});
}
markAsUnread(series: Series) {
this.seriesService.markUnread(series.id).subscribe(res => {
this.toastr.success(series.name + ' is now unread');
series.pagesRead = 0;
if (this.data) {
this.data.pagesRead = 0;
}
this.dataChanged.emit(series);
});
}
markAsRead(series: Series) {
this.seriesService.markRead(series.id).subscribe(res => {
this.toastr.success(series.name + ' is now read');
series.pagesRead = series.pages;
if (this.data) {
this.data.pagesRead = series.pages;
}
this.dataChanged.emit(series);
});
}
handleClick() {
this.clicked.emit(this.data);
this.router.navigate(['library', this.libraryId, 'series', this.data?.id]);
}
}

View file

@ -3,14 +3,12 @@ import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { CardItemComponent } from './card-item/card-item.component';
import { NgbCollapseModule, NgbDropdownModule, NgbPaginationModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { LibraryCardComponent } from './library-card/library-card.component';
import { SeriesCardComponent } from './series-card/series-card.component';
import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { SafeHtmlPipe } from './safe-html.pipe';
import { LazyLoadImageModule } from 'ng-lazyload-image';
import { CardActionablesComponent } from './card-item/card-actionables/card-actionables.component';
import { RegisterMemberComponent } from './register-member/register-member.component';
import { RegisterMemberComponent } from '../register-member/register-member.component';
import { ReadMoreComponent } from './read-more/read-more.component';
import { RouterModule } from '@angular/router';
import { DrawerComponent } from './drawer/drawer.component';
@ -24,8 +22,6 @@ import { A11yClickDirective } from './a11y-click.directive';
declarations: [
RegisterMemberComponent,
CardItemComponent,
LibraryCardComponent,
SeriesCardComponent,
CardDetailsModalComponent,
ConfirmDialogComponent,
SafeHtmlPipe,
@ -49,10 +45,8 @@ import { A11yClickDirective } from './a11y-click.directive';
NgbPaginationModule // CardDetailLayoutComponent
],
exports: [
RegisterMemberComponent, // TODO: Move this out and put in normal app
RegisterMemberComponent,
CardItemComponent,
LibraryCardComponent, // TODO: Move this out and put in normal app
SeriesCardComponent, // TODO: Move this out and put in normal app
SafeHtmlPipe,
CardActionablesComponent,
ReadMoreComponent,