Bookmark and Reader bugs (#1632)

* Updated swiper and some packages for reported security issues

* Fixed reading lists promotion not working

* Refactor RenameFileForCopy to use iterative recursion, rather than functional.

* Ensured that bookmarks are fetched and ordered by Created date.

* Fixed a bug where bookmarks were coming back in the correct order, but due to filenames, would not sort correctly.

* Default installs to Debug log level given errors users have and Debug not being too noisy

* Added jumpbar to bookmarks page

* Now added jumpbar to bookmarks

* Refactored some code into pipes and added some debug messaging for prefetcher

* Try loading next and prev chapter's first/last page to cache so it renders faster

* Updated GetImage to do a bound check on max page.

Fixed a critical bug in how manga reader updates image elements src to prefetch/load pages. I was not creating a new reference which broke Angular's ability to update DOM on changes.

* Refactored the image setting code to use a single method which tries to use a cached image always.

* Refactored code to use getPage which favors cache and simplifies image creation code
This commit is contained in:
Joe Milazzo 2022-11-02 21:10:19 -04:00 committed by GitHub
parent dab42041d5
commit 38a169818b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 317 additions and 166 deletions

View File

@ -593,6 +593,23 @@ public class DirectoryServiceTests
|| outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip"))); || outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/file (3).zip")));
} }
[Fact]
public void CopyFilesToDirectory_ShouldRenameFilesToPassedNames()
{
const string testDirectory = "/manga/";
var fileSystem = new MockFileSystem();
fileSystem.AddFile(MockUnixSupport.Path($"{testDirectory}file.zip"), new MockFileData(""));
var ds = new DirectoryService(Substitute.For<ILogger<DirectoryService>>(), fileSystem);
ds.CopyFilesToDirectory(new []{MockUnixSupport.Path($"{testDirectory}file.zip")}, "/manga/output/", new [] {"01"});
var outputFiles = ds.GetFiles("/manga/output/").Select(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath).ToList();
Assert.Equal(1, outputFiles.Count()); // we have 2 already there and 2 copies
// For some reason, this has C:/ on directory even though everything is emulated (System.IO.Abstractions issue, not changing)
// https://github.com/TestableIO/System.IO.Abstractions/issues/831
Assert.True(outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("/manga/output/01.zip"))
|| outputFiles.Contains(API.Services.Tasks.Scanner.Parser.Parser.NormalizePath("C:/manga/output/01.zip")));
}
#endregion #endregion
#region ListDirectory #region ListDirectory

View File

