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:
Joe Milazzo 2023-01-30 00:50:19 -08:00 committed by GitHub
parent 2a47029209
commit 5e9bbd0768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1986 additions and 49 deletions

View File

@ -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" />

View File

@ -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,26 +867,22 @@ 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);
await _emailService.SendMigrationEmail(new EmailMigrationDto()
if (await _accountService.CheckIfAccessible(Request))
{
EmailAddress = user.Email,
Username = user.UserName,
ServerConfirmationLink = emailLink,
InstallId = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallId)).Value
});
await _emailService.SendMigrationEmail(new EmailMigrationDto()
{
EmailAddress = user.Email,
Username = user.UserName,
ServerConfirmationLink = emailLink,
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)
{
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>

View File

@ -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")]

View File

@ -104,7 +104,7 @@ public class SettingsController : BaseApiController
[HttpPost]
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto)
{
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
_logger.LogInformation("{UserName} is updating Server Settings", User.GetUsername());
// We do not allow CacheDirectory changes, so we will ignore.
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
@ -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)
{

View File

@ -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);

View File

@ -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; }
}

View File

@ -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>

File diff suppressed because it is too large Load Diff

View 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");
}
}
}

View File

@ -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");

View File

@ -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)

View File

@ -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;

View File

@ -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,
}

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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()

View File

@ -19,6 +19,7 @@ export interface Preferences {
backgroundColor: string;
showScreenHints: boolean;
emulateBook: boolean;
swipeToPaginate: boolean;
// Book Reader
bookReaderMargin: number;

View File

@ -14,4 +14,5 @@ export interface ServerSettings {
totalBackups: number;
totalLogs: number;
enableFolderWatching: boolean;
hostName: string;
}

View File

@ -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>&nbsp;<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>&nbsp;<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>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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();
@ -1097,6 +1100,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
}
this.resetSwipeModifiers();
this.isLoading = true;
this.cdRef.markForCheck();
this.pagingDirectionSubject.next(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) {

View File

@ -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>

View File

@ -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();

View File

@ -42,7 +42,7 @@ export class TimeAgoPipe implements PipeTransform, OnDestroy {
const seconds = Math.round(Math.abs((now.getTime() - d.getTime()) / 1000));
const timeToUpdate = (Number.isNaN(seconds)) ? 1000 : this.getSecondsUntilUpdate(seconds) * 1000;
this.timer = this.ngZone.runOutsideAngular(() => {
this.timer = this.ngZone.runOutsideAngular(() => {
if (typeof window !== 'undefined') {
return window.setTimeout(() => {
this.ngZone.run(() => this.changeDetectorRef.markForCheck());

View File

@ -1,7 +1,17 @@
<ng-container *ngIf="loading">
<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 class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</ng-template>
</ng-container>

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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) => {

View File

@ -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"