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

@ -156,6 +156,7 @@ namespace API.Tests.Parser
[InlineData("test.png", true)] [InlineData("test.png", true)]
[InlineData(".test.jpg", false)] [InlineData(".test.jpg", false)]
[InlineData("!test.jpg", false)] [InlineData("!test.jpg", false)]
[InlineData("test.webp", true)]
public void IsImageTest(string filename, bool expected) public void IsImageTest(string filename, bool expected)
{ {
Assert.Equal(expected, IsImage(filename)); Assert.Equal(expected, IsImage(filename));

View File

@ -10,7 +10,7 @@ namespace API.Interfaces.Services
/// </summary> /// </summary>
/// <param name="libraryId"></param> /// <param name="libraryId"></param>
/// <param name="forceUpdate"></param> /// <param name="forceUpdate"></param>
void RefreshMetadata(int libraryId, bool forceUpdate = false); Task RefreshMetadata(int libraryId, bool forceUpdate = false);
public bool UpdateMetadata(Chapter chapter, bool forceUpdate); public bool UpdateMetadata(Chapter chapter, bool forceUpdate);
public bool UpdateMetadata(Volume volume, bool forceUpdate); public bool UpdateMetadata(Volume volume, bool forceUpdate);

View File

@ -13,7 +13,7 @@ namespace API.Parser
public const string DefaultVolume = "0"; public const string DefaultVolume = "0";
private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500); private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(500);
public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg)"; public const string ImageFileExtensions = @"^(\.png|\.jpeg|\.jpg|\.webp)";
public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt"; public const string ArchiveFileExtensions = @"\.cbz|\.zip|\.rar|\.cbr|\.tar.gz|\.7zip|\.7z|\.cb7|\.cbt";
public const string BookFileExtensions = @"\.epub|\.pdf"; public const string BookFileExtensions = @"\.epub|\.pdf";
public const string MacOsMetadataFileStartsWith = @"._"; public const string MacOsMetadataFileStartsWith = @"._";

View File

@ -187,10 +187,10 @@ namespace API.Services
/// <remarks>This can be heavy on memory first run</remarks> /// <remarks>This can be heavy on memory first run</remarks>
/// <param name="libraryId"></param> /// <param name="libraryId"></param>
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param> /// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
public void RefreshMetadata(int libraryId, bool forceUpdate = false) public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult(); var library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId);
// PERF: See if we can break this up into multiple threads that process 20 series at a time then save so we can reduce amount of memory used // PERF: See if we can break this up into multiple threads that process 20 series at a time then save so we can reduce amount of memory used
_logger.LogInformation("Beginning metadata refresh of {LibraryName}", library.Name); _logger.LogInformation("Beginning metadata refresh of {LibraryName}", library.Name);
@ -213,7 +213,7 @@ namespace API.Services
} }
if (_unitOfWork.HasChanges() && Task.Run(() => _unitOfWork.CommitAsync()).Result) if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{ {
_logger.LogInformation("Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds); _logger.LogInformation("Updated metadata for {LibraryName} in {ElapsedMilliseconds} milliseconds", library.Name, sw.ElapsedMilliseconds);
} }
@ -228,7 +228,7 @@ namespace API.Services
public async Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = false) public async Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = false)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter().GetResult(); var library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId);
var series = library.Series.SingleOrDefault(s => s.Id == seriesId); var series = library.Series.SingleOrDefault(s => s.Id == seriesId);
if (series == null) if (series == null)

View File