@ -83,8 +83,9 @@ public class ReaderController : BaseApiController
} }
/// <summary> /// <summary>
/// Returns an image for a given chapter. Side effect: This will cache the chapter images for reading. /// Returns an image for a given chapter. Will perform bounding checks
/// </summary> /// </summary>
/// <remarks>This will cache the chapter images for reading</remarks>
/// <param name="chapterId"></param> /// <param name="chapterId"></param>
/// <param name="page"></param> /// <param name="page"></param>
/// <returns></returns> /// <returns></returns>
@ -99,6 +100,7 @@ public class ReaderController : BaseApiController
try try
{ {
// TODO: This code is very generic and repeated, see if we can refactor into a common method
var path = _cacheService.GetCachedPagePath(chapter, page); var path = _cacheService.GetCachedPagePath(chapter, page);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache."); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}. Try refreshing to allow re-cache.");
var format = Path.GetExtension(path).Replace(".", ""); var format = Path.GetExtension(path).Replace(".", "");
@ -128,7 +130,6 @@ public class ReaderController : BaseApiController
if (page < 0) page = 0; if (page < 0) page = 0;
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey); var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
// NOTE: I'm not sure why I need this flow here
var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId); var totalPages = await _cacheService.CacheBookmarkForSeries(userId, seriesId);
if (page > totalPages) if (page > totalPages)
{ {
@ -139,7 +140,7 @@ public class ReaderController : BaseApiController
{ {
var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page); var path = _cacheService.GetCachedBookmarkPagePath(seriesId, page);
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}"); if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path)) return BadRequest($"No such image for page {page}");
var format = Path.GetExtension(path).Replace(".", ""); var format = Path.GetExtension(path).Replace(".", string.Empty);
return PhysicalFile(path, "image/" + format, Path.GetFileName(path)); return PhysicalFile(path, "image/" + format, Path.GetFileName(path));
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Comparators; using API.Comparators;

View File

@ -1,10 +1,14 @@
namespace API.DTOs.ReadingLists; using System;
using System.ComponentModel.DataAnnotations;
namespace API.DTOs.ReadingLists;
public class UpdateReadingListDto public class UpdateReadingListDto
{ {
[Required]
public int ReadingListId { get; set; } public int ReadingListId { get; set; }
public string Title { get; set; } public string Title { get; set; } = string.Empty;
public string Summary { get; set; } public string Summary { get; set; } = string.Empty;
public bool Promoted { get; set; } public bool Promoted { get; set; }
public bool CoverImageLocked { get; set; } public bool CoverImageLocked { get; set; }
} }

View File

@ -305,7 +305,7 @@ public class UserRepository : IUserRepository
{ {
return await _context.AppUserBookmark return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.SeriesId == seriesId) .Where(x => x.AppUserId == userId && x.SeriesId == seriesId)
.OrderBy(x => x.Page) .OrderBy(x => x.Created)
.AsNoTracking() .AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider) .ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
@ -315,7 +315,7 @@ public class UserRepository : IUserRepository
{ {
return await _context.AppUserBookmark return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.VolumeId == volumeId) .Where(x => x.AppUserId == userId && x.VolumeId == volumeId)
.OrderBy(x => x.Page) .OrderBy(x => x.Created)
.AsNoTracking() .AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider) .ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
@ -325,7 +325,7 @@ public class UserRepository : IUserRepository
{ {
return await _context.AppUserBookmark return await _context.AppUserBookmark
.Where(x => x.AppUserId == userId && x.ChapterId == chapterId) .Where(x => x.AppUserId == userId && x.ChapterId == chapterId)
.OrderBy(x => x.Page) .OrderBy(x => x.Created)
.AsNoTracking() .AsNoTracking()
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider) .ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync(); .ToListAsync();
@ -341,25 +341,27 @@ public class UserRepository : IUserRepository
{ {
var query = _context.AppUserBookmark var query = _context.AppUserBookmark
.Where(x => x.AppUserId == userId) .Where(x => x.AppUserId == userId)
.OrderBy(x => x.Page) .OrderBy(x => x.Created)
.AsNoTracking(); .AsNoTracking();
if (!string.IsNullOrEmpty(filter.SeriesNameQuery)) if (string.IsNullOrEmpty(filter.SeriesNameQuery))
{ return await query
var seriesNameQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(filter.SeriesNameQuery); .ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new .ToListAsync();
{
bookmark,
series
})
.Where(o => EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%")
);
query = filterSeriesQuery.Select(o => o.bookmark); var seriesNameQueryNormalized = Services.Tasks.Scanner.Parser.Parser.Normalize(filter.SeriesNameQuery);
} var filterSeriesQuery = query.Join(_context.Series, b => b.SeriesId, s => s.Id, (bookmark, series) => new
{
bookmark,
series
})
.Where(o => EF.Functions.Like(o.series.Name, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.OriginalName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.LocalizedName, $"%{filter.SeriesNameQuery}%")
|| EF.Functions.Like(o.series.NormalizedName, $"%{seriesNameQueryNormalized}%")
);
query = filterSeriesQuery.Select(o => o.bookmark);
return await query return await query

View File

@ -79,10 +79,7 @@ public static class Seed
{ {
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory}, new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
new() {Key = ServerSettingKey.TaskScan, Value = "daily"}, new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
new() new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"},
{
Key = ServerSettingKey.LoggingLevel, Value = "Information"
}, // Not used from DB, but DB is sync with appSettings.json
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"}, new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
new() new()
{ {

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -101,7 +102,7 @@ public class CacheService : ICacheService
var extractPath = GetCachePath(chapterId); var extractPath = GetCachePath(chapterId);
if (_directoryService.Exists(extractPath)) return chapter; if (_directoryService.Exists(extractPath)) return chapter;
var files = chapter.Files.ToList(); var files = chapter?.Files.ToList();
ExtractChapterFiles(extractPath, files); ExtractChapterFiles(extractPath, files);
return chapter; return chapter;
@ -223,6 +224,8 @@ public class CacheService : ICacheService
return string.Empty; return string.Empty;
} }
if (page > files.Length) page = files.Length;
// Since array is 0 based, we need to keep that in account (only affects last image) // Since array is 0 based, we need to keep that in account (only affects last image)
return page == files.Length ? files.ElementAt(page - 1) : files.ElementAt(page); return page == files.Length ? files.ElementAt(page - 1) : files.ElementAt(page);
} }
@ -234,8 +237,8 @@ public class CacheService : ICacheService
var bookmarkDtos = await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(userId, seriesId); var bookmarkDtos = await _unitOfWork.UserRepository.GetBookmarkDtosForSeries(userId, seriesId);
var files = (await _bookmarkService.GetBookmarkFilesById(bookmarkDtos.Select(b => b.Id))).ToList(); var files = (await _bookmarkService.GetBookmarkFilesById(bookmarkDtos.Select(b => b.Id))).ToList();
_directoryService.CopyFilesToDirectory(files, destDirectory); _directoryService.CopyFilesToDirectory(files, destDirectory,
_directoryService.Flatten(destDirectory); Enumerable.Range(1, files.Count).Select(i => i + string.Empty).ToList());
return files.Count; return files.Count;
} }

View File

@ -37,6 +37,7 @@ public interface IDirectoryService
IEnumerable<DirectoryDto> ListDirectory(string rootPath); IEnumerable<DirectoryDto> ListDirectory(string rootPath);
Task<byte[]> ReadFileAsync(string path); Task<byte[]> ReadFileAsync(string path);
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = ""); bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, IList<string> newFilenames);
bool Exists(string directory); bool Exists(string directory);
void CopyFileToDirectory(string fullFilePath, string targetDirectory); void CopyFileToDirectory(string fullFilePath, string targetDirectory);
int TraverseTreeParallelForEach(string root, Action<string> action, string searchPattern, ILogger logger); int TraverseTreeParallelForEach(string root, Action<string> action, string searchPattern, ILogger logger);
@ -424,6 +425,46 @@ public class DirectoryService : IDirectoryService
return true; return true;
} }
/// <summary>
/// Copies files to a destination directory. If the destination directory doesn't exist, this will create it.
/// </summary>
/// <remarks>If a file already exists in dest, this will rename as (2). It does not support multiple iterations of this. Overwriting is not supported.</remarks>
/// <param name="filePaths"></param>
/// <param name="directoryPath"></param>
/// <param name="newFilenames">A list that matches one to one with filePaths. Each filepath will be renamed to newFilenames</param>
/// <returns></returns>
public bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, IList<string> newFilenames)
{
ExistOrCreate(directoryPath);
string currentFile = null;
var index = 0;
try
{
foreach (var file in filePaths)
{
currentFile = file;
if (!FileSystem.File.Exists(file))
{
_logger.LogError("Unable to copy {File} to {DirectoryPath} as it doesn't exist", file, directoryPath);
continue;
}
var fileInfo = FileSystem.FileInfo.FromFileName(file);
var targetFile = FileSystem.FileInfo.FromFileName(RenameFileForCopy(newFilenames[index] + fileInfo.Extension, directoryPath));
fileInfo.CopyTo(FileSystem.Path.Join(directoryPath, targetFile.Name));
index++;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to copy {File} to {DirectoryPath}", currentFile, directoryPath);
return false;
}
return true;
}
/// <summary> /// <summary>
/// Generates the combined filepath given a prepend (optional), output directory path, and a full input file path. /// Generates the combined filepath given a prepend (optional), output directory path, and a full input file path.
/// If the output file already exists, will append (1), (2), etc until it can be written out /// If the output file already exists, will append (1), (2), etc until it can be written out
@ -434,30 +475,32 @@ public class DirectoryService : IDirectoryService
/// <returns></returns> /// <returns></returns>
private string RenameFileForCopy(string fileToCopy, string directoryPath, string prepend = "") private string RenameFileForCopy(string fileToCopy, string directoryPath, string prepend = "")
{ {
var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy); while (true)
var filename = prepend + fileInfo.Name;
var targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename));
if (!targetFile.Exists)
{ {
return targetFile.FullName; var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy);
} var filename = prepend + fileInfo.Name;
var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name); var targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename));
if (FileCopyAppend.IsMatch(noExtension)) if (!targetFile.Exists)
{ {
var match = FileCopyAppend.Match(noExtension).Value; return targetFile.FullName;
var matchNumber = match.Replace("(", string.Empty).Replace(")", string.Empty); }
noExtension = noExtension.Replace(match, $"({int.Parse(matchNumber) + 1})");
}
else
{
noExtension += " (1)";
}
var newFilename = prepend + noExtension + var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name);
FileSystem.Path.GetExtension(fileInfo.Name); if (FileCopyAppend.IsMatch(noExtension))
return RenameFileForCopy(FileSystem.Path.Join(directoryPath, newFilename), directoryPath, prepend); {
var match = FileCopyAppend.Match(noExtension).Value;
var matchNumber = match.Replace("(", string.Empty).Replace(")", string.Empty);
noExtension = noExtension.Replace(match, $"({int.Parse(matchNumber) + 1})");
}
else
{
noExtension += " (1)";
}
var newFilename = prepend + noExtension + FileSystem.Path.GetExtension(fileInfo.Name);
fileToCopy = FileSystem.Path.Join(directoryPath, newFilename);
}
} }
/// <summary> /// <summary>

