mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-31 14:33:50 -04:00
Shakeout testing Fixes (#952)
* Cleaned up some old code in download bookmark that could create pointless temp folders. * Fixed a bad http call on reading list remove read and cleaned up the messaging * Undid an optimization in finding cover image due to it perfoming depth first rather than breadth. * Updated CleanComicInfo to have Translators and CoverArtists, which were previously missing. * Renamed Refresh Metadata to Refresh Covers on the UI, given Metadata refresh is done in Scan. * Library detail will now retain the search query in the UI. Reduced the amount of api calls to the backend on load. * Reverted allowing the filter to reside in the UI (even though it does work). * Updated the Age Rating to match the v2.1 spec. * Fixed a bug where progress wasn't being saved * Fixed line height not having any effect due to not applying to children elements in the reader * Fixed some wording for Refresh Covers confirmation * Delete Series will now send an event to the UI informing that series was deleted. * Change Progress widget to show Refreshing Covers for * When we exit early due to potential missing folders/drives in a scan, tell the UI that scan is 100% done. * Fixed manage library not supressing scan loader when a complete came in * Fixed a spelling difference for Publication Status between filter and series detail * Fixed a bug where collection detail page would flash on first load due to duplicate load events * Added bookmarks to backups * Fixed issues where fullscreen mode would break infinite scroller contiunous reader
This commit is contained in:
parent
b4229f5442
commit
680240af8d
@ -11,15 +11,15 @@ public class ComicInfoTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("G", AgeRating.G)]
|
[InlineData("G", AgeRating.G)]
|
||||||
[InlineData("Everyone", AgeRating.Everyone)]
|
[InlineData("Everyone", AgeRating.Everyone)]
|
||||||
[InlineData("Mature", AgeRating.Mature)]
|
|
||||||
[InlineData("Teen", AgeRating.Teen)]
|
[InlineData("Teen", AgeRating.Teen)]
|
||||||
[InlineData("Adults Only 18+", AgeRating.AdultsOnly)]
|
[InlineData("Adults Only 18+", AgeRating.AdultsOnly)]
|
||||||
[InlineData("Early Childhood", AgeRating.EarlyChildhood)]
|
[InlineData("Early Childhood", AgeRating.EarlyChildhood)]
|
||||||
[InlineData("Everyone 10+", AgeRating.Everyone10Plus)]
|
[InlineData("Everyone 10+", AgeRating.Everyone10Plus)]
|
||||||
[InlineData("Mature 15+", AgeRating.Mature15Plus)]
|
[InlineData("M", AgeRating.Mature)]
|
||||||
|
[InlineData("MA 15+", AgeRating.Mature15Plus)]
|
||||||
[InlineData("Mature 17+", AgeRating.Mature17Plus)]
|
[InlineData("Mature 17+", AgeRating.Mature17Plus)]
|
||||||
[InlineData("Rating Pending", AgeRating.RatingPending)]
|
[InlineData("Rating Pending", AgeRating.RatingPending)]
|
||||||
[InlineData("X 18+", AgeRating.X18Plus)]
|
[InlineData("X18+", AgeRating.X18Plus)]
|
||||||
[InlineData("Kids to Adults", AgeRating.KidsToAdults)]
|
[InlineData("Kids to Adults", AgeRating.KidsToAdults)]
|
||||||
[InlineData("NotValid", AgeRating.Unknown)]
|
[InlineData("NotValid", AgeRating.Unknown)]
|
||||||
public void ConvertAgeRatingToEnum_ShouldConvertCorrectly(string input, AgeRating expected)
|
public void ConvertAgeRatingToEnum_ShouldConvertCorrectly(string input, AgeRating expected)
|
||||||
|
@ -144,6 +144,7 @@ namespace API.Tests.Services
|
|||||||
[InlineData(new [] {"__MACOSX/cover.jpg", "vol1/page 01.jpg"}, "vol1/page 01.jpg")]
|
[InlineData(new [] {"__MACOSX/cover.jpg", "vol1/page 01.jpg"}, "vol1/page 01.jpg")]
|
||||||
[InlineData(new [] {"Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c060 (v10) - p200 [Digital] [LuCaZ].jpg", "folder.jpg"}, "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg")]
|
[InlineData(new [] {"Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg", "Akame ga KILL! ZERO - c060 (v10) - p200 [Digital] [LuCaZ].jpg", "folder.jpg"}, "Akame ga KILL! ZERO - c055 (v10) - p000 [Digital] [LuCaZ].jpg")]
|
||||||
[InlineData(new [] {"001.jpg", "001 - chapter 1/001.jpg"}, "001.jpg")]
|
[InlineData(new [] {"001.jpg", "001 - chapter 1/001.jpg"}, "001.jpg")]
|
||||||
|
[InlineData(new [] {"chapter 1/001.jpg", "chapter 2/002.jpg", "somefile.jpg"}, "somefile.jpg")]
|
||||||
public void FindFirstEntry(string[] files, string expected)
|
public void FindFirstEntry(string[] files, string expected)
|
||||||
{
|
{
|
||||||
var foundFile = ArchiveService.FirstFileEntry(files, string.Empty);
|
var foundFile = ArchiveService.FirstFileEntry(files, string.Empty);
|
||||||
|
@ -139,15 +139,6 @@ namespace API.Controllers
|
|||||||
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername());
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(downloadBookmarkDto.Bookmarks.First().SeriesId);
|
||||||
|
|
||||||
var tempFolder = $"download_{user.Id}_{series.Id}_bookmarks";
|
|
||||||
var fullExtractPath = Path.Join(_directoryService.TempDirectory, tempFolder);
|
|
||||||
if (_directoryService.FileSystem.DirectoryInfo.FromDirectoryName(fullExtractPath).Exists)
|
|
||||||
{
|
|
||||||
return BadRequest(
|
|
||||||
"Server is currently processing this exact download. Please try again in a few minutes.");
|
|
||||||
}
|
|
||||||
_directoryService.ExistOrCreate(fullExtractPath);
|
|
||||||
|
|
||||||
var bookmarkDirectory =
|
var bookmarkDirectory =
|
||||||
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||||
var files = (await _unitOfWork.UserRepository.GetAllBookmarksByIds(downloadBookmarkDto.Bookmarks
|
var files = (await _unitOfWork.UserRepository.GetAllBookmarksByIds(downloadBookmarkDto.Bookmarks
|
||||||
@ -156,8 +147,7 @@ namespace API.Controllers
|
|||||||
.Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, b.FileName)));
|
.Select(b => Parser.Parser.NormalizePath(_directoryService.FileSystem.Path.Join(bookmarkDirectory, b.FileName)));
|
||||||
|
|
||||||
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files,
|
var (fileBytes, _) = await _archiveService.CreateZipForDownload(files,
|
||||||
tempFolder);
|
$"download_{user.Id}_{series.Id}_bookmarks");
|
||||||
_directoryService.ClearAndDeleteDirectory(fullExtractPath);
|
|
||||||
return File(fileBytes, DefaultContentType, $"{series.Name} - Bookmarks.zip");
|
return File(fileBytes, DefaultContentType, $"{series.Name} - Bookmarks.zip");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +83,8 @@ namespace API.Controllers
|
|||||||
var username = User.GetUsername();
|
var username = User.GetUsername();
|
||||||
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username);
|
_logger.LogInformation("Series {SeriesId} is being deleted by {UserName}", seriesId, username);
|
||||||
|
|
||||||
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||||
|
|
||||||
var chapterIds = (await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{seriesId}));
|
var chapterIds = (await _unitOfWork.SeriesRepository.GetChapterIdsForSeriesAsync(new []{seriesId}));
|
||||||
var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId);
|
var result = await _unitOfWork.SeriesRepository.DeleteSeriesAsync(seriesId);
|
||||||
|
|
||||||
@ -92,6 +94,8 @@ namespace API.Controllers
|
|||||||
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
await _unitOfWork.CollectionTagRepository.RemoveTagsWithoutSeries();
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
_taskScheduler.CleanupChapters(chapterIds);
|
_taskScheduler.CleanupChapters(chapterIds);
|
||||||
|
await _messageHub.Clients.All.SendAsync(SignalREvents.SeriesRemoved,
|
||||||
|
MessageFactory.SeriesRemovedEvent(seriesId, series.Name, series.LibraryId));
|
||||||
}
|
}
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,33 @@ namespace API.Data.Metadata
|
|||||||
.SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown);
|
.SingleOrDefault(t => t.ToDescription().ToUpperInvariant().Equals(value.ToUpperInvariant()), Entities.Enums.AgeRating.Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void CleanComicInfo(ComicInfo info)
|
||||||
|
{
|
||||||
|
if (info == null) return;
|
||||||
|
|
||||||
|
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
|
||||||
|
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
|
||||||
|
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
|
||||||
|
info.Inker = Parser.Parser.CleanAuthor(info.Inker);
|
||||||
|
info.Letterer = Parser.Parser.CleanAuthor(info.Letterer);
|
||||||
|
info.Penciller = Parser.Parser.CleanAuthor(info.Penciller);
|
||||||
|
info.Publisher = Parser.Parser.CleanAuthor(info.Publisher);
|
||||||
|
info.Characters = Parser.Parser.CleanAuthor(info.Characters);
|
||||||
|
info.Translator = Parser.Parser.CleanAuthor(info.Translator);
|
||||||
|
info.CoverArtist = Parser.Parser.CleanAuthor(info.CoverArtist);
|
||||||
|
|
||||||
|
|
||||||
|
// if (!string.IsNullOrEmpty(info.Web))
|
||||||
|
// {
|
||||||
|
// // ComicVine stores the Issue number in Number field and does not use Volume.
|
||||||
|
// if (!info.Web.Contains("https://comicvine.gamespot.com/")) return;
|
||||||
|
// if (info.Volume.Equals("1"))
|
||||||
|
// {
|
||||||
|
// info.Volume = Parser.Parser.DefaultVolume;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
namespace API.Entities.Enums;
|
namespace API.Entities.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents Age Rating for content.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Based on ComicInfo.xml v2.1 https://github.com/anansi-project/comicinfo/blob/main/drafts/v2.1/ComicInfo.xsd</remarks>
|
||||||
public enum AgeRating
|
public enum AgeRating
|
||||||
{
|
{
|
||||||
[Description("Unknown")]
|
[Description("Unknown")]
|
||||||
@ -20,15 +24,15 @@ public enum AgeRating
|
|||||||
KidsToAdults = 6,
|
KidsToAdults = 6,
|
||||||
[Description("Teen")]
|
[Description("Teen")]
|
||||||
Teen = 7,
|
Teen = 7,
|
||||||
[Description("Mature 15+")]
|
[Description("MA 15+")]
|
||||||
Mature15Plus = 8,
|
Mature15Plus = 8,
|
||||||
[Description("Mature 17+")]
|
[Description("Mature 17+")]
|
||||||
Mature17Plus = 9,
|
Mature17Plus = 9,
|
||||||
[Description("Mature")]
|
[Description("M")]
|
||||||
Mature = 10,
|
Mature = 10,
|
||||||
[Description("Adults Only 18+")]
|
[Description("Adults Only 18+")]
|
||||||
AdultsOnly = 11,
|
AdultsOnly = 11,
|
||||||
[Description("X 18+")]
|
[Description("X18+")]
|
||||||
X18Plus = 12
|
X18Plus = 12
|
||||||
|
|
||||||
|
|
||||||
|
@ -142,10 +142,39 @@ namespace API.Services
|
|||||||
/// <returns>Entry name of match, null if no match</returns>
|
/// <returns>Entry name of match, null if no match</returns>
|
||||||
public static string? FirstFileEntry(IEnumerable<string> entryFullNames, string archiveName)
|
public static string? FirstFileEntry(IEnumerable<string> entryFullNames, string archiveName)
|
||||||
{
|
{
|
||||||
var result = entryFullNames
|
// First check if there are any files that are not in a nested folder before just comparing by filename. This is needed
|
||||||
|
// because NaturalSortComparer does not work with paths and doesn't seem 001.jpg as before chapter 1/001.jpg.
|
||||||
|
var fullNames = entryFullNames
|
||||||
.OrderByNatural(c => c.GetFullPathWithoutExtension())
|
.OrderByNatural(c => c.GetFullPathWithoutExtension())
|
||||||
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)))
|
.Where(path => !(Path.EndsInDirectorySeparator(path) || Parser.Parser.HasBlacklistedFolderInPath(path) || path.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)) && Parser.Parser.IsImage(path))
|
||||||
.FirstOrDefault(path => Parser.Parser.IsImage(path));
|
.ToList();
|
||||||
|
if (fullNames.Count == 0) return null;
|
||||||
|
|
||||||
|
var nonNestedFile = fullNames.Where(entry => (Path.GetDirectoryName(entry) ?? string.Empty).Equals(archiveName))
|
||||||
|
.OrderByNatural(c => c.GetFullPathWithoutExtension())
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(nonNestedFile)) return nonNestedFile;
|
||||||
|
|
||||||
|
// Check the first folder and sort within that to see if we can find a file, else fallback to first file with basic sort.
|
||||||
|
// Get first folder, then sort within that
|
||||||
|
var firstDirectoryFile = fullNames.OrderByNatural(Path.GetDirectoryName).FirstOrDefault();
|
||||||
|
if (!string.IsNullOrEmpty(firstDirectoryFile))
|
||||||
|
{
|
||||||
|
var firstDirectory = Path.GetDirectoryName(firstDirectoryFile);
|
||||||
|
if (!string.IsNullOrEmpty(firstDirectory))
|
||||||
|
{
|
||||||
|
var firstDirectoryResult = fullNames.Where(f => firstDirectory.Equals(Path.GetDirectoryName(f)))
|
||||||
|
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(firstDirectoryResult)) return firstDirectoryResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = fullNames
|
||||||
|
.OrderByNatural(Path.GetFileNameWithoutExtension)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
return string.IsNullOrEmpty(result) ? null : result;
|
return string.IsNullOrEmpty(result) ? null : result;
|
||||||
}
|
}
|
||||||
@ -235,6 +264,13 @@ namespace API.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Refactor CreateZipForDownload to return the temp file so we can stream it from temp
|
// TODO: Refactor CreateZipForDownload to return the temp file so we can stream it from temp
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="files"></param>
|
||||||
|
/// <param name="tempFolder">Temp folder name to use for preparing the files. Will be created and deleted</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="KavitaException"></exception>
|
||||||
public async Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder)
|
public async Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder)
|
||||||
{
|
{
|
||||||
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
|
var dateString = DateTime.Now.ToShortDateString().Replace("/", "_");
|
||||||
@ -309,30 +345,6 @@ namespace API.Services
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void CleanComicInfo(ComicInfo info)
|
|
||||||
{
|
|
||||||
if (info == null) return;
|
|
||||||
|
|
||||||
info.Writer = Parser.Parser.CleanAuthor(info.Writer);
|
|
||||||
info.Colorist = Parser.Parser.CleanAuthor(info.Colorist);
|
|
||||||
info.Editor = Parser.Parser.CleanAuthor(info.Editor);
|
|
||||||
info.Inker = Parser.Parser.CleanAuthor(info.Inker);
|
|
||||||
info.Letterer = Parser.Parser.CleanAuthor(info.Letterer);
|
|
||||||
info.Penciller = Parser.Parser.CleanAuthor(info.Penciller);
|
|
||||||
info.Publisher = Parser.Parser.CleanAuthor(info.Publisher);
|
|
||||||
info.Characters = Parser.Parser.CleanAuthor(info.Characters);
|
|
||||||
|
|
||||||
// if (!string.IsNullOrEmpty(info.Web))
|
|
||||||
// {
|
|
||||||
// // ComicVine stores the Issue number in Number field and does not use Volume.
|
|
||||||
// if (!info.Web.Contains("https://comicvine.gamespot.com/")) return;
|
|
||||||
// if (info.Volume.Equals("1"))
|
|
||||||
// {
|
|
||||||
// info.Volume = Parser.Parser.DefaultVolume;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This can be null if nothing is found or any errors occur during access
|
/// This can be null if nothing is found or any errors occur during access
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -363,7 +375,7 @@ namespace API.Services
|
|||||||
using var stream = entry.Open();
|
using var stream = entry.Open();
|
||||||
var serializer = new XmlSerializer(typeof(ComicInfo));
|
var serializer = new XmlSerializer(typeof(ComicInfo));
|
||||||
var info = (ComicInfo) serializer.Deserialize(stream);
|
var info = (ComicInfo) serializer.Deserialize(stream);
|
||||||
CleanComicInfo(info);
|
ComicInfo.CleanComicInfo(info);
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -383,7 +395,7 @@ namespace API.Services
|
|||||||
.Parser
|
.Parser
|
||||||
.MacOsMetadataFileStartsWith)
|
.MacOsMetadataFileStartsWith)
|
||||||
&& Parser.Parser.IsXml(entry.Key)));
|
&& Parser.Parser.IsXml(entry.Key)));
|
||||||
CleanComicInfo(info);
|
ComicInfo.CleanComicInfo(info);
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,8 @@ public class BackupService : IBackupService
|
|||||||
{
|
{
|
||||||
_backupFiles.Add(file);
|
_backupFiles.Add(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<string> GetLogFiles(int maxRollingFiles, string logFileName)
|
public IEnumerable<string> GetLogFiles(int maxRollingFiles, string logFileName)
|
||||||
@ -114,6 +116,10 @@ public class BackupService : IBackupService
|
|||||||
|
|
||||||
await CopyCoverImagesToBackupDirectory(tempDirectory);
|
await CopyCoverImagesToBackupDirectory(tempDirectory);
|
||||||
|
|
||||||
|
await SendProgress(0.5F);
|
||||||
|
|
||||||
|
await CopyBookmarksToBackupDirectory(tempDirectory);
|
||||||
|
|
||||||
await SendProgress(0.75F);
|
await SendProgress(0.75F);
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -154,7 +160,30 @@ public class BackupService : IBackupService
|
|||||||
// Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file.
|
// Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_directoryService.GetFiles(outputTempDir).Any())
|
if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any())
|
||||||
|
{
|
||||||
|
_directoryService.ClearAndDeleteDirectory(outputTempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopyBookmarksToBackupDirectory(string tempDirectory)
|
||||||
|
{
|
||||||
|
var bookmarkDirectory =
|
||||||
|
(await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BookmarkDirectory)).Value;
|
||||||
|
|
||||||
|
var outputTempDir = Path.Join(tempDirectory, "bookmarks");
|
||||||
|
_directoryService.ExistOrCreate(outputTempDir);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_directoryService.CopyDirectoryToDirectory(bookmarkDirectory, outputTempDir);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// Swallow exception.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_directoryService.GetFiles(outputTempDir, searchOption: SearchOption.AllDirectories).Any())
|
||||||
{
|
{
|
||||||
_directoryService.ClearAndDeleteDirectory(outputTempDir);
|
_directoryService.ClearAndDeleteDirectory(outputTempDir);
|
||||||
}
|
}
|
||||||
|
@ -227,6 +227,8 @@ public class ScannerService : IScannerService
|
|||||||
if (library.Folders.Any(f => !_directoryService.IsDriveMounted(f.Path)))
|
if (library.Folders.Any(f => !_directoryService.IsDriveMounted(f.Path)))
|
||||||
{
|
{
|
||||||
_logger.LogCritical("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted");
|
_logger.LogCritical("Some of the root folders for library are not accessible. Please check that drives are connected and rescan. Scan will be aborted");
|
||||||
|
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
||||||
|
MessageFactory.ScanLibraryProgressEvent(libraryId, 1F));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,6 +239,8 @@ public class ScannerService : IScannerService
|
|||||||
"Either your mount has been disconnected or you are trying to delete all series in the library. " +
|
"Either your mount has been disconnected or you are trying to delete all series in the library. " +
|
||||||
"Scan will be aborted. " +
|
"Scan will be aborted. " +
|
||||||
"Check that your mount is connected or change the library's root folder and rescan");
|
"Check that your mount is connected or change the library's root folder and rescan");
|
||||||
|
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
||||||
|
MessageFactory.ScanLibraryProgressEvent(libraryId, 1F));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,9 +275,9 @@ public class ScannerService : IScannerService
|
|||||||
|
|
||||||
await CleanupDbEntities();
|
await CleanupDbEntities();
|
||||||
|
|
||||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
|
|
||||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
||||||
MessageFactory.ScanLibraryProgressEvent(libraryId, 1F));
|
MessageFactory.ScanLibraryProgressEvent(libraryId, 1F));
|
||||||
|
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -79,7 +79,7 @@ export class ActionFactoryService {
|
|||||||
|
|
||||||
this.seriesActions.push({
|
this.seriesActions.push({
|
||||||
action: Action.RefreshMetadata,
|
action: Action.RefreshMetadata,
|
||||||
title: 'Refresh Metadata',
|
title: 'Refresh Covers',
|
||||||
callback: this.dummyCallback,
|
callback: this.dummyCallback,
|
||||||
requiresAdmin: true
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
@ -114,7 +114,7 @@ export class ActionFactoryService {
|
|||||||
|
|
||||||
this.libraryActions.push({
|
this.libraryActions.push({
|
||||||
action: Action.RefreshMetadata,
|
action: Action.RefreshMetadata,
|
||||||
title: 'Refresh Metadata',
|
title: 'Refresh Covers',
|
||||||
callback: this.dummyCallback,
|
callback: this.dummyCallback,
|
||||||
requiresAdmin: true
|
requiresAdmin: true
|
||||||
});
|
});
|
||||||
|
@ -76,7 +76,7 @@ export class ActionService implements OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!await this.confirmService.confirm('Refresh metadata will force all cover images and metadata to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) {
|
if (!await this.confirmService.confirm('Refresh covers will force all cover images to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(library);
|
callback(library);
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ export class ActionService implements OnDestroy {
|
|||||||
* @param callback Optional callback to perform actions after API completes
|
* @param callback Optional callback to perform actions after API completes
|
||||||
*/
|
*/
|
||||||
async refreshMetdata(series: Series, callback?: SeriesActionCallback) {
|
async refreshMetdata(series: Series, callback?: SeriesActionCallback) {
|
||||||
if (!await this.confirmService.confirm('Refresh metadata will force all cover images and metadata to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) {
|
if (!await this.confirmService.confirm('Refresh covers will force all cover images and metadata to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback(series);
|
callback(series);
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ export class ReadingListService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeRead(readingListId: number) {
|
removeRead(readingListId: number) {
|
||||||
return this.httpClient.post(this.baseUrl + 'readinglist/remove-read?readingListId=' + readingListId, { responseType: 'text' as 'json' });
|
return this.httpClient.post<string>(this.baseUrl + 'readinglist/remove-read?readingListId=' + readingListId, {}, { responseType: 'text' as 'json' });
|
||||||
}
|
}
|
||||||
|
|
||||||
actionListFilter(action: ActionItem<ReadingList>, readingList: ReadingList, isAdmin: boolean) {
|
actionListFilter(action: ActionItem<ReadingList>, readingList: ReadingList, isAdmin: boolean) {
|
||||||
|
@ -39,14 +39,16 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
|
|||||||
// when a progress event comes in, show it on the UI next to library
|
// when a progress event comes in, show it on the UI next to library
|
||||||
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {
|
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe((event) => {
|
||||||
if (event.event !== EVENTS.ScanLibraryProgress) return;
|
if (event.event !== EVENTS.ScanLibraryProgress) return;
|
||||||
|
|
||||||
|
console.log('scan event: ', event.payload);
|
||||||
|
|
||||||
const scanEvent = event.payload as ProgressEvent;
|
const scanEvent = event.payload as ProgressEvent;
|
||||||
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 100};
|
this.scanInProgress[scanEvent.libraryId] = {progress: scanEvent.progress !== 1};
|
||||||
if (scanEvent.progress === 0) {
|
if (scanEvent.progress === 0) {
|
||||||
this.scanInProgress[scanEvent.libraryId].timestamp = scanEvent.eventTime;
|
this.scanInProgress[scanEvent.libraryId].timestamp = scanEvent.eventTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.scanInProgress[scanEvent.libraryId].progress === false && scanEvent.progress === 100) {
|
if (this.scanInProgress[scanEvent.libraryId].progress === false && scanEvent.progress === 1) {
|
||||||
this.libraryService.getLibraries().pipe(take(1)).subscribe(libraries => {
|
this.libraryService.getLibraries().pipe(take(1)).subscribe(libraries => {
|
||||||
const newLibrary = libraries.find(lib => lib.id === scanEvent.libraryId);
|
const newLibrary = libraries.find(lib => lib.id === scanEvent.libraryId);
|
||||||
const existingLibrary = this.libraries.find(lib => lib.id === scanEvent.libraryId);
|
const existingLibrary = this.libraries.find(lib => lib.id === scanEvent.libraryId);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container" [ngStyle]="{overflow: (isFullscreen ? 'auto' : 'visible')}" #reader>
|
<div class="container-flex {{darkMode ? 'dark-mode' : ''}} reader-container" tabindex="0" #reader>
|
||||||
<div class="fixed-top" #stickyTop>
|
<div class="fixed-top" #stickyTop>
|
||||||
<a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
<a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||||
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
<ng-container [ngTemplateOutlet]="actionBar"></ng-container>
|
||||||
|
@ -161,6 +161,7 @@ $primary-color: #0062cc;
|
|||||||
|
|
||||||
.reader-container {
|
.reader-container {
|
||||||
outline: none; // Only the reading section itself shouldn't receive any outline. We use it to shift focus in fullscreen mode
|
outline: none; // Only the reading section itself shouldn't receive any outline. We use it to shift focus in fullscreen mode
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-content {
|
.book-content {
|
||||||
|
@ -748,11 +748,8 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.scrollService.scrollTo(0, this.reader.nativeElement);
|
this.scrollService.scrollTo(0, this.reader.nativeElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On fullscreen we need to click the document before arrow keys will scroll down.
|
// we need to click the document before arrow keys will scroll down.
|
||||||
if (this.isFullscreen) {
|
this.reader.nativeElement.focus();
|
||||||
this.renderer.setAttribute(this.reader.nativeElement, 'tabIndex', '0');
|
|
||||||
this.reader.nativeElement.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPageNum(pageNum: number) {
|
setPageNum(pageNum: number) {
|
||||||
@ -888,6 +885,20 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.renderer.setStyle(this.readingHtml.nativeElement, item[0], item[1], RendererStyleFlags2.Important);
|
this.renderer.setStyle(this.readingHtml.nativeElement, item[0], item[1], RendererStyleFlags2.Important);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for(let i = 0; i < this.readingHtml.nativeElement.children.length; i++) {
|
||||||
|
const elem = this.readingHtml.nativeElement.children.item(i);
|
||||||
|
if (elem?.tagName === 'STYLE') continue;
|
||||||
|
Object.entries(this.pageStyles).forEach(item => {
|
||||||
|
if (item[1] == '100%' || item[1] == '0px' || item[1] == 'inherit') {
|
||||||
|
// Remove the style or skip
|
||||||
|
this.renderer.removeStyle(elem, item[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.renderer.setStyle(elem, item[0], item[1], RendererStyleFlags2.Important);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +151,6 @@ export class CollectionDetailComponent implements OnInit, OnDestroy {
|
|||||||
this.collectionTag = matchingTags[0];
|
this.collectionTag = matchingTags[0];
|
||||||
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id));
|
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id));
|
||||||
this.titleService.setTitle('Kavita - ' + this.collectionTag.title + ' Collection');
|
this.titleService.setTitle('Kavita - ' + this.collectionTag.title + ' Collection');
|
||||||
this.loadPage();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter());
|
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.utilityService.filterPresetsFromUrl(this.route.snapshot, this.seriesService.createSeriesFilter());
|
||||||
this.filterSettings.presets.libraries = [this.libraryId];
|
this.filterSettings.presets.libraries = [this.libraryId];
|
||||||
|
|
||||||
this.loadPage();
|
//this.loadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -140,7 +140,6 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
updateFilter(data: SeriesFilter) {
|
updateFilter(data: SeriesFilter) {
|
||||||
this.filter = data;
|
this.filter = data;
|
||||||
console.log('filter: ', this.filter);
|
|
||||||
if (this.pagination !== undefined && this.pagination !== null) {
|
if (this.pagination !== undefined && this.pagination !== null) {
|
||||||
this.pagination.currentPage = 1;
|
this.pagination.currentPage = 1;
|
||||||
this.onPageChange(this.pagination);
|
this.onPageChange(this.pagination);
|
||||||
@ -171,7 +170,7 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPageChange(pagination: Pagination) {
|
onPageChange(pagination: Pagination) {
|
||||||
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?page=' + this.pagination.currentPage);
|
window.history.replaceState(window.location.href, '', window.location.href.split('?')[0] + '?' + 'page=' + this.pagination.currentPage);
|
||||||
this.loadPage();
|
this.loadPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +26,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngFor="let item of webtoonImages | async; let index = index;">
|
<ng-container *ngFor="let item of webtoonImages | async; let index = index;">
|
||||||
<img src="{{item.src}}" style="display: block" class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}" *ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image" (load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
|
<img src="{{item.src}}" style="display: block"
|
||||||
|
class="mx-auto {{pageNum === item.page && showDebugOutline() ? 'active': ''}} {{areImagesWiderThanWindow ? 'full-width' : ''}}"
|
||||||
|
*ngIf="pageNum >= pageNum - bufferPages && pageNum <= pageNum + bufferPages" rel="nofollow" alt="image"
|
||||||
|
(load)="onImageLoad($event)" id="page-{{item.page}}" [attr.page]="item.page" ondragstart="return false;" onselectstart="return false;">
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()">
|
<div *ngIf="atBottom" class="spacer bottom" role="alert" (click)="loadNextChapter.emit()">
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
import { Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, Renderer2, SimpleChanges } from '@angular/core';
|
||||||
import { BehaviorSubject, fromEvent, ReplaySubject, Subject } from 'rxjs';
|
import { BehaviorSubject, fromEvent, merge, ReplaySubject, Subject } from 'rxjs';
|
||||||
import { debounceTime, takeUntil } from 'rxjs/operators';
|
import { debounceTime, take, takeUntil } from 'rxjs/operators';
|
||||||
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';
|
||||||
@ -64,7 +64,9 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
@Input() goToPage: ReplaySubject<number> = new ReplaySubject<number>();
|
@Input() goToPage: ReplaySubject<number> = new ReplaySubject<number>();
|
||||||
@Input() bookmarkPage: ReplaySubject<number> = new ReplaySubject<number>();
|
@Input() bookmarkPage: ReplaySubject<number> = new ReplaySubject<number>();
|
||||||
@Input() fullscreenToggled: ReplaySubject<boolean> = new ReplaySubject<boolean>();
|
@Input() fullscreenToggled: ReplaySubject<boolean> = new ReplaySubject<boolean>();
|
||||||
|
|
||||||
|
readerElemRef!: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores and emits all the src urls
|
* Stores and emits all the src urls
|
||||||
*/
|
*/
|
||||||
@ -111,6 +113,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
* If the user has scrolled all the way to the top. This is used solely for continuous reading
|
* If the user has scrolled all the way to the top. This is used solely for continuous reading
|
||||||
*/
|
*/
|
||||||
atTop: boolean = false;
|
atTop: boolean = false;
|
||||||
|
/**
|
||||||
|
* If the manga reader is in fullscreen. Some math changes based on this value.
|
||||||
|
*/
|
||||||
|
isFullscreenMode: boolean = false;
|
||||||
/**
|
/**
|
||||||
* Keeps track of the previous scrolling height for restoring scroll position after we inject spacer block
|
* Keeps track of the previous scrolling height for restoring scroll position after we inject spacer block
|
||||||
*/
|
*/
|
||||||
@ -129,7 +135,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get areImagesWiderThanWindow() {
|
get areImagesWiderThanWindow() {
|
||||||
return this.webtoonImageWidth > (window.innerWidth || document.documentElement.clientWidth);
|
let [innerWidth, _] = this.getInnerDimensions();
|
||||||
|
return this.webtoonImageWidth > (innerWidth || document.documentElement.clientWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -137,7 +144,13 @@ 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) {}
|
constructor(private readerService: ReaderService, private renderer: Renderer2) {
|
||||||
|
// This will always exist at this point in time since this is used within manga reader
|
||||||
|
const reader = document.querySelector('.reader');
|
||||||
|
if (reader !== null) {
|
||||||
|
this.readerElemRef = new ElementRef(reader as HTMLDivElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
if (changes.hasOwnProperty('totalPages') && changes['totalPages'].previousValue != changes['totalPages'].currentValue) {
|
if (changes.hasOwnProperty('totalPages') && changes['totalPages'].previousValue != changes['totalPages'].currentValue) {
|
||||||
@ -152,12 +165,21 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.onDestroy.complete();
|
this.onDestroy.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
/**
|
||||||
const reader = document.querySelector('.reader') || window;
|
* Responsible for binding the scroll handler to the correct event. On non-fullscreen, window is correct. However, on fullscreen, we must use the reader as that is what
|
||||||
fromEvent(reader, 'scroll')
|
* gets promoted to fullscreen.
|
||||||
.pipe(debounceTime(20), takeUntil(this.onDestroy))
|
*/
|
||||||
|
initScrollHandler() {
|
||||||
|
fromEvent(this.isFullscreenMode ? this.readerElemRef.nativeElement : window, 'scroll')
|
||||||
|
.pipe(debounceTime(20), takeUntil(this.onDestroy))
|
||||||
.subscribe((event) => this.handleScrollEvent(event));
|
.subscribe((event) => this.handleScrollEvent(event));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.initScrollHandler();
|
||||||
|
|
||||||
if (this.goToPage) {
|
if (this.goToPage) {
|
||||||
this.goToPage.pipe(takeUntil(this.onDestroy)).subscribe(page => {
|
this.goToPage.pipe(takeUntil(this.onDestroy)).subscribe(page => {
|
||||||
this.debugLog('[GoToPage] jump has occured from ' + this.pageNum + ' to ' + page);
|
this.debugLog('[GoToPage] jump has occured from ' + this.pageNum + ' to ' + page);
|
||||||
@ -189,20 +211,39 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
if (this.fullscreenToggled) {
|
if (this.fullscreenToggled) {
|
||||||
this.fullscreenToggled.pipe(takeUntil(this.onDestroy)).subscribe(isFullscreen => {
|
this.fullscreenToggled.pipe(takeUntil(this.onDestroy)).subscribe(isFullscreen => {
|
||||||
this.debugLog('[FullScreen] Fullscreen mode: ', isFullscreen);
|
this.debugLog('[FullScreen] Fullscreen mode: ', isFullscreen);
|
||||||
|
this.isFullscreenMode = isFullscreen;
|
||||||
|
const [innerWidth, _] = this.getInnerDimensions();
|
||||||
|
this.webtoonImageWidth = innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||||
|
this.initScrollHandler();
|
||||||
this.setPageNum(this.pageNum, true);
|
this.setPageNum(this.pageNum, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVerticalOffset() {
|
||||||
|
const reader = this.isFullscreenMode ? this.readerElemRef.nativeElement : window;
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
if (reader instanceof Window) {
|
||||||
|
offset = reader.scrollY;
|
||||||
|
} else {
|
||||||
|
offset = reader.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (offset
|
||||||
|
|| document.documentElement.scrollTop
|
||||||
|
|| document.body.scrollTop || 0);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On scroll in document, calculate if the user/javascript has scrolled to the current image element (and it's visible), update that scrolling has ended completely,
|
* On scroll in document, calculate if the user/javascript has scrolled to the current image element (and it's visible), update that scrolling has ended completely,
|
||||||
* and calculate the direction the scrolling is occuring. This is not used for prefetching.
|
* and calculate the direction the scrolling is occuring. This is not used for prefetching.
|
||||||
* @param event Scroll Event
|
* @param event Scroll Event
|
||||||
*/
|
*/
|
||||||
handleScrollEvent(event?: any) {
|
handleScrollEvent(event?: any) {
|
||||||
const verticalOffset = (window.pageYOffset
|
// Need a fullscreen handler here too
|
||||||
|| document.documentElement.scrollTop
|
let verticalOffset = this.getVerticalOffset();
|
||||||
|| document.body.scrollTop || 0);
|
|
||||||
|
|
||||||
if (verticalOffset > this.prevScrollPosition) {
|
if (verticalOffset > this.prevScrollPosition) {
|
||||||
this.scrollingDirection = PAGING_DIRECTION.FORWARD;
|
this.scrollingDirection = PAGING_DIRECTION.FORWARD;
|
||||||
@ -211,6 +252,10 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.prevScrollPosition = verticalOffset;
|
this.prevScrollPosition = verticalOffset;
|
||||||
|
|
||||||
|
console.log('CurrentPageElem: ', this.currentPageElem);
|
||||||
|
if (this.currentPageElem != null) {
|
||||||
|
console.log('Element Visible: ', this.isElementVisible(this.currentPageElem));
|
||||||
|
}
|
||||||
if (this.isScrolling && this.currentPageElem != null && this.isElementVisible(this.currentPageElem)) {
|
if (this.isScrolling && this.currentPageElem != null && this.isElementVisible(this.currentPageElem)) {
|
||||||
this.debugLog('[Scroll] Image is visible from scroll, isScrolling is now false');
|
this.debugLog('[Scroll] Image is visible from scroll, isScrolling is now false');
|
||||||
this.isScrolling = false;
|
this.isScrolling = false;
|
||||||
@ -239,9 +284,15 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
return totalHeight;
|
return totalHeight;
|
||||||
}
|
}
|
||||||
getTotalScroll() {
|
getTotalScroll() {
|
||||||
|
if (this.isFullscreenMode) {
|
||||||
|
return this.readerElemRef.nativeElement.offsetHeight + this.readerElemRef.nativeElement.scrollTop;
|
||||||
|
}
|
||||||
return document.documentElement.offsetHeight + document.documentElement.scrollTop;
|
return document.documentElement.offsetHeight + document.documentElement.scrollTop;
|
||||||
}
|
}
|
||||||
getScrollTop() {
|
getScrollTop() {
|
||||||
|
if (this.isFullscreenMode) {
|
||||||
|
return this.readerElemRef.nativeElement.scrollTop;
|
||||||
|
}
|
||||||
return document.documentElement.scrollTop;
|
return document.documentElement.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,7 +327,7 @@ 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.documentElement.scrollHeight - document.documentElement.scrollTop;
|
||||||
requestAnimationFrame(() => window.scrollTo(0, SPACER_SCROLL_INTO_PX));
|
requestAnimationFrame(() => window.scrollTo(0, SPACER_SCROLL_INTO_PX)); // TODO: does this need to be fullscreen protected?
|
||||||
} 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();
|
||||||
@ -285,6 +336,17 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInnerDimensions() {
|
||||||
|
let innerHeight = window.innerHeight;
|
||||||
|
let innerWidth = window.innerWidth;
|
||||||
|
|
||||||
|
if (this.isFullscreenMode) {
|
||||||
|
innerHeight = this.readerElemRef.nativeElement.clientHeight;
|
||||||
|
innerWidth = this.readerElemRef.nativeElement.clientWidth;
|
||||||
|
}
|
||||||
|
return [innerHeight, innerWidth];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is any part of the element visible in the scrollport. Does not take into account
|
* Is any part of the element visible in the scrollport. Does not take into account
|
||||||
* style properites, just scroll port visibility.
|
* style properites, just scroll port visibility.
|
||||||
@ -297,10 +359,16 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
// NOTE: This will say an element is visible if it is 1 px offscreen on top
|
// NOTE: This will say an element is visible if it is 1 px offscreen on top
|
||||||
var rect = elem.getBoundingClientRect();
|
var rect = elem.getBoundingClientRect();
|
||||||
|
|
||||||
|
let [innerHeight, innerWidth] = this.getInnerDimensions();
|
||||||
|
|
||||||
|
|
||||||
|
console.log('innerHeight: ', innerHeight);
|
||||||
|
console.log('innerWidth: ', innerWidth);
|
||||||
|
|
||||||
return (rect.bottom >= 0 &&
|
return (rect.bottom >= 0 &&
|
||||||
rect.right >= 0 &&
|
rect.right >= 0 &&
|
||||||
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
|
rect.top <= (innerHeight || document.documentElement.clientHeight) &&
|
||||||
rect.left <= (window.innerWidth || document.documentElement.clientWidth)
|
rect.left <= (innerWidth || document.documentElement.clientWidth)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,12 +383,15 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
var rect = elem.getBoundingClientRect();
|
var rect = elem.getBoundingClientRect();
|
||||||
|
|
||||||
|
let [innerHeight, innerWidth] = this.getInnerDimensions();
|
||||||
|
|
||||||
|
|
||||||
if (rect.bottom >= 0 &&
|
if (rect.bottom >= 0 &&
|
||||||
rect.right >= 0 &&
|
rect.right >= 0 &&
|
||||||
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
|
rect.top <= (innerHeight || document.documentElement.clientHeight) &&
|
||||||
rect.left <= (window.innerWidth || document.documentElement.clientWidth)
|
rect.left <= (innerWidth || document.documentElement.clientWidth)
|
||||||
) {
|
) {
|
||||||
const topX = (window.innerHeight || document.documentElement.clientHeight);
|
const topX = (innerHeight || document.documentElement.clientHeight);
|
||||||
return Math.abs(rect.top / topX) <= 0.25;
|
return Math.abs(rect.top / topX) <= 0.25;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -328,6 +399,8 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
initWebtoonReader() {
|
initWebtoonReader() {
|
||||||
|
const [innerWidth, _] = this.getInnerDimensions();
|
||||||
|
this.webtoonImageWidth = innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||||
this.imagesLoaded = {};
|
this.imagesLoaded = {};
|
||||||
this.webtoonImages.next([]);
|
this.webtoonImages.next([]);
|
||||||
this.atBottom = false;
|
this.atBottom = false;
|
||||||
@ -424,7 +497,6 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
* Performs the scroll for the current page element. Updates any state variables needed.
|
* Performs the scroll for the current page element. Updates any state variables needed.
|
||||||
*/
|
*/
|
||||||
scrollToCurrentPage() {
|
scrollToCurrentPage() {
|
||||||
this.debugLog('Scrolling to ', this.pageNum);
|
|
||||||
this.currentPageElem = document.querySelector('img#page-' + this.pageNum);
|
this.currentPageElem = document.querySelector('img#page-' + this.pageNum);
|
||||||
if (!this.currentPageElem) { return; }
|
if (!this.currentPageElem) { return; }
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ export class NavEventsToggleComponent implements OnInit, OnDestroy {
|
|||||||
prettyPrintEvent(eventType: string, event: any) {
|
prettyPrintEvent(eventType: string, event: any) {
|
||||||
switch(eventType) {
|
switch(eventType) {
|
||||||
case (EVENTS.ScanLibraryProgress): return 'Scanning ';
|
case (EVENTS.ScanLibraryProgress): return 'Scanning ';
|
||||||
case (EVENTS.RefreshMetadataProgress): return 'Refreshing ';
|
case (EVENTS.RefreshMetadataProgress): return 'Refreshing Covers for ';
|
||||||
case (EVENTS.CleanupProgress): return 'Clearing Cache';
|
case (EVENTS.CleanupProgress): return 'Clearing Cache';
|
||||||
case (EVENTS.BackupDatabaseProgress): return 'Backing up Database';
|
case (EVENTS.BackupDatabaseProgress): return 'Backing up Database';
|
||||||
case (EVENTS.DownloadProgress): return event.rawBody.userName.charAt(0).toUpperCase() + event.rawBody.userName.substr(1) + ' is downloading ' + event.rawBody.downloadName;
|
case (EVENTS.DownloadProgress): return event.rawBody.userName.charAt(0).toUpperCase() + event.rawBody.userName.substr(1) + ' is downloading ' + event.rawBody.downloadName;
|
||||||
|
@ -8,7 +8,7 @@ export class PublicationStatusPipe implements PipeTransform {
|
|||||||
|
|
||||||
transform(value: PublicationStatus): string {
|
transform(value: PublicationStatus): string {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case PublicationStatus.OnGoing: return 'Ongoing';
|
case PublicationStatus.OnGoing: return 'On Going';
|
||||||
case PublicationStatus.Hiatus: return 'Hiatus';
|
case PublicationStatus.Hiatus: return 'Hiatus';
|
||||||
case PublicationStatus.Completed: return 'Completed';
|
case PublicationStatus.Completed: return 'Completed';
|
||||||
|
|
||||||
|
@ -157,7 +157,11 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
|
|
||||||
removeRead() {
|
removeRead() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.readingListService.removeRead(this.readingList.id).subscribe(() => {
|
this.readingListService.removeRead(this.readingList.id).subscribe((resp) => {
|
||||||
|
if (resp === 'Nothing to remove') {
|
||||||
|
this.toastr.info(resp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.getListItems();
|
this.getListItems();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user