@ -108,7 +108,7 @@ namespace API.Services.Tasks
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}", "Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name); totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
CleanupDbEntities(); await CleanupDbEntities();
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate)); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate));
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
// Tell UI that this series is done // Tell UI that this series is done
@ -128,7 +128,7 @@ namespace API.Services.Tasks
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task ScanLibraries() public async Task ScanLibraries()
{ {
var libraries = Task.Run(() => _unitOfWork.LibraryRepository.GetLibrariesAsync()).Result.ToList(); var libraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
foreach (var lib in libraries) foreach (var lib in libraries)
{ {
await ScanLibrary(lib.Id, false); await ScanLibrary(lib.Id, false);
@ -151,8 +151,7 @@ namespace API.Services.Tasks
Library library; Library library;
try try
{ {
library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter() library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId);
.GetResult();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -174,7 +173,7 @@ namespace API.Services.Tasks
UpdateLibrary(library, series); UpdateLibrary(library, series);
_unitOfWork.LibraryRepository.Update(library); _unitOfWork.LibraryRepository.Update(library);
if (Task.Run(() => _unitOfWork.CommitAsync()).Result) if (await _unitOfWork.CommitAsync())
{ {
_logger.LogInformation( _logger.LogInformation(
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}", "Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {LibraryName}",
@ -186,7 +185,7 @@ namespace API.Services.Tasks
"There was a critical error that resulted in a failed scan. Please check logs and rescan"); "There was a critical error that resulted in a failed scan. Please check logs and rescan");
} }
CleanupAbandonedChapters(); await CleanupAbandonedChapters();
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate)); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate));
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibrary, MessageFactory.ScanLibraryEvent(libraryId, "complete")); await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibrary, MessageFactory.ScanLibraryEvent(libraryId, "complete"));
@ -195,9 +194,9 @@ namespace API.Services.Tasks
/// <summary> /// <summary>
/// Remove any user progress rows that no longer exist since scan library ran and deleted series/volumes/chapters /// Remove any user progress rows that no longer exist since scan library ran and deleted series/volumes/chapters
/// </summary> /// </summary>
private void CleanupAbandonedChapters() private async Task CleanupAbandonedChapters()
{ {
var cleanedUp = Task.Run(() => _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters()).Result; var cleanedUp = await _unitOfWork.AppUserProgressRepository.CleanupAbandonedChapters();
_logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp); _logger.LogInformation("Removed {Count} abandoned progress rows", cleanedUp);
} }
@ -205,10 +204,10 @@ namespace API.Services.Tasks
/// <summary> /// <summary>
/// Cleans up any abandoned rows due to removals from Scan loop /// Cleans up any abandoned rows due to removals from Scan loop
/// </summary> /// </summary>
private void CleanupDbEntities() private async Task CleanupDbEntities()
{ {
CleanupAbandonedChapters(); await CleanupAbandonedChapters();
var cleanedUp = Task.Run( () => _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries()).Result; var cleanedUp = await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
_logger.LogInformation("Removed {Count} abandoned collection tags", cleanedUp); _logger.LogInformation("Removed {Count} abandoned collection tags", cleanedUp);
} }

View File

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

View File

