Disable Animations + Lots of bugfixes and Polish (#1561)

* Fixed inputs not showing inline validation due to a missing class

* Fixed some checks

* Increased the button size on manga reader (develop)

* Migrated a type cast to a pure pipe

* Sped up the check for if SendTo should render on the menu

* Don't allow user to bookmark in bookmark mode

* Fixed a bug where Scan Series would skip over Specials due to how new scan loop works.

* Fixed scroll to top button persisting when navigating between pages

* Edit Series modal now doesn't have a lock field for Series, which can't be locked as it is inheritently locked.

Added some validation to ensure Name and SortName are required.

* Fixed up some spacing

* Fixed actionable menu not opening submenu on mobile

* Cleaned up the layout of cover image on series detail

* Show all volume or chapters (if only one volume) for cover selection on series

* Don't open submenu to right if there is no space

* Fixed up cover image not allowing custom saves of existing series/chapter/volume images.

Fixed up logging so console output matches log file.

* Implemented the ability to turn off css transitions in the UI.

* Updated a note internally

* Code smells

* Added InstallId when pinging the email service to allow throughput tracking
This commit is contained in:
Joe Milazzo 2022-09-26 12:40:25 -05:00 committed by GitHub
parent ee7d109170
commit 28ab34c66d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 2103 additions and 444 deletions

View file

@ -16,9 +16,13 @@
<div class="row g-0">
<div class="mb-3" style="width: 100%">
<label for="name" class="form-label">Name</label>
<div class="input-group {{series.nameLocked ? 'lock-active' : ''}}">
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'nameLocked' }"></ng-container>
<input id="name" class="form-control" formControlName="name" type="text">
<div class="input-group">
<input id="name" class="form-control" formControlName="name" type="text" [class.is-invalid]="editSeriesForm.get('name')?.invalid && editSeriesForm.get('name')?.touched">
<ng-container *ngIf="editSeriesForm.get('name')?.errors as errors">
<div class="invalid-feedback" *ngIf="errors.required">
This field is required
</div>
</ng-container>
</div>
</div>
</div>
@ -26,9 +30,15 @@
<div class="row g-0">
<div class="mb-3" style="width: 100%">
<label for="sort-name" class="form-label">Sort Name</label>
<div class="input-group {{series.sortNameLocked ? 'lock-active' : ''}}">
<div class="input-group {{series.sortNameLocked ? 'lock-active' : ''}}"
[class.is-invalid]="editSeriesForm.get('sortName')?.invalid && editSeriesForm.get('sortName')?.touched">
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: series, field: 'sortNameLocked' }"></ng-container>
<input id="sort-name" class="form-control" formControlName="sortName" type="text">
<ng-container *ngIf="editSeriesForm.get('sortName')?.errors as errors">
<div class="invalid-feedback" *ngIf="errors.required">
This field is required
</div>
</ng-container>
</div>
</div>
</div>
@ -418,7 +428,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>
<button type="submit" class="btn btn-primary" (click)="save()">Save</button>
<button type="submit" class="btn btn-primary" [disabled]="!editSeriesForm.valid" (click)="save()">Save</button>
</div>
</div>

View file

@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { forkJoin, Observable, of, Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
@ -126,9 +126,9 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.editSeriesForm = this.fb.group({
id: new FormControl(this.series.id, []),
summary: new FormControl('', []),
name: new FormControl(this.series.name, []),
name: new FormControl(this.series.name, [Validators.required]),
localizedName: new FormControl(this.series.localizedName, []),
sortName: new FormControl(this.series.sortName, []),
sortName: new FormControl(this.series.sortName, [Validators.required]),
rating: new FormControl(this.series.userRating, []),
coverImageIndex: new FormControl(0, []),
@ -209,6 +209,12 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.seriesVolumes = volumes;
this.isLoadingVolumes = false;
if (this.seriesVolumes.length === 1) {
this.imageUrls.push(...this.seriesVolumes[0].chapters.map((c: Chapter) => this.imageService.getChapterCoverImage(c.id)));
} else {
this.imageUrls.push(...this.seriesVolumes.map(v => this.imageService.getVolumeCoverImage(v.id)));
}
volumes.forEach(v => {
this.volumeCollapsed[v.name] = true;
});

View file

@ -134,6 +134,12 @@ export class CardDetailDrawerComponent implements OnInit, OnDestroy {
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this))
.filter(item => item.action !== Action.Edit);
this.chapterActions.push({title: 'Read', action: Action.Read, callback: this.handleChapterActionCallback.bind(this), requiresAdmin: false, children: []});
if (this.isChapter) {
const chapter = this.utilityService.asChapter(this.data);
this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, chapter);
} else {
this.chapterActions = this.actionFactoryService.filterSendToAction(this.chapterActions, this.chapters[0]);
}
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
this.libraryType = type;

View file

@ -10,7 +10,7 @@
<!-- Non Submenu items -->
<ng-container *ngIf="action.children === undefined || action?.children?.length === 0 || action.dynamicList != undefined; else submenuDropdown">
<ng-container *ngIf="action.dynamicList != undefined && toDList(action.dynamicList | async) as dList; else justItem">
<ng-container *ngIf="action.dynamicList != undefined && (action.dynamicList | async | dynamicList) as dList; else justItem">
<ng-container *ngFor="let dynamicItem of dList">
<button ngbDropdownItem (click)="performDynamicClick($event, action, dynamicItem)">{{dynamicItem.title}}</button>
</ng-container>
@ -23,7 +23,7 @@
<ng-template #submenuDropdown>
<!-- Submenu items -->
<ng-container *ngIf="shouldRenderSubMenu(action, action.children[0].dynamicList | async)">
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right" (mouseover)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseleave)="preventEvent($event)">
<div ngbDropdown #subMenuHover="ngbDropdown" placement="right left" (click)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseover)="preventEvent($event); openSubmenu(action.title, subMenuHover)" (mouseleave)="preventEvent($event)">
<button id="actions-{{action.title}}" class="submenu-toggle" ngbDropdownToggle>{{action.title}} <i class="fa-solid fa-angle-right submenu-icon"></i></button>
<div ngbDropdownMenu attr.aria-labelledby="actions-{{action.title}}">
<ng-container *ngTemplateOutlet="submenu; context: { list: action.children }"></ng-container>

View file

@ -19,6 +19,7 @@ export class CardActionablesComponent implements OnInit {
@Input() disabled: boolean = false;
@Output() actionHandler = new EventEmitter<ActionItem<any>>();
isAdmin: boolean = false;
canDownload: boolean = false;
submenu: {[key: string]: NgbDropdown} = {};
@ -74,10 +75,4 @@ export class CardActionablesComponent implements OnInit {
action._extra = dynamicItem;
this.performAction(event, action);
}
toDList(d: any) {
console.log('d: ', d);
if (d === undefined || d === null) return [];
return d as {title: string, data: any}[];
}
}

View file

@ -13,7 +13,7 @@ import { Series } from 'src/app/_models/series';
import { User } from 'src/app/_models/user';
import { Volume } from 'src/app/_models/volume';
import { AccountService } from 'src/app/_services/account.service';
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
@ -126,9 +126,11 @@ export class CardItemComponent implements OnInit, OnDestroy {
public utilityService: UtilityService, private downloadService: DownloadService,
public bulkSelectionService: BulkSelectionService,
private messageHub: MessageHubService, private accountService: AccountService,
private scrollService: ScrollService, private readonly cdRef: ChangeDetectorRef) {}
private scrollService: ScrollService, private readonly cdRef: ChangeDetectorRef,
private actionFactoryService: ActionFactoryService) {}
ngOnInit(): void {
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
this.suppressArchiveWarning = true;
this.cdRef.markForCheck();
@ -172,6 +174,8 @@ export class CardItemComponent implements OnInit, OnDestroy {
} else if (this.utilityService.isSeries(this.entity)) {
this.tooltipTitle = this.title || (this.utilityService.asSeries(this.entity).name);
}
this.filterSendTo();
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
this.user = user;
});
@ -192,26 +196,10 @@ export class CardItemComponent implements OnInit, OnDestroy {
chapter.pagesRead = updateEvent.pagesRead;
}
} else {
// Ignore
return;
// re-request progress for the series
// const s = this.utilityService.asSeries(this.entity);
// let pagesRead = 0;
// if (s.hasOwnProperty('volumes')) {
// s.volumes.forEach(v => {
// v.chapters.forEach(c => {
// if (c.id === updateEvent.chapterId) {
// c.pagesRead = updateEvent.pagesRead;
// }
// pagesRead += c.pagesRead;
// });
// });
// s.pagesRead = pagesRead;
// }
}
}
this.read = updateEvent.pagesRead;
this.cdRef.detectChanges();
});
@ -312,4 +300,20 @@ export class CardItemComponent implements OnInit, OnDestroy {
this.selection.emit(this.selected);
this.cdRef.detectChanges();
}
filterSendTo() {
if (!this.actions || this.actions.length === 0) return;
if (this.utilityService.isChapter(this.entity)) {
this.actions = this.actionFactoryService.filterSendToAction(this.actions, this.entity as Chapter);
} else if (this.utilityService.isVolume(this.entity)) {
const vol = this.utilityService.asVolume(this.entity);
this.actions = this.actionFactoryService.filterSendToAction(this.actions, vol.chapters[0]);
} else if (this.utilityService.isSeries(this.entity)) {
const series = (this.entity as Series);
if (series.format === MangaFormat.EPUB || series.format === MangaFormat.PDF) {
this.actions = this.actions.filter(a => a.title !== 'Send To');
}
}
}
}

