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:
parent
ee7d109170
commit
28ab34c66d
59 changed files with 2103 additions and 444 deletions
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}[];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
14
UI/Web/src/app/cards/dynamic-list.pipe.ts
Normal file
14
UI/Web/src/app/cards/dynamic-list.pipe.ts
Normal 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}[];
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue