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 Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<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.IO.Abstractions" Version="17.2.3" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.0-alpha1" />
|
||||
|
@ -324,10 +324,11 @@ public class AccountController : BaseApiController
|
||||
// Send a confirmation email
|
||||
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);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
var accessible = await _emailService.CheckIfAccessible(host);
|
||||
|
||||
|
||||
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||
if (accessible)
|
||||
{
|
||||
try
|
||||
@ -495,7 +496,7 @@ public class AccountController : BaseApiController
|
||||
if (string.IsNullOrEmpty(user.ConfirmationToken))
|
||||
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
|
||||
{
|
||||
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]: Token {UserName}: {Token}", user.UserName, user.ConfirmationToken);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
var accessible = await _emailService.CheckIfAccessible(host);
|
||||
var accessible = await _accountService.CheckIfAccessible(Request);
|
||||
if (accessible)
|
||||
{
|
||||
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");
|
||||
|
||||
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);
|
||||
var host = _environment.IsDevelopment() ? "localhost:4200" : Request.Host.ToString();
|
||||
if (await _emailService.CheckIfAccessible(host))
|
||||
if (await _accountService.CheckIfAccessible(Request))
|
||||
{
|
||||
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")]
|
||||
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");
|
||||
|
||||
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]: Token {UserName}: {Token}", user.UserName, token);
|
||||
if (await _accountService.CheckIfAccessible(Request))
|
||||
{
|
||||
await _emailService.SendMigrationEmail(new EmailMigrationDto()
|
||||
{
|
||||
EmailAddress = user.Email,
|
||||
@ -873,16 +879,10 @@ public class AccountController : BaseApiController
|
||||
ServerConfirmationLink = emailLink,
|
||||
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
|
||||
});
|
||||
|
||||
|
||||
return Ok(emailLink);
|
||||
}
|
||||
|
||||
private string GenerateEmailLink(string token, string routePart, string email, bool withHost = true)
|
||||
{
|
||||
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)}";
|
||||
return Ok("The server is not accessible externally. Please ");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -36,10 +36,11 @@ public class ServerController : BaseApiController
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
private readonly IScannerService _scannerService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public ServerController(IHostApplicationLifetime applicationLifetime, ILogger<ServerController> logger,
|
||||
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;
|
||||
_logger = logger;
|
||||
@ -51,6 +52,7 @@ public class ServerController : BaseApiController
|
||||
_emailService = emailService;
|
||||
_bookmarkService = bookmarkService;
|
||||
_scannerService = scannerService;
|
||||
_accountService = accountService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -189,12 +191,13 @@ public class ServerController : BaseApiController
|
||||
/// <summary>
|
||||
/// Is this server accessible to the outside net
|
||||
/// </summary>
|
||||
/// <remarks>If the instance has the HostName set, this will return true whether or not it is accessible externally</remarks>
|
||||
/// <returns></returns>
|
||||
[HttpGet("accessible")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<bool>> IsServerAccessible()
|
||||
{
|
||||
return await _emailService.CheckIfAccessible(Request.Host.ToString());
|
||||
return Ok(await _accountService.CheckIfAccessible(Request));
|
||||
}
|
||||
|
||||
[HttpGet("jobs")]
|
||||
|
@ -182,6 +182,13 @@ public class SettingsController : BaseApiController
|
||||
_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)
|
||||
{
|
||||
|
@ -111,6 +111,7 @@ public class UsersController : BaseApiController
|
||||
existingPreferences.LayoutMode = preferencesDto.LayoutMode;
|
||||
existingPreferences.PromptForDownloadSize = preferencesDto.PromptForDownloadSize;
|
||||
existingPreferences.NoTransitions = preferencesDto.NoTransitions;
|
||||
existingPreferences.SwipeToPaginate = preferencesDto.SwipeToPaginate;
|
||||
|
||||
_unitOfWork.UserRepository.Update(existingPreferences);
|
||||
|
||||
|
@ -66,4 +66,8 @@ public class ServerSettingDto
|
||||
/// If the server should save covers as WebP encoding
|
||||
/// </summary>
|
||||
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>
|
||||
[Required]
|
||||
public string BackgroundColor { get; set; } = "#000000";
|
||||
[Required]
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Should swiping trigger pagination
|
||||
/// </summary>
|
||||
public bool SwipeToPaginate { get; set; }
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||
/// </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")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("SwipeToPaginate")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("ThemeId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
@ -102,6 +102,7 @@ public static class Seed
|
||||
new() {Key = ServerSettingKey.TotalLogs, Value = "30"},
|
||||
new() {Key = ServerSettingKey.EnableFolderWatching, Value = "false"},
|
||||
new() {Key = ServerSettingKey.ConvertCoverToWebP, Value = "false"},
|
||||
new() {Key = ServerSettingKey.HostName, Value = string.Empty},
|
||||
}.ToArray());
|
||||
|
||||
foreach (var defaultSetting in DefaultSettings)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using API.Entities.Enums;
|
||||
using System;
|
||||
using API.Entities.Enums;
|
||||
using API.Entities.Enums.UserPreferences;
|
||||
|
||||
namespace API.Entities;
|
||||
@ -26,7 +27,6 @@ public class AppUserPreferences
|
||||
/// </example>
|
||||
/// </summary>
|
||||
public ReaderMode ReaderMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Allow the menu to close after 6 seconds without interaction
|
||||
/// </summary>
|
||||
@ -48,6 +48,10 @@ public class AppUserPreferences
|
||||
/// </summary>
|
||||
public string BackgroundColor { get; set; } = "#000000";
|
||||
/// <summary>
|
||||
/// Manga Reader Option: Should swiping trigger pagination
|
||||
/// </summary>
|
||||
public bool SwipeToPaginate { get; set; }
|
||||
/// <summary>
|
||||
/// Book Reader Option: Override extra Margin
|
||||
/// </summary>
|
||||
public int BookReaderMargin { get; set; } = 15;
|
||||
|
@ -105,4 +105,10 @@ public enum ServerSettingKey
|
||||
/// </summary>
|
||||
[Description("ConvertCoverToWebP")]
|
||||
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:
|
||||
destination.TotalLogs = int.Parse(row.Value);
|
||||
break;
|
||||
case ServerSettingKey.HostName:
|
||||
destination.HostName = row.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,15 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using API.Constants;
|
||||
using API.Data;
|
||||
using API.Entities;
|
||||
using API.Errors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services;
|
||||
@ -20,6 +23,8 @@ public interface IAccountService
|
||||
Task<IEnumerable<ApiException>> ValidateEmail(string email);
|
||||
Task<bool> HasBookmarkPermission(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
|
||||
@ -27,13 +32,44 @@ public class AccountService : IAccountService
|
||||
private readonly UserManager<AppUser> _userManager;
|
||||
private readonly ILogger<AccountService> _logger;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IHostEnvironment _environment;
|
||||
private readonly IEmailService _emailService;
|
||||
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;
|
||||
_logger = logger;
|
||||
_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)
|
||||
|
@ -74,7 +74,14 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
|
||||
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())
|
||||
.Where(l => l.FolderWatching)
|
||||
@ -84,6 +91,8 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
.Where(_directoryService.Exists)
|
||||
.ToList();
|
||||
|
||||
_logger.LogInformation("[LibraryWatcher] Starting file watchers for {Count} library folders", libraryFolders.Count);
|
||||
|
||||
foreach (var libraryFolder in libraryFolders)
|
||||
{
|
||||
_logger.LogDebug("[LibraryWatcher] Watching {FolderPath}", libraryFolder);
|
||||
@ -107,7 +116,7 @@ public class LibraryWatcher : ILibraryWatcher
|
||||
|
||||
WatcherDictionary[libraryFolder].Add(watcher);
|
||||
}
|
||||
_logger.LogInformation("[LibraryWatcher] Watching {Count} folders", FileWatchers.Count);
|
||||
_logger.LogInformation("[LibraryWatcher] Watching {Count} folders", libraryFolders.Count);
|
||||
}
|
||||
|
||||
public void StopWatching()
|
||||
|
@ -19,6 +19,7 @@ export interface Preferences {
|
||||
backgroundColor: string;
|
||||
showScreenHints: boolean;
|
||||
emulateBook: boolean;
|
||||
swipeToPaginate: boolean;
|
||||
|
||||
// Book Reader
|
||||
bookReaderMargin: number;
|
||||
|
@ -14,4 +14,5 @@ export interface ServerSettings {
|
||||
totalBackups: number;
|
||||
totalLogs: number;
|
||||
enableFolderWatching: boolean;
|
||||
hostName: string;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="container-fluid">
|
||||
<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">
|
||||
<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>
|
||||
@ -20,6 +20,19 @@
|
||||
</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="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>
|
||||
|
@ -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('enableFolderWatching', new FormControl(this.serverSettings.enableFolderWatching, [Validators.required]));
|
||||
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('enableFolderWatching')?.setValue(this.serverSettings.enableFolderWatching);
|
||||
this.settingsForm.get('convertBookmarkToWebP')?.setValue(this.serverSettings.convertBookmarkToWebP);
|
||||
this.settingsForm.get('hostName')?.setValue(this.serverSettings.hostName);
|
||||
this.settingsForm.markAsPristine();
|
||||
}
|
||||
|
||||
|
@ -134,14 +134,12 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
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>');
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setup(member: Member) {
|
||||
this.accountService.getInviteUrl(member.id, false).subscribe(url => {
|
||||
console.log('Invite Url: ', url);
|
||||
if (url) {
|
||||
this.router.navigateByUrl(url);
|
||||
}
|
||||
|
@ -26,7 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-loading [loading]="isLoading"></app-loading>
|
||||
<app-loading [loading]="isLoading" [absolute]="true"></app-loading>
|
||||
<div class="reading-area"
|
||||
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
|
||||
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
|
||||
@ -233,6 +233,15 @@
|
||||
</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 class="col-md-3 col-sm-12">
|
||||
<div class="mb-3">
|
||||
@ -251,8 +260,10 @@
|
||||
<input type="range" class="form-range" id="darkness"
|
||||
min="10" max="100" step="1" formControlName="darkness">
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
</form>
|
||||
|
@ -467,7 +467,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
fittingOption: new FormControl(this.mangaReaderService.translateScalingOption(this.scalingOption)),
|
||||
layoutMode: new FormControl(this.layoutMode),
|
||||
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);
|
||||
@ -973,6 +974,8 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
triggerSwipePagination(direction: KeyDirection) {
|
||||
if (!this.generalSettingsForm.get('swipeToPaginate')?.value) return;
|
||||
|
||||
switch(direction) {
|
||||
case KeyDirection.Down:
|
||||
this.nextPage();
|
||||
@ -1098,6 +1101,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.resetSwipeModifiers();
|
||||
|
||||
this.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
this.pagingDirectionSubject.next(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.isLoading = true;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
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
|
||||
this.readingArea.nativeElement.scroll(0,0);
|
||||
|
||||
this.isLoading = false;
|
||||
this.cdRef.markForCheck();
|
||||
}
|
||||
|
||||
@ -1601,6 +1611,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
data.autoCloseMenu = this.autoCloseMenu;
|
||||
data.readingDirection = this.readingDirection;
|
||||
data.emulateBook = modelSettings.emulateBook;
|
||||
data.swipeToPaginate = modelSettings.swipeToPaginate;
|
||||
this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||
this.toastr.success('User preferences updated');
|
||||
if (this.user) {
|
||||
|
@ -165,9 +165,13 @@
|
||||
<li class="list-group-item dark-menu-item" *ngIf="onlineUsers.length > 1">
|
||||
<div>{{onlineUsers.length}} Users online</div>
|
||||
</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>
|
||||
</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>
|
||||
</ng-template>
|
||||
</ng-container>
|
@ -55,7 +55,8 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(public messageHub: MessageHubService, private modalService: NgbModal,
|
||||
private accountService: AccountService, private confirmService: ConfirmService,
|
||||
private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) { }
|
||||
private readonly cdRef: ChangeDetectorRef, public downloadService: DownloadService) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
|
@ -1,7 +1,17 @@
|
||||
<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">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</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() message: string = '';
|
||||
/**
|
||||
* Uses absolute positioning to ensure it loads over content
|
||||
*/
|
||||
@Input() absolute: boolean = false;
|
||||
|
||||
constructor(private readonly cdRef: ChangeDetectorRef) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
console.log('absolute: ', this.absolute);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -179,6 +179,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
|
@ -127,6 +127,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
this.settingsForm.addControl('readerMode', new FormControl(this.user.preferences.readerMode, []));
|
||||
this.settingsForm.addControl('layoutMode', new FormControl(this.user.preferences.layoutMode, []));
|
||||
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('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('noTransitions')?.setValue(this.user.preferences.noTransitions);
|
||||
this.settingsForm.get('emulateBook')?.setValue(this.user.preferences.emulateBook);
|
||||
this.settingsForm.get('swipeToPaginate')?.setValue(this.user.preferences.swipeToPaginate);
|
||||
this.cdRef.markForCheck();
|
||||
this.settingsForm.markAsPristine();
|
||||
}
|
||||
@ -217,7 +219,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
||||
blurUnreadSummaries: modelSettings.blurUnreadSummaries,
|
||||
promptForDownloadSize: modelSettings.promptForDownloadSize,
|
||||
noTransitions: modelSettings.noTransitions,
|
||||
emulateBook: modelSettings.emulateBook
|
||||
emulateBook: modelSettings.emulateBook,
|
||||
swipeToPaginate: modelSettings.swipeToPaginate
|
||||
};
|
||||
|
||||
this.observableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||
|
20
openapi.json
20
openapi.json
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.6.1.28"
|
||||
"version": "0.6.1.29"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
@ -729,10 +729,12 @@
|
||||
"tags": [
|
||||
"Account"
|
||||
],
|
||||
"summary": "Resend an invite to a user already invited",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "query",
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
@ -7245,6 +7247,7 @@
|
||||
"Server"
|
||||
],
|
||||
"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": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
@ -9511,6 +9514,10 @@
|
||||
"description": "Manga Reader Option: Background color of the reader",
|
||||
"nullable": true
|
||||
},
|
||||
"swipeToPaginate": {
|
||||
"type": "boolean",
|
||||
"description": "Manga Reader Option: Should swiping trigger pagination"
|
||||
},
|
||||
"bookReaderMargin": {
|
||||
"type": "integer",
|
||||
"description": "Book Reader Option: Override extra Margin",
|
||||
@ -13318,6 +13325,11 @@
|
||||
"convertCoverToWebP": {
|
||||
"type": "boolean",
|
||||
"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
|
||||
@ -14285,7 +14297,8 @@
|
||||
"readerMode",
|
||||
"readingDirection",
|
||||
"scalingOption",
|
||||
"showScreenHints"
|
||||
"showScreenHints",
|
||||
"swipeToPaginate"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -14313,6 +14326,9 @@
|
||||
"type": "string",
|
||||
"description": "Manga Reader Option: Background color of the reader"
|
||||
},
|
||||
"swipeToPaginate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"autoCloseMenu": {
|
||||
"type": "boolean",
|
||||
"description": "Manga Reader Option: Allow the menu to close after 6 seconds without interaction"
|
||||
|
Loading…
x
Reference in New Issue
Block a user