WebP Support (#581)

* Added trackby so when series scan event comes through, cards can update too

* Added chapter boundary toasts on book reader

* Handle closing the reader when in a reading list

* Somehow the trackby save didn't happen

* Fixed an issue where after opening a chapter info modal, then trying to open another in specials tab it would fail due to a pass by reference issue with our factory.

* When a series update occurs, if we loose specials tab, but we were on it, reselect volumes/chapters tab

* Fixed an issue where older releases would show as available, even though they were already installed.

* Converted tabs within modals to use vertical orientation (except on mobile)

* Implemented webp support. Only Safari does not support this format natively. MacOS users can use an alternative browser.

* Refactored ScannerService and MetadataService to be fully async
This commit is contained in:
Joseph Milazzo 2021-09-15 17:25:18 -07:00 committed by GitHub
parent d92cfb0b2b
commit 2725e6042b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 102 additions and 65 deletions

View file

@ -113,17 +113,10 @@ export class ActionFactoryService {
this.chapterActions.push({
action: Action.Edit,
title: 'Edit',
title: 'Info',
callback: this.dummyCallback,
requiresAdmin: false
});
// this.readingListActions.push({
// action: Action.Promote, // Should I just use CollectionTag modal-like instead?
// title: 'Delete',
// callback: this.dummyCallback,
// requiresAdmin: true
// });
}
if (this.hasDownloadRole || this.isAdmin) {
@ -145,33 +138,39 @@ export class ActionFactoryService {
}
getLibraryActions(callback: (action: Action, library: Library) => void) {
this.libraryActions.forEach(action => action.callback = callback);
return this.libraryActions;
const actions = this.libraryActions.map(a => {return {...a}});
actions.forEach(action => action.callback = callback);
return actions;
}
getSeriesActions(callback: (action: Action, series: Series) => void) {
this.seriesActions.forEach(action => action.callback = callback);
return this.seriesActions;
const actions = this.seriesActions.map(a => {return {...a}});
actions.forEach(action => action.callback = callback);
return actions;
}
getVolumeActions(callback: (action: Action, volume: Volume) => void) {
this.volumeActions.forEach(action => action.callback = callback);
return this.volumeActions;
const actions = this.volumeActions.map(a => {return {...a}});
actions.forEach(action => action.callback = callback);
return actions;
}
getChapterActions(callback: (action: Action, chapter: Chapter) => void) {
this.chapterActions.forEach(action => action.callback = callback);
return this.chapterActions;
const actions = this.chapterActions.map(a => {return {...a}});
actions.forEach(action => action.callback = callback);
return actions;
}
getCollectionTagActions(callback: (action: Action, collectionTag: CollectionTag) => void) {
this.collectionTagActions.forEach(action => action.callback = callback);
return this.collectionTagActions;
const actions = this.collectionTagActions.map(a => {return {...a}});
actions.forEach(action => action.callback = callback);
return actions;
}
getReadingListActions(callback: (action: Action, readingList: ReadingList) => void) {
this.readingListActions.forEach(action => action.callback = callback);
return this.readingListActions;
const actions = this.readingListActions.map(a => {return {...a}});
actions.forEach(action => action.callback = callback);
return actions;
}
filterBookmarksForFormat(action: ActionItem<Series>, series: Series) {

View file

@ -1,8 +1,8 @@
import { Injectable, OnDestroy } from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { forkJoin, Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import { BookmarksModalComponent } from '../cards/_modals/bookmarks-modal/bookmarks-modal.component';
import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component';

View file

@ -150,6 +150,7 @@ export class ReaderService {
return newRoute;
}
getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) {
let params: {[key: string]: any} = {};
if (incognitoMode) {

View file

@ -2,10 +2,8 @@
<div class="card w-100 mb-2" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title">{{update.updateTitle}}&nbsp;
<span class="badge badge-secondary" *ngIf="update.updateVersion <= update.currentVersion; else available">Installed</span>
<ng-template #available>
<span class="badge badge-secondary">Available</span>
</ng-template>
<span class="badge badge-secondary" *ngIf="update.updateVersion === update.currentVersion">Installed</span>
<span class="badge badge-secondary" *ngIf="update.updateVersion > update.currentVersion">Available</span>
</h5>
<pre class="card-text update-body" [innerHtml]="update.updateBody | safeHtml"></pre>
<a *ngIf="!update.isDocker" href="{{update.updateUrl}}" class="btn btn-{{indx === 0 ? 'primary' : 'secondary'}} float-right" target="_blank">Download</a>

View file

@ -172,8 +172,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
*/
lastSeenScrollPartPath: string = '';
/**
* Hack: Override background color for reader and restore it onDestroy
*/
@ -479,10 +477,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.nextChapterId === CHAPTER_ID_NOT_FETCHED || this.nextChapterId === this.chapterId) {
this.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.nextChapterId = chapterId;
this.loadChapter(chapterId, 'next');
this.loadChapter(chapterId, 'Next');
});
} else {
this.loadChapter(this.nextChapterId, 'next');
this.loadChapter(this.nextChapterId, 'Next');
}
}
@ -502,14 +500,14 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (this.prevChapterId === CHAPTER_ID_NOT_FETCHED || this.prevChapterId === this.chapterId) {
this.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.prevChapterId = chapterId;
this.loadChapter(chapterId, 'prev');
this.loadChapter(chapterId, 'Prev');
});
} else {
this.loadChapter(this.prevChapterId, 'prev');
this.loadChapter(this.prevChapterId, 'Prev');
}
}
loadChapter(chapterId: number, direction: 'next' | 'prev') {
loadChapter(chapterId: number, direction: 'Next' | 'Prev') {
if (chapterId >= 0) {
this.chapterId = chapterId;
this.continuousChaptersStack.push(chapterId);
@ -517,11 +515,12 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId);
window.history.replaceState({}, '', newRoute);
this.init();
this.toastr.info(direction + ' chapter loaded', '', {timeOut: 3000});
} else {
// This will only happen if no actual chapter can be found
this.toastr.warning('Could not find ' + direction + ' chapter');
this.isLoading = false;
if (direction === 'prev') {
if (direction === 'Prev') {
this.prevPageDisabled = true;
} else {
this.nextPageDisabled = true;
@ -535,7 +534,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
closeReader() {
this.location.back();
if (this.readingListMode) {
this.router.navigateByUrl('lists/' + this.readingListId);
} else {
this.location.back();
}
}
resetSettings() {

View file

@ -6,13 +6,13 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body scrollable-modal">
<form [formGroup]="editSeriesForm">
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs">
<div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
<li [ngbNavItem]="tabs[0]">
<a ngbNavLink>{{tabs[0]}}</a>
<ng-template ngbNavContent>
<form [formGroup]="editSeriesForm">
<div class="row no-gutters">
<div class="form-group" style="width: 100%">
<label for="name">Name</label>
@ -77,6 +77,7 @@
<textarea id="summary" class="form-control" formControlName="summary" rows="4"></textarea>
</div>
</div>
</form>
</ng-template>
</li>
@ -151,9 +152,8 @@
</ng-template>
</li>
</ul>
</form>
<div [ngbNavOutlet]="nav" class="mt-3"></div>
<div [ngbNavOutlet]="nav" class="tab-content {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'mt-3' : 'ml-4 flex-fill'}}"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="close()">Close</button>

View file

@ -3,7 +3,7 @@ import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { forkJoin, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
import { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings';
import { Chapter } from 'src/app/_models/chapter';
import { CollectionTag } from 'src/app/_models/collection-tag';
@ -43,6 +43,10 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
*/
selectedCover: string = '';
get Breakpoint(): typeof Breakpoint {
return Breakpoint;
}
constructor(public modal: NgbActiveModal,
private seriesService: SeriesService,
public utilityService: UtilityService,

View file

@ -94,7 +94,6 @@ export class SeriesCardComponent implements OnInit, OnChanges {
if (closeResult.success) {
if (closeResult.coverImageUpdate) {
this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(closeResult.series.id));
console.log('image url: ', this.imageUrl);
}
this.seriesService.getSeries(data.id).subscribe(series => {
this.data = series;

View file

@ -468,7 +468,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
closeReader() {
this.location.back();
if (this.readingListMode) {
this.router.navigateByUrl('lists/' + this.readingListId);
} else {
this.location.back();
}
}
updateTitle(chapterInfo: ChapterInfo) {

View file

@ -102,8 +102,8 @@
<a ngbNavLink>Specials</a>
<ng-template ngbNavContent>
<div class="row">
<div *ngFor="let chapter of specials">
<app-card-item class="col-auto" *ngIf="chapter.isSpecial" [entity]="chapter" [title]="chapter.title || chapter.range" (click)="openChapter(chapter)"
<div *ngFor="let chapter of specials; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto" *ngIf="chapter.isSpecial" [entity]="chapter" [title]="chapter.title || chapter.range" (click)="openChapter(chapter)"
[imageUrl]="imageService.getChapterCoverImage(chapter.id)"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions"></app-card-item>
</div>
@ -114,12 +114,12 @@
<a ngbNavLink>Volumes/Chapters</a>
<ng-template ngbNavContent>
<div class="row">
<div *ngFor="let volume of volumes">
<div *ngFor="let volume of volumes; trackBy: trackByVolumeIdentity">
<app-card-item class="col-auto" *ngIf="volume.number != 0" [entity]="volume" [title]="'Volume ' + volume.name" (click)="openVolume(volume)"
[imageUrl]="imageService.getVolumeCoverImage(volume.id) + '&offset=' + coverImageOffset"
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions"></app-card-item>
</div>
<div *ngFor="let chapter of chapters">
<div *ngFor="let chapter of chapters; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="'Chapter ' + chapter.range" (click)="openChapter(chapter)"
[imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions"></app-card-item>

View file

@ -79,6 +79,15 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
*/
actionInProgress: boolean = false;
/**
* Track by function for Volume to tell when to refresh card data
*/
trackByVolumeIdentity = (index: number, item: Volume) => `${item.name}_${item.pagesRead}`;
/**
* Track by function for Chapter to tell when to refresh card data
*/
trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.number}_${item.pagesRead}`;
private onDestroy: Subject<void> = new Subject();
@ -296,6 +305,11 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.hasNonSpecialVolumeChapters = false;
}
// If an update occured and we were on specials, re-activate Volumes/Chapters
if (!this.hasSpecials && this.activeTabId != 2) {
this.activeTabId = 2;
}
this.isLoading = false;
});
}, err => {

View file

@ -18,6 +18,13 @@ export enum KEY_CODES {
DELETE = 'Delete'
}
export enum Breakpoint {
Mobile = 768,
Tablet = 1280,
Desktop = 1440
}
@Injectable({
providedIn: 'root'
})
@ -111,4 +118,12 @@ export class UtilityService {
return <Series>d;
}
getActiveBreakpoint(): Breakpoint {
if (window.innerWidth <= Breakpoint.Mobile) return Breakpoint.Mobile;
else if (window.innerWidth > Breakpoint.Mobile && window.innerWidth <= Breakpoint.Tablet) return Breakpoint.Tablet;
else if (window.innerWidth > Breakpoint.Tablet) return Breakpoint.Desktop
return Breakpoint.Desktop;
}
}