View File

@ -12469,9 +12469,9 @@
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
}, },
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -15417,9 +15417,9 @@
"dev": true "dev": true
}, },
"swiper": { "swiper": {
"version": "8.0.6", "version": "8.4.4",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.0.6.tgz", "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.4.tgz",
"integrity": "sha512-Ssyu1+FeNATF/G8e84QG+ZUNtUOAZ5vngdgxzczh0oWZPhGUVgkdv+BoePUuaCXLAFXnwVpNjgLIcGnxMdmWPA==", "integrity": "sha512-jA/8BfOZwT8PqPSnMX0TENZYitXEhNa7ZSNj1Diqh5LZyUJoBQaZcqAiPQ/PIg1+IPaRn/V8ZYVb0nxHMh51yw==",
"requires": { "requires": {
"dom7": "^4.0.4", "dom7": "^4.0.4",
"ssr-window": "^4.0.2" "ssr-window": "^4.0.2"

View File

@ -44,7 +44,7 @@
"ngx-toastr": "^14.2.1", "ngx-toastr": "^14.2.1",
"requires": "^1.0.2", "requires": "^1.0.2",
"rxjs": "~7.5.4", "rxjs": "~7.5.4",
"swiper": "^8.0.6", "swiper": "^8.4.4",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"webpack-bundle-analyzer": "^4.5.0", "webpack-bundle-analyzer": "^4.5.0",
"zone.js": "~0.11.4" "zone.js": "~0.11.4"

View File

@ -36,15 +36,15 @@ export class JumpbarService {
const removalTimes = Math.ceil(removeCount / 2); const removalTimes = Math.ceil(removeCount / 2);
const midPoint = Math.floor(jumpBarKeys.length / 2); const midPoint = Math.floor(jumpBarKeys.length / 2);
jumpBarKeysToRender.push(jumpBarKeys[0]); jumpBarKeysToRender.push(jumpBarKeys[0]);
this.removeFirstPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender); this._removeFirstPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender);
jumpBarKeysToRender.push(jumpBarKeys[midPoint]); jumpBarKeysToRender.push(jumpBarKeys[midPoint]);
this.removeSecondPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender); this._removeSecondPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender);
jumpBarKeysToRender.push(jumpBarKeys[jumpBarKeys.length - 1]); jumpBarKeysToRender.push(jumpBarKeys[jumpBarKeys.length - 1]);
return jumpBarKeysToRender; return jumpBarKeysToRender;
} }
removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array<JumpKey>, jumpBarKeysToRender: Array<JumpKey>) { _removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array<JumpKey>, jumpBarKeysToRender: Array<JumpKey>) {
const removedIndexes: Array<number> = []; const removedIndexes: Array<number> = [];
for(let removal = 0; removal < numberOfRemovals; removal++) { for(let removal = 0; removal < numberOfRemovals; removal++) {
let min = 100000000; let min = 100000000;
@ -62,7 +62,7 @@ export class JumpbarService {
} }
} }
removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array<JumpKey>, jumpBarKeysToRender: Array<JumpKey>) { _removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1, jumpBarKeys: Array<JumpKey>, jumpBarKeysToRender: Array<JumpKey>) {
const removedIndexes: Array<number> = []; const removedIndexes: Array<number> = [];
for(let removal = 0; removal < numberOfRemovals; removal++) { for(let removal = 0; removal < numberOfRemovals; removal++) {
let min = 100000000; let min = 100000000;
@ -80,4 +80,35 @@ export class JumpbarService {
if (!removedIndexes.includes(i)) jumpBarKeysToRender.push(jumpBarKeys[i]); if (!removedIndexes.includes(i)) jumpBarKeysToRender.push(jumpBarKeys[i]);
} }
} }
/**
*
* @param data An array of objects
* @param keySelector A method to fetch a string from the object, which is used to classify the JumpKey
* @returns
*/
getJumpKeys(data :Array<any>, keySelector: (data: any) => string) {
const keys: {[key: string]: number} = {};
data.forEach(obj => {
let ch = keySelector(obj).charAt(0);
if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) {
ch = '#';
}
if (!keys.hasOwnProperty(ch)) {
keys[ch] = 0;
}
keys[ch] += 1;
});
return Object.keys(keys).map(k => {
return {
key: k,
size: keys[k],
title: k.toUpperCase()
}
}).sort((a, b) => {
if (a.key < b.key) return -1;
if (a.key > b.key) return 1;
return 0;
});
}
} }

