mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-06-02 13:14:28 -04:00
Bugfixes (#1177)
* Fixed an underline on hover of pagination link * Ensure title of companion bar eats full width if there is no filter * If a user doesn't have the Download role, they will not be able to download over OPDS. * Fixed a bug where after going into webtoon reader mode then leaving, the bookmark effect would continue using the webtoon mode styling * Fixed a bug where continuous reader wasn't being triggered due to moving scrollbar to body and a floating point percision error on scroll top * Fixed how continuous trigger is shown so that we properly adjust scroll on the top (for prev chapter) * Fixed a bad merge that broke saving any edits to series metadata * When a epub key is not correct, even after we correct it, ignore the inlining of the style so the book is at least still readable. * Disabled double rendering (this feature is being postponed to a later release) * Disabled user setting and forced it to Single on any save * Removed cache directory from UpdateSettings validation as we don't allow changing it. * Fix security issue with url parse * After all migrations run, update the installed version in the Database. Send that installed version on the stat service. * Dependency bot to update some security stuff * Some misc code cleanup and fixes on the typeahead (still broken)
This commit is contained in:
parent
1a011e30c2
commit
242d8b106d
@ -89,8 +89,7 @@ namespace API.Controllers
|
|||||||
private async Task<bool> HasDownloadPermission()
|
private async Task<bool> HasDownloadPermission()
|
||||||
{
|
{
|
||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
var roles = await _userManager.GetRolesAsync(user);
|
return await _downloadService.HasDownloadPermission(user);
|
||||||
return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ActionResult> GetFirstFileDownload(IEnumerable<MangaFile> files)
|
private async Task<ActionResult> GetFirstFileDownload(IEnumerable<MangaFile> files)
|
||||||
|
@ -765,7 +765,7 @@ public class OpdsController : BaseApiController
|
|||||||
filename);
|
filename);
|
||||||
accLink.TotalPages = chapter.Pages;
|
accLink.TotalPages = chapter.Pages;
|
||||||
|
|
||||||
return new FeedEntry()
|
var entry = new FeedEntry()
|
||||||
{
|
{
|
||||||
Id = mangaFile.Id.ToString(),
|
Id = mangaFile.Id.ToString(),
|
||||||
Title = title,
|
Title = title,
|
||||||
@ -776,7 +776,6 @@ public class OpdsController : BaseApiController
|
|||||||
{
|
{
|
||||||
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
CreateLink(FeedLinkRelation.Image, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||||
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
CreateLink(FeedLinkRelation.Thumbnail, FeedLinkType.Image, $"/api/image/chapter-cover?chapterId={chapterId}"),
|
||||||
accLink,
|
|
||||||
CreatePageStreamLink(seriesId, volumeId, chapterId, mangaFile, apiKey)
|
CreatePageStreamLink(seriesId, volumeId, chapterId, mangaFile, apiKey)
|
||||||
},
|
},
|
||||||
Content = new FeedEntryContent()
|
Content = new FeedEntryContent()
|
||||||
@ -785,6 +784,15 @@ public class OpdsController : BaseApiController
|
|||||||
Type = "text"
|
Type = "text"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(await GetUser(apiKey));
|
||||||
|
if (await _downloadService.HasDownloadPermission(user))
|
||||||
|
{
|
||||||
|
entry.Links.Add(accLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{apiKey}/image")]
|
[HttpGet("{apiKey}/image")]
|
||||||
|
@ -105,16 +105,6 @@ namespace API.Controllers
|
|||||||
{
|
{
|
||||||
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
|
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
|
||||||
|
|
||||||
if (updateSettingsDto.CacheDirectory.Equals(string.Empty))
|
|
||||||
{
|
|
||||||
return BadRequest("Cache Directory cannot be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(updateSettingsDto.CacheDirectory))
|
|
||||||
{
|
|
||||||
return BadRequest("Directory does not exist or is not accessible.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// We do not allow CacheDirectory changes, so we will ignore.
|
// We do not allow CacheDirectory changes, so we will ignore.
|
||||||
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
|
||||||
var updateBookmarks = false;
|
var updateBookmarks = false;
|
||||||
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -87,6 +88,9 @@ namespace API.Controllers
|
|||||||
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
existingPreferences.BookReaderReadingDirection = preferencesDto.BookReaderReadingDirection;
|
||||||
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
existingPreferences.Theme = await _unitOfWork.SiteThemeRepository.GetThemeById(preferencesDto.Theme.Id);
|
||||||
|
|
||||||
|
// TODO: Remove this code - this overrides layout mode to be single until the mode is released
|
||||||
|
existingPreferences.LayoutMode = LayoutMode.Single;
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
|
@ -37,5 +37,6 @@ namespace API.DTOs.Settings
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="EmailService.DefaultApiUrl"/></remarks>
|
/// <remarks>If null or empty string, will default back to default install setting aka <see cref="EmailService.DefaultApiUrl"/></remarks>
|
||||||
public string EmailServiceUrl { get; set; }
|
public string EmailServiceUrl { get; set; }
|
||||||
|
public string InstallVersion { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -610,8 +610,6 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true)
|
public async Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true)
|
||||||
{
|
{
|
||||||
//var allSeriesWithProgress = await _context.AppUserProgresses.Select(p => p.SeriesId).ToListAsync();
|
|
||||||
//var allChapters = await GetChapterIdsForSeriesAsync(allSeriesWithProgress);
|
|
||||||
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
||||||
var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter))
|
var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter))
|
||||||
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
|
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
|
||||||
@ -625,7 +623,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
.Where(p => p.Id == progress.Id && p.AppUserId == userId)
|
.Where(p => p.Id == progress.Id && p.AppUserId == userId)
|
||||||
.Max(p => p.LastModified),
|
.Max(p => p.LastModified),
|
||||||
// This is only taking into account chapters that have progress on them, not all chapters in said series
|
// This is only taking into account chapters that have progress on them, not all chapters in said series
|
||||||
LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created)
|
LastChapterCreated = _context.Chapter.Where(c => progress.ChapterId == c.Id).Max(c => c.Created),
|
||||||
//LastChapterCreated = _context.Chapter.Where(c => allChapters.Contains(c.Id)).Max(c => c.Created)
|
//LastChapterCreated = _context.Chapter.Where(c => allChapters.Contains(c.Id)).Max(c => c.Created)
|
||||||
});
|
});
|
||||||
if (cutoffOnDate)
|
if (cutoffOnDate)
|
||||||
|
@ -45,6 +45,9 @@ namespace API.Helpers.Converters
|
|||||||
case ServerSettingKey.EmailServiceUrl:
|
case ServerSettingKey.EmailServiceUrl:
|
||||||
destination.EmailServiceUrl = row.Value;
|
destination.EmailServiceUrl = row.Value;
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.InstallVersion:
|
||||||
|
destination.InstallVersion = row.Value;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,6 @@ namespace API
|
|||||||
|
|
||||||
|
|
||||||
var directoryService = new DirectoryService(null, new FileSystem());
|
var directoryService = new DirectoryService(null, new FileSystem());
|
||||||
//MigrateConfigFiles.Migrate(isDocker, directoryService);
|
|
||||||
|
|
||||||
// Before anything, check if JWT has been generated properly or if user still has default
|
// Before anything, check if JWT has been generated properly or if user still has default
|
||||||
if (!Configuration.CheckIfJwtTokenSet() &&
|
if (!Configuration.CheckIfJwtTokenSet() &&
|
||||||
|
@ -150,7 +150,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
// @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be
|
// @Import statements will be handled by browser, so we must inline the css into the original file that request it, so they can be
|
||||||
// Scoped
|
// Scoped
|
||||||
var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), "") : string.Empty;
|
var prepend = filename.Length > 0 ? filename.Replace(Path.GetFileName(filename), string.Empty) : string.Empty;
|
||||||
var importBuilder = new StringBuilder();
|
var importBuilder = new StringBuilder();
|
||||||
foreach (Match match in Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml))
|
foreach (Match match in Parser.Parser.CssImportUrlRegex.Matches(stylesheetHtml))
|
||||||
{
|
{
|
||||||
@ -343,7 +343,7 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
foreach (var styleLinks in styleNodes)
|
foreach (var styleLinks in styleNodes)
|
||||||
{
|
{
|
||||||
var key = BookService.CleanContentKeys(styleLinks.Attributes["href"].Value);
|
var key = CleanContentKeys(styleLinks.Attributes["href"].Value);
|
||||||
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
|
// Some epubs are malformed the key in content.opf might be: content/resources/filelist_0_0.xml but the actual html links to resources/filelist_0_0.xml
|
||||||
// In this case, we will do a search for the key that ends with
|
// In this case, we will do a search for the key that ends with
|
||||||
if (!book.Content.Css.ContainsKey(key))
|
if (!book.Content.Css.ContainsKey(key))
|
||||||
@ -358,13 +358,22 @@ namespace API.Services
|
|||||||
key = correctedKey;
|
key = correctedKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
var styleContent = await ScopeStyles(await book.Content.Css[key].ReadContentAsync(), apiBase,
|
try
|
||||||
book.Content.Css[key].FileName, book);
|
{
|
||||||
|
var cssFile = book.Content.Css[key];
|
||||||
|
|
||||||
|
var styleContent = await ScopeStyles(await cssFile.ReadContentAsync(), apiBase,
|
||||||
|
cssFile.FileName, book);
|
||||||
if (styleContent != null)
|
if (styleContent != null)
|
||||||
{
|
{
|
||||||
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
body.PrependChild(HtmlNode.CreateNode($"<style>{styleContent}</style>"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "There was an error reading css file for inlining likely due to a key mismatch in metadata");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -460,7 +469,7 @@ namespace API.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes the leading ../
|
/// Removes all leading ../
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
|
@ -84,6 +84,8 @@ namespace API.Services
|
|||||||
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
|
ConfigDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config");
|
||||||
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
BookmarkDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "bookmarks");
|
||||||
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
SiteThemeDirectory = FileSystem.Path.Join(FileSystem.Directory.GetCurrentDirectory(), "config", "themes");
|
||||||
|
|
||||||
|
ExistOrCreate(SiteThemeDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Constants;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.StaticFiles;
|
using Microsoft.AspNetCore.StaticFiles;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
@ -11,15 +13,18 @@ public interface IDownloadService
|
|||||||
{
|
{
|
||||||
Task<(byte[], string, string)> GetFirstFileDownload(IEnumerable<MangaFile> files);
|
Task<(byte[], string, string)> GetFirstFileDownload(IEnumerable<MangaFile> files);
|
||||||
string GetContentTypeFromFile(string filepath);
|
string GetContentTypeFromFile(string filepath);
|
||||||
|
Task<bool> HasDownloadPermission(AppUser user);
|
||||||
}
|
}
|
||||||
public class DownloadService : IDownloadService
|
public class DownloadService : IDownloadService
|
||||||
{
|
{
|
||||||
private readonly IDirectoryService _directoryService;
|
private readonly IDirectoryService _directoryService;
|
||||||
|
private readonly UserManager<AppUser> _userManager;
|
||||||
private readonly FileExtensionContentTypeProvider _fileTypeProvider = new FileExtensionContentTypeProvider();
|
private readonly FileExtensionContentTypeProvider _fileTypeProvider = new FileExtensionContentTypeProvider();
|
||||||
|
|
||||||
public DownloadService(IDirectoryService directoryService)
|
public DownloadService(IDirectoryService directoryService, UserManager<AppUser> userManager)
|
||||||
{
|
{
|
||||||
_directoryService = directoryService;
|
_directoryService = directoryService;
|
||||||
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -53,4 +58,10 @@ public class DownloadService : IDownloadService
|
|||||||
|
|
||||||
return contentType;
|
return contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasDownloadPermission(AppUser user)
|
||||||
|
{
|
||||||
|
var roles = await _userManager.GetRolesAsync(user);
|
||||||
|
return roles.Contains(PolicyConstants.DownloadRole) || roles.Contains(PolicyConstants.AdminRole);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,11 +99,12 @@ public class StatsService : IStatsService
|
|||||||
public async Task<ServerInfoDto> GetServerInfo()
|
public async Task<ServerInfoDto> GetServerInfo()
|
||||||
{
|
{
|
||||||
var installId = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId);
|
var installId = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId);
|
||||||
|
var installVersion = await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||||
var serverInfo = new ServerInfoDto
|
var serverInfo = new ServerInfoDto
|
||||||
{
|
{
|
||||||
InstallId = installId.Value,
|
InstallId = installId.Value,
|
||||||
Os = RuntimeInformation.OSDescription,
|
Os = RuntimeInformation.OSDescription,
|
||||||
KavitaVersion = BuildInfo.Version.ToString(),
|
KavitaVersion = installVersion.Value,
|
||||||
DotnetVersion = Environment.Version.ToString(),
|
DotnetVersion = Environment.Version.ToString(),
|
||||||
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker,
|
IsDocker = new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker,
|
||||||
NumOfCores = Math.Max(Environment.ProcessorCount, 1),
|
NumOfCores = Math.Max(Environment.ProcessorCount, 1),
|
||||||
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Middleware;
|
using API.Middleware;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
@ -148,13 +149,19 @@ namespace API
|
|||||||
// Apply all migrations on startup
|
// Apply all migrations on startup
|
||||||
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
|
||||||
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
|
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
|
||||||
|
var context = serviceProvider.GetRequiredService<DataContext>();
|
||||||
|
|
||||||
await MigrateBookmarks.Migrate(directoryService, unitOfWork,
|
await MigrateBookmarks.Migrate(directoryService, unitOfWork,
|
||||||
logger, cacheService);
|
logger, cacheService);
|
||||||
|
|
||||||
// Only run this if we are upgrading
|
// Only run this if we are upgrading
|
||||||
await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager);
|
await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager);
|
||||||
|
|
||||||
|
// Update the version in the DB after all migrations are run
|
||||||
|
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||||
|
installVersion.Value = BuildInfo.Version.ToString();
|
||||||
|
unitOfWork.SettingsRepository.Update(installVersion);
|
||||||
|
await unitOfWork.CommitAsync();
|
||||||
}).GetAwaiter()
|
}).GetAwaiter()
|
||||||
.GetResult();
|
.GetResult();
|
||||||
}
|
}
|
||||||
@ -206,21 +213,6 @@ namespace API
|
|||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
|
|
||||||
// This is not implemented completely. Commenting out until implemented
|
|
||||||
// var service = serviceProvider.GetRequiredService<IUnitOfWork>();
|
|
||||||
// var settings = service.SettingsRepository.GetSettingsDto();
|
|
||||||
// if (!string.IsNullOrEmpty(settings.BaseUrl) && !settings.BaseUrl.Equals("/"))
|
|
||||||
// {
|
|
||||||
// var path = !settings.BaseUrl.StartsWith("/")
|
|
||||||
// ? $"/{settings.BaseUrl}"
|
|
||||||
// : settings.BaseUrl;
|
|
||||||
// path = !path.EndsWith("/")
|
|
||||||
// ? $"{path}/"
|
|
||||||
// : path;
|
|
||||||
// app.UsePathBase(path);
|
|
||||||
// Console.WriteLine("Starting with base url as " + path);
|
|
||||||
// }
|
|
||||||
|
|
||||||
app.UseStaticFiles(new StaticFileOptions
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
{
|
{
|
||||||
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
||||||
|
24
UI/Web/package-lock.json
generated
24
UI/Web/package-lock.json
generated
@ -8995,9 +8995,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "3.1.6",
|
"version": "3.1.6",
|
||||||
@ -9224,9 +9224,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node-forge": {
|
"node-forge": {
|
||||||
"version": "1.2.1",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz",
|
||||||
"integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==",
|
"integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node-gyp": {
|
"node-gyp": {
|
||||||
@ -10498,8 +10498,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
"resolved": "",
|
||||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"strip-ansi": {
|
"strip-ansi": {
|
||||||
@ -10704,8 +10703,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
"resolved": "",
|
||||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
@ -12296,9 +12294,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url-parse": {
|
"url-parse": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||||
"integrity": "sha512-ITeAByWWoqutFClc/lRZnFplgXgEZr3WJ6XngMM/N9DMIm4K8zXPCZ1Jdu0rERwO84w1WC5wkle2ubwTA4NTBg==",
|
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"querystringify": "^2.1.1",
|
"querystringify": "^2.1.1",
|
||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
|
@ -102,6 +102,8 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
this.libraryName = names[this.series.libraryId];
|
this.libraryName = names[this.series.libraryId];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.initSeries = Object.assign({}, this.series);
|
||||||
|
|
||||||
|
|
||||||
this.editSeriesForm = this.fb.group({
|
this.editSeriesForm = this.fb.group({
|
||||||
id: new FormControl(this.series.id, []),
|
id: new FormControl(this.series.id, []),
|
||||||
@ -217,10 +219,14 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false };
|
return {id: 0, title: title, promoted: false, coverImage: '', summary: '', coverImageLocked: false };
|
||||||
});
|
});
|
||||||
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
this.collectionTagSettings.compareFn = (options: CollectionTag[], filter: string) => {
|
||||||
|
console.log('compareFN:')
|
||||||
|
console.log('options: ', options);
|
||||||
|
console.log('filter: ', filter);
|
||||||
|
console.log('results: ', options.filter(m => this.utilityService.filter(m.title, filter)));
|
||||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||||
}
|
}
|
||||||
this.collectionTagSettings.singleCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
this.collectionTagSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||||
return a.id == b.id;
|
return a.title === b.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.metadata.collectionTags) {
|
if (this.metadata.collectionTags) {
|
||||||
@ -248,7 +254,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
this.tagsSettings.addTransformFn = ((title: string) => {
|
this.tagsSettings.addTransformFn = ((title: string) => {
|
||||||
return {id: 0, title: title };
|
return {id: 0, title: title };
|
||||||
});
|
});
|
||||||
this.tagsSettings.singleCompareFn = (a: Tag, b: Tag) => {
|
this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => {
|
||||||
return a.id == b.id;
|
return a.id == b.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +278,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
||||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||||
}
|
}
|
||||||
this.genreSettings.singleCompareFn = (a: Genre, b: Genre) => {
|
this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => {
|
||||||
return a.title == b.title;
|
return a.title == b.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,7 +322,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
|
this.languageSettings.fetchFn = (filter: string) => of(this.validLanguages)
|
||||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||||
|
|
||||||
this.languageSettings.singleCompareFn = (a: Language, b: Language) => {
|
this.languageSettings.selectionCompareFn = (a: Language, b: Language) => {
|
||||||
return a.isoCode == b.isoCode;
|
return a.isoCode == b.isoCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,7 +372,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
personSettings.singleCompareFn = (a: Person, b: Person) => {
|
personSettings.selectionCompareFn = (a: Person, b: Person) => {
|
||||||
return a.name == b.name && a.role == b.role;
|
return a.name == b.name && a.role == b.role;
|
||||||
}
|
}
|
||||||
personSettings.fetchFn = (filter: string) => {
|
personSettings.fetchFn = (filter: string) => {
|
||||||
|
@ -110,7 +110,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
break;
|
break;
|
||||||
case 'ended':
|
case 'ended':
|
||||||
data = this.progressEventsSource.getValue();
|
data = this.progressEventsSource.getValue();
|
||||||
data = data.filter(m => m.name !== message.name); // This does not work // && m.title !== message.title
|
data = data.filter(m => m.name !== message.name);
|
||||||
this.progressEventsSource.next(data);
|
this.progressEventsSource.next(data);
|
||||||
this.activeEvents = Math.max(this.activeEvents - 1, 0);
|
this.activeEvents = Math.max(this.activeEvents - 1, 0);
|
||||||
break;
|
break;
|
||||||
|
@ -42,10 +42,10 @@ img, .full-width {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bookmark-effect {
|
.bookmark-effect {
|
||||||
animation: bookmark 1s cubic-bezier(0.165, 0.84, 0.44, 1);
|
animation: infinite-scroll-bookmark 1s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes bookmark {
|
@keyframes infinite-scroll-bookmark {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
filter: opacity(1);
|
filter: opacity(1);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { DOCUMENT } from '@angular/common';
|
|||||||
import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
import { Component, ElementRef, EventEmitter, Inject, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
||||||
import { BehaviorSubject, fromEvent, merge, ReplaySubject, Subject } from 'rxjs';
|
import { BehaviorSubject, fromEvent, merge, ReplaySubject, Subject } from 'rxjs';
|
||||||
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||||
|
import { ScrollService } from 'src/app/scroll.service';
|
||||||
import { ReaderService } from '../../_services/reader.service';
|
import { ReaderService } from '../../_services/reader.service';
|
||||||
import { PAGING_DIRECTION } from '../_models/reader-enums';
|
import { PAGING_DIRECTION } from '../_models/reader-enums';
|
||||||
import { WebtoonImage } from '../_models/webtoon-image';
|
import { WebtoonImage } from '../_models/webtoon-image';
|
||||||
@ -93,7 +94,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* The minimum width of images in webtoon. On image loading, this is checked and updated. All images will get this assigned to them for rendering.
|
* The minimum width of images in webtoon. On image loading, this is checked and updated. All images will get this assigned to them for rendering.
|
||||||
*/
|
*/
|
||||||
webtoonImageWidth: number = window.innerWidth || this.document.documentElement.clientWidth || this.document.body.clientWidth;
|
webtoonImageWidth: number = window.innerWidth || this.document.body.clientWidth || this.document.documentElement.clientWidth;
|
||||||
/**
|
/**
|
||||||
* Used to tell if a scrollTo() operation is in progress
|
* Used to tell if a scrollTo() operation is in progress
|
||||||
*/
|
*/
|
||||||
@ -145,7 +146,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
get areImagesWiderThanWindow() {
|
get areImagesWiderThanWindow() {
|
||||||
let [_, innerWidth] = this.getInnerDimensions();
|
let [_, innerWidth] = this.getInnerDimensions();
|
||||||
return this.webtoonImageWidth > (innerWidth || document.documentElement.clientWidth);
|
return this.webtoonImageWidth > (innerWidth || document.body.clientWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -153,7 +154,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
constructor(private readerService: ReaderService, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document) {
|
constructor(private readerService: ReaderService, private renderer: Renderer2, @Inject(DOCUMENT) private document: Document, private scrollService: ScrollService) {
|
||||||
// This will always exist at this point in time since this is used within manga reader
|
// This will always exist at this point in time since this is used within manga reader
|
||||||
const reader = document.querySelector('.reader');
|
const reader = document.querySelector('.reader');
|
||||||
if (reader !== null) {
|
if (reader !== null) {
|
||||||
@ -230,7 +231,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
recalculateImageWidth() {
|
recalculateImageWidth() {
|
||||||
const [_, innerWidth] = this.getInnerDimensions();
|
const [_, innerWidth] = this.getInnerDimensions();
|
||||||
this.webtoonImageWidth = innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
this.webtoonImageWidth = innerWidth || document.body.clientWidth || document.documentElement.clientWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
getVerticalOffset() {
|
getVerticalOffset() {
|
||||||
@ -244,8 +245,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (offset
|
return (offset
|
||||||
|
|| document.body.scrollTop
|
||||||
|| document.documentElement.scrollTop
|
|| document.documentElement.scrollTop
|
||||||
|| document.body.scrollTop || 0);
|
|| 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -290,19 +292,20 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
getTotalHeight() {
|
getTotalHeight() {
|
||||||
let totalHeight = 0;
|
let totalHeight = 0;
|
||||||
document.querySelectorAll('img[id^="page-"]').forEach(img => totalHeight += img.getBoundingClientRect().height);
|
document.querySelectorAll('img[id^="page-"]').forEach(img => totalHeight += img.getBoundingClientRect().height);
|
||||||
return totalHeight;
|
return Math.round(totalHeight);
|
||||||
}
|
}
|
||||||
getTotalScroll() {
|
getTotalScroll() {
|
||||||
if (this.isFullscreenMode) {
|
if (this.isFullscreenMode) {
|
||||||
return this.readerElemRef.nativeElement.offsetHeight + this.readerElemRef.nativeElement.scrollTop;
|
return this.readerElemRef.nativeElement.offsetHeight + this.readerElemRef.nativeElement.scrollTop;
|
||||||
}
|
}
|
||||||
return document.documentElement.offsetHeight + document.documentElement.scrollTop;
|
return document.body.offsetHeight + document.body.scrollTop;
|
||||||
}
|
}
|
||||||
getScrollTop() {
|
getScrollTop() {
|
||||||
if (this.isFullscreenMode) {
|
if (this.isFullscreenMode) {
|
||||||
return this.readerElemRef.nativeElement.scrollTop;
|
return this.readerElemRef.nativeElement.scrollTop;
|
||||||
}
|
}
|
||||||
return document.documentElement.scrollTop;
|
|
||||||
|
return document.body.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIfShouldTriggerContinuousReader() {
|
checkIfShouldTriggerContinuousReader() {
|
||||||
@ -323,7 +326,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
// Scroll user back to original location
|
// Scroll user back to original location
|
||||||
this.previousScrollHeightMinusTop = this.getScrollTop();
|
this.previousScrollHeightMinusTop = this.getScrollTop();
|
||||||
requestAnimationFrame(() => document.documentElement.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2));
|
requestAnimationFrame(() => document.body.scrollTop = this.previousScrollHeightMinusTop + (SPACER_SCROLL_INTO_PX / 2));
|
||||||
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
|
} else if (totalScroll >= totalHeight + SPACER_SCROLL_INTO_PX && this.atBottom) {
|
||||||
// This if statement will fire once we scroll into the spacer at all
|
// This if statement will fire once we scroll into the spacer at all
|
||||||
this.loadNextChapter.emit();
|
this.loadNextChapter.emit();
|
||||||
@ -335,8 +338,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
this.atTop = true;
|
this.atTop = true;
|
||||||
// Scroll user back to original location
|
// Scroll user back to original location
|
||||||
this.previousScrollHeightMinusTop = document.documentElement.scrollHeight - document.documentElement.scrollTop;
|
this.previousScrollHeightMinusTop = document.body.scrollHeight - document.body.scrollTop;
|
||||||
requestAnimationFrame(() => window.scrollTo(0, SPACER_SCROLL_INTO_PX)); // TODO: does this need to be fullscreen protected?
|
|
||||||
|
const reader = this.isFullscreenMode ? this.readerElemRef.nativeElement : this.document.body;
|
||||||
|
requestAnimationFrame(() => this.scrollService.scrollTo((SPACER_SCROLL_INTO_PX / 2), reader));
|
||||||
} else if (this.getScrollTop() < 5 && this.pageNum === 0 && this.atTop) {
|
} else if (this.getScrollTop() < 5 && this.pageNum === 0 && this.atTop) {
|
||||||
// If already at top, then we moving on
|
// If already at top, then we moving on
|
||||||
this.loadPrevChapter.emit();
|
this.loadPrevChapter.emit();
|
||||||
@ -377,8 +382,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
return (rect.bottom >= 0 &&
|
return (rect.bottom >= 0 &&
|
||||||
rect.right >= 0 &&
|
rect.right >= 0 &&
|
||||||
rect.top <= (innerHeight || document.documentElement.clientHeight) &&
|
rect.top <= (innerHeight || document.body.clientHeight) &&
|
||||||
rect.left <= (innerWidth || document.documentElement.clientWidth)
|
rect.left <= (innerWidth || document.body.clientWidth)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,10 +403,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
if (rect.bottom >= 0 &&
|
if (rect.bottom >= 0 &&
|
||||||
rect.right >= 0 &&
|
rect.right >= 0 &&
|
||||||
rect.top <= (innerHeight || document.documentElement.clientHeight) &&
|
rect.top <= (innerHeight || document.body.clientHeight) &&
|
||||||
rect.left <= (innerWidth || document.documentElement.clientWidth)
|
rect.left <= (innerWidth || document.body.clientWidth)
|
||||||
) {
|
) {
|
||||||
const topX = (innerHeight || document.documentElement.clientHeight);
|
const topX = (innerHeight || document.body.clientHeight);
|
||||||
return Math.abs(rect.top / topX) <= 0.25;
|
return Math.abs(rect.top / topX) <= 0.25;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
<span class="visually-hidden">Keyboard Shortcuts Modal</span>
|
<span class="visually-hidden">Keyboard Shortcuts Modal</span>
|
||||||
</button>
|
</button>
|
||||||
<!-- {{this.pageNum}} -->
|
<!-- {{this.pageNum}} -->
|
||||||
|
{{readerService.imageUrlToPageNum(canvasImage.src)}}<ng-container *ngIf="ShouldRenderDoublePage && (this.pageNum + 1 <= maxPages - 1 && this.pageNum > 0)"> - {{PageNumber + 1}}</ng-container>
|
||||||
|
|
||||||
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="isCurrentPageBookmarked" title="{{isCurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{isCurrentPageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="visually-hidden">{{isCurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
<button class="btn btn-icon btn-small" role="checkbox" [attr.aria-checked]="isCurrentPageBookmarked" title="{{isCurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}" (click)="bookmarkPage()"><i class="{{isCurrentPageBookmarked ? 'fa' : 'far'}} fa-bookmark" aria-hidden="true"></i><span class="visually-hidden">{{isCurrentPageBookmarked ? 'Unbookmark Page' : 'Bookmark Page'}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
@ -154,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-md-6 col-sm-12" *ngIf="false">
|
||||||
<label for="layout-mode" class="form-label">Layout Mode</label>
|
<label for="layout-mode" class="form-label">Layout Mode</label>
|
||||||
<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>
|
||||||
|
@ -851,10 +851,13 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART);
|
const notInSplit = this.currentImageSplitPart !== (this.isSplitLeftToRight() ? SPLIT_PAGE_PART.RIGHT_PART : SPLIT_PAGE_PART.LEFT_PART);
|
||||||
|
|
||||||
const pageAmount = (this.layoutMode !== LayoutMode.Single && !this.isCoverImage()) ? 2: 1;
|
// If the prev page before we change current page is a cover image, we actually are skipping a page
|
||||||
|
console.log('Page ', this.PageNumber, ' is cover image: ', this.isCoverImage(this.cachedImages.prev()))
|
||||||
|
console.log('Page ', this.pageNum, ' is cover image: ', this.isCoverImage())
|
||||||
|
const pageAmount = (this.layoutMode !== LayoutMode.Single && !this.isCoverImage(this.cachedImages.prev())) ? 2: 1;
|
||||||
|
// BUG: isCoverImage works on canvasImage, where we need to know if the previous image is a cover image or not.
|
||||||
console.log('pageAmt: ', pageAmount);
|
console.log('pageAmt: ', pageAmount);
|
||||||
if ((this.pageNum - 1 < 0 && notInSplit) || this.isLoading) {
|
if ((this.pageNum - 1 < 0 && notInSplit) || this.isLoading) {
|
||||||
|
|
||||||
if (this.isLoading) { return; }
|
if (this.isLoading) { return; }
|
||||||
|
|
||||||
// Move to next volume/chapter automatically
|
// Move to next volume/chapter automatically
|
||||||
@ -1021,8 +1024,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.generalSettingsForm.get('fittingOption')?.setValue(newScale, {emitEvent: false});
|
this.generalSettingsForm.get('fittingOption')?.setValue(newScale, {emitEvent: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
isCoverImage() {
|
isCoverImage(elem?: HTMLImageElement) {
|
||||||
return this.canvasImage.width > this.canvasImage.height;
|
if (elem) return elem.width > elem.height;
|
||||||
|
const element = elem || this.canvasImage;
|
||||||
|
return element.width > element.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.formatSettings.singleCompareFn = (a: FilterItem<MangaFormat>, b: FilterItem<MangaFormat>) => {
|
this.formatSettings.selectionCompareFn = (a: FilterItem<MangaFormat>, b: FilterItem<MangaFormat>) => {
|
||||||
return a.title == b.title;
|
return a.title == b.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +225,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.librarySettings.compareFn = (options: Library[], filter: string) => {
|
this.librarySettings.compareFn = (options: Library[], filter: string) => {
|
||||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||||
}
|
}
|
||||||
this.librarySettings.singleCompareFn = (a: Library, b: Library) => {
|
this.librarySettings.selectionCompareFn = (a: Library, b: Library) => {
|
||||||
return a.name == b.name;
|
return a.name == b.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
this.genreSettings.compareFn = (options: Genre[], filter: string) => {
|
||||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||||
}
|
}
|
||||||
this.genreSettings.singleCompareFn = (a: Genre, b: Genre) => {
|
this.genreSettings.selectionCompareFn = (a: Genre, b: Genre) => {
|
||||||
return a.title == b.title;
|
return a.title == b.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,7 +279,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ageRatingSettings.singleCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => {
|
this.ageRatingSettings.selectionCompareFn = (a: AgeRatingDto, b: AgeRatingDto) => {
|
||||||
return a.title == b.title;
|
return a.title == b.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,7 +306,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
return options.filter(m => this.utilityService.filter(m.title, filter));
|
return options.filter(m => this.utilityService.filter(m.title, filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.publicationStatusSettings.singleCompareFn = (a: PublicationStatusDto, b: PublicationStatusDto) => {
|
this.publicationStatusSettings.selectionCompareFn = (a: PublicationStatusDto, b: PublicationStatusDto) => {
|
||||||
return a.title == b.title;
|
return a.title == b.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,7 +332,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags(this.filter.libraries)
|
this.tagsSettings.fetchFn = (filter: string) => this.metadataService.getAllTags(this.filter.libraries)
|
||||||
.pipe(map(items => this.tagsSettings.compareFn(items, filter)));
|
.pipe(map(items => this.tagsSettings.compareFn(items, filter)));
|
||||||
|
|
||||||
this.tagsSettings.singleCompareFn = (a: Tag, b: Tag) => {
|
this.tagsSettings.selectionCompareFn = (a: Tag, b: Tag) => {
|
||||||
return a.id == b.id;
|
return a.id == b.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,7 +358,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.languageSettings.fetchFn = (filter: string) => this.metadataService.getAllLanguages(this.filter.libraries)
|
this.languageSettings.fetchFn = (filter: string) => this.metadataService.getAllLanguages(this.filter.libraries)
|
||||||
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
.pipe(map(items => this.languageSettings.compareFn(items, filter)));
|
||||||
|
|
||||||
this.languageSettings.singleCompareFn = (a: Language, b: Language) => {
|
this.languageSettings.selectionCompareFn = (a: Language, b: Language) => {
|
||||||
return a.isoCode == b.isoCode;
|
return a.isoCode == b.isoCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +384,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
this.collectionSettings.fetchFn = (filter: string) => this.collectionTagService.allTags()
|
this.collectionSettings.fetchFn = (filter: string) => this.collectionTagService.allTags()
|
||||||
.pipe(map(items => this.collectionSettings.compareFn(items, filter)));
|
.pipe(map(items => this.collectionSettings.compareFn(items, filter)));
|
||||||
|
|
||||||
this.collectionSettings.singleCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
this.collectionSettings.selectionCompareFn = (a: CollectionTag, b: CollectionTag) => {
|
||||||
return a.id == b.id;
|
return a.id == b.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,7 +453,7 @@ export class MetadataFilterComponent implements OnInit, OnDestroy {
|
|||||||
return options.filter(m => this.utilityService.filter(m.name, filter));
|
return options.filter(m => this.utilityService.filter(m.name, filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
personSettings.singleCompareFn = (a: Person, b: Person) => {
|
personSettings.selectionCompareFn = (a: Person, b: Person) => {
|
||||||
return a.name == b.name && a.role == b.role;
|
return a.name == b.name && a.role == b.role;
|
||||||
}
|
}
|
||||||
personSettings.fetchFn = (filter: string) => {
|
personSettings.fetchFn = (filter: string) => {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<ng-content select="[title]"></ng-content>
|
<ng-content select="[title]"></ng-content>
|
||||||
<ng-content select="[subtitle]"></ng-content>
|
<ng-content select="[subtitle]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
<div class="col mr-auto">
|
<div class="col mr-auto hide-if-empty">
|
||||||
<ng-content select="[main]"></ng-content>
|
<ng-content select="[main]"></ng-content>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" *ngIf="hasFilter">
|
<div class="col" *ngIf="hasFilter">
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.hide-if-empty:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
@ -28,13 +28,15 @@ export class TypeaheadSettings<T> {
|
|||||||
*/
|
*/
|
||||||
savedData!: T[] | T;
|
savedData!: T[] | T;
|
||||||
/**
|
/**
|
||||||
* Function to compare the elements. Should return all elements that fit the matching criteria. This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead (TODO)
|
* Function to compare the elements. Should return all elements that fit the matching criteria.
|
||||||
|
* This is only used with non-Observable based fetchFn, but must be defined for all uses of typeahead (TODO)
|
||||||
*/
|
*/
|
||||||
compareFn!: ((optionList: T[], filter: string) => T[]);
|
compareFn!: ((optionList: T[], filter: string) => T[]);
|
||||||
/**
|
/**
|
||||||
* Function which is used for comparing objects when keeping track of state. Useful over shallow equal when you have image urls that have random numbers on them.
|
* Function which is used for comparing objects when keeping track of state.
|
||||||
|
* Useful over shallow equal when you have image urls that have random numbers on them.
|
||||||
*/
|
*/
|
||||||
singleCompareFn?: SelectionCompareFn<T>;
|
selectionCompareFn?: SelectionCompareFn<T>;
|
||||||
/**
|
/**
|
||||||
* Function to fetch the data from the server. If data is mainatined in memory, wrap in an observable.
|
* Function to fetch the data from the server. If data is mainatined in memory, wrap in an observable.
|
||||||
*/
|
*/
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core';
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
import { Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, RendererStyleFlags2, TemplateRef, ViewChild } from '@angular/core';
|
||||||
import { FormControl, FormGroup } from '@angular/forms';
|
import { FormControl, FormGroup } from '@angular/forms';
|
||||||
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
||||||
import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
|
import { debounceTime, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
|
||||||
@ -168,7 +169,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private readonly onDestroy = new Subject<void>();
|
private readonly onDestroy = new Subject<void>();
|
||||||
|
|
||||||
constructor(private renderer2: Renderer2) { }
|
constructor(private renderer2: Renderer2, @Inject(DOCUMENT) private document: Document) { }
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onDestroy.next();
|
this.onDestroy.next();
|
||||||
@ -224,9 +225,9 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
let results: Observable<any[]>;
|
let results: Observable<any[]>;
|
||||||
if (Array.isArray(this.settings.fetchFn)) {
|
if (Array.isArray(this.settings.fetchFn)) {
|
||||||
const filteredArray = this.settings.compareFn(this.settings.fetchFn, val.trim());
|
const filteredArray = this.settings.compareFn(this.settings.fetchFn, val.trim());
|
||||||
results = of(filteredArray).pipe(map((items: any[]) => items.filter(item => this.filterSelected(item))));
|
results = of(filteredArray).pipe(takeUntil(this.onDestroy), map((items: any[]) => items.filter(item => this.filterSelected(item))));
|
||||||
} else {
|
} else {
|
||||||
results = this.settings.fetchFn(val.trim()).pipe(map((items: any[]) => items.filter(item => this.filterSelected(item))));
|
results = this.settings.fetchFn(val.trim()).pipe(takeUntil(this.onDestroy), map((items: any[]) => items.filter(item => this.filterSelected(item))));
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
@ -234,10 +235,11 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
tap((val) => {
|
tap((val) => {
|
||||||
this.isLoadingOptions = false;
|
this.isLoadingOptions = false;
|
||||||
this.focusedIndex = 0;
|
this.focusedIndex = 0;
|
||||||
setTimeout(() => {
|
|
||||||
this.updateShowAddItem(val);
|
this.updateShowAddItem(val);
|
||||||
this.updateHighlight();
|
// setTimeout(() => {
|
||||||
}, 10);
|
// this.updateShowAddItem(val);
|
||||||
|
// this.updateHighlight();
|
||||||
|
// }, 10);
|
||||||
setTimeout(() => this.updateHighlight(), 20);
|
setTimeout(() => this.updateHighlight(), 20);
|
||||||
}),
|
}),
|
||||||
shareReplay(),
|
shareReplay(),
|
||||||
@ -279,7 +281,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
case KEY_CODES.DOWN_ARROW:
|
case KEY_CODES.DOWN_ARROW:
|
||||||
case KEY_CODES.RIGHT_ARROW:
|
case KEY_CODES.RIGHT_ARROW:
|
||||||
{
|
{
|
||||||
this.focusedIndex = Math.min(this.focusedIndex + 1, document.querySelectorAll('.list-group-item').length - 1);
|
this.focusedIndex = Math.min(this.focusedIndex + 1, this.document.querySelectorAll('.list-group-item').length - 1);
|
||||||
this.updateHighlight();
|
this.updateHighlight();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -292,14 +294,14 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
case KEY_CODES.ENTER:
|
case KEY_CODES.ENTER:
|
||||||
{
|
{
|
||||||
document.querySelectorAll('.list-group-item').forEach((item, index) => {
|
this.document.querySelectorAll('.list-group-item').forEach((item, index) => {
|
||||||
if (item.classList.contains('active')) {
|
if (item.classList.contains('active')) {
|
||||||
this.filteredOptions.pipe(take(1)).subscribe((res: any[]) => {
|
this.filteredOptions.pipe(take(1)).subscribe((res: any[]) => {
|
||||||
// This isn't giving back the filtered array, but everything
|
// This isn't giving back the filtered array, but everything
|
||||||
|
|
||||||
|
console.log(item.classList.contains('add-item'));
|
||||||
if (this.settings.addIfNonExisting && item.classList.contains('add-item')) {
|
if (this.settings.addIfNonExisting && item.classList.contains('add-item')) {
|
||||||
this.addNewItem(this.typeaheadControl.value);
|
this.addNewItem(this.typeaheadControl.value);
|
||||||
this.resetField();
|
|
||||||
this.focusedIndex = 0;
|
this.focusedIndex = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -337,12 +339,12 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleSelection(opt: any): void {
|
toggleSelection(opt: any): void {
|
||||||
this.optionSelection.toggle(opt, undefined, this.settings.singleCompareFn);
|
this.optionSelection.toggle(opt, undefined, this.settings.selectionCompareFn);
|
||||||
this.selectedData.emit(this.optionSelection.selected());
|
this.selectedData.emit(this.optionSelection.selected());
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSelectedOption(opt: any) {
|
removeSelectedOption(opt: any) {
|
||||||
this.optionSelection.toggle(opt, undefined, this.settings.singleCompareFn);
|
this.optionSelection.toggle(opt, undefined, this.settings.selectionCompareFn);
|
||||||
this.selectedData.emit(this.optionSelection.selected());
|
this.selectedData.emit(this.optionSelection.selected());
|
||||||
this.resetField();
|
this.resetField();
|
||||||
}
|
}
|
||||||
@ -376,9 +378,14 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
this.onInputFocus(undefined);
|
this.onInputFocus(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param item
|
||||||
|
* @returns True if the item is NOT selected already
|
||||||
|
*/
|
||||||
filterSelected(item: any) {
|
filterSelected(item: any) {
|
||||||
if (this.settings.unique && this.settings.multiple) {
|
if (this.settings.unique && this.settings.multiple) {
|
||||||
return !this.optionSelection.isSelected(item, this.settings.singleCompareFn);
|
return !this.optionSelection.isSelected(item, this.settings.selectionCompareFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -402,7 +409,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (this.inputElem) {
|
if (this.inputElem) {
|
||||||
// hack: To prevent multiple typeaheads from being open at once, click document then trigger the focus
|
// hack: To prevent multiple typeaheads from being open at once, click document then trigger the focus
|
||||||
document.querySelector('body')?.click();
|
this.document.body.click();
|
||||||
this.inputElem.nativeElement.focus();
|
this.inputElem.nativeElement.focus();
|
||||||
this.hasFocus = true;
|
this.hasFocus = true;
|
||||||
}
|
}
|
||||||
@ -422,7 +429,7 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Updates the highlight to focus on the selected item
|
// Updates the highlight to focus on the selected item
|
||||||
updateHighlight() {
|
updateHighlight() {
|
||||||
document.querySelectorAll('.list-group-item').forEach((item, index) => {
|
this.document.querySelectorAll('.list-group-item').forEach((item, index) => {
|
||||||
if (index === this.focusedIndex && !item.classList.contains('no-hover')) {
|
if (index === this.focusedIndex && !item.classList.contains('no-hover')) {
|
||||||
// apply active class
|
// apply active class
|
||||||
this.renderer2.addClass(item, 'active');
|
this.renderer2.addClass(item, 'active');
|
||||||
@ -438,6 +445,8 @@ export class TypeaheadComponent implements OnInit, OnDestroy {
|
|||||||
&& this.typeaheadControl.value.trim().length >= Math.max(this.settings.minCharacters, 1)
|
&& this.typeaheadControl.value.trim().length >= Math.max(this.settings.minCharacters, 1)
|
||||||
&& this.typeaheadControl.dirty
|
&& this.typeaheadControl.dirty
|
||||||
&& (typeof this.settings.compareFn == 'function' && this.settings.compareFn(options, this.typeaheadControl.value.trim()).length === 0);
|
&& (typeof this.settings.compareFn == 'function' && this.settings.compareFn(options, this.typeaheadControl.value.trim()).length === 0);
|
||||||
|
console.log('show Add item: ', this.showAddItem);
|
||||||
|
console.log('compare func: ', this.settings.compareFn(options, this.typeaheadControl.value.trim()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="false">
|
||||||
<label for="settings-layoutmode-option" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
<label for="settings-layoutmode-option" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #layoutModeTooltip>Render a single image to the screen to two side-by-side images</ng-template>
|
<ng-template #layoutModeTooltip>Render a single image to the screen to two side-by-side images</ng-template>
|
||||||
<span class="visually-hidden" id="settings-layoutmode-option-help"><ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container></span>
|
<span class="visually-hidden" id="settings-layoutmode-option-help"><ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container></span>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
border-color: var(--pagination-active-link-border-color);
|
border-color: var(--pagination-active-link-border-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--pagination-link-text-color);;
|
color: var(--pagination-link-text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--pagination-link-hover-color);
|
color: var(--pagination-link-hover-color);
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user