mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-31 14:33:50 -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.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));
|
||||||
|
@ -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);
|
||||||
|
@ -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 = @"._";
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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';
|
||||||
|
@ -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) {
|
||||||
|
@ -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}}
|
<h5 class="card-title">{{update.updateTitle}}
|
||||||
<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>
|
||||||
|
@ -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() {
|
||||||
|
@ -6,13 +6,13 @@
|
|||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</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>
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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>
|
||||||
|
@ -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 => {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user