View File

@ -11,6 +11,7 @@
[filterSettings]="filterSettings" [filterSettings]="filterSettings"
[trackByIdentity]="trackByIdentity" [trackByIdentity]="trackByIdentity"
[refresh]="refresh" [refresh]="refresh"
[jumpBarKeys]="jumpbarKeys"
(applyFilter)="updateFilter($event)" (applyFilter)="updateFilter($event)"
> >
<ng-template #cardItem let-item let-position="idx"> <ng-template #cardItem let-item let-position="idx">

View File

@ -8,12 +8,14 @@ import { ConfirmService } from 'src/app/shared/confirm.service';
import { DownloadService } from 'src/app/shared/_services/download.service'; import { DownloadService } from 'src/app/shared/_services/download.service';
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service'; import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
import { KEY_CODES } from 'src/app/shared/_services/utility.service'; import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
import { PageBookmark } from 'src/app/_models/page-bookmark'; import { PageBookmark } from 'src/app/_models/page-bookmark';
import { Pagination } from 'src/app/_models/pagination'; import { Pagination } from 'src/app/_models/pagination';
import { Series } from 'src/app/_models/series'; import { Series } from 'src/app/_models/series';
import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter'; import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { ReaderService } from 'src/app/_services/reader.service'; import { ReaderService } from 'src/app/_services/reader.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
@ -32,6 +34,7 @@ export class BookmarksComponent implements OnInit, OnDestroy {
downloadingSeries: {[id: number]: boolean} = {}; downloadingSeries: {[id: number]: boolean} = {};
clearingSeries: {[id: number]: boolean} = {}; clearingSeries: {[id: number]: boolean} = {};
actions: ActionItem<Series>[] = []; actions: ActionItem<Series>[] = [];
jumpbarKeys: Array<JumpKey> = [];
pagination!: Pagination; pagination!: Pagination;
filter: SeriesFilter | undefined = undefined; filter: SeriesFilter | undefined = undefined;
@ -50,7 +53,8 @@ export class BookmarksComponent implements OnInit, OnDestroy {
private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService, private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService,
public imageService: ImageService, private actionFactoryService: ActionFactoryService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
private router: Router, private readonly cdRef: ChangeDetectorRef, private router: Router, private readonly cdRef: ChangeDetectorRef,
private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute) { private filterUtilityService: FilterUtilitiesService, private route: ActivatedRoute,
private jumpbarService: JumpbarService) {
this.filterSettings.ageRatingDisabled = true; this.filterSettings.ageRatingDisabled = true;
this.filterSettings.collectionDisabled = true; this.filterSettings.collectionDisabled = true;
this.filterSettings.formatDisabled = true; this.filterSettings.formatDisabled = true;
@ -158,6 +162,7 @@ export class BookmarksComponent implements OnInit, OnDestroy {
const ids = Object.keys(this.seriesIds).map(k => parseInt(k, 10)); const ids = Object.keys(this.seriesIds).map(k => parseInt(k, 10));
this.seriesService.getAllSeriesByIds(ids).subscribe(series => { this.seriesService.getAllSeriesByIds(ids).subscribe(series => {
this.jumpbarKeys = this.jumpbarService.getJumpKeys(series, (t: Series) => t.name);
this.series = series; this.series = series;
this.loadingBookmarks = false; this.loadingBookmarks = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -3,13 +3,13 @@ import { Title } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component'; import { EditCollectionTagsComponent } from 'src/app/cards/_modals/edit-collection-tags/edit-collection-tags.component';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { CollectionTag } from 'src/app/_models/collection-tag'; import { CollectionTag } from 'src/app/_models/collection-tag';
import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
import { Tag } from 'src/app/_models/tag'; import { Tag } from 'src/app/_models/tag';
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service'; import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
import { CollectionTagService } from 'src/app/_services/collection-tag.service'; import { CollectionTagService } from 'src/app/_services/collection-tag.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
@Component({ @Component({
@ -30,7 +30,7 @@ export class AllCollectionsComponent implements OnInit {
constructor(private collectionService: CollectionTagService, private router: Router, constructor(private collectionService: CollectionTagService, private router: Router,
private actionFactoryService: ActionFactoryService, private modalService: NgbModal, private actionFactoryService: ActionFactoryService, private modalService: NgbModal,
private titleService: Title, private utilityService: UtilityService, private titleService: Title, private jumpbarService: JumpbarService,
private readonly cdRef: ChangeDetectorRef, public imageSerivce: ImageService) { private readonly cdRef: ChangeDetectorRef, public imageSerivce: ImageService) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.titleService.setTitle('Kavita - Collections'); this.titleService.setTitle('Kavita - Collections');
@ -54,7 +54,7 @@ export class AllCollectionsComponent implements OnInit {
this.collectionService.allTags().subscribe(tags => { this.collectionService.allTags().subscribe(tags => {
this.collections = tags; this.collections = tags;
this.isLoading = false; this.isLoading = false;
this.jumpbarKeys = this.utilityService.getJumpKeys(tags, (t: Tag) => t.title); this.jumpbarKeys = this.jumpbarService.getJumpKeys(tags, (t: Tag) => t.title);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} }

View File

@ -21,6 +21,7 @@ import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/acti
import { ActionService } from 'src/app/_services/action.service'; import { ActionService } from 'src/app/_services/action.service';
import { CollectionTagService } from 'src/app/_services/collection-tag.service'; import { CollectionTagService } from 'src/app/_services/collection-tag.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service'; import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
import { ScrollService } from 'src/app/_services/scroll.service'; import { ScrollService } from 'src/app/_services/scroll.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
@ -124,7 +125,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten
constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute, constructor(public imageService: ImageService, private collectionService: CollectionTagService, private router: Router, private route: ActivatedRoute,
private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService, private seriesService: SeriesService, private toastr: ToastrService, private actionFactoryService: ActionFactoryService,
private modalService: NgbModal, private titleService: Title, private modalService: NgbModal, private titleService: Title, private jumpbarService: JumpbarService,
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService, public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document, private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService) { private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService) {
@ -210,7 +211,7 @@ export class CollectionDetailComponent implements OnInit, OnDestroy, AfterConten
this.seriesService.getAllSeries(undefined, undefined, this.filter).pipe(take(1)).subscribe(series => { this.seriesService.getAllSeries(undefined, undefined, this.filter).pipe(take(1)).subscribe(series => {
this.series = series.result; this.series = series.result;
this.seriesPagination = series.pagination; this.seriesPagination = series.pagination;
this.jumpbarKeys = this.utilityService.getJumpKeys(this.series, (series: Series) => series.name); this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.series, (series: Series) => series.name);
this.isLoading = false; this.isLoading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
import { LayoutMode } from '../_models/layout-mode';
@Pipe({
name: 'layoutModeIcon'
})
export class LayoutModeIconPipe implements PipeTransform {
transform(layoutMode: LayoutMode): string {
switch (layoutMode) {
case LayoutMode.Single:
return 'none';
case LayoutMode.Double:
return 'double';
case LayoutMode.DoubleReversed:
return 'double-reversed';
}
}
}

View File

@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core';
import { ReaderMode } from 'src/app/_models/preferences/reader-mode';
@Pipe({
name: 'readerModeIcon'
})
export class ReaderModeIconPipe implements PipeTransform {
transform(readerMode: ReaderMode): string {
switch(readerMode) {
case ReaderMode.LeftRight:
return 'fa-exchange-alt';
case ReaderMode.UpDown:
return 'fa-exchange-alt fa-rotate-90';
case ReaderMode.Webtoon:
return 'fa-arrows-alt-v';
default:
return '';
}
}
}

View File

@ -121,7 +121,7 @@
</div> </div>
<div class="col"> <div class="col">
<button class="btn btn-icon" title="Reading Mode" (click)="toggleReaderMode();resetMenuCloseTimer();"> <button class="btn btn-icon" title="Reading Mode" (click)="toggleReaderMode();resetMenuCloseTimer();">
<i class="fa {{ReaderModeIcon}}" aria-hidden="true"></i> <i class="fa {{this.readerMode | readerModeIcon}}" aria-hidden="true"></i>
<span class="visually-hidden">Reading Mode</span> <span class="visually-hidden">Reading Mode</span>
</button> </button>
</div> </div>
@ -197,7 +197,6 @@
</span> </span>
</div> </div>
</ng-container> </ng-container>
<!-- <div class="{{LayoutModeIconClass}}"></div> -->
</ng-container> </ng-container>
<select class="form-control" id="page-fitting" formControlName="layoutMode"> <select class="form-control" id="page-fitting" formControlName="layoutMode">
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option> <option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>

View File

@ -1,4 +1,4 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Inject, NgZone, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { debounceTime, take, takeUntil } from 'rxjs/operators'; import { debounceTime, take, takeUntil } from 'rxjs/operators';
@ -290,9 +290,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
bookmarkPageHandler = this.bookmarkPage.bind(this); bookmarkPageHandler = this.bookmarkPage.bind(this);
getPageUrl = (pageNum: number) => { getPageUrl = (pageNum: number, chapterId: number = this.chapterId) => {
if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum); if (this.bookmarkMode) return this.readerService.getBookmarkPageUrl(this.seriesId, this.user.apiKey, pageNum);
return this.readerService.getPageUrl(this.chapterId, pageNum); return this.readerService.getPageUrl(chapterId, pageNum);
} }
private readonly onDestroy = new Subject<void>(); private readonly onDestroy = new Subject<void>();
@ -377,29 +377,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
return 'right-side'; return 'right-side';
} }
get LayoutModeIconClass() {
switch (this.layoutMode) {
case LayoutMode.Single:
return 'none';
case LayoutMode.Double:
return 'double';
case LayoutMode.DoubleReversed:
return 'double-reversed';
}
}
get ReaderModeIcon() {
switch(this.readerMode) {
case ReaderMode.LeftRight:
return 'fa-exchange-alt';
case ReaderMode.UpDown:
return 'fa-exchange-alt fa-rotate-90';
case ReaderMode.Webtoon:
return 'fa-arrows-alt-v';
default:
return '';
}
}
get ReaderMode() { get ReaderMode() {
return ReaderMode; return ReaderMode;
@ -433,7 +410,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
private toastr: ToastrService, private memberService: MemberService, private toastr: ToastrService, private memberService: MemberService,
public utilityService: UtilityService, private renderer: Renderer2, public utilityService: UtilityService, private renderer: Renderer2,
@Inject(DOCUMENT) private document: Document, private modalService: NgbModal, @Inject(DOCUMENT) private document: Document, private modalService: NgbModal,
private readonly cdRef: ChangeDetectorRef) { private readonly cdRef: ChangeDetectorRef, private readonly ngZone: NgZone) {
this.navService.hideNavBar(); this.navService.hideNavBar();
this.navService.hideSideNav(); this.navService.hideSideNav();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
@ -626,6 +603,23 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
/**
* Gets a page from cache else gets a brand new Image
* @param pageNum Page Number to load
* @param forceNew Forces to fetch a new image
* @param chapterId ChapterId to fetch page from. Defaults to current chapterId
* @returns
*/
getPage(pageNum: number, chapterId: number = this.chapterId, forceNew: boolean = false) {
let img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === pageNum);
if (!img || forceNew) {
img = new Image();
img.src = this.getPageUrl(this.pageNum, chapterId);
}
return img;
}
// if there is scroll room and on original, then don't paginate // if there is scroll room and on original, then don't paginate
checkIfPaginationAllowed() { checkIfPaginationAllowed() {
// This is not used atm due to the complexity it adds with keyboard. // This is not used atm due to the complexity it adds with keyboard.
@ -748,6 +742,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) { if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
this.nextChapterDisabled = true; this.nextChapterDisabled = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} else {
// Fetch the first page of next chapter
this.getPage(0, this.nextChapterId);
} }
}); });
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 => {
@ -755,6 +752,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) { if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
this.prevChapterDisabled = true; this.prevChapterDisabled = true;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} else {
// Fetch the last page of prev chapter
this.getPage(1000000, this.nextChapterId);
} }
}); });
@ -961,6 +961,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
onSwipeEvent(event: any) {
console.log('Swipe event occured: ', event);
}
handlePageChange(event: any, direction: string) { handlePageChange(event: any, direction: string) {
if (this.readerMode === ReaderMode.Webtoon) { if (this.readerMode === ReaderMode.Webtoon) {
if (direction === 'right') { if (direction === 'right') {
@ -1073,25 +1077,15 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* Sets canvasImage's src to current page, but first attempts to use a pre-fetched image * Sets canvasImage's src to current page, but first attempts to use a pre-fetched image
*/ */
setCanvasImage() { setCanvasImage() {
if (this.layoutMode === LayoutMode.Single) { this.canvasImage = this.getPage(this.pageNum);
const img = this.cachedImages.find(img => this.readerService.imageUrlToPageNum(img.src) === this.pageNum);
if (img) {
this.canvasImage = img; // If we tried to use this for double, then the loadPage might not render correctly when switching layout mode
} else {
this.canvasImage.src = this.getPageUrl(this.pageNum);
}
} else {
this.canvasImage.src = this.getPageUrl(this.pageNum);
}
this.canvasImage.onload = () => { this.canvasImage.onload = () => {
this.cdRef.markForCheck(); this.renderPage();
}; };
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
loadNextChapter() { loadNextChapter() {
if (this.nextPageDisabled || this.nextChapterDisabled || this.bookmarkMode) { if (this.nextPageDisabled || this.nextChapterDisabled || this.bookmarkMode) {
this.toastr.info('No Next Chapter'); this.toastr.info('No Next Chapter');
@ -1301,18 +1295,32 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
* and also maintains page info (wide image, etc) due to onload event. * and also maintains page info (wide image, etc) due to onload event.
*/ */
prefetch() { prefetch() {
// NOTE: This doesn't allow for any directionality
// NOTE: This doesn't maintain 1 image behind at all times
// NOTE: I may want to provide a different prefetcher for double renderer
for(let i = 0; i <= PREFETCH_PAGES - 3; i++) { for(let i = 0; i <= PREFETCH_PAGES - 3; i++) {
const numOffset = this.pageNum + i; const numOffset = this.pageNum + i;
if (numOffset > this.maxPages - 1) continue; if (numOffset > this.maxPages - 1) continue;
const index = (numOffset % this.cachedImages.length + this.cachedImages.length) % this.cachedImages.length; const index = (numOffset % this.cachedImages.length + this.cachedImages.length) % this.cachedImages.length;
if (this.readerService.imageUrlToPageNum(this.cachedImages[index].src) !== numOffset) { if (this.readerService.imageUrlToPageNum(this.cachedImages[index].src) !== numOffset) {
this.cachedImages[index] = new Image();
this.cachedImages[index].src = this.getPageUrl(numOffset); this.cachedImages[index].src = this.getPageUrl(numOffset);
this.cachedImages[index].onload = () => this.cdRef.markForCheck(); this.cachedImages[index].onload = () => {
//console.log('Page ', numOffset, ' loaded');
//this.cdRef.markForCheck();
};
} }
} }
//console.log(this.pageNum, ' Prefetched pages: ', this.cachedImages.map(img => this.readerService.imageUrlToPageNum(img.src))); const pages = this.cachedImages.map(img => this.readerService.imageUrlToPageNum(img.src));
const pagesBefore = pages.filter(p => p >= 0 && p < this.pageNum).length;
const pagesAfter = pages.filter(p => p >= 0 && p > this.pageNum).length;
console.log('Buffer Health: Before: ', pagesBefore, ' After: ', pagesAfter);
console.log(this.pageNum, ' Prefetched pages: ', pages.map(p => {
if (this.pageNum === p) return '[' + p + ']';
return '' + p
}));
} }
@ -1327,30 +1335,54 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
this.setCanvasImage(); this.setCanvasImage();
// ?! This logic is hella complex and confusing to read
// ?! We need to refactor into separate methods and keep it clean
// ?! In addition, we shouldn't update canvasImage outside of this code
if (this.layoutMode !== LayoutMode.Single) { if (this.layoutMode !== LayoutMode.Single) {
this.canvasImageNext.src = this.getPageUrl(this.pageNum + 1); // This needs to be capped at maxPages !this.isLastImage()
this.canvasImagePrev.src = this.getPageUrl(this.pageNum - 1); this.canvasImageNext = new Image();
// If prev page was a spread, then we don't do + 1
console.log('Current canvas image page: ', this.readerService.imageUrlToPageNum(this.canvasImage.src));
console.log('Prev canvas image page: ', this.readerService.imageUrlToPageNum(this.canvasImage2.src));
// if (this.isWideImage(this.canvasImage2)) {
// this.canvasImagePrev = this.getPage(this.pageNum); // this.getPageUrl(this.pageNum);
// console.log('Setting Prev to ', this.pageNum);
// } else {
// this.canvasImagePrev = this.getPage(this.pageNum - 1); //this.getPageUrl(this.pageNum - 1);
// console.log('Setting Prev to ', this.pageNum - 1);
// }
// TODO: Validate this statement: This needs to be capped at maxPages !this.isLastImage()
this.canvasImageNext = this.getPage(this.pageNum + 1);
console.log('Setting Next to ', this.pageNum + 1);
this.canvasImagePrev = this.getPage(this.pageNum - 1);
console.log('Setting Prev to ', this.pageNum - 1);
if (this.pageNum + 2 < this.maxPages - 1) { if (this.pageNum + 2 < this.maxPages - 1) {
this.canvasImageAheadBy2.src = this.getPageUrl(this.pageNum + 2); this.canvasImageAheadBy2 = this.getPage(this.pageNum + 2);
} }
if (this.pageNum - 2 >= 0) { if (this.pageNum - 2 >= 0) {
this.canvasImageBehindBy2.src = this.getPageUrl(this.pageNum - 2 || 0); this.canvasImageBehindBy2 = this.getPage(this.pageNum - 2 || 0);
} }
if (this.ShouldRenderDoublePage || this.ShouldRenderReverseDouble) { if (this.ShouldRenderDoublePage || this.ShouldRenderReverseDouble) {
console.log('Rendering Double Page');
if (this.layoutMode === LayoutMode.Double) { if (this.layoutMode === LayoutMode.Double) {
this.canvasImage2.src = this.canvasImageNext.src; this.canvasImage2 = this.canvasImageNext;
} else { } else {
this.canvasImage2.src = this.canvasImagePrev.src; this.canvasImage2 = this.canvasImagePrev;
} }
} }
} }
this.cdRef.markForCheck();
this.cdRef.markForCheck();
this.renderPage(); this.renderPage();
this.prefetch(); this.prefetch();
this.isLoading = false;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }

View File

@ -8,14 +8,18 @@ import { SharedModule } from '../shared/shared.module';
import { NgxSliderModule } from '@angular-slider/ngx-slider'; import { NgxSliderModule } from '@angular-slider/ngx-slider';
import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller.component'; import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller.component';
import { ReaderSharedModule } from '../reader-shared/reader-shared.module'; import { ReaderSharedModule } from '../reader-shared/reader-shared.module';
import { FullscreenIconPipe } from './fullscreen-icon.pipe';
import { PipeModule } from '../pipe/pipe.module'; import { PipeModule } from '../pipe/pipe.module';
import { FullscreenIconPipe } from './_pipes/fullscreen-icon.pipe';
import { LayoutModeIconPipe } from './_pipes/layout-mode-icon.pipe';
import { ReaderModeIconPipe } from './_pipes/reader-mode-icon.pipe';
@NgModule({ @NgModule({
declarations: [ declarations: [
MangaReaderComponent, MangaReaderComponent,
InfiniteScrollerComponent, InfiniteScrollerComponent,
FullscreenIconPipe FullscreenIconPipe,
ReaderModeIconPipe,
LayoutModeIconPipe,
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -56,9 +56,7 @@ export class EditReadingListModalComponent implements OnInit {
save() { save() {
if (this.reviewGroup.value.title.trim() === '') return; if (this.reviewGroup.value.title.trim() === '') return;
const model = {...this.reviewGroup.value, readingListId: this.readingList.id, promoted: this.readingList.promoted, coverImageLocked: this.coverImageLocked}; const model = {...this.reviewGroup.value, readingListId: this.readingList.id, promoted: this.readingList.promoted, coverImageLocked: this.coverImageLocked};
const apis = [this.readingListService.update(model)]; const apis = [this.readingListService.update(model)];
if (this.selectedCover !== '') { if (this.selectedCover !== '') {
@ -77,7 +75,7 @@ export class EditReadingListModalComponent implements OnInit {
togglePromotion() { togglePromotion() {
const originalPromotion = this.readingList.promoted; const originalPromotion = this.readingList.promoted;
this.readingList.promoted = !this.readingList.promoted; this.readingList.promoted = !this.readingList.promoted;
const model = {readingListId: this.readingList.id, promoted: this.readingList.promoted}; const model = {...this.reviewGroup.value, readingListId: this.readingList.id, promoted: this.readingList.promoted, coverImageLocked: this.coverImageLocked};
this.readingListService.update(model).subscribe(res => { this.readingListService.update(model).subscribe(res => {
/* No Operation */ /* No Operation */
}, err => { }, err => {

View File

@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { JumpKey } from 'src/app/_models/jumpbar/jump-key'; import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
import { PaginatedResult, Pagination } from 'src/app/_models/pagination'; import { PaginatedResult, Pagination } from 'src/app/_models/pagination';
import { ReadingList } from 'src/app/_models/reading-list'; import { ReadingList } from 'src/app/_models/reading-list';
@ -10,6 +9,7 @@ import { AccountService } from 'src/app/_services/account.service';
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service'; import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.service';
import { ActionService } from 'src/app/_services/action.service'; import { ActionService } from 'src/app/_services/action.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { ReadingListService } from 'src/app/_services/reading-list.service'; import { ReadingListService } from 'src/app/_services/reading-list.service';
@Component({ @Component({
@ -28,7 +28,7 @@ export class ReadingListsComponent implements OnInit {
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService, constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService, private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) { } private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef) { }
ngOnInit(): void { ngOnInit(): void {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => { this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -84,7 +84,7 @@ export class ReadingListsComponent implements OnInit {
this.readingListService.getReadingLists(true).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => { this.readingListService.getReadingLists(true).pipe(take(1)).subscribe((readingLists: PaginatedResult<ReadingList[]>) => {
this.lists = readingLists.result; this.lists = readingLists.result;
this.pagination = readingLists.pagination; this.pagination = readingLists.pagination;
this.jumpbarKeys = this.utilityService.getJumpKeys(readingLists.result, (rl: ReadingList) => rl.title); this.jumpbarKeys = this.jumpbarService.getJumpKeys(readingLists.result, (rl: ReadingList) => rl.title);
this.loadingLists = false; this.loadingLists = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.cdRef.markForCheck(); this.cdRef.markForCheck();

View File

@ -203,36 +203,4 @@ export class UtilityService {
|| document.body.clientHeight; || document.body.clientHeight;
return [windowWidth, windowHeight]; return [windowWidth, windowHeight];
} }
/**
*
* @param data An array of objects
* @param keySelector A method to fetch a string from the object, which is used to classify the JumpKey
* @returns
*/
getJumpKeys(data :Array<any>, keySelector: (data: any) => string) {
const keys: {[key: string]: number} = {};
data.forEach(obj => {
let ch = keySelector(obj).charAt(0);
if (/\d|\#|!|%|@|\(|\)|\^|\.|_|\*/g.test(ch)) {
ch = '#';
}
if (!keys.hasOwnProperty(ch)) {
keys[ch] = 0;
}
keys[ch] += 1;
});
return Object.keys(keys).map(k => {
return {
key: k,
size: keys[k],
title: k.toUpperCase()
}
}).sort((a, b) => {
if (a.key < b.key) return -1;
if (a.key > b.key) return 1;
return 0;
});
}
} }

View File

@ -15,6 +15,7 @@ import { SeriesFilter, FilterEvent } from 'src/app/_models/series-filter';
import { Action, ActionItem } from 'src/app/_services/action-factory.service'; import { Action, ActionItem } from 'src/app/_services/action-factory.service';
import { ActionService } from 'src/app/_services/action.service'; import { ActionService } from 'src/app/_services/action.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { JumpbarService } from 'src/app/_services/jumpbar.service';
import { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service'; import { MessageHubService, EVENTS } from 'src/app/_services/message-hub.service';
import { ScrollService } from 'src/app/_services/scroll.service'; import { ScrollService } from 'src/app/_services/scroll.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
@ -80,7 +81,8 @@ export class WantToReadComponent implements OnInit, OnDestroy {
private seriesService: SeriesService, private titleService: Title, private seriesService: SeriesService, private titleService: Title,
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService, public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document, private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService, private hubService: MessageHubService) { private readonly cdRef: ChangeDetectorRef, private scrollService: ScrollService, private hubService: MessageHubService,
private jumpbarService: JumpbarService) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false; this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.titleService.setTitle('Want To Read'); this.titleService.setTitle('Want To Read');
@ -152,7 +154,7 @@ export class WantToReadComponent implements OnInit, OnDestroy {
this.seriesService.getWantToRead(undefined, undefined, this.filter).pipe(take(1)).subscribe(paginatedList => { this.seriesService.getWantToRead(undefined, undefined, this.filter).pipe(take(1)).subscribe(paginatedList => {
this.series = paginatedList.result; this.series = paginatedList.result;
this.seriesPagination = paginatedList.pagination; this.seriesPagination = paginatedList.pagination;
this.jumpbarKeys = this.utilityService.getJumpKeys(this.series, (series: Series) => series.name); this.jumpbarKeys = this.jumpbarService.getJumpKeys(this.series, (series: Series) => series.name);
this.isLoading = false; this.isLoading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.cdRef.markForCheck(); this.cdRef.markForCheck();