View file

@ -26,6 +26,7 @@ import { ListItemComponent } from './list-item/list-item.component';
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
import { SeriesInfoCardsComponent } from './series-info-cards/series-info-cards.component';
import { DownloadIndicatorComponent } from './download-indicator/download-indicator.component';
import { DynamicListPipe } from './dynamic-list.pipe';
@ -48,6 +49,7 @@ import { DownloadIndicatorComponent } from './download-indicator/download-indica
ListItemComponent,
SeriesInfoCardsComponent,
DownloadIndicatorComponent,
DynamicListPipe,
],
imports: [
CommonModule,

View file

@ -71,6 +71,11 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
this.form = this.fb.group({
coverImageUrl: new FormControl('', [])
});
this.imageUrls.forEach(url => {
});
console.log('imageUrls: ', this.imageUrls);
this.cdRef.markForCheck();
}
@ -79,6 +84,11 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
this.onDestroy.complete();
}
/**
* Generates a base64 encoding for an Image. Used in manual file upload flow.
* @param img
* @returns
*/
getBase64Image(img: HTMLImageElement) {
const canvas = document.createElement("canvas");
canvas.width = img.width;
@ -95,6 +105,25 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
selectImage(index: number) {
if (this.selectedIndex === index) { return; }
// If we load custom images of series/chapters/covers, then those urls are not properly encoded, so on select we have to clean them up
if (!this.imageUrls[index].startsWith('data:image/')) {
const imgUrl = this.imageUrls[index];
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = imgUrl;
img.onload = (e) => this.handleUrlImageAdd(img, index);
img.onerror = (e) => {
this.toastr.error('The image could not be fetched due to server refusing request. Please download and upload from file instead.');
this.form.get('coverImageUrl')?.setValue('');
this.cdRef.markForCheck();
};
this.form.get('coverImageUrl')?.setValue('');
this.cdRef.markForCheck();
this.selectedBase64Url.emit(this.imageUrls[this.selectedIndex]);
return;
}
this.selectedIndex = index;
this.cdRef.markForCheck();
this.imageSelected.emit(this.selectedIndex);
@ -115,9 +144,9 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
}
}
loadImage() {
const url = this.form.get('coverImageUrl')?.value.trim();
if (!url && url === '') return;
loadImage(url?: string) {
url = url || this.form.get('coverImageUrl')?.value.trim();
if (!url || url === '') return;
this.uploadService.uploadByUrl(url).subscribe(filename => {
const img = new Image();
@ -134,6 +163,8 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
});
}
changeMode(mode: 'url') {
this.mode = mode;
this.setupEnterHandler();
@ -161,7 +192,7 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
handleFileImageAdd(e: any) {
if (e.target == null) return;
this.imageUrls.push(e.target.result);
this.imageUrls.push(e.target.result); // This is base64 already
this.imageUrlsChange.emit(this.imageUrls);
this.selectedIndex += 1;
this.imageSelected.emit(this.selectedIndex); // Auto select newly uploaded image
@ -169,9 +200,14 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
this.cdRef.markForCheck();
}
handleUrlImageAdd(img: HTMLImageElement) {
handleUrlImageAdd(img: HTMLImageElement, index: number = -1) {
const url = this.getBase64Image(img);
this.imageUrls.push(url);
if (index >= 0) {
this.imageUrls[index] = url;
} else {
this.imageUrls.push(url);
}
this.imageUrlsChange.emit(this.imageUrls);
this.cdRef.markForCheck();

View file

@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'dynamicList',
pure: true
})
export class DynamicListPipe implements PipeTransform {
transform(value: any): Array<{title: string, data: any}> {
if (value === undefined || value === null) return [];
return value as {title: string, data: any}[];
}
}