mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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:
parent
d92cfb0b2b
commit
2725e6042b
@ -156,6 +156,7 @@ namespace API.Tests.Parser
|
||||
[InlineData("test.png", true)]
|
||||
[InlineData(".test.jpg", false)]
|
||||
[InlineData("!test.jpg", false)]
|
||||
[InlineData("test.webp", true)]
|
||||
public void IsImageTest(string filename, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IsImage(filename));
|
||||
|
@ -10,7 +10,7 @@ namespace API.Interfaces.Services
|
||||
/// </summary>
|
||||
/// <param name="libraryId"></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(Volume volume, bool forceUpdate);
|
||||
|
@ -13,7 +13,7 @@ namespace API.Parser
|
||||
public const string DefaultVolume = "0";
|
||||
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 BookFileExtensions = @"\.epub|\.pdf";
|
||||
public const string MacOsMetadataFileStartsWith = @"._";
|
||||
|
@ -187,10 +187,10 @@ namespace API.Services
|
||||
/// <remarks>This can be heavy on memory first run</remarks>
|
||||
/// <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>
|
||||
public void RefreshMetadata(int libraryId, bool forceUpdate = false)
|
||||
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false)
|
||||
{
|
||||
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
|
||||
_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);
|
||||
}
|
||||
@ -228,7 +228,7 @@ namespace API.Services
|
||||
public async Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = false)
|
||||
{
|
||||
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);
|
||||
if (series == null)
|
||||
|
@ -108,7 +108,7 @@ namespace API.Services.Tasks
|
||||
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
|
||||
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
|
||||
|
||||
CleanupDbEntities();
|
||||
await CleanupDbEntities();
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate));
|
||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||
// Tell UI that this series is done
|
||||
@ -128,7 +128,7 @@ namespace API.Services.Tasks
|
||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||
public async Task ScanLibraries()
|
||||
{
|
||||
var libraries = Task.Run(() => _unitOfWork.LibraryRepository.GetLibrariesAsync()).Result.ToList();
|
||||
var libraries = await _unitOfWork.LibraryRepository.GetLibrariesAsync();
|
||||
foreach (var lib in libraries)
|
||||
{
|
||||
await ScanLibrary(lib.Id, false);
|
||||
@ -151,8 +151,7 @@ namespace API.Services.Tasks
|
||||
Library library;
|
||||
try
|
||||
{
|
||||
library = Task.Run(() => _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId)).GetAwaiter()
|
||||
.GetResult();
|
||||
library = await _unitOfWork.LibraryRepository.GetFullLibraryForIdAsync(libraryId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -174,7 +173,7 @@ namespace API.Services.Tasks
|
||||
UpdateLibrary(library, series);
|
||||
|
||||
_unitOfWork.LibraryRepository.Update(library);
|
||||
if (Task.Run(() => _unitOfWork.CommitAsync()).Result)
|
||||
if (await _unitOfWork.CommitAsync())
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"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");
|
||||
}
|
||||
|
||||
CleanupAbandonedChapters();
|
||||
await CleanupAbandonedChapters();
|
||||
|
||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate));
|
||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibrary, MessageFactory.ScanLibraryEvent(libraryId, "complete"));
|
||||
@ -195,9 +194,9 @@ namespace API.Services.Tasks
|
||||
/// <summary>
|
||||
/// Remove any user progress rows that no longer exist since scan library ran and deleted series/volumes/chapters
|
||||
/// </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);
|
||||
}
|
||||
|
||||
@ -205,10 +204,10 @@ namespace API.Services.Tasks
|
||||
/// <summary>
|
||||
/// Cleans up any abandoned rows due to removals from Scan loop
|
||||
/// </summary>
|
||||
private void CleanupDbEntities()
|
||||
private async Task CleanupDbEntities()
|
||||
{
|
||||
CleanupAbandonedChapters();
|
||||
var cleanedUp = Task.Run( () => _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries()).Result;
|
||||
await CleanupAbandonedChapters();
|
||||
var cleanedUp = await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||
_logger.LogInformation("Removed {Count} abandoned collection tags", cleanedUp);
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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';
|
||||
|
@ -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) {
|
||||
|
@ -2,10 +2,8 @@
|
||||
<div class="card w-100 mb-2" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{update.updateTitle}}
|
||||
<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>
|
||||
|
@ -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() {
|
||||
|
@ -6,13 +6,13 @@
|
||||
<span aria-hidden="true">×</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>
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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 => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user