@ -1,8 +1,8 @@
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { forkJoin, Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { BookmarksModalComponent } from '../cards/_modals/bookmarks-modal/bookmarks-modal.component'; 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 { 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'; 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; return newRoute;
} }
getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) { getQueryParamsObject(incognitoMode: boolean = false, readingListMode: boolean = false, readingListId: number = -1) {
let params: {[key: string]: any} = {}; let params: {[key: string]: any} = {};
if (incognitoMode) { if (incognitoMode) {

View File

@ -2,10 +2,8 @@
<div class="card w-100 mb-2" style="width: 18rem;"> <div class="card w-100 mb-2" style="width: 18rem;">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{{update.updateTitle}}&nbsp; <h5 class="card-title">{{update.updateTitle}}&nbsp;
<span class="badge badge-secondary" *ngIf="update.updateVersion <= update.currentVersion; else available">Installed</span> <span class="badge badge-secondary" *ngIf="update.updateVersion === update.currentVersion">Installed</span>
<ng-template #available> <span class="badge badge-secondary" *ngIf="update.updateVersion > update.currentVersion">Available</span>
<span class="badge badge-secondary">Available</span>
</ng-template>
</h5> </h5>
<pre class="card-text update-body" [innerHtml]="update.updateBody | safeHtml"></pre> <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> <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 = ''; lastSeenScrollPartPath: string = '';
/** /**
* Hack: Override background color for reader and restore it onDestroy * 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) { 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.readerService.getNextChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.nextChapterId = chapterId; this.nextChapterId = chapterId;
this.loadChapter(chapterId, 'next'); this.loadChapter(chapterId, 'Next');
}); });
} else { } 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) { 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.readerService.getPrevChapter(this.seriesId, this.volumeId, this.chapterId, this.readingListId).pipe(take(1)).subscribe(chapterId => {
this.prevChapterId = chapterId; this.prevChapterId = chapterId;
this.loadChapter(chapterId, 'prev'); this.loadChapter(chapterId, 'Prev');
}); });
} else { } 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) { if (chapterId >= 0) {
this.chapterId = chapterId; this.chapterId = chapterId;
this.continuousChaptersStack.push(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); const newRoute = this.readerService.getNextChapterUrl(this.router.url, this.chapterId, this.incognitoMode, this.readingListMode, this.readingListId);
window.history.replaceState({}, '', newRoute); window.history.replaceState({}, '', newRoute);
this.init(); this.init();
this.toastr.info(direction + ' chapter loaded', '', {timeOut: 3000});
} else { } else {
// This will only happen if no actual chapter can be found // This will only happen if no actual chapter can be found
this.toastr.warning('Could not find ' + direction + ' chapter'); this.toastr.warning('Could not find ' + direction + ' chapter');
this.isLoading = false; this.isLoading = false;
if (direction === 'prev') { if (direction === 'Prev') {
this.prevPageDisabled = true; this.prevPageDisabled = true;
} else { } else {
this.nextPageDisabled = true; this.nextPageDisabled = true;
@ -535,7 +534,11 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
closeReader() { closeReader() {
this.location.back(); if (this.readingListMode) {
this.router.navigateByUrl('lists/' + this.readingListId);
} else {
this.location.back();
}
} }
resetSettings() { resetSettings() {

View File

@ -6,13 +6,13 @@
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body scrollable-modal"> <div class="modal-body scrollable-modal {{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? '' : 'd-flex'}}">
<form [formGroup]="editSeriesForm">
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs"> <ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-pills" orientation="{{utilityService.getActiveBreakpoint() === Breakpoint.Mobile ? 'horizontal' : 'vertical'}}" style="min-width: 135px;">
<li [ngbNavItem]="tabs[0]"> <li [ngbNavItem]="tabs[0]">
<a ngbNavLink>{{tabs[0]}}</a> <a ngbNavLink>{{tabs[0]}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<form [formGroup]="editSeriesForm">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="form-group" style="width: 100%"> <div class="form-group" style="width: 100%">
<label for="name">Name</label> <label for="name">Name</label>
@ -77,6 +77,7 @@
<textarea id="summary" class="form-control" formControlName="summary" rows="4"></textarea> <textarea id="summary" class="form-control" formControlName="summary" rows="4"></textarea>
</div> </div>
</div> </div>
</form>
</ng-template> </ng-template>
</li> </li>
@ -151,9 +152,8 @@
</ng-template> </ng-template>
</li> </li>
</ul> </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>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="close()">Close</button> <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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { forkJoin, Subject } from 'rxjs'; import { forkJoin, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; 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 { TypeaheadSettings } from 'src/app/typeahead/typeahead-settings';
import { Chapter } from 'src/app/_models/chapter'; import { Chapter } from 'src/app/_models/chapter';
import { CollectionTag } from 'src/app/_models/collection-tag'; import { CollectionTag } from 'src/app/_models/collection-tag';
@ -43,6 +43,10 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
*/ */
selectedCover: string = ''; selectedCover: string = '';
get Breakpoint(): typeof Breakpoint {
return Breakpoint;
}
constructor(public modal: NgbActiveModal, constructor(public modal: NgbActiveModal,
private seriesService: SeriesService, private seriesService: SeriesService,
public utilityService: UtilityService, public utilityService: UtilityService,

View File

@ -94,7 +94,6 @@ export class SeriesCardComponent implements OnInit, OnChanges {
if (closeResult.success) { if (closeResult.success) {
if (closeResult.coverImageUpdate) { if (closeResult.coverImageUpdate) {
this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(closeResult.series.id)); 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.seriesService.getSeries(data.id).subscribe(series => {
this.data = series; this.data = series;

View File

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

View File

@ -102,8 +102,8 @@
<a ngbNavLink>Specials</a> <a ngbNavLink>Specials</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div class="row"> <div class="row">
<div *ngFor="let chapter of specials"> <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)" <app-card-item class="col-auto" *ngIf="chapter.isSpecial" [entity]="chapter" [title]="chapter.title || chapter.range" (click)="openChapter(chapter)"
[imageUrl]="imageService.getChapterCoverImage(chapter.id)" [imageUrl]="imageService.getChapterCoverImage(chapter.id)"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions"></app-card-item> [read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions"></app-card-item>
</div> </div>
@ -114,12 +114,12 @@
<a ngbNavLink>Volumes/Chapters</a> <a ngbNavLink>Volumes/Chapters</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div class="row"> <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)" <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" [imageUrl]="imageService.getVolumeCoverImage(volume.id) + '&offset=' + coverImageOffset"
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions"></app-card-item> [read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions"></app-card-item>
</div> </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)" <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" [imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions"></app-card-item> [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; 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(); private onDestroy: Subject<void> = new Subject();
@ -296,6 +305,11 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.hasNonSpecialVolumeChapters = false; 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; this.isLoading = false;
}); });
}, err => { }, err => {

View File

@ -18,6 +18,13 @@ export enum KEY_CODES {
DELETE = 'Delete' DELETE = 'Delete'
} }
export enum Breakpoint {
Mobile = 768,
Tablet = 1280,
Desktop = 1440
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@ -111,4 +118,12 @@ export class UtilityService {
return <Series>d; 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;
}
} }