mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-12-23 05:17:22 -05:00
KOReader Progress Sync Fix (again) and OPDS Settings (#4174)
Co-authored-by: Fesaa <77553571+Fesaa@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
parent
4b2bcda4a0
commit
23e91b51d9
@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using API.Data.Misc;
|
using API.Data.Misc;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Enums.UserPreferences;
|
||||||
using API.Entities.Metadata;
|
using API.Entities.Metadata;
|
||||||
using API.Entities.Person;
|
using API.Entities.Person;
|
||||||
using API.Extensions.QueryExtensions;
|
using API.Extensions.QueryExtensions;
|
||||||
|
|||||||
@ -13,30 +13,44 @@ public class KoreaderHelperTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("/body/DocFragment[11]/body/div/a", 10, null)]
|
[InlineData("/body/DocFragment[11]/body/div/a", 10, null)]
|
||||||
[InlineData("/body/DocFragment[1]/body/div/p[40]", 0, 40)]
|
[InlineData("/body/DocFragment[1]/body/div/p[40]", 0, 40)]
|
||||||
[InlineData("/body/DocFragment[8]/body/div/p[28]/text().264", 7, 28)]
|
|
||||||
public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber)
|
public void GetEpubPositionDto(string koreaderPosition, int page, int? pNumber)
|
||||||
{
|
{
|
||||||
var expected = EmptyProgressDto();
|
var expected = EmptyProgressDto();
|
||||||
expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[{pNumber}]" : null;
|
expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/DIV/P[{pNumber}]" : null;
|
||||||
expected.PageNum = page;
|
expected.PageNum = page;
|
||||||
var actual = EmptyProgressDto();
|
var actual = EmptyProgressDto();
|
||||||
|
|
||||||
KoreaderHelper.UpdateProgressDto(actual, koreaderPosition);
|
KoreaderHelper.UpdateProgressDto(actual, koreaderPosition);
|
||||||
Assert.Equal(expected.BookScrollId, actual.BookScrollId);
|
Assert.Equal(expected.BookScrollId?.ToLowerInvariant(), actual.BookScrollId);
|
||||||
|
Assert.Equal(expected.PageNum, actual.PageNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("/body/DocFragment[8]/body/div/p[28]/text().264", 7, 28)]
|
||||||
|
public void GetEpubPositionDtoWithExtraXpath(string koreaderPosition, int page, int? pNumber)
|
||||||
|
{
|
||||||
|
var expected = EmptyProgressDto();
|
||||||
|
expected.BookScrollId = pNumber.HasValue ? $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/DIV/P[{pNumber}]/text().264" : null;
|
||||||
|
expected.PageNum = page;
|
||||||
|
var actual = EmptyProgressDto();
|
||||||
|
|
||||||
|
KoreaderHelper.UpdateProgressDto(actual, koreaderPosition);
|
||||||
|
Assert.Equal(expected.BookScrollId?.ToLowerInvariant(), actual.BookScrollId);
|
||||||
Assert.Equal(expected.PageNum, actual.PageNum);
|
Assert.Equal(expected.PageNum, actual.PageNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[20]", 5, "/body/DocFragment[6]/body/div/p[20]")]
|
[InlineData("//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/P[20]", 5, "/body/DocFragment[6]/body/p[20]")]
|
||||||
[InlineData(null, 10, "/body/DocFragment[11]/body/div/a")]
|
[InlineData(null, 10, "/body/DocFragment[11]/body/a")] // I've not seen a null/just an a from Koreader in testing
|
||||||
public void GetKoreaderPosition(string scrollId, int page, string koreaderPosition)
|
public void GetKoreaderPosition(string scrollId, int page, string koreaderPosition)
|
||||||
{
|
{
|
||||||
var given = EmptyProgressDto();
|
var given = EmptyProgressDto();
|
||||||
given.BookScrollId = scrollId;
|
given.BookScrollId = scrollId;
|
||||||
given.PageNum = page;
|
given.PageNum = page;
|
||||||
|
|
||||||
Assert.Equal(koreaderPosition, KoreaderHelper.GetKoreaderPosition(given));
|
Assert.Equal(koreaderPosition.ToUpperInvariant(), KoreaderHelper.GetKoreaderPosition(given).ToUpperInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -46,7 +60,7 @@ public class KoreaderHelperTests
|
|||||||
Assert.Equal(KoreaderHelper.HashContents(filePath), hash);
|
Assert.Equal(KoreaderHelper.HashContents(filePath), hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ProgressDto EmptyProgressDto()
|
private static ProgressDto EmptyProgressDto()
|
||||||
{
|
{
|
||||||
return new ProgressDto
|
return new ProgressDto
|
||||||
{
|
{
|
||||||
|
|||||||
@ -241,6 +241,37 @@ public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTe
|
|||||||
Assert.StartsWith("Continue Reading from", feed.Entries.First().Title);
|
Assert.StartsWith("Continue Reading from", feed.Entries.First().Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ContinuePoint_WithProgress_NotEnabled()
|
||||||
|
{
|
||||||
|
var (unitOfWork, context, mapper) = await CreateDatabase();
|
||||||
|
var (opdsService, readerService) = SetupService(unitOfWork, mapper);
|
||||||
|
var user = await SetupSeriesAndUser(context, unitOfWork);
|
||||||
|
|
||||||
|
// Disable Continue Point
|
||||||
|
var user2 = await unitOfWork.UserRepository.GetUserByIdAsync(user.Id, AppUserIncludes.UserPreferences);
|
||||||
|
user2.UserPreferences.OpdsPreferences.IncludeContinueFrom = false;
|
||||||
|
unitOfWork.UserRepository.Update(user2);
|
||||||
|
await unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1);
|
||||||
|
await readerService.MarkChaptersAsRead(user, 1, [firstChapter]);
|
||||||
|
await unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest
|
||||||
|
{
|
||||||
|
ApiKey = user.ApiKey,
|
||||||
|
Prefix = OpdsService.DefaultApiPrefix,
|
||||||
|
BaseUrl = string.Empty,
|
||||||
|
UserId = user.Id,
|
||||||
|
EntityId = 1,
|
||||||
|
PageNumber = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(2, feed.Entries.Count);
|
||||||
|
Assert.False(feed.Entries.First().Title.StartsWith("Continue Reading from"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ContinuePoint_DoesntExist_WhenNoProgress()
|
public async Task ContinuePoint_DoesntExist_WhenNoProgress()
|
||||||
{
|
{
|
||||||
@ -309,6 +340,48 @@ public class OpdsServiceTests(ITestOutputHelper testOutputHelper) : AbstractDbTe
|
|||||||
Assert.Contains(expectedIcon, feed.Entries[entryIndex].Title);
|
Assert.Contains(expectedIcon, feed.Entries[entryIndex].Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadingIcon_NotEnabled()
|
||||||
|
{
|
||||||
|
var (unitOfWork, context, mapper) = await CreateDatabase();
|
||||||
|
var (opdsService, readerService) = SetupService(unitOfWork, mapper);
|
||||||
|
var user = await SetupSeriesAndUser(context, unitOfWork);
|
||||||
|
|
||||||
|
// Disable Continue Point
|
||||||
|
var user2 = await unitOfWork.UserRepository.GetUserByIdAsync(user.Id, AppUserIncludes.UserPreferences);
|
||||||
|
user2.UserPreferences.OpdsPreferences.EmbedProgressIndicator = false;
|
||||||
|
unitOfWork.UserRepository.Update(user2);
|
||||||
|
await unitOfWork.CommitAsync();
|
||||||
|
|
||||||
|
var firstChapter = await unitOfWork.ChapterRepository.GetChapterAsync(1);
|
||||||
|
Assert.NotNull(firstChapter);
|
||||||
|
|
||||||
|
await readerService.SaveReadingProgress(new ProgressDto
|
||||||
|
{
|
||||||
|
VolumeId = firstChapter.VolumeId,
|
||||||
|
ChapterId = firstChapter.Id,
|
||||||
|
PageNum = 2,
|
||||||
|
SeriesId = 1,
|
||||||
|
LibraryId = 1,
|
||||||
|
BookScrollId = null,
|
||||||
|
LastModifiedUtc = default
|
||||||
|
}, user.Id);
|
||||||
|
|
||||||
|
var feed = await opdsService.GetSeriesDetail(new OpdsItemsFromEntityIdRequest
|
||||||
|
{
|
||||||
|
ApiKey = user.ApiKey,
|
||||||
|
Prefix = OpdsService.DefaultApiPrefix,
|
||||||
|
BaseUrl = string.Empty,
|
||||||
|
UserId = user.Id,
|
||||||
|
EntityId = 1,
|
||||||
|
PageNumber = 0
|
||||||
|
});
|
||||||
|
|
||||||
|
List<string> icons = [OpdsService.NoReadingProgressIcon, OpdsService.QuarterReadingProgressIcon, OpdsService.HalfReadingProgressIcon, OpdsService.AboveHalfReadingProgressIcon, OpdsService.FullReadingProgressIcon];
|
||||||
|
Assert.NotEmpty(feed.Entries);
|
||||||
|
Assert.DoesNotContain(feed.Entries, e => icons.Any(icon => e.Title.Contains(icon)));
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Misc
|
#region Misc
|
||||||
|
|||||||
@ -130,6 +130,8 @@ public class UsersController : BaseApiController
|
|||||||
.Where(l => allLibs.Contains(l)).ToList();
|
.Where(l => allLibs.Contains(l)).ToList();
|
||||||
existingPreferences.SocialPreferences = preferencesDto.SocialPreferences;
|
existingPreferences.SocialPreferences = preferencesDto.SocialPreferences;
|
||||||
|
|
||||||
|
existingPreferences.OpdsPreferences = preferencesDto.OpdsPreferences;
|
||||||
|
|
||||||
if (await _licenseService.HasActiveLicense())
|
if (await _licenseService.HasActiveLicense())
|
||||||
{
|
{
|
||||||
existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled;
|
existingPreferences.AniListScrobblingEnabled = preferencesDto.AniListScrobblingEnabled;
|
||||||
|
|||||||
@ -55,7 +55,12 @@ public sealed record UserPreferencesDto
|
|||||||
#region Social
|
#region Social
|
||||||
|
|
||||||
/// <inheritdoc cref="AppUserPreferences.SocialPreferences"/>
|
/// <inheritdoc cref="AppUserPreferences.SocialPreferences"/>
|
||||||
|
[Required]
|
||||||
public AppUserSocialPreferences SocialPreferences { get; set; } = new();
|
public AppUserSocialPreferences SocialPreferences { get; set; } = new();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <inheritdoc cref="AppUserPreferences.OpdsPreferences"/>
|
||||||
|
[Required]
|
||||||
|
public AppUserOpdsPreferences OpdsPreferences { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -312,6 +312,12 @@ public sealed class DataContext : IdentityDbContext<AppUser, AppRole, int,
|
|||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
.HasDefaultValue(new AppUserSocialPreferences());
|
.HasDefaultValue(new AppUserSocialPreferences());
|
||||||
|
|
||||||
|
builder.Entity<AppUserPreferences>()
|
||||||
|
.Property(a => a.OpdsPreferences)
|
||||||
|
.HasJsonConversion(new AppUserOpdsPreferences())
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue(new AppUserOpdsPreferences());
|
||||||
|
|
||||||
builder.Entity<AppUserAnnotation>()
|
builder.Entity<AppUserAnnotation>()
|
||||||
.Property(a => a.Likes)
|
.Property(a => a.Likes)
|
||||||
.HasJsonConversion(new HashSet<int>())
|
.HasJsonConversion(new HashSet<int>())
|
||||||
|
|||||||
3946
API/Data/Migrations/20251101152738_OpdsSettings.Designer.cs
generated
Normal file
3946
API/Data/Migrations/20251101152738_OpdsSettings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
API/Data/Migrations/20251101152738_OpdsSettings.cs
Normal file
29
API/Data/Migrations/20251101152738_OpdsSettings.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class OpdsSettings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "OpdsPreferences",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
defaultValue: "{\"EmbedProgressIndicator\":true,\"IncludeContinueFrom\":true}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "OpdsPreferences",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -599,6 +599,11 @@ namespace API.Data.Migrations
|
|||||||
b.Property<bool>("NoTransitions")
|
b.Property<bool>("NoTransitions")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("OpdsPreferences")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasDefaultValue("{\"EmbedProgressIndicator\":true,\"IncludeContinueFrom\":true}");
|
||||||
|
|
||||||
b.Property<int>("PageSplitOption")
|
b.Property<int>("PageSplitOption")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
|||||||
14
API/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs
Normal file
14
API/Entities/Enums/UserPreferences/AppUserOpdsPreferences.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace API.Entities.Enums.UserPreferences;
|
||||||
|
|
||||||
|
public class AppUserOpdsPreferences
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Embed Progress Indicator in Title
|
||||||
|
/// </summary>
|
||||||
|
public bool EmbedProgressIndicator { get; set; } = true;
|
||||||
|
/// <summary>
|
||||||
|
/// Insert a "Continue From X" entry in OPDS fields to avoid finding your last reading point (Emulates Kavita's Continue button)
|
||||||
|
/// </summary>
|
||||||
|
public bool IncludeContinueFrom { get; set; } = true;
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
@ -212,6 +211,12 @@ public class AppUserPreferences
|
|||||||
/// <remarks>Saved as a JSON obj in the DB</remarks>
|
/// <remarks>Saved as a JSON obj in the DB</remarks>
|
||||||
public AppUserSocialPreferences SocialPreferences { get; set; } = new();
|
public AppUserSocialPreferences SocialPreferences { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The opds preferences of the AppUser
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Saved as a JSON obj in the DB</remarks>
|
||||||
|
public AppUserOpdsPreferences OpdsPreferences { get; set; } = new();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -219,47 +224,3 @@ public class AppUserPreferences
|
|||||||
public AppUser AppUser { get; set; } = null!;
|
public AppUser AppUser { get; set; } = null!;
|
||||||
public int AppUserId { get; set; }
|
public int AppUserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppUserSocialPreferences
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// UI Site Global Setting: Should series reviews be shared with all users in the server
|
|
||||||
/// </summary>
|
|
||||||
public bool ShareReviews { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI Site Global Setting: Share your annotations with other users
|
|
||||||
/// </summary>
|
|
||||||
public bool ShareAnnotations { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI Site Global Setting: See other users' annotations while reading
|
|
||||||
/// </summary>
|
|
||||||
public bool ViewOtherAnnotations { get; set; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI Site Global Setting: For which libraries should social features be enabled
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>Empty array means all, disable specific social features to opt out everywhere</remarks>
|
|
||||||
public IList<int> SocialLibraries { get; set; } = [];
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI Site Global Setting: Highest age rating for which social features are enabled
|
|
||||||
/// </summary>
|
|
||||||
public AgeRating SocialMaxAgeRating { get; set; } = AgeRating.NotApplicable;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI Site Global Setting: Enable social features for unknown age ratings
|
|
||||||
/// </summary>
|
|
||||||
public bool SocialIncludeUnknowns { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record KeyBind
|
|
||||||
{
|
|
||||||
public string Key { get; set; }
|
|
||||||
public bool Control { get; set; }
|
|
||||||
public bool Shift { get; set; }
|
|
||||||
public bool Meta { get; set; }
|
|
||||||
public bool Alt { get; set; }
|
|
||||||
public IList<string>? ControllerSequence { get; set; }
|
|
||||||
}
|
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.Entities.Enums.UserPreferences;
|
||||||
|
|
||||||
|
public class AppUserSocialPreferences
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: Should series reviews be shared with all users in the server
|
||||||
|
/// </summary>
|
||||||
|
public bool ShareReviews { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: Share your annotations with other users
|
||||||
|
/// </summary>
|
||||||
|
public bool ShareAnnotations { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: See other users' annotations while reading
|
||||||
|
/// </summary>
|
||||||
|
public bool ViewOtherAnnotations { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: For which libraries should social features be enabled
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Empty array means all, disable specific social features to opt out everywhere</remarks>
|
||||||
|
public IList<int> SocialLibraries { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: Highest age rating for which social features are enabled
|
||||||
|
/// </summary>
|
||||||
|
public AgeRating SocialMaxAgeRating { get; set; } = AgeRating.NotApplicable;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// UI Site Global Setting: Enable social features for unknown age ratings
|
||||||
|
/// </summary>
|
||||||
|
public bool SocialIncludeUnknowns { get; set; } = true;
|
||||||
|
}
|
||||||
14
API/Entities/Enums/UserPreferences/KeyBind.cs
Normal file
14
API/Entities/Enums/UserPreferences/KeyBind.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace API.Entities.Enums.UserPreferences;
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
public sealed record KeyBind
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
public bool Control { get; set; }
|
||||||
|
public bool Shift { get; set; }
|
||||||
|
public bool Meta { get; set; }
|
||||||
|
public bool Alt { get; set; }
|
||||||
|
public IList<string>? ControllerSequence { get; set; }
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using API.Services;
|
||||||
using API.Services.Tasks.Scanner.Parser;
|
using API.Services.Tasks.Scanner.Parser;
|
||||||
|
|
||||||
namespace API.Helpers;
|
namespace API.Helpers;
|
||||||
@ -70,16 +71,29 @@ public static class KoreaderHelper
|
|||||||
|
|
||||||
public static void UpdateProgressDto(ProgressDto progress, string koreaderPosition)
|
public static void UpdateProgressDto(ProgressDto progress, string koreaderPosition)
|
||||||
{
|
{
|
||||||
|
// #_doc_fragment26
|
||||||
|
string docNumber;
|
||||||
|
if (koreaderPosition.StartsWith("#_doc_fragment"))
|
||||||
|
{
|
||||||
|
docNumber = koreaderPosition.Replace("#_doc_fragment", string.Empty);
|
||||||
|
progress.PageNum = int.Parse(docNumber) - 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var path = koreaderPosition.Split('/');
|
var path = koreaderPosition.Split('/');
|
||||||
if (path.Length < 6)
|
if (path.Length < 6)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var docNumber = path[2].Replace("DocFragment[", string.Empty).Replace("]", string.Empty);
|
docNumber = path[2].Replace("DocFragment[", string.Empty).Replace("]", string.Empty);
|
||||||
progress.PageNum = int.Parse(docNumber) - 1;
|
progress.PageNum = int.Parse(docNumber) - 1;
|
||||||
|
|
||||||
|
var lastPart = koreaderPosition.Split("/body/")[^1];
|
||||||
var lastTag = path[5].ToUpper();
|
var lastTag = path[5].ToUpper();
|
||||||
|
|
||||||
|
// TODO: Enhance this code: /body/DocFragment[27]/body/section/p[3]/text().229 -> p[3] but we probably can get more
|
||||||
|
|
||||||
if (lastTag == "A")
|
if (lastTag == "A")
|
||||||
{
|
{
|
||||||
progress.BookScrollId = null;
|
progress.BookScrollId = null;
|
||||||
@ -87,27 +101,28 @@ public static class KoreaderHelper
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// The format that Kavita accepts as a progress string. It tells Kavita where Koreader last left off.
|
// The format that Kavita accepts as a progress string. It tells Kavita where Koreader last left off.
|
||||||
progress.BookScrollId = $"//html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/{lastTag}";
|
progress.BookScrollId = $"//html[1]/{BookService.BookReaderBodyScope[2..].ToLowerInvariant()}/{lastPart}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static string GetKoreaderPosition(ProgressDto progressDto)
|
public static string GetKoreaderPosition(ProgressDto progressDto)
|
||||||
{
|
{
|
||||||
string lastTag;
|
string nonBodyTag;
|
||||||
var koreaderPageNumber = progressDto.PageNum + 1;
|
var koreaderPageNumber = progressDto.PageNum + 1;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(progressDto.BookScrollId))
|
if (string.IsNullOrEmpty(progressDto.BookScrollId))
|
||||||
{
|
{
|
||||||
lastTag = "a";
|
nonBodyTag = "a";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var tokens = progressDto.BookScrollId.Split('/');
|
// What we Store: //html[1]/BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]/section/p[62]/text().0
|
||||||
lastTag = tokens[^1].ToLower();
|
// What we Need to send back: section/p[62]/text().0
|
||||||
|
nonBodyTag = progressDto.BookScrollId.Replace("//html[1]/", "//", StringComparison.InvariantCultureIgnoreCase).Replace(BookService.BookReaderBodyScope + "/", string.Empty, StringComparison.InvariantCultureIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off.
|
// The format that Koreader accepts as a progress string. It tells Koreader where Kavita last left off.
|
||||||
return $"/body/DocFragment[{koreaderPageNumber}]/body/div/{lastTag}";
|
return $"/body/DocFragment[{koreaderPageNumber}]/body/{nonBodyTag}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,6 +77,7 @@ public partial class BookService : IBookService
|
|||||||
private static readonly RecyclableMemoryStreamManager StreamManager = new ();
|
private static readonly RecyclableMemoryStreamManager StreamManager = new ();
|
||||||
private const string CssScopeClass = ".book-content";
|
private const string CssScopeClass = ".book-content";
|
||||||
private const string BookApiUrl = "book-resources?file=";
|
private const string BookApiUrl = "book-resources?file=";
|
||||||
|
public const string BookReaderBodyScope = "//BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]";
|
||||||
private readonly PdfComicInfoExtractor _pdfComicInfoExtractor;
|
private readonly PdfComicInfoExtractor _pdfComicInfoExtractor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -327,7 +328,7 @@ public partial class BookService : IBookService
|
|||||||
{
|
{
|
||||||
var unscopedSelector = bookmark.BookScrollId!
|
var unscopedSelector = bookmark.BookScrollId!
|
||||||
.Replace(
|
.Replace(
|
||||||
"//BODY/APP-ROOT[1]/DIV[1]/DIV[1]/DIV[1]/APP-BOOK-READER[1]/DIV[1]/DIV[2]/DIV[1]/DIV[1]/DIV[1]",
|
BookReaderBodyScope,
|
||||||
"//BODY").ToLowerInvariant();
|
"//BODY").ToLowerInvariant();
|
||||||
var elem = doc.DocumentNode.SelectSingleNode(unscopedSelector);
|
var elem = doc.DocumentNode.SelectSingleNode(unscopedSelector);
|
||||||
if (elem == null) continue;
|
if (elem == null) continue;
|
||||||
|
|||||||
@ -61,7 +61,9 @@ public class KoreaderService : IKoreaderService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Update the bookScrollId if possible
|
// Update the bookScrollId if possible
|
||||||
|
var reportedProgress = koreaderBookDto.progress;
|
||||||
KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.progress);
|
KoreaderHelper.UpdateProgressDto(userProgressDto, koreaderBookDto.progress);
|
||||||
|
_logger.LogDebug("Converting KOReader progress from {ReportedProgress} to {ScopedProgress}", reportedProgress.Sanitize(), userProgressDto.BookScrollId?.Sanitize());
|
||||||
|
|
||||||
await _readerService.SaveReadingProgress(userProgressDto, userId);
|
await _readerService.SaveReadingProgress(userProgressDto, userId);
|
||||||
}
|
}
|
||||||
@ -81,7 +83,10 @@ public class KoreaderService : IKoreaderService
|
|||||||
if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing"));
|
if (file == null) throw new KavitaException(await _localizationService.Translate(userId, "file-missing"));
|
||||||
|
|
||||||
var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId);
|
var progressDto = await _unitOfWork.AppUserProgressRepository.GetUserProgressDtoAsync(file.ChapterId, userId);
|
||||||
|
var originalScrollId = progressDto?.BookScrollId;
|
||||||
var koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto);
|
var koreaderProgress = KoreaderHelper.GetKoreaderPosition(progressDto);
|
||||||
|
_logger.LogDebug("Converting KOReader progress from {KavitaProgress} to {KOReaderProgress}", originalScrollId?.Sanitize() ?? string.Empty, progressDto?.BookScrollId?.Sanitize() ?? string.Empty);
|
||||||
|
|
||||||
|
|
||||||
return new KoreaderBookDtoBuilder(bookHash).WithProgress(koreaderProgress)
|
return new KoreaderBookDtoBuilder(bookHash).WithProgress(koreaderProgress)
|
||||||
.WithPercentage(progressDto?.PageNum, file.Pages)
|
.WithPercentage(progressDto?.PageNum, file.Pages)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ using API.DTOs.ReadingLists;
|
|||||||
using API.DTOs.Search;
|
using API.DTOs.Search;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
using API.Entities.Enums.UserPreferences;
|
||||||
using API.Exceptions;
|
using API.Exceptions;
|
||||||
using API.Helpers;
|
using API.Helpers;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
@ -597,17 +598,17 @@ public class OpdsService : IOpdsService
|
|||||||
|
|
||||||
// Check if there is reading progress or not, if so, inject a "continue-reading" item
|
// Check if there is reading progress or not, if so, inject a "continue-reading" item
|
||||||
var anyProgress = await _unitOfWork.ReadingListRepository.AnyUserReadingProgressAsync(readingListId, userId);
|
var anyProgress = await _unitOfWork.ReadingListRepository.AnyUserReadingProgressAsync(readingListId, userId);
|
||||||
if (anyProgress)
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences);
|
||||||
|
if (user!.UserPreferences.OpdsPreferences.IncludeContinueFrom && anyProgress)
|
||||||
{
|
{
|
||||||
var firstReadReadingListItem = await _unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId);
|
var firstReadReadingListItem = await _unitOfWork.ReadingListRepository.GetContinueReadingPoint(readingListId, userId);
|
||||||
if (firstReadReadingListItem != null && request.PageNumber == FirstPageNumber)
|
if (firstReadReadingListItem != null && request.PageNumber == FirstPageNumber)
|
||||||
{
|
{
|
||||||
await AddContinueReadingPoint(firstReadReadingListItem, feed, request);
|
await AddContinueReadingPoint(firstReadReadingListItem, feed, request, user.UserPreferences.OpdsPreferences);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(item.ChapterId, userId);
|
var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(item.ChapterId, userId);
|
||||||
@ -617,7 +618,7 @@ public class OpdsService : IOpdsService
|
|||||||
{
|
{
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(item.SeriesId, userId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(item.SeriesId, userId);
|
||||||
feed.Entries.Add(await CreateChapterWithFile(item.SeriesId, item.VolumeId, item.ChapterId,
|
feed.Entries.Add(await CreateChapterWithFile(item.SeriesId, item.VolumeId, item.ChapterId,
|
||||||
chapterDto.Files.First(), series!, chapterDto, request));
|
chapterDto.Files.First(), series!, chapterDto, request, user.UserPreferences.OpdsPreferences));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -646,10 +647,11 @@ public class OpdsService : IOpdsService
|
|||||||
|
|
||||||
// Check if there is reading progress or not, if so, inject a "continue-reading" item
|
// Check if there is reading progress or not, if so, inject a "continue-reading" item
|
||||||
var anyUserProgress = await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId);
|
var anyUserProgress = await _unitOfWork.AppUserProgressRepository.AnyUserProgressForSeriesAsync(seriesId, userId);
|
||||||
if (anyUserProgress)
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences);
|
||||||
|
if (user!.UserPreferences.OpdsPreferences.IncludeContinueFrom && anyUserProgress)
|
||||||
{
|
{
|
||||||
var chapterDto = await _readerService.GetContinuePoint(seriesId, userId);
|
var chapterDto = await _readerService.GetContinuePoint(seriesId, userId);
|
||||||
await AddContinueReadingPoint(seriesId, chapterDto, feed, request);
|
await AddContinueReadingPoint(seriesId, chapterDto, feed, request, user.UserPreferences.OpdsPreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -672,7 +674,7 @@ public class OpdsService : IOpdsService
|
|||||||
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
||||||
|
|
||||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volume.Id, chapterId, _mapper.Map<MangaFileDto>(mangaFile), series,
|
feed.Entries.Add(await CreateChapterWithFile(seriesId, volume.Id, chapterId, _mapper.Map<MangaFileDto>(mangaFile), series,
|
||||||
chapterDto, request));
|
chapterDto, request, user.UserPreferences.OpdsPreferences));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -692,7 +694,7 @@ public class OpdsService : IOpdsService
|
|||||||
// If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception
|
// If a chapter has multiple files that are within one chapter, this dict prevents duplicate key exception
|
||||||
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
||||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, chapter.VolumeId, chapter.Id, _mapper.Map<MangaFileDto>(mangaFile), series,
|
feed.Entries.Add(await CreateChapterWithFile(seriesId, chapter.VolumeId, chapter.Id, _mapper.Map<MangaFileDto>(mangaFile), series,
|
||||||
chapterDto, request));
|
chapterDto, request, user.UserPreferences.OpdsPreferences));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -706,7 +708,7 @@ public class OpdsService : IOpdsService
|
|||||||
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
if (!fileDict.TryAdd(mangaFile.Id, 0)) continue;
|
||||||
|
|
||||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, special.VolumeId, special.Id, _mapper.Map<MangaFileDto>(mangaFile), series,
|
feed.Entries.Add(await CreateChapterWithFile(seriesId, special.VolumeId, special.Id, _mapper.Map<MangaFileDto>(mangaFile), series,
|
||||||
chapterDto, request));
|
chapterDto, request, user.UserPreferences.OpdsPreferences));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,17 +742,18 @@ public class OpdsService : IOpdsService
|
|||||||
// Check if there is reading progress or not, if so, inject a "continue-reading" item
|
// Check if there is reading progress or not, if so, inject a "continue-reading" item
|
||||||
var firstChapterWithProgress = chapterDtos.FirstOrDefault(i => i.PagesRead > 0 && i.PagesRead != i.Pages) ??
|
var firstChapterWithProgress = chapterDtos.FirstOrDefault(i => i.PagesRead > 0 && i.PagesRead != i.Pages) ??
|
||||||
chapterDtos.FirstOrDefault(i => i.PagesRead == 0 && i.PagesRead != i.Pages);
|
chapterDtos.FirstOrDefault(i => i.PagesRead == 0 && i.PagesRead != i.Pages);
|
||||||
if (firstChapterWithProgress != null && request.PageNumber == FirstPageNumber)
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences);
|
||||||
|
if (user!.UserPreferences.OpdsPreferences.IncludeContinueFrom && firstChapterWithProgress != null && request.PageNumber == FirstPageNumber)
|
||||||
{
|
{
|
||||||
var chapterDto = await _readerService.GetContinuePoint(seriesId, userId);
|
var chapterDto = await _readerService.GetContinuePoint(seriesId, userId);
|
||||||
await AddContinueReadingPoint(seriesId, chapterDto, feed, request);
|
await AddContinueReadingPoint(seriesId, chapterDto, feed, request, user.UserPreferences.OpdsPreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var chapterDto in chapterDtos)
|
foreach (var chapterDto in chapterDtos)
|
||||||
{
|
{
|
||||||
foreach (var mangaFile in chapterDto.Files)
|
foreach (var mangaFile in chapterDto.Files)
|
||||||
{
|
{
|
||||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterDto.Id, mangaFile, series, chapterDto!, request));
|
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterDto.Id, mangaFile, series, chapterDto, request, user.UserPreferences.OpdsPreferences));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -785,9 +788,10 @@ public class OpdsService : IOpdsService
|
|||||||
$"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix);
|
$"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}", apiKey, prefix);
|
||||||
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{_seriesService.FormatChapterName(userId, libraryType)}-{chapterId}-files");
|
SetFeedId(feed, $"series-{series.Id}-volume-{volumeId}-{_seriesService.FormatChapterName(userId, libraryType)}-{chapterId}-files");
|
||||||
|
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId, AppUserIncludes.UserPreferences);
|
||||||
foreach (var mangaFile in chapter.Files)
|
foreach (var mangaFile in chapter.Files)
|
||||||
{
|
{
|
||||||
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, request));
|
feed.Entries.Add(await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, request, user!.UserPreferences.OpdsPreferences));
|
||||||
}
|
}
|
||||||
|
|
||||||
return feed;
|
return feed;
|
||||||
@ -1147,17 +1151,20 @@ public class OpdsService : IOpdsService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<FeedEntry> CreateContinueReadingFromFile(int seriesId, int volumeId, int chapterId,
|
private async Task<FeedEntry> CreateContinueReadingFromFile(int seriesId, int volumeId, int chapterId,
|
||||||
MangaFileDto mangaFile, SeriesDto series, ChapterDto chapter, IOpdsRequest request)
|
MangaFileDto mangaFile, SeriesDto series, ChapterDto chapter, IOpdsRequest request, AppUserOpdsPreferences pref)
|
||||||
{
|
{
|
||||||
var entry = await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, request);
|
var entry = await CreateChapterWithFile(seriesId, volumeId, chapterId, mangaFile, series, chapter, request, pref);
|
||||||
|
|
||||||
|
if (pref.EmbedProgressIndicator)
|
||||||
|
{
|
||||||
entry.Title = await _localizationService.Translate(request.UserId, "opds-continue-reading-title", entry.Title);
|
entry.Title = await _localizationService.Translate(request.UserId, "opds-continue-reading-title", entry.Title);
|
||||||
|
}
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<FeedEntry> CreateChapterWithFile(int seriesId, int volumeId, int chapterId,
|
private async Task<FeedEntry> CreateChapterWithFile(int seriesId, int volumeId, int chapterId,
|
||||||
MangaFileDto mangaFile, SeriesDto series, ChapterDto chapter, IOpdsRequest request)
|
MangaFileDto mangaFile, SeriesDto series, ChapterDto chapter, IOpdsRequest request, AppUserOpdsPreferences pref)
|
||||||
{
|
{
|
||||||
var fileSize =
|
var fileSize =
|
||||||
mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) :
|
mangaFile.Bytes > 0 ? DirectoryService.GetHumanReadableBytes(mangaFile.Bytes) :
|
||||||
@ -1229,7 +1236,10 @@ public class OpdsService : IOpdsService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Patch in reading status on the item (as OPDS is seriously lacking)
|
// Patch in reading status on the item (as OPDS is seriously lacking)
|
||||||
|
if (pref.EmbedProgressIndicator)
|
||||||
|
{
|
||||||
entry.Title = $"{GetReadingProgressIcon(chapter.PagesRead, chapter.Pages)} {entry.Title}";
|
entry.Title = $"{GetReadingProgressIcon(chapter.PagesRead, chapter.Pages)} {entry.Title}";
|
||||||
|
}
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
@ -1284,24 +1294,24 @@ public class OpdsService : IOpdsService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddContinueReadingPoint(int seriesId, ChapterDto chapterDto, Feed feed, IOpdsRequest request)
|
private async Task AddContinueReadingPoint(int seriesId, ChapterDto chapterDto, Feed feed, IOpdsRequest request, AppUserOpdsPreferences pref)
|
||||||
{
|
{
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, request.UserId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, request.UserId);
|
||||||
if (chapterDto is {Files.Count: 1})
|
if (chapterDto is {Files.Count: 1})
|
||||||
{
|
{
|
||||||
feed.Entries.Add(await CreateContinueReadingFromFile(seriesId, chapterDto.VolumeId, chapterDto.Id,
|
feed.Entries.Add(await CreateContinueReadingFromFile(seriesId, chapterDto.VolumeId, chapterDto.Id,
|
||||||
chapterDto.Files.First(), series!, chapterDto, request));
|
chapterDto.Files.First(), series!, chapterDto, request, pref));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task AddContinueReadingPoint(ReadingListItemDto firstReadReadingListItem, Feed feed, IOpdsRequest request)
|
private async Task AddContinueReadingPoint(ReadingListItemDto firstReadReadingListItem, Feed feed, IOpdsRequest request, AppUserOpdsPreferences pref)
|
||||||
{
|
{
|
||||||
var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(firstReadReadingListItem.ChapterId, request.UserId);
|
var chapterDto = await _unitOfWork.ChapterRepository.GetChapterDtoAsync(firstReadReadingListItem.ChapterId, request.UserId);
|
||||||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(firstReadReadingListItem.SeriesId, request.UserId);
|
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(firstReadReadingListItem.SeriesId, request.UserId);
|
||||||
if (chapterDto is {Files.Count: 1})
|
if (chapterDto is {Files.Count: 1})
|
||||||
{
|
{
|
||||||
feed.Entries.Add(await CreateContinueReadingFromFile(firstReadReadingListItem.SeriesId, firstReadReadingListItem.VolumeId,
|
feed.Entries.Add(await CreateContinueReadingFromFile(firstReadReadingListItem.SeriesId, firstReadReadingListItem.VolumeId,
|
||||||
firstReadReadingListItem.ChapterId, chapterDto.Files.First(), series!, chapterDto, request));
|
firstReadReadingListItem.ChapterId, chapterDto.Files.First(), series!, chapterDto, request, pref));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -286,7 +286,7 @@ public class ReaderService : IReaderService
|
|||||||
_unitOfWork.AppUserProgressRepository.Update(userProgress);
|
_unitOfWork.AppUserProgressRepository.Update(userProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Saving Progress on Chapter {ChapterId} from Series {SeriesId} to {PageNum}", progressDto.ChapterId, progressDto.SeriesId, progressDto.PageNum);
|
_logger.LogDebug("Saving Progress on Series {SeriesId}, Chapter {ChapterId} to Page {PageNum}", progressDto.SeriesId, progressDto.ChapterId, progressDto.PageNum);
|
||||||
userProgress?.MarkModified();
|
userProgress?.MarkModified();
|
||||||
|
|
||||||
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
if (!_unitOfWork.HasChanges() || await _unitOfWork.CommitAsync())
|
||||||
|
|||||||
@ -883,7 +883,7 @@ public class ProcessSeries : IProcessSeries
|
|||||||
|
|
||||||
private void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info, bool forceUpdate = false)
|
private void AddOrUpdateFileForChapter(Chapter chapter, ParserInfo info, bool forceUpdate = false)
|
||||||
{
|
{
|
||||||
chapter.Files ??= new List<MangaFile>();
|
chapter.Files ??= [];
|
||||||
var existingFile = chapter.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
var existingFile = chapter.Files.SingleOrDefault(f => f.FilePath == info.FullFilePath);
|
||||||
var fileInfo = _directoryService.FileSystem.FileInfo.New(info.FullFilePath);
|
var fileInfo = _directoryService.FileSystem.FileInfo.New(info.FullFilePath);
|
||||||
if (existingFile != null)
|
if (existingFile != null)
|
||||||
|
|||||||
@ -25,6 +25,8 @@ export interface Preferences {
|
|||||||
|
|
||||||
// Social
|
// Social
|
||||||
socialPreferences: SocialPreferences;
|
socialPreferences: SocialPreferences;
|
||||||
|
|
||||||
|
opdsPreferences: OpdsPreferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SocialPreferences {
|
export interface SocialPreferences {
|
||||||
@ -36,6 +38,11 @@ export interface SocialPreferences {
|
|||||||
socialIncludeUnknowns: boolean;
|
socialIncludeUnknowns: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpdsPreferences {
|
||||||
|
embedProgressIndicator: boolean;
|
||||||
|
includeContinueFrom: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface KeyBind {
|
export interface KeyBind {
|
||||||
meta?: boolean;
|
meta?: boolean;
|
||||||
control?: boolean;
|
control?: boolean;
|
||||||
@ -60,3 +67,8 @@ export enum KeyBindTarget {
|
|||||||
Escape = 'Escape',
|
Escape = 'Escape',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpdsPreferences {
|
||||||
|
embedProgressIndicator: boolean;
|
||||||
|
includeContinueFrom: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -201,7 +201,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="setting-section-break"></div>
|
||||||
|
|
||||||
|
<h4>{{t('opds-settings-title')}}</h4>
|
||||||
|
<ng-container formGroupName="opdsPreferences">
|
||||||
|
<div class="row g-0 mt-4">
|
||||||
|
<app-setting-switch [title]="t('embed-progress-indicator-label')" [subtitle]="t('embed-progress-indicator-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input type="checkbox" role="switch" id="embed-progress-indicator"
|
||||||
|
formControlName="embedProgressIndicator" class="form-check-input"
|
||||||
|
aria-labelledby="auto-close-label">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-0 mt-4">
|
||||||
|
<app-setting-switch [title]="t('include-continue-from-label')" [subtitle]="t('include-continue-from-tooltip')">
|
||||||
|
<ng-template #switch>
|
||||||
|
<div class="form-check form-switch float-end">
|
||||||
|
<input type="checkbox" role="switch" id="include-continue-from"
|
||||||
|
formControlName="includeContinueFrom" class="form-check-input">
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</app-setting-switch>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
@if (licenseService.hasValidLicense$ | async) {
|
@if (licenseService.hasValidLicense$ | async) {
|
||||||
<div class="setting-section-break"></div>
|
<div class="setting-section-break"></div>
|
||||||
|
|||||||
@ -1,13 +1,4 @@
|
|||||||
import {
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit, signal} from '@angular/core';
|
||||||
ChangeDetectionStrategy,
|
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
computed,
|
|
||||||
DestroyRef, effect,
|
|
||||||
inject,
|
|
||||||
OnInit,
|
|
||||||
signal
|
|
||||||
} from '@angular/core';
|
|
||||||
import {TranslocoDirective} from "@jsverse/transloco";
|
import {TranslocoDirective} from "@jsverse/transloco";
|
||||||
import {Preferences} from "../../_models/preferences/preferences";
|
import {Preferences} from "../../_models/preferences/preferences";
|
||||||
import {AccountService} from "../../_services/account.service";
|
import {AccountService} from "../../_services/account.service";
|
||||||
@ -60,6 +51,11 @@ type UserPreferencesForm = FormGroup<{
|
|||||||
socialMaxAgeRating: FormControl<AgeRating>,
|
socialMaxAgeRating: FormControl<AgeRating>,
|
||||||
socialIncludeUnknowns: FormControl<boolean>,
|
socialIncludeUnknowns: FormControl<boolean>,
|
||||||
}>,
|
}>,
|
||||||
|
|
||||||
|
opdsPreferences: FormGroup<{
|
||||||
|
embedProgressIndicator: FormControl<boolean>,
|
||||||
|
includeContinueFrom: FormControl<boolean>,
|
||||||
|
}>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -176,6 +172,11 @@ export class ManageUserPreferencesComponent implements OnInit {
|
|||||||
socialMaxAgeRating: this.fb.control<AgeRating>(pref.socialPreferences.socialMaxAgeRating),
|
socialMaxAgeRating: this.fb.control<AgeRating>(pref.socialPreferences.socialMaxAgeRating),
|
||||||
socialIncludeUnknowns: this.fb.control<boolean>(pref.socialPreferences.socialIncludeUnknowns),
|
socialIncludeUnknowns: this.fb.control<boolean>(pref.socialPreferences.socialIncludeUnknowns),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
opdsPreferences: this.fb.group({
|
||||||
|
embedProgressIndicator: this.fb.control<boolean>(pref.opdsPreferences.embedProgressIndicator),
|
||||||
|
includeContinueFrom: this.fb.control<boolean>(pref.opdsPreferences.includeContinueFrom),
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// Automatically save settings as we edit them
|
// Automatically save settings as we edit them
|
||||||
|
|||||||
@ -196,6 +196,7 @@
|
|||||||
|
|
||||||
"global-settings-title": "Global Settings",
|
"global-settings-title": "Global Settings",
|
||||||
"social-settings-title": "Social Settings",
|
"social-settings-title": "Social Settings",
|
||||||
|
"opds-settings-title": "OPDS Settings",
|
||||||
"page-layout-mode-label": "Page Layout Mode",
|
"page-layout-mode-label": "Page Layout Mode",
|
||||||
"page-layout-mode-tooltip": "Show items as cards or list view on Series Detail page.",
|
"page-layout-mode-tooltip": "Show items as cards or list view on Series Detail page.",
|
||||||
"locale-label": "Locale",
|
"locale-label": "Locale",
|
||||||
@ -234,6 +235,11 @@
|
|||||||
"social-include-unknowns-label": "Include unknowns",
|
"social-include-unknowns-label": "Include unknowns",
|
||||||
"social-include-unknowns-tooltip": "Enable social features for series and chapters with an unknown age rating",
|
"social-include-unknowns-tooltip": "Enable social features for series and chapters with an unknown age rating",
|
||||||
|
|
||||||
|
"embed-progress-indicator-label": "Embed Progress Indicator",
|
||||||
|
"embed-progress-indicator-tooltip": "Embed Progress Indicator in Title",
|
||||||
|
"include-continue-from-label": "Include Continue From Entry",
|
||||||
|
"include-continue-from-tooltip": "Insert a \"Continue From X\" entry in OPDS fields to avoid finding your last reading point (Emulates Kavita's Continue button)",
|
||||||
|
|
||||||
"clients-opds-alert": "OPDS is not enabled on this server. This will not affect Mihon users.",
|
"clients-opds-alert": "OPDS is not enabled on this server. This will not affect Mihon users.",
|
||||||
"clients-opds-description": "All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.",
|
"clients-opds-description": "All 3rd Party clients will either use the API key or the Connection Url below. These are like passwords, keep it private.",
|
||||||
"clients-api-key-tooltip": "The API key is like a password. Resetting it will invalidate any existing clients.",
|
"clients-api-key-tooltip": "The API key is like a password. Resetting it will invalidate any existing clients.",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user