mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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:
parent
dab42041d5
commit
38a169818b
@ -593,6 +593,23 @@ public class DirectoryServiceTests
|
||||
|| 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
|
||||
|
||||
#region ListDirectory
|
||||
|
@ -83,8 +83,9 @@ public class ReaderController : BaseApiController
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <remarks>This will cache the chapter images for reading</remarks>
|
||||
/// <param name="chapterId"></param>
|
||||
/// <param name="page"></param>
|
||||
/// <returns></returns>
|
||||
@ -99,6 +100,7 @@ public class ReaderController : BaseApiController
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: This code is very generic and repeated, see if we can refactor into a common method
|
||||
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.");
|
||||
var format = Path.GetExtension(path).Replace(".", "");
|
||||
@ -128,7 +130,6 @@ public class ReaderController : BaseApiController
|
||||
if (page < 0) page = 0;
|
||||
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);
|
||||
if (page > totalPages)
|
||||
{
|
||||
@ -139,7 +140,7 @@ public class ReaderController : BaseApiController
|
||||
{
|
||||
var path = _cacheService.GetCachedBookmarkPagePath(seriesId, 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));
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Comparators;
|
||||
|
@ -1,10 +1,14 @@
|
||||
namespace API.DTOs.ReadingLists;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace API.DTOs.ReadingLists;
|
||||
|
||||
public class UpdateReadingListDto
|
||||
{
|
||||
[Required]
|
||||
public int ReadingListId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Summary { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
public bool Promoted { get; set; }
|
||||
public bool CoverImageLocked { get; set; }
|
||||
}
|
||||
|
@ -305,7 +305,7 @@ public class UserRepository : IUserRepository
|
||||
{
|
||||
return await _context.AppUserBookmark
|
||||
.Where(x => x.AppUserId == userId && x.SeriesId == seriesId)
|
||||
.OrderBy(x => x.Page)
|
||||
.OrderBy(x => x.Created)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
@ -315,7 +315,7 @@ public class UserRepository : IUserRepository
|
||||
{
|
||||
return await _context.AppUserBookmark
|
||||
.Where(x => x.AppUserId == userId && x.VolumeId == volumeId)
|
||||
.OrderBy(x => x.Page)
|
||||
.OrderBy(x => x.Created)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
@ -325,7 +325,7 @@ public class UserRepository : IUserRepository
|
||||
{
|
||||
return await _context.AppUserBookmark
|
||||
.Where(x => x.AppUserId == userId && x.ChapterId == chapterId)
|
||||
.OrderBy(x => x.Page)
|
||||
.OrderBy(x => x.Created)
|
||||
.AsNoTracking()
|
||||
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
@ -341,25 +341,27 @@ public class UserRepository : IUserRepository
|
||||
{
|
||||
var query = _context.AppUserBookmark
|
||||
.Where(x => x.AppUserId == userId)
|
||||
.OrderBy(x => x.Page)
|
||||
.OrderBy(x => x.Created)
|
||||
.AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrEmpty(filter.SeriesNameQuery))
|
||||
{
|
||||
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}%")
|
||||
);
|
||||
if (string.IsNullOrEmpty(filter.SeriesNameQuery))
|
||||
return await query
|
||||
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
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
|
||||
|
@ -79,10 +79,7 @@ public static class Seed
|
||||
{
|
||||
new() {Key = ServerSettingKey.CacheDirectory, Value = directoryService.CacheDirectory},
|
||||
new() {Key = ServerSettingKey.TaskScan, Value = "daily"},
|
||||
new()
|
||||
{
|
||||
Key = ServerSettingKey.LoggingLevel, Value = "Information"
|
||||
}, // Not used from DB, but DB is sync with appSettings.json
|
||||
new() {Key = ServerSettingKey.LoggingLevel, Value = "Debug"},
|
||||
new() {Key = ServerSettingKey.TaskBackup, Value = "daily"},
|
||||
new()
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -101,7 +102,7 @@ public class CacheService : ICacheService
|
||||
var extractPath = GetCachePath(chapterId);
|
||||
|
||||
if (_directoryService.Exists(extractPath)) return chapter;
|
||||
var files = chapter.Files.ToList();
|
||||
var files = chapter?.Files.ToList();
|
||||
ExtractChapterFiles(extractPath, files);
|
||||
|
||||
return chapter;
|
||||
@ -223,6 +224,8 @@ public class CacheService : ICacheService
|
||||
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)
|
||||
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 files = (await _bookmarkService.GetBookmarkFilesById(bookmarkDtos.Select(b => b.Id))).ToList();
|
||||
_directoryService.CopyFilesToDirectory(files, destDirectory);
|
||||
_directoryService.Flatten(destDirectory);
|
||||
_directoryService.CopyFilesToDirectory(files, destDirectory,
|
||||
Enumerable.Range(1, files.Count).Select(i => i + string.Empty).ToList());
|
||||
return files.Count;
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ public interface IDirectoryService
|
||||
IEnumerable<DirectoryDto> ListDirectory(string rootPath);
|
||||
Task<byte[]> ReadFileAsync(string path);
|
||||
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, string prepend = "");
|
||||
bool CopyFilesToDirectory(IEnumerable<string> filePaths, string directoryPath, IList<string> newFilenames);
|
||||
bool Exists(string directory);
|
||||
void CopyFileToDirectory(string fullFilePath, string targetDirectory);
|
||||
int TraverseTreeParallelForEach(string root, Action<string> action, string searchPattern, ILogger logger);
|
||||
@ -424,6 +425,46 @@ public class DirectoryService : IDirectoryService
|
||||
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>
|
||||
/// 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
|
||||
@ -434,30 +475,32 @@ public class DirectoryService : IDirectoryService
|
||||
/// <returns></returns>
|
||||
private string RenameFileForCopy(string fileToCopy, string directoryPath, string prepend = "")
|
||||
{
|
||||
var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy);
|
||||
var filename = prepend + fileInfo.Name;
|
||||
|
||||
var targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename));
|
||||
if (!targetFile.Exists)
|
||||
while (true)
|
||||
{
|
||||
return targetFile.FullName;
|
||||
}
|
||||
var fileInfo = FileSystem.FileInfo.FromFileName(fileToCopy);
|
||||
var filename = prepend + fileInfo.Name;
|
||||
|
||||
var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name);
|
||||
if (FileCopyAppend.IsMatch(noExtension))
|
||||
{
|
||||
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 targetFile = FileSystem.FileInfo.FromFileName(FileSystem.Path.Join(directoryPath, filename));
|
||||
if (!targetFile.Exists)
|
||||
{
|
||||
return targetFile.FullName;
|
||||
}
|
||||
|
||||
var newFilename = prepend + noExtension +
|
||||
FileSystem.Path.GetExtension(fileInfo.Name);
|
||||
return RenameFileForCopy(FileSystem.Path.Join(directoryPath, newFilename), directoryPath, prepend);
|
||||
var noExtension = FileSystem.Path.GetFileNameWithoutExtension(fileInfo.Name);
|
||||
if (FileCopyAppend.IsMatch(noExtension))
|
||||
{
|
||||
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>
|
||||
|
12
UI/Web/package-lock.json
generated
12
UI/Web/package-lock.json
generated
@ -12469,9 +12469,9 @@
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -15417,9 +15417,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"swiper": {
|
||||
"version": "8.0.6",
|
||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.0.6.tgz",
|
||||
"integrity": "sha512-Ssyu1+FeNATF/G8e84QG+ZUNtUOAZ5vngdgxzczh0oWZPhGUVgkdv+BoePUuaCXLAFXnwVpNjgLIcGnxMdmWPA==",
|
||||
"version": "8.4.4",
|
||||
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.4.tgz",
|
||||
"integrity": "sha512-jA/8BfOZwT8PqPSnMX0TENZYitXEhNa7ZSNj1Diqh5LZyUJoBQaZcqAiPQ/PIg1+IPaRn/V8ZYVb0nxHMh51yw==",
|
||||
"requires": {
|
||||
"dom7": "^4.0.4",
|
||||
"ssr-window": "^4.0.2"
|
||||
|
@ -44,7 +44,7 @@
|
||||
"ngx-toastr": "^14.2.1",
|
||||
"requires": "^1.0.2",
|
||||
"rxjs": "~7.5.4",
|
||||
"swiper": "^8.0.6",
|
||||
"swiper": "^8.4.4",
|
||||
"tslib": "^2.3.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"zone.js": "~0.11.4"
|
||||
|
@ -36,15 +36,15 @@ export class JumpbarService {
|
||||
const removalTimes = Math.ceil(removeCount / 2);
|
||||
const midPoint = Math.floor(jumpBarKeys.length / 2);
|
||||
jumpBarKeysToRender.push(jumpBarKeys[0]);
|
||||
this.removeFirstPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender);
|
||||
this._removeFirstPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender);
|
||||
jumpBarKeysToRender.push(jumpBarKeys[midPoint]);
|
||||
this.removeSecondPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender);
|
||||
this._removeSecondPartOfJumpBar(midPoint, removalTimes, jumpBarKeys, jumpBarKeysToRender);
|
||||
jumpBarKeysToRender.push(jumpBarKeys[jumpBarKeys.length - 1]);
|
||||
|
||||
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> = [];
|
||||
for(let removal = 0; removal < numberOfRemovals; removal++) {
|
||||
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> = [];
|
||||
for(let removal = 0; removal < numberOfRemovals; removal++) {
|
||||
let min = 100000000;
|
||||
@ -80,4 +80,35 @@ export class JumpbarService {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
[filterSettings]="filterSettings"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[refresh]="refresh"
|
||||
[jumpBarKeys]="jumpbarKeys"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
|
@ -8,12 +8,14 @@ import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.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 { Pagination } from 'src/app/_models/pagination';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { FilterEvent, SeriesFilter } from 'src/app/_models/series-filter';
|
||||
import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/action-factory.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 { SeriesService } from 'src/app/_services/series.service';
|
||||
|
||||
@ -32,6 +34,7 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
||||
downloadingSeries: {[id: number]: boolean} = {};
|
||||
clearingSeries: {[id: number]: boolean} = {};
|
||||
actions: ActionItem<Series>[] = [];
|
||||
jumpbarKeys: Array<JumpKey> = [];
|
||||
|
||||
pagination!: Pagination;
|
||||
filter: SeriesFilter | undefined = undefined;
|
||||
@ -50,7 +53,8 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
||||
private confirmService: ConfirmService, public bulkSelectionService: BulkSelectionService,
|
||||
public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||
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.collectionDisabled = 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));
|
||||
this.seriesService.getAllSeriesByIds(ids).subscribe(series => {
|
||||
this.jumpbarKeys = this.jumpbarService.getJumpKeys(series, (t: Series) => t.name);
|
||||
this.series = series;
|
||||
this.loadingBookmarks = false;
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -3,13 +3,13 @@ import { Title } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
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 { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||
import { Tag } from 'src/app/_models/tag';
|
||||
import { ActionItem, ActionFactoryService, Action } from 'src/app/_services/action-factory.service';
|
||||
import { CollectionTagService } from 'src/app/_services/collection-tag.service';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { JumpbarService } from 'src/app/_services/jumpbar.service';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -30,7 +30,7 @@ export class AllCollectionsComponent implements OnInit {
|
||||
|
||||
constructor(private collectionService: CollectionTagService, private router: Router,
|
||||
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) {
|
||||
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||
this.titleService.setTitle('Kavita - Collections');
|
||||
@ -54,7 +54,7 @@ export class AllCollectionsComponent implements OnInit {
|
||||
this.collectionService.allTags().subscribe(tags => {
|
||||
this.collections = tags;
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import { Action, ActionFactoryService, ActionItem } from 'src/app/_services/acti
|
||||
import { ActionService } from 'src/app/_services/action.service';
|
||||
import { CollectionTagService } from 'src/app/_services/collection-tag.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 { ScrollService } from 'src/app/_services/scroll.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,
|
||||
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,
|
||||
private filterUtilityService: FilterUtilitiesService, private utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
|
||||
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.series = series.result;
|
||||
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;
|
||||
window.scrollTo(0, 0);
|
||||
this.cdRef.markForCheck();
|
||||
|
20
UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts
Normal file
20
UI/Web/src/app/manga-reader/_pipes/layout-mode-icon.pipe.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
22
UI/Web/src/app/manga-reader/_pipes/reader-mode-icon.pipe.ts
Normal file
22
UI/Web/src/app/manga-reader/_pipes/reader-mode-icon.pipe.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -121,7 +121,7 @@
|
||||
</div>
|
||||
<div class="col">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
@ -197,7 +197,6 @@
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- <div class="{{LayoutModeIconClass}}"></div> -->
|
||||
</ng-container>
|
||||
<select class="form-control" id="page-fitting" formControlName="layoutMode">
|
||||
<option [value]="opt.value" *ngFor="let opt of layoutModes">{{opt.text}}</option>
|
||||
|
@ -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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||
@ -290,9 +290,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
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);
|
||||
return this.readerService.getPageUrl(this.chapterId, pageNum);
|
||||
return this.readerService.getPageUrl(chapterId, pageNum);
|
||||
}
|
||||
|
||||
private readonly onDestroy = new Subject<void>();
|
||||
@ -377,29 +377,6 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
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() {
|
||||
return ReaderMode;
|
||||
@ -433,7 +410,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private toastr: ToastrService, private memberService: MemberService,
|
||||
public utilityService: UtilityService, private renderer: Renderer2,
|
||||
@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.hideSideNav();
|
||||
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
|
||||
checkIfPaginationAllowed() {
|
||||
// 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) {
|
||||
this.nextChapterDisabled = true;
|
||||
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 => {
|
||||
@ -755,6 +752,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
if (chapterId === CHAPTER_ID_DOESNT_EXIST || chapterId === this.chapterId) {
|
||||
this.prevChapterDisabled = true;
|
||||
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) {
|
||||
if (this.readerMode === ReaderMode.Webtoon) {
|
||||
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
|
||||
*/
|
||||
setCanvasImage() {
|
||||
if (this.layoutMode === LayoutMode.Single) {
|
||||
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 = this.getPage(this.pageNum);
|
||||
this.canvasImage.onload = () => {
|
||||
this.cdRef.markForCheck();
|
||||
this.renderPage();
|
||||
};
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
loadNextChapter() {
|
||||
if (this.nextPageDisabled || this.nextChapterDisabled || this.bookmarkMode) {
|
||||
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.
|
||||
*/
|
||||
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++) {
|
||||
const numOffset = this.pageNum + i;
|
||||
if (numOffset > this.maxPages - 1) continue;
|
||||
|
||||
const index = (numOffset % this.cachedImages.length + this.cachedImages.length) % this.cachedImages.length;
|
||||
if (this.readerService.imageUrlToPageNum(this.cachedImages[index].src) !== numOffset) {
|
||||
this.cachedImages[index] = new Image();
|
||||
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 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) {
|
||||
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) {
|
||||
this.canvasImageAheadBy2.src = this.getPageUrl(this.pageNum + 2);
|
||||
this.canvasImageAheadBy2 = this.getPage(this.pageNum + 2);
|
||||
}
|
||||
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) {
|
||||
console.log('Rendering Double Page');
|
||||
if (this.layoutMode === LayoutMode.Double) {
|
||||
this.canvasImage2.src = this.canvasImageNext.src;
|
||||
this.canvasImage2 = this.canvasImageNext;
|
||||
} else {
|
||||
this.canvasImage2.src = this.canvasImagePrev.src;
|
||||
this.canvasImage2 = this.canvasImagePrev;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
|
||||
this.cdRef.markForCheck();
|
||||
this.renderPage();
|
||||
this.prefetch();
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
|
@ -8,14 +8,18 @@ import { SharedModule } from '../shared/shared.module';
|
||||
import { NgxSliderModule } from '@angular-slider/ngx-slider';
|
||||
import { InfiniteScrollerComponent } from './infinite-scroller/infinite-scroller.component';
|
||||
import { ReaderSharedModule } from '../reader-shared/reader-shared.module';
|
||||
import { FullscreenIconPipe } from './fullscreen-icon.pipe';
|
||||
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({
|
||||
declarations: [
|
||||
MangaReaderComponent,
|
||||
InfiniteScrollerComponent,
|
||||
FullscreenIconPipe
|
||||
FullscreenIconPipe,
|
||||
ReaderModeIconPipe,
|
||||
LayoutModeIconPipe,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -56,9 +56,7 @@ export class EditReadingListModalComponent implements OnInit {
|
||||
save() {
|
||||
if (this.reviewGroup.value.title.trim() === '') return;
|
||||
|
||||
|
||||
const model = {...this.reviewGroup.value, readingListId: this.readingList.id, promoted: this.readingList.promoted, coverImageLocked: this.coverImageLocked};
|
||||
|
||||
const apis = [this.readingListService.update(model)];
|
||||
|
||||
if (this.selectedCover !== '') {
|
||||
@ -77,7 +75,7 @@ export class EditReadingListModalComponent implements OnInit {
|
||||
togglePromotion() {
|
||||
const originalPromotion = 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 => {
|
||||
/* No Operation */
|
||||
}, err => {
|
||||
|
@ -2,7 +2,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@
|
||||
import { Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||
import { PaginatedResult, Pagination } from 'src/app/_models/pagination';
|
||||
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 { ActionService } from 'src/app/_services/action.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';
|
||||
|
||||
@Component({
|
||||
@ -28,7 +28,7 @@ export class ReadingListsComponent implements OnInit {
|
||||
|
||||
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
|
||||
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 {
|
||||
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.lists = readingLists.result;
|
||||
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;
|
||||
window.scrollTo(0, 0);
|
||||
this.cdRef.markForCheck();
|
||||
|
@ -203,36 +203,4 @@ export class UtilityService {
|
||||
|| document.body.clientHeight;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { SeriesFilter, FilterEvent } from 'src/app/_models/series-filter';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ActionService } from 'src/app/_services/action.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 { ScrollService } from 'src/app/_services/scroll.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,
|
||||
public bulkSelectionService: BulkSelectionService, private actionService: ActionService, private messageHub: MessageHubService,
|
||||
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.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.series = paginatedList.result;
|
||||
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;
|
||||
window.scrollTo(0, 0);
|
||||
this.cdRef.markForCheck();
|
||||
|
Loading…
x
Reference in New Issue
Block a user