mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Holiday Bugfixes (#1762)
* Don't show "not much going on" when we are actively downloading * Swipe to paginate is now behind a flag in the user preferences. * Added a new server setting for host name, if the server sits behind a reverse proxy. If this is set, email link generation will use it and will not perform any checks on accessibility (thus email will always send) * Refactored the code that checks if the server is accessible to check if host name is set, and thus return rue if so. * Added back the system drawing library for markdown parsing. * Fixed a validation error * Fixed a bug where folder watching could get re-triggered when it was disabled at a server level. * Made the manga reader loader absolute positioned for better visibility * Indentation
This commit is contained in:
parent
2a47029209
commit
5e9bbd0768
@ -96,6 +96,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.24.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions" Version="17.2.3" />
|
<PackageReference Include="System.IO.Abstractions" Version="17.2.3" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
|
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
|
||||||
|
@ -324,10 +324,11 @@ public class AccountController : BaseApiController
|
|||||||
// Send a confirmation email
|
// Send a confirmation email
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var emailLink = GenerateEmailLink(user.ConfirmationToken, "confirm-email-update", dto.Email);
|
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email-update", dto.Email);
|
||||||
_logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
_logger.LogCritical("[Update Email]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
|
||||||
var accessible = await _emailService.CheckIfAccessible(host);
|
|
||||||
|
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||||
if (accessible)
|
if (accessible)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -495,7 +496,7 @@ public class AccountController : BaseApiController
|
|||||||
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
||||||
return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite.");
|
return BadRequest("Manual setup is unable to be completed. Please cancel and recreate the invite.");
|
||||||
|
|
||||||
return GenerateEmailLink(user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl);
|
return await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", user.Email, withBaseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -603,11 +604,10 @@ public class AccountController : BaseApiController
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var emailLink = GenerateEmailLink(user.ConfirmationToken, "confirm-email", dto.Email);
|
var emailLink = await _accountService.GenerateEmailLink(Request, user.ConfirmationToken, "confirm-email", dto.Email);
|
||||||
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
_logger.LogCritical("[Invite User]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||||
_logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken);
|
_logger.LogCritical("[Invite User]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken);
|
||||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||||
var accessible = await _emailService.CheckIfAccessible(host);
|
|
||||||
if (accessible)
|
if (accessible)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -795,10 +795,9 @@ public class AccountController : BaseApiController
|
|||||||
return BadRequest("You do not have an email on account or it has not been confirmed");
|
return BadRequest("You do not have an email on account or it has not been confirmed");
|
||||||
|
|
||||||
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||||
var emailLink = GenerateEmailLink(token, "confirm-reset-password", user.Email);
|
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-reset-password", user.Email);
|
||||||
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
_logger.LogCritical("[Forgot Password]: Email Link for {UserName}: {Link}", user.UserName, emailLink);
|
||||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
if (await _accountService.CheckIfAccessible(Request))
|
||||||
if (await _emailService.CheckIfAccessible(host))
|
|
||||||
{
|
{
|
||||||
await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto()
|
await _emailService.SendPasswordResetEmail(new PasswordResetEmailDto()
|
||||||
{
|
{
|
||||||
@ -851,6 +850,11 @@ public class AccountController : BaseApiController
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resend an invite to a user already invited
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("resend-confirmation-email")]
|
[HttpPost("resend-confirmation-email")]
|
||||||
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
public async Task<ActionResult<string>> ResendConfirmationSendEmail([FromQuery] int userId)
|
||||||
{
|
{
|
||||||
@ -863,9 +867,11 @@ public class AccountController : BaseApiController
|
|||||||
if (user.EmailConfirmed) return BadRequest("User already confirmed");
|
if (user.EmailConfirmed) return BadRequest("User already confirmed");
|
||||||
|
|
||||||
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||||
var emailLink = GenerateEmailLink(token, "confirm-email", user.Email);
|
var emailLink = await _accountService.GenerateEmailLink(Request, token, "confirm-email", user.Email);
|
||||||
_logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink);
|
_logger.LogCritical("[Email Migration]: Email Link: {Link}", emailLink);
|
||||||
_logger.LogCritical("[Email Migration]: Token {UserName}: {Token}", user.UserName, token);
|
_logger.LogCritical("[Email Migration]: Token {UserName}: {Token}", user.UserName, token);
|
||||||
|
if (await _accountService.CheckIfAccessible(Request))
|
||||||
|
{
|
||||||
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
||||||
{
|
{
|
||||||
EmailAddress = user.Email,
|
EmailAddress = user.Email,
|
||||||
@ -873,16 +879,10 @@ public class AccountController : BaseApiController
|
|||||||
ServerConfirmationLink = emailLink,
|
ServerConfirmationLink = emailLink,
|
||||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return Ok(emailLink);
|
return Ok(emailLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GenerateEmailLink(string token, string routePart, string email, bool withHost = true)
|
return Ok("The server is not accessible externally. Please ");
|
||||||
{
|
|
||||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
|
||||||
if (withHost) return $"{Request.Scheme}://{host}{Request.PathBase}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
|
||||||
return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -36,10 +36,11 @@ public class ServerController : BaseApiController
|
|||||||
private readonly IEmailService _emailService;
|
private readonly IEmailService _emailService;
|
||||||
private readonly IBookmarkService _bookmarkService;
|
private readonly IBookmarkService _bookmarkService;
|
||||||
private readonly IScannerService _scannerService;
|
private readonly IScannerService _scannerService;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
|
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
|
||||||
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService,
|
||||||
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService, IScannerService scannerService)
|
ICleanupService cleanupService, IEmailService emailService, IBookmarkService bookmarkService, IScannerService scannerService, IAccountService accountService)
|
||||||
{
|
{
|
||||||
_applicationLifetime = applicationLifetime;
|
_applicationLifetime = applicationLifetime;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -51,6 +52,7 @@ public class ServerController : BaseApiController
|
|||||||
_emailService = emailService;
|
_emailService = emailService;
|
||||||
_bookmarkService = bookmarkService;
|
_bookmarkService = bookmarkService;
|
||||||
_scannerService = scannerService;
|
_scannerService = scannerService;
|
||||||
|
_accountService = accountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -189,12 +191,13 @@ public class ServerController : BaseApiController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is this server accessible to the outside net
|
/// Is this server accessible to the outside net
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>If the instance has the HostName set, this will return true whether or not it is accessible externally</remarks>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("accessible")]
|
[HttpGet("accessible")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<ActionResult<bool>> IsServerAccessible()
|
public async Task<ActionResult<bool>> IsServerAccessible()
|
||||||
{
|
{
|
||||||
return await _emailService.CheckIfAccessible(Request.Host.ToString());
|
return Ok(await _accountService.CheckIfAccessible(Request));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("jobs")]
|
[HttpGet("jobs")]
|
||||||
|
@ -182,6 +182,13 @@ public class SettingsController : BaseApiController
|
|||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.HostName && updateSettingsDto.HostName + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
setting.Value = (updateSettingsDto.HostName + string.Empty).Trim();
|
||||||
|
if (setting.Value.EndsWith("/")) setting.Value = setting.Value.Substring(0, setting.Value.Length - 1);
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
if (setting.Key == ServerSettingKey.BookmarkDirectory && bookmarkDirectory != setting.Value)
|
||||||
{
|
{
|
||||||
|
@ -111,6 +111,7 @@ public class UsersController : BaseApiController
|
|||||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||||
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
||||||
|
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||||
|
|
||||||
|
@ -66,4 +66,8 @@ public class ServerSettingDto
|
|||||||
/// If the server should save covers as WebP encoding
|
/// If the server should save covers as WebP encoding
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ConvertCoverToWebP { get; set; }
|
public bool ConvertCoverToWebP { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The Host name (ie Reverse proxy domain name) for the server
|
||||||
|
/// </summary>
|
||||||
|
public string HostName { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,11 @@ public class UserPreferencesDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Required]
|
[Required]
|
||||||
public string BackgroundColor { get; set; } = "#000000";
|
public string BackgroundColor { get; set; } = "#000000";
|
||||||
|
[Required]
|
||||||
|
/// <summary>
|
||||||
|
/// Manga Reader Option: Should swiping trigger pagination
|
||||||
|
/// </summary>
|
||||||
|
public bool SwipeToPaginate { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
1746
API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs
generated
Normal file
1746
API/Data/Migrations/20230129210741_SwipeToPaginatePref.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs
Normal file
26
API/Data/Migrations/20230129210741_SwipeToPaginatePref.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class SwipeToPaginatePref : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "SwipeToPaginate",
|
||||||
|
table: "AppUserPreferences",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SwipeToPaginate",
|
||||||
|
table: "AppUserPreferences");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -249,6 +249,9 @@ namespace API.Data.Migrations
|
|||||||
b.Property<bool>("ShowScreenHints")
|
b.Property<bool>("ShowScreenHints")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("SwipeToPaginate")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int?>("ThemeId")
|
b.Property<int?>("ThemeId")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
@ -102,6 +102,7 @@ public static class Seed
|
|||||||
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
||||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||||
new() {Key = ServerSettingKey.ConvertCoverToWebP, Value = "false"},
|
new() {Key = ServerSettingKey.ConvertCoverToWebP, Value = "false"},
|
||||||
|
new() {Key = ServerSettingKey.HostName, Value = string.Empty},
|
||||||
}.ToArray());
|
}.ToArray());
|
||||||
|
|
||||||
foreach (var defaultSetting in DefaultSettings)
|
foreach (var defaultSetting in DefaultSettings)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using API.Entities.Enums;
|
using System;
|
||||||
|
using API.Entities.Enums;
|
||||||
using API.Entities.Enums.UserPreferences;
|
using API.Entities.Enums.UserPreferences;
|
||||||
|
|
||||||
namespace API.Entities;
|
namespace API.Entities;
|
||||||
@ -26,7 +27,6 @@ public class AppUserPreferences
|
|||||||
/// </example>
|
/// </example>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReaderMode ReaderMode { get; set; }
|
public ReaderMode ReaderMode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -48,6 +48,10 @@ public class AppUserPreferences
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string BackgroundColor { get; set; } = "#000000";
|
public string BackgroundColor { get; set; } = "#000000";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Manga Reader Option: Should swiping trigger pagination
|
||||||
|
/// </summary>
|
||||||
|
public bool SwipeToPaginate { get; set; }
|
||||||
|
/// <summary>
|
||||||
/// Book Reader Option: Override extra Margin
|
/// Book Reader Option: Override extra Margin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int BookReaderMargin { get; set; } = 15;
|
public int BookReaderMargin { get; set; } = 15;
|
||||||
|
@ -105,4 +105,10 @@ public enum ServerSettingKey
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("ConvertCoverToWebP")]
|
[Description("ConvertCoverToWebP")]
|
||||||
ConvertCoverToWebP = 19,
|
ConvertCoverToWebP = 19,
|
||||||
|
/// <summary>
|
||||||
|
/// The Host name (ie Reverse proxy domain name) for the server. Used for email link generation
|
||||||
|
/// </summary>
|
||||||
|
[Description("HostName")]
|
||||||
|
HostName = 20,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,9 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
|
|||||||
case ServerSettingKey.TotalLogs:
|
case ServerSettingKey.TotalLogs:
|
||||||
destination.TotalLogs = int.Parse(row.Value);
|
destination.TotalLogs = int.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.HostName:
|
||||||
|
destination.HostName = row.Value;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Web;
|
||||||
using API.Constants;
|
using API.Constants;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Errors;
|
using API.Errors;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services;
|
namespace API.Services;
|
||||||
@ -20,6 +23,8 @@ public interface IAccountService
|
|||||||
Task<IEnumerable<ApiException>> ValidateEmail(string email);
|
Task<IEnumerable<ApiException>> ValidateEmail(string email);
|
||||||
Task<bool> HasBookmarkPermission(AppUser user);
|
Task<bool> HasBookmarkPermission(AppUser user);
|
||||||
Task<bool> HasDownloadPermission(AppUser user);
|
Task<bool> HasDownloadPermission(AppUser user);
|
||||||
|
Task<bool> CheckIfAccessible(HttpRequest request);
|
||||||
|
Task<string> GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AccountService : IAccountService
|
public class AccountService : IAccountService
|
||||||
@ -27,13 +32,44 @@ public class AccountService : IAccountService
|
|||||||
private readonly UserManager<AppUser> _userManager;
|
private readonly UserManager<AppUser> _userManager;
|
||||||
private readonly ILogger<AccountService> _logger;
|
private readonly ILogger<AccountService> _logger;
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IHostEnvironment _environment;
|
||||||
|
private readonly IEmailService _emailService;
|
||||||
public const string DefaultPassword = "[k.2@RZ!mxCQkJzE";
|
public const string DefaultPassword = "[k.2@RZ!mxCQkJzE";
|
||||||
|
private const string LocalHost = "localhost:4200";
|
||||||
|
|
||||||
public AccountService(UserManager<AppUser> userManager, ILogger<AccountService> logger, IUnitOfWork unitOfWork)
|
public AccountService(UserManager<AppUser> userManager, ILogger<AccountService> logger, IUnitOfWork unitOfWork,
|
||||||
|
IHostEnvironment environment, IEmailService emailService)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
|
_environment = environment;
|
||||||
|
_emailService = emailService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the instance is accessible. If the host name is filled out, then it will assume it is accessible as email generation will use host name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<bool> CheckIfAccessible(HttpRequest request)
|
||||||
|
{
|
||||||
|
var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString();
|
||||||
|
return !string.IsNullOrEmpty((await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).HostName) || await _emailService.CheckIfAccessible(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GenerateEmailLink(HttpRequest request, string token, string routePart, string email, bool withHost = true)
|
||||||
|
{
|
||||||
|
var serverSettings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
|
||||||
|
var host = _environment.IsDevelopment() ? LocalHost : request.Host.ToString();
|
||||||
|
var basePart = $"{request.Scheme}://{host}{request.PathBase}/";
|
||||||
|
if (!string.IsNullOrEmpty(serverSettings.HostName))
|
||||||
|
{
|
||||||
|
basePart = serverSettings.HostName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withHost) return $"{basePart}/registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
||||||
|
return $"registration/{routePart}?token={HttpUtility.UrlEncode(token)}&email={HttpUtility.UrlEncode(email)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword)
|
public async Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword)
|
||||||
|
@ -74,7 +74,14 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
|
|
||||||
public async Task StartWatching()
|
public async Task StartWatching()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("[LibraryWatcher] Starting file watchers");
|
FileWatchers.Clear();
|
||||||
|
WatcherDictionary.Clear();
|
||||||
|
|
||||||
|
if (!(await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EnableFolderWatching)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Folder watching is disabled at the server level, thus ignoring any requests to create folder watching");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
var libraryFolders = (await _unitOfWork.LibraryRepository.GetLibraryDtosAsync())
|
||||||
.Where(l => l.FolderWatching)
|
.Where(l => l.FolderWatching)
|
||||||
@ -84,6 +91,8 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
.Where(_directoryService.Exists)
|
.Where(_directoryService.Exists)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
_logger.LogInformation("[LibraryWatcher] Starting file watchers for {Count} library folders", libraryFolders.Count);
|
||||||
|
|
||||||
foreach (var libraryFolder in libraryFolders)
|
foreach (var libraryFolder in libraryFolders)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("[LibraryWatcher] Watching {FolderPath}", libraryFolder);
|
_logger.LogDebug("[LibraryWatcher] Watching {FolderPath}", libraryFolder);
|
||||||
@ -107,7 +116,7 @@ public class LibraryWatcher : ILibraryWatcher
|
|||||||
|
|
||||||
WatcherDictionary[libraryFolder].Add(watcher);
|
WatcherDictionary[libraryFolder].Add(watcher);
|
||||||
}
|
}
|
||||||
_logger.LogInformation("[LibraryWatcher] Watching {Count} folders", FileWatchers.Count);
|
_logger.LogInformation("[LibraryWatcher] Watching {Count} folders", libraryFolders.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopWatching()
|
public void StopWatching()
|
||||||
|
@ -19,6 +19,7 @@ export interface Preferences {
|
|||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
showScreenHints: boolean;
|
showScreenHints: boolean;
|
||||||
emulateBook: boolean;
|
emulateBook: boolean;
|
||||||
|
swipeToPaginate: boolean;
|
||||||
|
|
||||||
// Book Reader
|
// Book Reader
|
||||||
bookReaderMargin: number;
|
bookReaderMargin: number;
|
||||||
|
@ -14,4 +14,5 @@ export interface ServerSettings {
|
|||||||
totalBackups: number;
|
totalBackups: number;
|
||||||
totalLogs: number;
|
totalLogs: number;
|
||||||
enableFolderWatching: boolean;
|
enableFolderWatching: boolean;
|
||||||
|
hostName: string;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||||
<p class="text-warning pt-2">Port and Swagger require a manual restart of Kavita to take effect.</p>
|
<p class="text-warning pt-2">Changing Port requires a manual restart of Kavita to take effect.</p>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="settings-cachedir" class="form-label">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
<label for="settings-cachedir" class="form-label">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #cacheDirectoryTooltip>Where the server place temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
<ng-template #cacheDirectoryTooltip>Where the server place temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
||||||
@ -20,6 +20,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="settings-hostname" class="form-label">Host Name</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #hostNameTooltip>Domain Name (of Reverse Proxy). If set, email generation will always use this.</ng-template>
|
||||||
|
<span class="visually-hidden" id="settings-hostname-help">Domain Name (of Reverse Proxy). If set, email generation will always use this.</span>
|
||||||
|
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
|
||||||
|
[class.is-invalid]="settingsForm.get('hostName')?.invalid && settingsForm.get('hostName')?.touched">
|
||||||
|
<div id="hostname-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||||
|
<div *ngIf="settingsForm.get('hostName')?.errors?.pattern">
|
||||||
|
Host name must start with http(s) and not end in /
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 mb-2">
|
<div class="row g-0 mb-2">
|
||||||
<div class="col-md-3 col-sm-12 pe-2">
|
<div class="col-md-3 col-sm-12 pe-2">
|
||||||
<label for="settings-port" class="form-label">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
<label for="settings-port" class="form-label">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
||||||
|
@ -51,6 +51,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.addControl('totalLogs', new FormControl(this.serverSettings.totalLogs, [Validators.required, Validators.min(1), Validators.max(30)]));
|
this.settingsForm.addControl('totalLogs', new FormControl(this.serverSettings.totalLogs, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||||
this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
|
this.settingsForm.addControl('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
|
||||||
this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, []));
|
this.settingsForm.addControl('convertBookmarkToWebP', new FormControl(this.serverSettings.convertBookmarkToWebP, []));
|
||||||
|
this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [Validators.pattern(/^(http:|https:)+[^\s]+[\w]$/)]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.get('totalLogs')?.setValue(this.serverSettings.totalLogs);
|
this.settingsForm.get('totalLogs')?.setValue(this.serverSettings.totalLogs);
|
||||||
this.settingsForm.get('enableFolderWatching')?.setValue(this.serverSettings.enableFolderWatching);
|
this.settingsForm.get('enableFolderWatching')?.setValue(this.serverSettings.enableFolderWatching);
|
||||||
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
|
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
|
||||||
|
this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName);
|
||||||
this.settingsForm.markAsPristine();
|
this.settingsForm.markAsPristine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,14 +134,12 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
await this.confirmService.alert(
|
await this.confirmService.alert(
|
||||||
'Please click this link to confirm your email. You must confirm to be able to login. You may need to log out of the current account before clicking. <br/> <a href="' + email + '" target="_blank" rel="noopener noreferrer">' + email + '</a>');
|
'Please click this link to confirm your email. You must confirm to be able to login. You may need to log out of the current account before clicking. <br/> <a href="' + email + '" target="_blank" rel="noopener noreferrer">' + email + '</a>');
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setup(member: Member) {
|
setup(member: Member) {
|
||||||
this.accountService.getInviteUrl(member.id, false).subscribe(url => {
|
this.accountService.getInviteUrl(member.id, false).subscribe(url => {
|
||||||
console.log('Invite Url: ', url);
|
|
||||||
if (url) {
|
if (url) {
|
||||||
this.router.navigateByUrl(url);
|
this.router.navigateByUrl(url);
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<app-loading [loading]="isLoading"></app-loading>
|
<app-loading [loading]="isLoading" [absolute]="true"></app-loading>
|
||||||
<div class="reading-area"
|
<div class="reading-area"
|
||||||
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
|
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
|
||||||
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
|
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
|
||||||
@ -233,6 +233,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" id="swipe-to-paginate" formControlName="swipeToPaginate" class="form-check-input" >
|
||||||
|
<label class="form-check-label" for="swipe-to-paginate">Swipe Enabled</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 col-sm-12">
|
<div class="col-md-3 col-sm-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -251,8 +260,10 @@
|
|||||||
<input type="range" class="form-range" id="darkness"
|
<input type="range" class="form-range" id="darkness"
|
||||||
min="10" max="100" step="1" formControlName="darkness">
|
min="10" max="100" step="1" formControlName="darkness">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-md-6 col-sm-12">
|
||||||
<button class="btn btn-primary" (click)="savePref()">Save to Preferences</button>
|
<button class="btn btn-primary" (click)="savePref()">Save Globally</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -467,7 +467,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)),
|
fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)),
|
||||||
layoutMode: new FormControl(this.layoutMode),
|
layoutMode: new FormControl(this.layoutMode),
|
||||||
darkness: new FormControl(100),
|
darkness: new FormControl(100),
|
||||||
emulateBook: new FormControl(this.user.preferences.emulateBook)
|
emulateBook: new FormControl(this.user.preferences.emulateBook),
|
||||||
|
swipeToPaginate: new FormControl(this.user.preferences.swipeToPaginate)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.readerModeSubject.next(this.readerMode);
|
this.readerModeSubject.next(this.readerMode);
|
||||||
@ -973,6 +974,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
triggerSwipePagination(direction: KeyDirection) {
|
triggerSwipePagination(direction: KeyDirection) {
|
||||||
|
if (!this.generalSettingsForm.get('swipeToPaginate')?.value) return;
|
||||||
|
|
||||||
switch(direction) {
|
switch(direction) {
|
||||||
case KeyDirection.Down:
|
case KeyDirection.Down:
|
||||||
this.nextPage();
|
this.nextPage();
|
||||||
@ -1098,6 +1101,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.resetSwipeModifiers();
|
this.resetSwipeModifiers();
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
|
||||||
this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD);
|
this.pagingDirectionSubject.next(PAGING_DIRECTION.FORWARD);
|
||||||
|
|
||||||
const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.singleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
|
const pageAmount = Math.max(this.canvasRenderer.getPageAmount(PAGING_DIRECTION.FORWARD), this.singleRenderer.getPageAmount(PAGING_DIRECTION.FORWARD),
|
||||||
@ -1125,6 +1131,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
this.resetSwipeModifiers();
|
this.resetSwipeModifiers();
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
|
||||||
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
|
this.pagingDirectionSubject.next(PAGING_DIRECTION.BACKWARDS);
|
||||||
|
|
||||||
|
|
||||||
@ -1241,6 +1250,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
// Originally this was only for fit to height, but when swiping was introduced, it made more sense to do it always to reset to the same view
|
// Originally this was only for fit to height, but when swiping was introduced, it made more sense to do it always to reset to the same view
|
||||||
this.readingArea.nativeElement.scroll(0,0);
|
this.readingArea.nativeElement.scroll(0,0);
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1601,6 +1611,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
data.autoCloseMenu = this.autoCloseMenu;
|
data.autoCloseMenu = this.autoCloseMenu;
|
||||||
data.readingDirection = this.readingDirection;
|
data.readingDirection = this.readingDirection;
|
||||||
data.emulateBook = modelSettings.emulateBook;
|
data.emulateBook = modelSettings.emulateBook;
|
||||||
|
data.swipeToPaginate = modelSettings.swipeToPaginate;
|
||||||
this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||||
this.toastr.success('User preferences updated');
|
this.toastr.success('User preferences updated');
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
|
@ -165,9 +165,13 @@
|
|||||||
<li class="list-group-item dark-menu-item" *ngIf="onlineUsers.length > 1">
|
<li class="list-group-item dark-menu-item" *ngIf="onlineUsers.length > 1">
|
||||||
<div>{{onlineUsers.length}} Users online</div>
|
<div>{{onlineUsers.length}} Users online</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item dark-menu-item" *ngIf="activeEvents === 0 && onlineUsers.length <= 1">Not much going on here</li>
|
|
||||||
<li class="list-group-item dark-menu-item" *ngIf="debugMode">Active Events: {{activeEvents}}</li>
|
<li class="list-group-item dark-menu-item" *ngIf="debugMode">Active Events: {{activeEvents}}</li>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="downloadService.activeDownloads$ | async as activeDownloads">
|
||||||
|
<li class="list-group-item dark-menu-item" *ngIf="activeEvents === 0 && activeDownloads.length === 0">Not much going on here</li>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
@ -55,7 +55,8 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
|
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
|
||||||
private accountService: AccountService, private confirmService: ConfirmService,
|
private accountService: AccountService, private confirmService: ConfirmService,
|
||||||
private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) { }
|
private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) {
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onDestroy.next();
|
this.onDestroy.next();
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<div class="d-flex justify-content-center">
|
<ng-container *ngIf="absolute; else relative">
|
||||||
|
<div class="position-absolute top-50 start-50 translate-middle" style="z-index: 999">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Loading...</span>
|
<span class="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #relative>
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
@ -10,10 +10,15 @@ export class LoadingComponent implements OnInit {
|
|||||||
|
|
||||||
@Input() loading: boolean = false;
|
@Input() loading: boolean = false;
|
||||||
@Input() message: string = '';
|
@Input() message: string = '';
|
||||||
|
/**
|
||||||
|
* Uses absolute positioning to ensure it loads over content
|
||||||
|
*/
|
||||||
|
@Input() absolute: boolean = false;
|
||||||
|
|
||||||
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
console.log('absolute: ', this.absolute);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -179,6 +179,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||||
|
<div class="mb-3 mt-1">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input type="checkbox" id="swipe-to-paginate" role="switch" formControlName="swipeToPaginate" class="form-check-input" [value]="true">
|
||||||
|
<label class="form-check-label me-1" for="swipe-to-paginate">Swipe to Paginate</label><i class="fa fa-info-circle" aria-hidden="true" placement="top" ngbTooltip="Should swiping on the screen cause the next or previous page to be triggered" role="button" tabindex="0"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -127,6 +127,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
|
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
|
||||||
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, []));
|
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, []));
|
||||||
this.settingsForm.addControl('emulateBook', new FormControl(this.user.preferences.emulateBook, []));
|
this.settingsForm.addControl('emulateBook', new FormControl(this.user.preferences.emulateBook, []));
|
||||||
|
this.settingsForm.addControl('swipeToPaginate', new FormControl(this.user.preferences.swipeToPaginate, []));
|
||||||
|
|
||||||
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
|
this.settingsForm.addControl('bookReaderFontFamily', new FormControl(this.user.preferences.bookReaderFontFamily, []));
|
||||||
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
|
this.settingsForm.addControl('bookReaderFontSize', new FormControl(this.user.preferences.bookReaderFontSize, []));
|
||||||
@ -187,6 +188,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
this.settingsForm.get('promptForDownloadSize')?.setValue(this.user.preferences.promptForDownloadSize);
|
this.settingsForm.get('promptForDownloadSize')?.setValue(this.user.preferences.promptForDownloadSize);
|
||||||
this.settingsForm.get('noTransitions')?.setValue(this.user.preferences.noTransitions);
|
this.settingsForm.get('noTransitions')?.setValue(this.user.preferences.noTransitions);
|
||||||
this.settingsForm.get('emulateBook')?.setValue(this.user.preferences.emulateBook);
|
this.settingsForm.get('emulateBook')?.setValue(this.user.preferences.emulateBook);
|
||||||
|
this.settingsForm.get('swipeToPaginate')?.setValue(this.user.preferences.swipeToPaginate);
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
this.settingsForm.markAsPristine();
|
this.settingsForm.markAsPristine();
|
||||||
}
|
}
|
||||||
@ -217,7 +219,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
blurUnreadSummaries: modelSettings.blurUnreadSummaries,
|
blurUnreadSummaries: modelSettings.blurUnreadSummaries,
|
||||||
promptForDownloadSize: modelSettings.promptForDownloadSize,
|
promptForDownloadSize: modelSettings.promptForDownloadSize,
|
||||||
noTransitions: modelSettings.noTransitions,
|
noTransitions: modelSettings.noTransitions,
|
||||||
emulateBook: modelSettings.emulateBook
|
emulateBook: modelSettings.emulateBook,
|
||||||
|
swipeToPaginate: modelSettings.swipeToPaginate
|
||||||
};
|
};
|
||||||
|
|
||||||
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||||
|
20
openapi.json
20
openapi.json
@ -7,7 +7,7 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"version": "0.6.1.28"
|
"version": "0.6.1.29"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
@ -729,10 +729,12 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Account"
|
"Account"
|
||||||
],
|
],
|
||||||
|
"summary": "Resend an invite to a user already invited",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "userId",
|
"name": "userId",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
|
"description": "",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int32"
|
"format": "int32"
|
||||||
@ -7245,6 +7247,7 @@
|
|||||||
"Server"
|
"Server"
|
||||||
],
|
],
|
||||||
"summary": "Is this server accessible to the outside net",
|
"summary": "Is this server accessible to the outside net",
|
||||||
|
"description": "If the instance has the HostName set, this will return true whether or not it is accessible externally",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Success",
|
"description": "Success",
|
||||||
@ -9511,6 +9514,10 @@
|
|||||||
"description": "Manga Reader Option: Background color of the reader",
|
"description": "Manga Reader Option: Background color of the reader",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
},
|
},
|
||||||
|
"swipeToPaginate": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Manga Reader Option: Should swiping trigger pagination"
|
||||||
|
},
|
||||||
"bookReaderMargin": {
|
"bookReaderMargin": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Book Reader Option: Override extra Margin",
|
"description": "Book Reader Option: Override extra Margin",
|
||||||
@ -13318,6 +13325,11 @@
|
|||||||
"convertCoverToWebP": {
|
"convertCoverToWebP": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "If the server should save covers as WebP encoding"
|
"description": "If the server should save covers as WebP encoding"
|
||||||
|
},
|
||||||
|
"hostName": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The Host name (ie Reverse proxy domain name) for the server",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@ -14285,7 +14297,8 @@
|
|||||||
"readerMode",
|
"readerMode",
|
||||||
"readingDirection",
|
"readingDirection",
|
||||||
"scalingOption",
|
"scalingOption",
|
||||||
"showScreenHints"
|
"showScreenHints",
|
||||||
|
"swipeToPaginate"
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -14313,6 +14326,9 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Manga Reader Option: Background color of the reader"
|
"description": "Manga Reader Option: Background color of the reader"
|
||||||
},
|
},
|
||||||
|
"swipeToPaginate": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"autoCloseMenu": {
|
"autoCloseMenu": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Manga Reader Option: Allow the menu to close after 6 seconds without interaction"
|
"description": "Manga Reader Option: Allow the menu to close after 6 seconds without interaction"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user