mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-31 12:14:44 -04:00
Random Fixes (#3549)
Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
ea81a2f432
commit
39726f8c4d
@ -398,4 +398,153 @@ public class ScannerServiceTests : AbstractDbTest
|
|||||||
Assert.Equal(3, series.Volumes.Count);
|
Assert.Equal(3, series.Volumes.Count);
|
||||||
Assert.Equal(2, series.Volumes.First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)).Chapters.Count);
|
Assert.Equal(2, series.Volumes.First(v => v.MinNumber.Is(Parser.LooseLeafVolumeNumber)).Chapters.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanLibrary_LocalizedSeries_MatchesFilename()
|
||||||
|
{
|
||||||
|
const string testcase = "Localized Name matches Filename - Manga.json";
|
||||||
|
|
||||||
|
// Get the first file and generate a ComicInfo
|
||||||
|
var infos = new Dictionary<string, ComicInfo>();
|
||||||
|
infos.Add("Futoku no Guild v01.cbz", new ComicInfo()
|
||||||
|
{
|
||||||
|
Series = "Immoral Guild",
|
||||||
|
LocalizedSeries = "Futoku no Guild"
|
||||||
|
});
|
||||||
|
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
|
||||||
|
|
||||||
|
|
||||||
|
var scanner = _scannerHelper.CreateServices();
|
||||||
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
|
||||||
|
Assert.NotNull(postLib);
|
||||||
|
Assert.Single(postLib.Series);
|
||||||
|
var s = postLib.Series.First();
|
||||||
|
Assert.Equal("Immoral Guild", s.Name);
|
||||||
|
Assert.Equal("Futoku no Guild", s.LocalizedName);
|
||||||
|
Assert.Single(s.Volumes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanLibrary_LocalizedSeries_MatchesFilename_SameNames()
|
||||||
|
{
|
||||||
|
const string testcase = "Localized Name matches Filename - Manga.json";
|
||||||
|
|
||||||
|
// Get the first file and generate a ComicInfo
|
||||||
|
var infos = new Dictionary<string, ComicInfo>();
|
||||||
|
infos.Add("Futoku no Guild v01.cbz", new ComicInfo()
|
||||||
|
{
|
||||||
|
Series = "Futoku no Guild",
|
||||||
|
LocalizedSeries = "Futoku no Guild"
|
||||||
|
});
|
||||||
|
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
|
||||||
|
|
||||||
|
|
||||||
|
var scanner = _scannerHelper.CreateServices();
|
||||||
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
|
||||||
|
Assert.NotNull(postLib);
|
||||||
|
Assert.Single(postLib.Series);
|
||||||
|
var s = postLib.Series.First();
|
||||||
|
Assert.Equal("Futoku no Guild", s.Name);
|
||||||
|
Assert.Equal("Futoku no Guild", s.LocalizedName);
|
||||||
|
Assert.Single(s.Volumes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanLibrary_ExcludePattern_Works()
|
||||||
|
{
|
||||||
|
const string testcase = "Exclude Pattern 1 - Manga.json";
|
||||||
|
|
||||||
|
// Get the first file and generate a ComicInfo
|
||||||
|
var infos = new Dictionary<string, ComicInfo>();
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
|
||||||
|
|
||||||
|
library.LibraryExcludePatterns = [new LibraryExcludePattern() {Pattern = "**/Extra/*"}];
|
||||||
|
_unitOfWork.LibraryRepository.Update(library);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var scanner = _scannerHelper.CreateServices();
|
||||||
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
|
||||||
|
Assert.NotNull(postLib);
|
||||||
|
Assert.Single(postLib.Series);
|
||||||
|
var s = postLib.Series.First();
|
||||||
|
Assert.Equal(2, s.Volumes.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanLibrary_ExcludePattern_FlippedSlashes_Works()
|
||||||
|
{
|
||||||
|
const string testcase = "Exclude Pattern 1 - Manga.json";
|
||||||
|
|
||||||
|
// Get the first file and generate a ComicInfo
|
||||||
|
var infos = new Dictionary<string, ComicInfo>();
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
|
||||||
|
|
||||||
|
library.LibraryExcludePatterns = [new LibraryExcludePattern() {Pattern = "**\\Extra\\*"}];
|
||||||
|
_unitOfWork.LibraryRepository.Update(library);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var scanner = _scannerHelper.CreateServices();
|
||||||
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
|
||||||
|
Assert.NotNull(postLib);
|
||||||
|
Assert.Single(postLib.Series);
|
||||||
|
var s = postLib.Series.First();
|
||||||
|
Assert.Equal(2, s.Volumes.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanLibrary_MultipleRoots_MultipleScans_DataPersists()
|
||||||
|
{
|
||||||
|
const string testcase = "Multiple Roots - Manga.json";
|
||||||
|
|
||||||
|
// Get the first file and generate a ComicInfo
|
||||||
|
var infos = new Dictionary<string, ComicInfo>();
|
||||||
|
var library = await _scannerHelper.GenerateScannerData(testcase, infos);
|
||||||
|
|
||||||
|
var testDirectoryPath =
|
||||||
|
Path.Join(
|
||||||
|
Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ScannerService/ScanTests"),
|
||||||
|
testcase.Replace(".json", string.Empty));
|
||||||
|
library.Folders =
|
||||||
|
[
|
||||||
|
new FolderPath() {Path = Path.Join(testDirectoryPath, "Root 1")},
|
||||||
|
new FolderPath() {Path = Path.Join(testDirectoryPath, "Root 2")}
|
||||||
|
];
|
||||||
|
|
||||||
|
_unitOfWork.LibraryRepository.Update(library);
|
||||||
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
|
||||||
|
var scanner = _scannerHelper.CreateServices();
|
||||||
|
await scanner.ScanLibrary(library.Id);
|
||||||
|
var postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
|
||||||
|
Assert.NotNull(postLib);
|
||||||
|
Assert.Equal(2, postLib.Series.Count);
|
||||||
|
var s = postLib.Series.First(s => s.Name == "Plush");
|
||||||
|
Assert.Equal(2, s.Volumes.Count);
|
||||||
|
var s2 = postLib.Series.First(s => s.Name == "Accel");
|
||||||
|
Assert.Single(s2.Volumes);
|
||||||
|
|
||||||
|
// Rescan to ensure nothing changes yet again
|
||||||
|
await scanner.ScanLibrary(library.Id, true);
|
||||||
|
|
||||||
|
postLib = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(library.Id, LibraryIncludes.Series);
|
||||||
|
Assert.Equal(2, postLib.Series.Count);
|
||||||
|
s = postLib.Series.First(s => s.Name == "Plush");
|
||||||
|
Assert.Equal(2, s.Volumes.Count);
|
||||||
|
s2 = postLib.Series.First(s => s.Name == "Accel");
|
||||||
|
Assert.Single(s2.Volumes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
"Antarctic Press/Plush/Plush v01.cbz",
|
||||||
|
"Antarctic Press/Plush/Plush v02.cbz",
|
||||||
|
"Antarctic Press/Plush/Extra/Plush v03.cbz"
|
||||||
|
]
|
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Immoral Guild/Futoku no Guild v01.cbz"
|
||||||
|
]
|
@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
"Root 1/Antarctic Press/Plush/Plush v01.cbz",
|
||||||
|
"Root 1/Antarctic Press/Plush/Plush v02.cbz",
|
||||||
|
"Root 2/Accel/Accel v01.cbz"
|
||||||
|
]
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs.Downloads;
|
using API.DTOs.Downloads;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
@ -157,7 +158,7 @@ public class DownloadController : BaseApiController
|
|||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.DownloadProgressEvent(username,
|
MessageFactory.DownloadProgressEvent(username,
|
||||||
filename, $"Downloading {filename}", 0F, "started"));
|
filename, $"Downloading {filename}", 0F, "started"));
|
||||||
if (files.Count == 1)
|
if (files.Count == 1 && files.First().Format != MangaFormat.Image)
|
||||||
{
|
{
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.DownloadProgressEvent(username,
|
MessageFactory.DownloadProgressEvent(username,
|
||||||
|
@ -4,6 +4,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
@ -1363,9 +1364,40 @@ public class OpdsController : BaseApiController
|
|||||||
{
|
{
|
||||||
if (feed == null) return string.Empty;
|
if (feed == null) return string.Empty;
|
||||||
|
|
||||||
|
// Remove invalid XML characters from the feed object
|
||||||
|
SanitizeFeed(feed);
|
||||||
|
|
||||||
using var sm = new StringWriter();
|
using var sm = new StringWriter();
|
||||||
_xmlSerializer.Serialize(sm, feed);
|
_xmlSerializer.Serialize(sm, feed);
|
||||||
|
|
||||||
return sm.ToString().Replace("utf-16", "utf-8"); // Chunky cannot accept UTF-16 feeds
|
return sm.ToString().Replace("utf-16", "utf-8"); // Chunky cannot accept UTF-16 feeds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursively sanitize all string properties in the object
|
||||||
|
private static void SanitizeFeed(object? obj)
|
||||||
|
{
|
||||||
|
if (obj == null) return;
|
||||||
|
|
||||||
|
var properties = obj.GetType().GetProperties();
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
if (property.PropertyType == typeof(string) && property.CanWrite)
|
||||||
|
{
|
||||||
|
var value = (string?)property.GetValue(obj);
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
property.SetValue(obj, RemoveInvalidXmlChars(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (property.PropertyType.IsClass) // Handle nested objects
|
||||||
|
{
|
||||||
|
SanitizeFeed(property.GetValue(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveInvalidXmlChars(string input)
|
||||||
|
{
|
||||||
|
return new string(input.Where(XmlConvert.IsXmlChar).ToArray());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ namespace API.Entities;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the progress a single user has on a given Chapter.
|
/// Represents the progress a single user has on a given Chapter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppUserProgress
|
public class AppUserProgress : IEntityDate
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Id of Entity
|
/// Id of Entity
|
||||||
|
@ -16,6 +16,7 @@ using Kavita.Common;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SharpCompress.Archives;
|
using SharpCompress.Archives;
|
||||||
using SharpCompress.Common;
|
using SharpCompress.Common;
|
||||||
|
using YamlDotNet.Core;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
|
|
||||||
@ -354,6 +355,14 @@ public class ArchiveService : IArchiveService
|
|||||||
foreach (var path in files)
|
foreach (var path in files)
|
||||||
{
|
{
|
||||||
var tempPath = Path.Join(tempLocation, _directoryService.FileSystem.Path.GetFileNameWithoutExtension(_directoryService.FileSystem.FileInfo.New(path).Name));
|
var tempPath = Path.Join(tempLocation, _directoryService.FileSystem.Path.GetFileNameWithoutExtension(_directoryService.FileSystem.FileInfo.New(path).Name));
|
||||||
|
|
||||||
|
// Image series need different handling
|
||||||
|
if (Tasks.Scanner.Parser.Parser.IsImage(path))
|
||||||
|
{
|
||||||
|
var parentDirectory = _directoryService.FileSystem.DirectoryInfo.New(path).Parent?.Name;
|
||||||
|
tempPath = Path.Join(tempLocation, parentDirectory ?? _directoryService.FileSystem.FileInfo.New(path).Name);
|
||||||
|
}
|
||||||
|
|
||||||
progressCallback(Tuple.Create(_directoryService.FileSystem.FileInfo.New(path).Name, (1.0f * totalFiles) / count));
|
progressCallback(Tuple.Create(_directoryService.FileSystem.FileInfo.New(path).Name, (1.0f * totalFiles) / count));
|
||||||
if (Tasks.Scanner.Parser.Parser.IsArchive(path))
|
if (Tasks.Scanner.Parser.Parser.IsArchive(path))
|
||||||
{
|
{
|
||||||
|
@ -173,7 +173,22 @@ public class CacheService : ICacheService
|
|||||||
|
|
||||||
await extractLock.WaitAsync();
|
await extractLock.WaitAsync();
|
||||||
try {
|
try {
|
||||||
if(_directoryService.Exists(extractPath)) return chapter;
|
if (_directoryService.Exists(extractPath))
|
||||||
|
{
|
||||||
|
if (extractPdfToImages)
|
||||||
|
{
|
||||||
|
var pdfImages = _directoryService.GetFiles(extractPath,
|
||||||
|
Tasks.Scanner.Parser.Parser.ImageFileExtensions);
|
||||||
|
if (pdfImages.Any())
|
||||||
|
{
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return chapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var files = chapter?.Files.ToList();
|
var files = chapter?.Files.ToList();
|
||||||
ExtractChapterFiles(extractPath, files, extractPdfToImages);
|
ExtractChapterFiles(extractPath, files, extractPdfToImages);
|
||||||
|
@ -122,6 +122,7 @@ public class ReaderService : IReaderService
|
|||||||
var seenVolume = new Dictionary<int, bool>();
|
var seenVolume = new Dictionary<int, bool>();
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId);
|
||||||
if (series == null) throw new KavitaException("series-doesnt-exist");
|
if (series == null) throw new KavitaException("series-doesnt-exist");
|
||||||
|
|
||||||
foreach (var chapter in chapters)
|
foreach (var chapter in chapters)
|
||||||
{
|
{
|
||||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||||
@ -135,10 +136,6 @@ public class ReaderService : IReaderService
|
|||||||
SeriesId = seriesId,
|
SeriesId = seriesId,
|
||||||
ChapterId = chapter.Id,
|
ChapterId = chapter.Id,
|
||||||
LibraryId = series.LibraryId,
|
LibraryId = series.LibraryId,
|
||||||
Created = DateTime.Now,
|
|
||||||
CreatedUtc = DateTime.UtcNow,
|
|
||||||
LastModified = DateTime.Now,
|
|
||||||
LastModifiedUtc = DateTime.UtcNow
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -206,7 +203,7 @@ public class ReaderService : IReaderService
|
|||||||
/// <param name="user">Must have Progresses populated</param>
|
/// <param name="user">Must have Progresses populated</param>
|
||||||
/// <param name="chapter"></param>
|
/// <param name="chapter"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private static AppUserProgress? GetUserProgressForChapter(AppUser user, Chapter chapter)
|
private AppUserProgress? GetUserProgressForChapter(AppUser user, Chapter chapter)
|
||||||
{
|
{
|
||||||
AppUserProgress? userProgress = null;
|
AppUserProgress? userProgress = null;
|
||||||
|
|
||||||
@ -226,11 +223,12 @@ public class ReaderService : IReaderService
|
|||||||
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
||||||
if (progresses.Count > 1)
|
if (progresses.Count > 1)
|
||||||
{
|
{
|
||||||
user.Progresses = new List<AppUserProgress>
|
var highestProgress = progresses.Max(x => x.PagesRead);
|
||||||
{
|
var firstProgress = progresses.OrderBy(p => p.LastModifiedUtc).First();
|
||||||
user.Progresses.First()
|
firstProgress.PagesRead = highestProgress;
|
||||||
};
|
user.Progresses = [firstProgress];
|
||||||
userProgress = user.Progresses.First();
|
userProgress = user.Progresses.First();
|
||||||
|
_logger.LogInformation("Trying to save progress and multiple progress entries exist, deleting and rewriting with highest progress rate: {@Progress}", userProgress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,10 +272,6 @@ public class ReaderService : IReaderService
|
|||||||
ChapterId = progressDto.ChapterId,
|
ChapterId = progressDto.ChapterId,
|
||||||
LibraryId = progressDto.LibraryId,
|
LibraryId = progressDto.LibraryId,
|
||||||
BookScrollId = progressDto.BookScrollId,
|
BookScrollId = progressDto.BookScrollId,
|
||||||
Created = DateTime.Now,
|
|
||||||
CreatedUtc = DateTime.UtcNow,
|
|
||||||
LastModified = DateTime.Now,
|
|
||||||
LastModifiedUtc = DateTime.UtcNow
|
|
||||||
});
|
});
|
||||||
_unitOfWork.UserRepository.Update(userWithProgress);
|
_unitOfWork.UserRepository.Update(userWithProgress);
|
||||||
}
|
}
|
||||||
|
@ -674,6 +674,12 @@ public class ParseScannedFiles
|
|||||||
|
|
||||||
private static void RemapSeries(IList<ScanResult> scanResults, List<ParserInfo> allInfos, string localizedSeries, string nonLocalizedSeries)
|
private static void RemapSeries(IList<ScanResult> scanResults, List<ParserInfo> allInfos, string localizedSeries, string nonLocalizedSeries)
|
||||||
{
|
{
|
||||||
|
// If the series names are identical, no remapping is needed (rare but valid)
|
||||||
|
if (localizedSeries.ToNormalized().Equals(nonLocalizedSeries.ToNormalized()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Find all infos that need to be remapped from the localized series to the non-localized series
|
// Find all infos that need to be remapped from the localized series to the non-localized series
|
||||||
var normalizedLocalizedSeries = localizedSeries.ToNormalized();
|
var normalizedLocalizedSeries = localizedSeries.ToNormalized();
|
||||||
var seriesToBeRemapped = allInfos.Where(i => i.Series.ToNormalized().Equals(normalizedLocalizedSeries)).ToList();
|
var seriesToBeRemapped = allInfos.Where(i => i.Series.ToNormalized().Equals(normalizedLocalizedSeries)).ToList();
|
||||||
|
@ -109,6 +109,7 @@ export class ReaderService {
|
|||||||
return this.httpClient.post<PageBookmark[]>(this.baseUrl + 'reader/all-bookmarks', filter);
|
return this.httpClient.post<PageBookmark[]>(this.baseUrl + 'reader/all-bookmarks', filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getBookmarks(chapterId: number) {
|
getBookmarks(chapterId: number) {
|
||||||
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/chapter-bookmarks?chapterId=' + chapterId);
|
return this.httpClient.get<PageBookmark[]>(this.baseUrl + 'reader/chapter-bookmarks?chapterId=' + chapterId);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<ng-container *transloco="let t; read: 'related-tab'">
|
<ng-container *transloco="let t; read: 'related-tab'">
|
||||||
<div style="padding-bottom: 1rem;">
|
<div class="pb-2">
|
||||||
@if (relations.length > 0) {
|
@if (relations.length > 0) {
|
||||||
<app-carousel-reel [items]="relations" [title]="t('relations-title')">
|
<app-carousel-reel [items]="relations" [title]="t('relations-title')">
|
||||||
<ng-template #carouselItem let-item>
|
<ng-template #carouselItem let-item>
|
||||||
@ -30,5 +30,18 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (bookmarks.length > 0) {
|
||||||
|
<app-carousel-reel [items]="bookmarks" [title]="t('bookmarks-title')">
|
||||||
|
<ng-template #carouselItem let-item>
|
||||||
|
<app-card-item [entity]="item" [title]="t('bookmarks-title')" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
|
||||||
|
[suppressArchiveWarning]="true"
|
||||||
|
[linkUrl]="'/library/' + libraryId + '/series/' + item.seriesId + '/manga/0?bookmarkMode=true'"
|
||||||
|
(clicked)="viewBookmark(item)"
|
||||||
|
[count]="bookmarks.length"
|
||||||
|
[allowSelection]="false"></app-card-item>
|
||||||
|
</ng-template>
|
||||||
|
</app-carousel-reel>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core';
|
import {ChangeDetectionStrategy, Component, inject, Input, OnInit} from '@angular/core';
|
||||||
import {ReadingList} from "../../_models/reading-list";
|
import {ReadingList} from "../../_models/reading-list";
|
||||||
import {CardItemComponent} from "../../cards/card-item/card-item.component";
|
import {CardItemComponent} from "../../cards/card-item/card-item.component";
|
||||||
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
|
import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/carousel-reel.component";
|
||||||
@ -9,6 +9,7 @@ import {Router} from "@angular/router";
|
|||||||
import {SeriesCardComponent} from "../../cards/series-card/series-card.component";
|
import {SeriesCardComponent} from "../../cards/series-card/series-card.component";
|
||||||
import {Series} from "../../_models/series";
|
import {Series} from "../../_models/series";
|
||||||
import {RelationKind} from "../../_models/series-detail/relation-kind";
|
import {RelationKind} from "../../_models/series-detail/relation-kind";
|
||||||
|
import {PageBookmark} from "../../_models/readers/page-bookmark";
|
||||||
|
|
||||||
export interface RelatedSeriesPair {
|
export interface RelatedSeriesPair {
|
||||||
series: Series;
|
series: Series;
|
||||||
@ -28,7 +29,7 @@ export interface RelatedSeriesPair {
|
|||||||
styleUrl: './related-tab.component.scss',
|
styleUrl: './related-tab.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class RelatedTabComponent {
|
export class RelatedTabComponent implements OnInit {
|
||||||
|
|
||||||
protected readonly imageService = inject(ImageService);
|
protected readonly imageService = inject(ImageService);
|
||||||
protected readonly router = inject(Router);
|
protected readonly router = inject(Router);
|
||||||
@ -36,6 +37,12 @@ export class RelatedTabComponent {
|
|||||||
@Input() readingLists: Array<ReadingList> = [];
|
@Input() readingLists: Array<ReadingList> = [];
|
||||||
@Input() collections: Array<UserCollection> = [];
|
@Input() collections: Array<UserCollection> = [];
|
||||||
@Input() relations: Array<RelatedSeriesPair> = [];
|
@Input() relations: Array<RelatedSeriesPair> = [];
|
||||||
|
@Input() bookmarks: Array<PageBookmark> = [];
|
||||||
|
@Input() libraryId!: number;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
console.log('bookmarks: ', this.bookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
openReadingList(readingList: ReadingList) {
|
openReadingList(readingList: ReadingList) {
|
||||||
this.router.navigate(['lists', readingList.id]);
|
this.router.navigate(['lists', readingList.id]);
|
||||||
@ -45,4 +52,8 @@ export class RelatedTabComponent {
|
|||||||
this.router.navigate(['collections', collection.id]);
|
this.router.navigate(['collections', collection.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewBookmark(bookmark: PageBookmark) {
|
||||||
|
this.router.navigate(['library', this.libraryId, 'series', bookmark.seriesId, 'manga', 0], {queryParams: {incognitoMode: false, bookmarkMode: true}});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,18 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "FastFontSerif";
|
||||||
|
src: url(../../../../assets/fonts/Fast_Font/Fast_Serif.woff2) format("woff2");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "FastFontSans";
|
||||||
|
src: url(../../../../assets/fonts/Fast_Font/Fast_Sans.woff2) format("woff2");
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--br-actionbar-button-text-color: #6c757d;
|
--br-actionbar-button-text-color: #6c757d;
|
||||||
--accordion-body-bg-color: black;
|
--accordion-body-bg-color: black;
|
||||||
|
@ -103,7 +103,7 @@ export const BookWhiteTheme = `
|
|||||||
|
|
||||||
|
|
||||||
.book-content *:not(input), .book-content *:not(select), .book-content *:not(code), .book-content *:not(:link), .book-content *:not(.ngx-toastr) {
|
.book-content *:not(input), .book-content *:not(select), .book-content *:not(code), .book-content *:not(:link), .book-content *:not(.ngx-toastr) {
|
||||||
color: black !important;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-content code {
|
.book-content code {
|
||||||
@ -125,7 +125,7 @@ export const BookWhiteTheme = `
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
border-radius: unset;
|
border-radius: unset;
|
||||||
color: #dcdcdc !important;
|
color: #dcdcdc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {
|
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {
|
||||||
|
@ -28,7 +28,7 @@ export class BookService {
|
|||||||
getFontFamilies(): Array<FontFamily> {
|
getFontFamilies(): Array<FontFamily> {
|
||||||
return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'},
|
return [{title: 'default', family: 'default'}, {title: 'EBGaramond', family: 'EBGaramond'}, {title: 'Fira Sans', family: 'Fira_Sans'},
|
||||||
{title: 'Lato', family: 'Lato'}, {title: 'Libre Baskerville', family: 'Libre_Baskerville'}, {title: 'Merriweather', family: 'Merriweather'},
|
{title: 'Lato', family: 'Lato'}, {title: 'Libre Baskerville', family: 'Libre_Baskerville'}, {title: 'Merriweather', family: 'Merriweather'},
|
||||||
{title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}];
|
{title: 'Nanum Gothic', family: 'Nanum_Gothic'}, {title: 'Open Dyslexic', family: 'OpenDyslexic2'}, {title: 'RocknRoll One', family: 'RocknRoll_One'}, {title: 'Fast Font Serif (Bionic)', family: 'FastFontSerif'}, {title: 'Fast Font Sans (Bionic)', family: 'FastFontSans'}];
|
||||||
}
|
}
|
||||||
|
|
||||||
getBookChapters(chapterId: number) {
|
getBookChapters(chapterId: number) {
|
||||||
|
@ -7,27 +7,29 @@
|
|||||||
<h5 subtitle>{{t('series-count', {num: series.length | number})}}</h5>
|
<h5 subtitle>{{t('series-count', {num: series.length | number})}}</h5>
|
||||||
</app-side-nav-companion-bar>
|
</app-side-nav-companion-bar>
|
||||||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||||
<app-card-detail-layout *ngIf="filter"
|
@if (filter) {
|
||||||
[isLoading]="loadingBookmarks"
|
<app-card-detail-layout
|
||||||
[items]="series"
|
[isLoading]="loadingBookmarks"
|
||||||
[filterSettings]="filterSettings"
|
[items]="series"
|
||||||
[trackByIdentity]="trackByIdentity"
|
[filterSettings]="filterSettings"
|
||||||
[refresh]="refresh"
|
[trackByIdentity]="trackByIdentity"
|
||||||
[jumpBarKeys]="jumpbarKeys"
|
[refresh]="refresh"
|
||||||
(applyFilter)="updateFilter($event)"
|
[jumpBarKeys]="jumpbarKeys"
|
||||||
>
|
(applyFilter)="updateFilter($event)"
|
||||||
<ng-template #cardItem let-item let-position="idx">
|
>
|
||||||
<app-card-item [entity]="item" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
[suppressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
|
<app-card-item [entity]="item" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
|
||||||
[actions]="actions"
|
[suppressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
|
||||||
[selected]="bulkSelectionService.isCardSelected('bookmark', position)"
|
[actions]="actions"
|
||||||
(selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
|
[selected]="bulkSelectionService.isCardSelected('bookmark', position)"
|
||||||
></app-card-item>
|
(selection)="bulkSelectionService.handleCardSelection('bookmark', position, series.length, $event)"
|
||||||
</ng-template>
|
></app-card-item>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #noData>
|
<ng-template #noData>
|
||||||
{{t('no-data')}} <a [href]="WikiLink.Bookmarks" rel="noopener noreferrer" target="_blank">{{t('no-data-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
|
{{t('no-data')}} <a [href]="WikiLink.Bookmarks" rel="noopener noreferrer" target="_blank">{{t('no-data-2')}}<i class="fa fa-external-link-alt ms-1" aria-hidden="true"></i></a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-card-detail-layout>
|
</app-card-detail-layout>
|
||||||
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,7 +15,6 @@ import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
|||||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||||
import {DownloadService} from 'src/app/shared/_services/download.service';
|
import {DownloadService} from 'src/app/shared/_services/download.service';
|
||||||
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
|
import { FilterUtilitiesService } from 'src/app/shared/_services/filter-utilities.service';
|
||||||
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
|
||||||
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||||
import { PageBookmark } from 'src/app/_models/readers/page-bookmark';
|
import { PageBookmark } from 'src/app/_models/readers/page-bookmark';
|
||||||
import { Pagination } from 'src/app/_models/pagination';
|
import { Pagination } from 'src/app/_models/pagination';
|
||||||
@ -103,13 +102,13 @@ export class BookmarksComponent implements OnInit {
|
|||||||
async handleAction(action: ActionItem<Series>, series: Series) {
|
async handleAction(action: ActionItem<Series>, series: Series) {
|
||||||
switch (action.action) {
|
switch (action.action) {
|
||||||
case(Action.Delete):
|
case(Action.Delete):
|
||||||
this.clearBookmarks(series);
|
await this.clearBookmarks(series);
|
||||||
break;
|
break;
|
||||||
case(Action.DownloadBookmark):
|
case(Action.DownloadBookmark):
|
||||||
this.downloadBookmarks(series);
|
this.downloadBookmarks(series);
|
||||||
break;
|
break;
|
||||||
case(Action.ViewSeries):
|
case(Action.ViewSeries):
|
||||||
this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
await this.router.navigate(['library', series.libraryId, 'series', series.id]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="progress-banner">
|
<div class="progress-banner">
|
||||||
@if (read > 0 && read < total && total > 0 && read !== total) {
|
@if (read > 0 && read < total && total > 0 && read !== total) {
|
||||||
<p ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read">
|
<p ngbTooltip="{{((read / total) * 100) | number:'1.0-1'}}% Read" container="body">
|
||||||
<ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar>
|
<ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="progress-banner">
|
<div class="progress-banner">
|
||||||
@if (chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages && chapter.pages > 0 && chapter.pagesRead !== chapter.pages) {
|
@if (chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages && chapter.pages > 0 && chapter.pagesRead !== chapter.pages) {
|
||||||
<p ngbTooltip="{{((chapter.pagesRead / chapter.pages) * 100) | number:'1.0-1'}}% Read">
|
<p ngbTooltip="{{((chapter.pagesRead / chapter.pages) * 100) | number:'1.0-1'}}% Read" container="body">
|
||||||
<ngb-progressbar type="primary" height="5px" [value]="chapter.pagesRead" [max]="chapter.pages"></ngb-progressbar>
|
<ngb-progressbar type="primary" height="5px" [value]="chapter.pagesRead" [max]="chapter.pages"></ngb-progressbar>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (chapter.files.length > 1) {
|
@if (chapter.files.length > 1 && chapter.files[0].format !== MangaFormat.IMAGE) {
|
||||||
<div class="count">
|
<div class="count">
|
||||||
<span class="badge bg-primary">{{chapter.files.length}}</span>
|
<span class="badge bg-primary">{{chapter.files.length}}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,6 +35,7 @@ import {ReaderService} from "../../_services/reader.service";
|
|||||||
import {LibraryType} from "../../_models/library/library";
|
import {LibraryType} from "../../_models/library/library";
|
||||||
import {Device} from "../../_models/device/device";
|
import {Device} from "../../_models/device/device";
|
||||||
import {ActionService} from "../../_services/action.service";
|
import {ActionService} from "../../_services/action.service";
|
||||||
|
import {MangaFormat} from "../../_models/manga-format";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-chapter-card',
|
selector: 'app-chapter-card',
|
||||||
@ -49,8 +50,7 @@ import {ActionService} from "../../_services/action.service";
|
|||||||
EntityTitleComponent,
|
EntityTitleComponent,
|
||||||
CardActionablesComponent,
|
CardActionablesComponent,
|
||||||
RouterLink,
|
RouterLink,
|
||||||
TranslocoDirective,
|
TranslocoDirective
|
||||||
DefaultValuePipe
|
|
||||||
],
|
],
|
||||||
templateUrl: './chapter-card.component.html',
|
templateUrl: './chapter-card.component.html',
|
||||||
styleUrl: './chapter-card.component.scss',
|
styleUrl: './chapter-card.component.scss',
|
||||||
@ -213,4 +213,5 @@ export class ChapterCardComponent implements OnInit {
|
|||||||
|
|
||||||
|
|
||||||
protected readonly LibraryType = LibraryType;
|
protected readonly LibraryType = LibraryType;
|
||||||
|
protected readonly MangaFormat = MangaFormat;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="progress-banner">
|
<div class="progress-banner">
|
||||||
@if (series.pagesRead > 0 && series.pagesRead < series.pages && series.pages > 0 && series.pagesRead !== series.pages) {
|
@if (series.pagesRead > 0 && series.pagesRead < series.pages && series.pages > 0 && series.pagesRead !== series.pages) {
|
||||||
<p ngbTooltip="{{((series.pagesRead / series.pages) * 100) | number:'1.0-1'}}%">
|
<p ngbTooltip="{{((series.pagesRead / series.pages) * 100) | number:'1.0-1'}}%" container="body">
|
||||||
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
|
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
<div class="progress-banner">
|
<div class="progress-banner">
|
||||||
@if (volume.pagesRead > 0 && volume.pagesRead < volume.pages && volume.pages > 0 && volume.pagesRead !== volume.pages) {
|
@if (volume.pagesRead > 0 && volume.pagesRead < volume.pages && volume.pages > 0 && volume.pagesRead !== volume.pages) {
|
||||||
<p ngbTooltip="{{((volume.pagesRead / volume.pages) * 100) | number:'1.0-1'}}% Read">
|
<p ngbTooltip="{{((volume.pagesRead / volume.pages) * 100) | number:'1.0-1'}}% Read" container="body">
|
||||||
<ngb-progressbar type="primary" height="5px" [value]="volume.pagesRead" [max]="volume.pages"></ngb-progressbar>
|
<ngb-progressbar type="primary" height="5px" [value]="volume.pagesRead" [max]="volume.pages"></ngb-progressbar>
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
@ -199,7 +199,9 @@
|
|||||||
<div class="{{SplitIconClass}}"></div>
|
<div class="{{SplitIconClass}}"></div>
|
||||||
</div>
|
</div>
|
||||||
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
|
<select class="form-control" id="page-splitting" formControlName="pageSplitOption">
|
||||||
<option *ngFor="let opt of pageSplitOptionsTranslated" [value]="opt.value">{{opt.text}}</option>
|
@for (opt of pageSplitOptionsTranslated; track opt.value) {
|
||||||
|
<option [value]="opt.value">{{opt.text}}</option>
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -216,42 +218,45 @@
|
|||||||
<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">
|
||||||
<label for="layout-mode" class="form-label">Layout Mode</label>
|
<label for="layout-mode" class="form-label">Layout Mode</label>
|
||||||
<ng-container [ngSwitch]="layoutMode">
|
@switch (layoutMode) {
|
||||||
<ng-container *ngSwitchCase="LayoutMode.Single">
|
@case (LayoutMode.Single) {
|
||||||
<div class="split-double">
|
<div class="split-double">
|
||||||
<span class="fa-stack fa-1x">
|
<span class="fa-stack fa-1x">
|
||||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||||
<i class="fa fa-image fa-stack-1x"></i>
|
<i class="fa fa-image fa-stack-1x"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngSwitchCase="LayoutMode.Double">
|
@case (LayoutMode.Double) {
|
||||||
<div class="split-double">
|
<div class="split-double">
|
||||||
<span class="fa-stack fa-1x">
|
<span class="fa-stack fa-1x">
|
||||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||||
<i class="fab fa-1 fa-stack-1x"></i>
|
<i class="fab fa-1 fa-stack-1x"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="fa-stack fa right">
|
<span class="fa-stack fa right">
|
||||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||||
<i class="fab fa-2 fa-stack-1x"></i>
|
<i class="fab fa-2 fa-stack-1x"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngSwitchCase="LayoutMode.DoubleReversed">
|
@case (LayoutMode.DoubleReversed) {
|
||||||
<div class="split-double">
|
<div class="split-double">
|
||||||
<span class="fa-stack fa-1x">
|
<span class="fa-stack fa-1x">
|
||||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||||
<i class="fab fa-2 fa-stack-1x"></i>
|
<i class="fab fa-2 fa-stack-1x"></i>
|
||||||
</span>
|
</span>
|
||||||
<span class="fa-stack fa right">
|
<span class="fa-stack fa right">
|
||||||
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
<i class="fa-regular fa-square-full fa-stack-2x"></i>
|
||||||
<i class="fab fa-1 fa-stack-1x"></i>
|
<i class="fab fa-1 fa-stack-1x"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
}
|
||||||
</ng-container>
|
}
|
||||||
|
|
||||||
<select class="form-control" id="layout-mode" formControlName="layoutMode">
|
<select class="form-control" id="layout-mode" formControlName="layoutMode">
|
||||||
<option [value]="opt.value" *ngFor="let opt of layoutModesTranslated">{{opt.text}}</option>
|
@for (opt of layoutModesTranslated; track opt.value) {
|
||||||
|
<option [value]="opt.value">{{opt.text}}</option>
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-sm-12">
|
<div class="col-md-3 col-sm-12">
|
||||||
|
@ -8,12 +8,11 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
HostListener,
|
HostListener,
|
||||||
inject,
|
inject,
|
||||||
Inject,
|
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {AsyncPipe, DOCUMENT, NgClass, NgFor, NgStyle, NgSwitch, NgSwitchCase, PercentPipe} from '@angular/common';
|
import {AsyncPipe, NgClass, NgStyle, PercentPipe} from '@angular/common';
|
||||||
import {ActivatedRoute, Router} from '@angular/router';
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
@ -33,7 +32,7 @@ import {
|
|||||||
import {ChangeContext, LabelType, NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
import {ChangeContext, LabelType, NgxSliderModule, Options} from '@angular-slider/ngx-slider';
|
||||||
import {animate, state, style, transition, trigger} from '@angular/animations';
|
import {animate, state, style, transition, trigger} from '@angular/animations';
|
||||||
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
|
||||||
import {NgbModal, NgbProgressbar} from '@ng-bootstrap/ng-bootstrap';
|
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {ToastrService} from 'ngx-toastr';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import {ShortcutsModalComponent} from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component';
|
import {ShortcutsModalComponent} from 'src/app/reader-shared/_modals/shortcuts-modal/shortcuts-modal.component';
|
||||||
import {Stack} from 'src/app/shared/data-structures/stack';
|
import {Stack} from 'src/app/shared/data-structures/stack';
|
||||||
@ -126,7 +125,7 @@ enum KeyDirection {
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgStyle, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
|
imports: [NgStyle, LoadingComponent, SwipeDirective, CanvasRendererComponent, SingleRendererComponent,
|
||||||
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
|
DoubleRendererComponent, DoubleReverseRendererComponent, DoubleNoCoverRendererComponent, InfiniteScrollerComponent,
|
||||||
NgxSliderModule, ReactiveFormsModule, NgFor, NgSwitch, NgSwitchCase, FittingIconPipe, ReaderModeIconPipe,
|
NgxSliderModule, ReactiveFormsModule, FittingIconPipe, ReaderModeIconPipe,
|
||||||
FullscreenIconPipe, TranslocoDirective, PercentPipe, NgClass, AsyncPipe, DblClickDirective]
|
FullscreenIconPipe, TranslocoDirective, PercentPipe, NgClass, AsyncPipe, DblClickDirective]
|
||||||
})
|
})
|
||||||
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
@ -275,7 +274,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
step: 1,
|
step: 1,
|
||||||
boundPointerLabels: true,
|
boundPointerLabels: true,
|
||||||
showSelectionBar: true,
|
showSelectionBar: true,
|
||||||
translate: (value: number, label: LabelType) => {
|
translate: (_: number, label: LabelType) => {
|
||||||
if (label == LabelType.Floor) {
|
if (label == LabelType.Floor) {
|
||||||
return 1 + '';
|
return 1 + '';
|
||||||
} else if (label === LabelType.Ceil) {
|
} else if (label === LabelType.Ceil) {
|
||||||
@ -467,7 +466,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
constructor(@Inject(DOCUMENT) private document: Document) {
|
constructor() {
|
||||||
this.navService.hideNavBar();
|
this.navService.hideNavBar();
|
||||||
this.navService.hideSideNav();
|
this.navService.hideSideNav();
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
@ -784,6 +783,17 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
return pageNum;
|
return pageNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switchToWebtoonReaderIfPagesLikelyWebtoon() {
|
||||||
|
if (this.readerMode === ReaderMode.Webtoon) return;
|
||||||
|
|
||||||
|
if (this.mangaReaderService.shouldBeWebtoonMode()) {
|
||||||
|
this.readerMode = ReaderMode.Webtoon;
|
||||||
|
this.toastr.info(translate('manga-reader.webtoon-override'));
|
||||||
|
this.readerModeSubject.next(this.readerMode);
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
disableDoubleRendererIfScreenTooSmall() {
|
disableDoubleRendererIfScreenTooSmall() {
|
||||||
if (window.innerWidth > window.innerHeight) {
|
if (window.innerWidth > window.innerHeight) {
|
||||||
this.generalSettingsForm.get('layoutMode')?.enable();
|
this.generalSettingsForm.get('layoutMode')?.enable();
|
||||||
@ -991,6 +1001,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.inSetup = false;
|
this.inSetup = false;
|
||||||
|
|
||||||
this.disableDoubleRendererIfScreenTooSmall();
|
this.disableDoubleRendererIfScreenTooSmall();
|
||||||
|
this.switchToWebtoonReaderIfPagesLikelyWebtoon();
|
||||||
|
|
||||||
|
|
||||||
// From bookmarks, create map of pages to make lookup time O(1)
|
// From bookmarks, create map of pages to make lookup time O(1)
|
||||||
|
@ -6,6 +6,7 @@ import { ChapterInfo } from '../_models/chapter-info';
|
|||||||
import { DimensionMap } from '../_models/file-dimension';
|
import { DimensionMap } from '../_models/file-dimension';
|
||||||
import { FITTING_OPTION } from '../_models/reader-enums';
|
import { FITTING_OPTION } from '../_models/reader-enums';
|
||||||
import { BookmarkInfo } from 'src/app/_models/manga-reader/bookmark-info';
|
import { BookmarkInfo } from 'src/app/_models/manga-reader/bookmark-info';
|
||||||
|
import {ReaderMode} from "../../_models/preferences/reader-mode";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -150,6 +151,35 @@ export class ManagaReaderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the page dimensions are all "webtoon-like", then reader mode will be converted for the user
|
||||||
|
*/
|
||||||
|
shouldBeWebtoonMode() {
|
||||||
|
const pages = Object.values(this.pageDimensions);
|
||||||
|
|
||||||
|
let webtoonScore = 0;
|
||||||
|
pages.forEach(info => {
|
||||||
|
const aspectRatio = info.height / info.width;
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
// Strong webtoon indicator: If aspect ratio is at least 2:1
|
||||||
|
if (aspectRatio >= 2) {
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boost score if width is small (≤ 800px, common in webtoons)
|
||||||
|
if (info.width <= 800) {
|
||||||
|
score += 0.5; // Adjust weight as needed
|
||||||
|
}
|
||||||
|
|
||||||
|
webtoonScore += score;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// If at least 50% of the pages fit the webtoon criteria, switch to Webtoon mode.
|
||||||
|
return webtoonScore / pages.length >= 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
applyBookmarkEffect(elements: Array<Element | ElementRef>) {
|
applyBookmarkEffect(elements: Array<Element | ElementRef>) {
|
||||||
if (elements.length > 0) {
|
if (elements.length > 0) {
|
||||||
@ -160,7 +190,4 @@ export class ManagaReaderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -266,15 +266,19 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (hasRelations || readingLists.length > 0 || collections.length > 0) {
|
@if (hasRelations || readingLists.length > 0 || collections.length > 0 || bookmarks.length > 0) {
|
||||||
<li [ngbNavItem]="TabID.Related">
|
<li [ngbNavItem]="TabID.Related">
|
||||||
<a ngbNavLink>
|
<a ngbNavLink>
|
||||||
{{t(TabID.Related)}}
|
{{t(TabID.Related)}}
|
||||||
<span class="badge rounded-pill text-bg-secondary">{{relations.length + readingLists.length + collections.length}}</span>
|
<span class="badge rounded-pill text-bg-secondary">{{relations.length + readingLists.length + collections.length + (bookmarks.length > 0 ? 1 : 0)}}</span>
|
||||||
</a>
|
</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
@defer (when activeTabId === TabID.Related; prefetch on idle) {
|
@defer (when activeTabId === TabID.Related; prefetch on idle) {
|
||||||
<app-related-tab [readingLists]="readingLists" [collections]="collections" [relations]="relations"></app-related-tab>
|
<app-related-tab [readingLists]="readingLists"
|
||||||
|
[collections]="collections"
|
||||||
|
[relations]="relations"
|
||||||
|
[libraryId]="libraryId"
|
||||||
|
[bookmarks]="bookmarks"></app-related-tab>
|
||||||
}
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
import {
|
import {AsyncPipe, DOCUMENT, Location, NgClass, NgStyle, NgTemplateOutlet} from '@angular/common';
|
||||||
AsyncPipe,
|
|
||||||
DOCUMENT,
|
|
||||||
Location,
|
|
||||||
NgClass,
|
|
||||||
NgStyle,
|
|
||||||
NgTemplateOutlet
|
|
||||||
} from '@angular/common';
|
|
||||||
import {
|
import {
|
||||||
AfterContentChecked,
|
AfterContentChecked,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@ -121,7 +114,7 @@ import {UserCollection} from "../../../_models/collection-tag";
|
|||||||
import {CoverImageComponent} from "../../../_single-module/cover-image/cover-image.component";
|
import {CoverImageComponent} from "../../../_single-module/cover-image/cover-image.component";
|
||||||
import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
import {DefaultModalOptions} from "../../../_models/default-modal-options";
|
||||||
import {LicenseService} from "../../../_services/license.service";
|
import {LicenseService} from "../../../_services/license.service";
|
||||||
|
import {PageBookmark} from "../../../_models/readers/page-bookmark";
|
||||||
|
|
||||||
|
|
||||||
enum TabID {
|
enum TabID {
|
||||||
@ -233,6 +226,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
|
|
||||||
reviews: Array<UserReview> = [];
|
reviews: Array<UserReview> = [];
|
||||||
plusReviews: Array<UserReview> = [];
|
plusReviews: Array<UserReview> = [];
|
||||||
|
bookmarks: Array<PageBookmark> = [];
|
||||||
ratings: Array<Rating> = [];
|
ratings: Array<Rating> = [];
|
||||||
libraryType: LibraryType = LibraryType.Manga;
|
libraryType: LibraryType = LibraryType.Manga;
|
||||||
seriesMetadata: SeriesMetadata | null = null;
|
seriesMetadata: SeriesMetadata | null = null;
|
||||||
@ -712,7 +706,24 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
|
|||||||
this.collectionTagService.allCollectionsForSeries(seriesId, false).subscribe(tags => {
|
this.collectionTagService.allCollectionsForSeries(seriesId, false).subscribe(tags => {
|
||||||
this.collections = tags;
|
this.collections = tags;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
})
|
});
|
||||||
|
|
||||||
|
|
||||||
|
this.readerService.getBookmarksForSeries(seriesId).subscribe(bookmarks => {
|
||||||
|
if (bookmarks.length > 0) {
|
||||||
|
this.bookmarks = Object.values(
|
||||||
|
bookmarks.reduce((acc, bookmark) => {
|
||||||
|
if (!acc[bookmark.seriesId]) {
|
||||||
|
acc[bookmark.seriesId] = bookmark; // Select the first one per seriesId
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, PageBookmark>)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.bookmarks = [];
|
||||||
|
}
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
this.readerService.getTimeLeft(seriesId).subscribe((timeLeft) => {
|
this.readerService.getTimeLeft(seriesId).subscribe((timeLeft) => {
|
||||||
this.readingTimeLeft = timeLeft;
|
this.readingTimeLeft = timeLeft;
|
||||||
|
BIN
UI/Web/src/assets/fonts/Fast_Font/Fast_Sans.woff2
Normal file
BIN
UI/Web/src/assets/fonts/Fast_Font/Fast_Sans.woff2
Normal file
Binary file not shown.
BIN
UI/Web/src/assets/fonts/Fast_Font/Fast_Serif.woff2
Normal file
BIN
UI/Web/src/assets/fonts/Fast_Font/Fast_Serif.woff2
Normal file
Binary file not shown.
@ -1243,7 +1243,8 @@
|
|||||||
"related-tab": {
|
"related-tab": {
|
||||||
"reading-lists-title": "{{reading-lists.title}}",
|
"reading-lists-title": "{{reading-lists.title}}",
|
||||||
"collections-title": "{{side-nav.collections}}",
|
"collections-title": "{{side-nav.collections}}",
|
||||||
"relations-title": "{{tabs.related-tab}}"
|
"relations-title": "{{tabs.related-tab}}",
|
||||||
|
"bookmarks-title": "{{side-nav.bookmarks}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"cover-image-chooser": {
|
"cover-image-chooser": {
|
||||||
@ -2613,7 +2614,8 @@
|
|||||||
"bulk-covers": "Refreshing covers on multiple libraries is intensive and can take a long time. Are you sure you want to continue?",
|
"bulk-covers": "Refreshing covers on multiple libraries is intensive and can take a long time. Are you sure you want to continue?",
|
||||||
"person-image-downloaded": "Person cover was downloaded and applied.",
|
"person-image-downloaded": "Person cover was downloaded and applied.",
|
||||||
"bulk-delete-libraries": "Are you sure you want to delete {{count}} libraries?",
|
"bulk-delete-libraries": "Are you sure you want to delete {{count}} libraries?",
|
||||||
"match-success": "Series matched correctly"
|
"match-success": "Series matched correctly",
|
||||||
|
"webtoon-override": "Switching to Webtoon mode due to images representing a webtoon."
|
||||||
},
|
},
|
||||||
|
|
||||||
"read-time-pipe": {
|
"read-time-pipe": {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.0.1",
|
"openapi": "3.0.1",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Kavita",
|
"title": "Kavita",
|
||||||
"description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.4.13",
|
"description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.4.14",
|
||||||
"license": {
|
"license": {
|
||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user