Misc Polishing (#413)

* Ensure that after we assign a role to a user, we show it immediately

* Cached libraryType api as that is not going to change in a viewing session. Moved some components around to tighten bundles.

* Cleaned up more TODOs
* Refactored Configuration to use getter and setters so that the interface is a lot cleaner. Updated HashUtil to use JWT Secret instead of Machine name (as docker machine name is random each boot).
This commit is contained in:
Joseph Milazzo 2021-07-20 21:39:44 -05:00 committed by GitHub
parent ef5b22b585
commit b8165b311c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 408 additions and 307 deletions

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using API.Data;
using API.DTOs; using API.DTOs;
using API.Entities.Enums; using API.Entities.Enums;
using API.Extensions; using API.Extensions;
@ -13,7 +12,6 @@ using Kavita.Common;
using Kavita.Common.Extensions; using Kavita.Common.Extensions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace API.Controllers namespace API.Controllers
@ -24,26 +22,24 @@ namespace API.Controllers
private readonly ILogger<SettingsController> _logger; private readonly ILogger<SettingsController> _logger;
private readonly IUnitOfWork _unitOfWork; private readonly IUnitOfWork _unitOfWork;
private readonly ITaskScheduler _taskScheduler; private readonly ITaskScheduler _taskScheduler;
private readonly IConfiguration _configuration;
public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler, IConfiguration configuration) public SettingsController(ILogger<SettingsController> logger, IUnitOfWork unitOfWork, ITaskScheduler taskScheduler)
{ {
_logger = logger; _logger = logger;
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_taskScheduler = taskScheduler; _taskScheduler = taskScheduler;
_configuration = configuration;
} }
[HttpGet("")] [HttpGet]
public async Task<ActionResult<ServerSettingDto>> GetSettings() public async Task<ActionResult<ServerSettingDto>> GetSettings()
{ {
var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var settingsDto = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
settingsDto.Port = Configuration.GetPort(Program.GetAppSettingFilename()); settingsDto.Port = Configuration.Port;
settingsDto.LoggingLevel = Configuration.GetLogLevel(Program.GetAppSettingFilename()); settingsDto.LoggingLevel = Configuration.LogLevel;
return Ok(settingsDto); return Ok(settingsDto);
} }
[HttpPost("")] [HttpPost]
public async Task<ActionResult<ServerSettingDto>> UpdateSettings(ServerSettingDto updateSettingsDto) 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());
@ -61,9 +57,6 @@ namespace API.Controllers
// We do not allow CacheDirectory changes, so we will ignore. // We do not allow CacheDirectory changes, so we will ignore.
var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync(); var currentSettings = await _unitOfWork.SettingsRepository.GetSettingsAsync();
var logLevelOptions = new LogLevelOptions();
_configuration.GetSection("Logging:LogLevel").Bind(logLevelOptions);
foreach (var setting in currentSettings) foreach (var setting in currentSettings)
{ {
if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value) if (setting.Key == ServerSettingKey.TaskBackup && updateSettingsDto.TaskBackup != setting.Value)
@ -78,24 +71,24 @@ namespace API.Controllers
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + "" != setting.Value) if (setting.Key == ServerSettingKey.Port && updateSettingsDto.Port + string.Empty != setting.Value)
{ {
setting.Value = updateSettingsDto.Port + ""; setting.Value = updateSettingsDto.Port + string.Empty;
// Port is managed in appSetting.json // Port is managed in appSetting.json
Configuration.UpdatePort(Program.GetAppSettingFilename(), updateSettingsDto.Port); Configuration.Port = updateSettingsDto.Port;
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + "" != setting.Value) if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
{ {
setting.Value = updateSettingsDto.LoggingLevel + ""; setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
Configuration.UpdateLogLevel(Program.GetAppSettingFilename(), updateSettingsDto.LoggingLevel); Configuration.LogLevel = updateSettingsDto.LoggingLevel;
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + "" != setting.Value) if (setting.Key == ServerSettingKey.AllowStatCollection && updateSettingsDto.AllowStatCollection + string.Empty != setting.Value)
{ {
setting.Value = updateSettingsDto.AllowStatCollection + ""; setting.Value = updateSettingsDto.AllowStatCollection + string.Empty;
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
if (!updateSettingsDto.AllowStatCollection) if (!updateSettingsDto.AllowStatCollection)
{ {
@ -108,7 +101,6 @@ namespace API.Controllers
} }
} }
_configuration.GetSection("Logging:LogLevel:Default").Value = updateSettingsDto.LoggingLevel + "";
if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated"); if (!_unitOfWork.HasChanges()) return Ok("Nothing was updated");
if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync()) if (!_unitOfWork.HasChanges() || !await _unitOfWork.CommitAsync())

View File

@ -61,11 +61,10 @@ namespace API.Data
await context.SaveChangesAsync(); await context.SaveChangesAsync();
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match // Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
var configFile = Program.GetAppSettingFilename();
context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.Port).Value = context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.Port).Value =
Configuration.GetPort(configFile) + ""; Configuration.Port + string.Empty;
context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.LoggingLevel).Value = context.ServerSetting.FirstOrDefault(s => s.Key == ServerSettingKey.LoggingLevel).Value =
Configuration.GetLogLevel(configFile); Configuration.LogLevel + string.Empty;
await context.SaveChangesAsync(); await context.SaveChangesAsync();

View File

@ -43,7 +43,7 @@ namespace API.Extensions
services.AddDbContext<DataContext>(options => services.AddDbContext<DataContext>(options =>
{ {
options.UseSqlite(config.GetConnectionString("DefaultConnection")); options.UseSqlite(config.GetConnectionString("DefaultConnection"));
options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.GetLogLevel(Program.GetAppSettingFilename()).Equals("Debug")); options.EnableSensitiveDataLogging(env.IsDevelopment() || Configuration.LogLevel.Equals("Debug"));
}); });
} }

View File

@ -20,37 +20,26 @@ namespace API
{ {
public class Program public class Program
{ {
private static int _httpPort; private static readonly int HttpPort = Configuration.Port;
protected Program() protected Program()
{ {
} }
public static string GetAppSettingFilename()
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var isDevelopment = environment == Environments.Development;
return "appsettings" + (isDevelopment ? ".Development" : "") + ".json";
}
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
Console.OutputEncoding = System.Text.Encoding.UTF8; Console.OutputEncoding = System.Text.Encoding.UTF8;
// Before anything, check if JWT has been generated properly or if user still has default // Before anything, check if JWT has been generated properly or if user still has default
if (!Configuration.CheckIfJwtTokenSet(GetAppSettingFilename()) && Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development) if (!Configuration.CheckIfJwtTokenSet() &&
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != Environments.Development)
{ {
Console.WriteLine("Generating JWT TokenKey for encrypting user sessions..."); Console.WriteLine("Generating JWT TokenKey for encrypting user sessions...");
var rBytes = new byte[128]; var rBytes = new byte[128];
using (var crypto = new RNGCryptoServiceProvider()) crypto.GetBytes(rBytes); using (var crypto = new RNGCryptoServiceProvider()) crypto.GetBytes(rBytes);
var base64 = Convert.ToBase64String(rBytes).Replace("/", ""); Configuration.JwtToken = Convert.ToBase64String(rBytes).Replace("/", string.Empty);
Configuration.UpdateJwtToken(GetAppSettingFilename(), base64);
} }
// Get HttpPort from Config
_httpPort = Configuration.GetPort(GetAppSettingFilename());
var host = CreateHostBuilder(args).Build(); var host = CreateHostBuilder(args).Build();
using var scope = host.Services.CreateScope(); using var scope = host.Services.CreateScope();
@ -67,7 +56,7 @@ namespace API
} }
catch (Exception ex) catch (Exception ex)
{ {
var logger = services.GetRequiredService <ILogger<Program>>(); var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred during migration"); logger.LogError(ex, "An error occurred during migration");
} }
@ -80,10 +69,7 @@ namespace API
{ {
webBuilder.UseKestrel((opts) => webBuilder.UseKestrel((opts) =>
{ {
opts.ListenAnyIP(_httpPort, options => opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; });
{
options.Protocols = HttpProtocols.Http1AndHttp2;
});
}); });
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
@ -139,7 +125,6 @@ namespace API
scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name); scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name);
scope.SetTag("branch", BuildInfo.Branch); scope.SetTag("branch", BuildInfo.Branch);
}); });
}); });
} }

View File

@ -146,7 +146,7 @@ namespace API
}); });
} }
private void OnShutdown() private static void OnShutdown()
{ {
Console.WriteLine("Server is shutting down. Please allow a few seconds to stop any background jobs..."); Console.WriteLine("Server is shutting down. Please allow a few seconds to stop any background jobs...");
TaskScheduler.Client.Dispose(); TaskScheduler.Client.Dispose();

View File

@ -2,38 +2,80 @@
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using Kavita.Common.EnvironmentInfo; using Kavita.Common.EnvironmentInfo;
using Microsoft.Extensions.Hosting;
namespace Kavita.Common namespace Kavita.Common
{ {
public static class Configuration public static class Configuration
{ {
#region JWT Token private static string AppSettingsFilename = GetAppSettingFilename();
public static bool CheckIfJwtTokenSet(string filePath) public static string Branch
{
get => GetBranch(GetAppSettingFilename());
set => SetBranch(GetAppSettingFilename(), value);
}
public static int Port
{
get => GetPort(GetAppSettingFilename());
set => SetPort(GetAppSettingFilename(), value);
}
public static string JwtToken
{
get => GetJwtToken(GetAppSettingFilename());
set => SetJwtToken(GetAppSettingFilename(), value);
}
public static string LogLevel
{
get => GetLogLevel(GetAppSettingFilename());
set => SetLogLevel(GetAppSettingFilename(), value);
}
private static string GetAppSettingFilename()
{
if (!string.IsNullOrEmpty(AppSettingsFilename))
{
return AppSettingsFilename;
}
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var isDevelopment = environment == Environments.Development;
return "appsettings" + (isDevelopment ? ".Development" : "") + ".json";
}
#region JWT Token
private static string GetJwtToken(string filePath)
{
try
{ {
try {
var json = File.ReadAllText(filePath); var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json); var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
const string key = "TokenKey"; const string key = "TokenKey";
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{ {
return tokenElement.GetString() != "super secret unguessable key"; return tokenElement.GetString();
} }
return false; return string.Empty;
} }
catch (Exception ex) { catch (Exception ex)
Console.WriteLine("Error writing app settings: " + ex.Message); {
Console.WriteLine("Error reading app settings: " + ex.Message);
} }
return false; return string.Empty;
} }
public static bool UpdateJwtToken(string filePath, string token)
private static bool SetJwtToken(string filePath, string token)
{ {
try try
{ {
var json = File.ReadAllText(filePath).Replace("super secret unguessable key", token); var currentToken = GetJwtToken(filePath);
var json = File.ReadAllText(filePath)
.Replace("\"TokenKey\": \"" + currentToken, "\"TokenKey\": \"" + token);
File.WriteAllText(filePath, json); File.WriteAllText(filePath, json);
return true; return true;
} }
@ -42,9 +84,42 @@ namespace Kavita.Common
return false; return false;
} }
} }
public static bool CheckIfJwtTokenSet()
{
//string filePath
try
{
return GetJwtToken(GetAppSettingFilename()) != "super secret unguessable key";
}
catch (Exception ex)
{
Console.WriteLine("Error writing app settings: " + ex.Message);
}
return false;
}
public static bool UpdateJwtToken(string token)
{
try
{
var filePath = GetAppSettingFilename();
var json = File.ReadAllText(filePath).Replace("super secret unguessable key", token);
File.WriteAllText(GetAppSettingFilename(), json);
return true;
}
catch (Exception)
{
return false;
}
}
#endregion #endregion
#region Port #region Port
public static bool UpdatePort(string filePath, int port)
public static bool SetPort(string filePath, int port)
{ {
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker) if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
{ {
@ -63,15 +138,18 @@ namespace Kavita.Common
return false; return false;
} }
} }
public static int GetPort(string filePath) public static int GetPort(string filePath)
{ {
Console.WriteLine(GetAppSettingFilename());
const int defaultPort = 5000; const int defaultPort = 5000;
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker) if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
{ {
return defaultPort; return defaultPort;
} }
try { try
{
var json = File.ReadAllText(filePath); var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json); var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
const string key = "Port"; const string key = "Port";
@ -81,20 +159,25 @@ namespace Kavita.Common
return tokenElement.GetInt32(); return tokenElement.GetInt32();
} }
} }
catch (Exception ex) { catch (Exception ex)
{
Console.WriteLine("Error writing app settings: " + ex.Message); Console.WriteLine("Error writing app settings: " + ex.Message);
} }
return defaultPort; return defaultPort;
} }
#endregion #endregion
#region LogLevel #region LogLevel
public static bool UpdateLogLevel(string filePath, string logLevel)
public static bool SetLogLevel(string filePath, string logLevel)
{ {
try try
{ {
var currentLevel = GetLogLevel(filePath); var currentLevel = GetLogLevel(filePath);
var json = File.ReadAllText(filePath).Replace($"\"Default\": \"{currentLevel}\"", $"\"Default\": \"{logLevel}\""); var json = File.ReadAllText(filePath)
.Replace($"\"Default\": \"{currentLevel}\"", $"\"Default\": \"{logLevel}\"");
File.WriteAllText(filePath, json); File.WriteAllText(filePath, json);
return true; return true;
} }
@ -103,9 +186,11 @@ namespace Kavita.Common
return false; return false;
} }
} }
public static string GetLogLevel(string filePath) public static string GetLogLevel(string filePath)
{ {
try { try
{
var json = File.ReadAllText(filePath); var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json); var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
if (jsonObj.TryGetProperty("Logging", out JsonElement tokenElement)) if (jsonObj.TryGetProperty("Logging", out JsonElement tokenElement))
@ -123,12 +208,53 @@ namespace Kavita.Common
} }
} }
} }
catch (Exception ex) { catch (Exception ex)
{
Console.WriteLine("Error writing app settings: " + ex.Message); Console.WriteLine("Error writing app settings: " + ex.Message);
} }
return "Information"; return "Information";
} }
#endregion #endregion
public static string GetBranch(string filePath)
{
const string defaultBranch = "main";
try
{
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
const string key = "Branch";
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
{
return tokenElement.GetString();
}
}
catch (Exception ex)
{
Console.WriteLine("Error reading app settings: " + ex.Message);
}
return defaultBranch;
}
public static bool SetBranch(string filePath, string updatedBranch)
{
try
{
var currentBranch = GetBranch(filePath);
var json = File.ReadAllText(filePath)
.Replace("\"Branch\": " + currentBranch, "\"Branch\": " + updatedBranch);
File.WriteAllText(filePath, json);
return true;
}
catch (Exception)
{
return false;
}
}
} }
} }

View File

@ -34,7 +34,7 @@ namespace Kavita.Common
/// <returns></returns> /// <returns></returns>
public static string AnonymousToken() public static string AnonymousToken()
{ {
var seed = $"{Environment.ProcessorCount}_{Environment.OSVersion.Platform}_{Environment.MachineName}_{Environment.UserName}"; var seed = $"{Environment.ProcessorCount}_{Environment.OSVersion.Platform}_{Configuration.JwtToken}_{Environment.UserName}";
return CalculateCrc(seed); return CalculateCrc(seed);
} }
} }

View File

@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Sentry" Version="3.7.0" /> <PackageReference Include="Sentry" Version="3.7.0" />
</ItemGroup> </ItemGroup>

View File

@ -160,7 +160,6 @@
<div [ngbNavOutlet]="nav" class="mt-3"></div> <div [ngbNavOutlet]="nav" class="mt-3"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<!-- TODO: Replace secondary buttons in modals with btn-light -->
<button type="button" class="btn btn-secondary" (click)="close()">Close</button> <button type="button" class="btn btn-secondary" (click)="close()">Close</button>
<button type="submit" class="btn btn-primary" (click)="save()">Save</button> <button type="submit" class="btn btn-primary" (click)="save()">Save</button>
</div> </div>

View File

@ -1,4 +1,3 @@
//TODO: Refactor this name to something better
export interface InProgressChapter { export interface InProgressChapter {
id: number; id: number;
range: string; range: string;

View File

@ -14,7 +14,8 @@ export class LibraryService {
baseUrl = environment.apiUrl; baseUrl = environment.apiUrl;
libraryNames: {[key:number]: string} | undefined = undefined; private libraryNames: {[key:number]: string} | undefined = undefined;
private libraryTypes: {[key: number]: LibraryType} | undefined = undefined;
constructor(private httpClient: HttpClient) {} constructor(private httpClient: HttpClient) {}
@ -75,8 +76,17 @@ export class LibraryService {
} }
getLibraryType(libraryId: number) { getLibraryType(libraryId: number) {
// TODO: Cache this in browser if (this.libraryTypes != undefined && this.libraryTypes.hasOwnProperty(libraryId)) {
return this.httpClient.get<LibraryType>(this.baseUrl + 'library/type?libraryId=' + libraryId); return of(this.libraryTypes[libraryId]);
}
return this.httpClient.get<LibraryType>(this.baseUrl + 'library/type?libraryId=' + libraryId).pipe(map(l => {
if (this.libraryTypes === undefined) {
this.libraryTypes = {};
}
this.libraryTypes[libraryId] = l;
return this.libraryTypes[libraryId];
}));
} }
search(term: string) { search(term: string) {

View File

@ -42,7 +42,6 @@ export class DirectoryPickerComponent implements OnInit {
} }
goBack() { goBack() {
// BUG: When Going back to initial listing, this code gets stuck on first drive
this.routeStack.pop(); this.routeStack.pop();
const stackPeek = this.routeStack.peek(); const stackPeek = this.routeStack.peek();
if (stackPeek !== undefined) { if (stackPeek !== undefined) {
@ -53,7 +52,6 @@ export class DirectoryPickerComponent implements OnInit {
this.currentRoot = ''; this.currentRoot = '';
this.loadChildren(this.currentRoot); this.loadChildren(this.currentRoot);
} }
} }
loadChildren(path: string) { loadChildren(path: string) {

View File

@ -30,7 +30,7 @@ export class EditRbsModalComponent implements OnInit {
} }
close() { close() {
this.modal.close(false); this.modal.close(undefined);
} }
save() { save() {
@ -42,8 +42,10 @@ export class EditRbsModalComponent implements OnInit {
this.memberService.updateMemberRoles(this.member?.username, selectedRoles).subscribe(() => { this.memberService.updateMemberRoles(this.member?.username, selectedRoles).subscribe(() => {
if (this.member) { if (this.member) {
this.member.roles = selectedRoles; this.member.roles = selectedRoles;
this.modal.close(this.member);
return;
} }
this.modal.close(true); this.modal.close(undefined);
}); });
} }

View File

@ -69,7 +69,7 @@ export class ManageLibraryComponent implements OnInit, OnDestroy {
this.libraryService.delete(library.id).pipe(take(1)).subscribe(() => { this.libraryService.delete(library.id).pipe(take(1)).subscribe(() => {
this.deletionInProgress = false; this.deletionInProgress = false;
this.getLibraries(); this.getLibraries();
this.toastr.success('Library has been removed'); // BUG: This is not causing a refresh this.toastr.success('Library has been removed');
}); });
} }
} }

View File

@ -25,18 +25,14 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="stat-collection">Allow Anonymous Usage Collection</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="statTooltip" role="button" tabindex="0"></i> <label for="stat-collection" aria-describedby="collection-info">Allow Anonymous Usage Collection</label>
<ng-template #statTooltip>Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes. Requires restart to take effect.</ng-template> <p class="accent" id="collection-info">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features, bug fixes, and preformance tuning. Requires restart to take effect.</p>
<span class="sr-only" id="logging-level-port-help">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes. Requires restart to take effect.</span>
<p class="accent">Send anonymous usage and error information to Kavita's servers. This includes information on your browser, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes. Requires restart to take effect</p>
<div class="form-check"> <div class="form-check">
<input id="stat-collection" type="checkbox" aria-label="Admin" class="form-check-input" formControlName="allowStatCollection"> <input id="stat-collection" type="checkbox" aria-label="Admin" class="form-check-input" formControlName="allowStatCollection">
<label for="stat-collection" class="form-check-label">Send Data</label> <label for="stat-collection" class="form-check-label">Send Data</label>
</div> </div>
</div> </div>
<h4>Reoccuring Tasks</h4> <h4>Reoccuring Tasks</h4>
<div class="form-group"> <div class="form-group">
<label for="settings-tasks-scan">Library Scan</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i> <label for="settings-tasks-scan">Library Scan</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="taskScanTooltip" role="button" tabindex="0"></i>

View File

@ -82,6 +82,11 @@ export class ManageUsersComponent implements OnInit {
openEditRole(member: Member) { openEditRole(member: Member) {
const modalRef = this.modalService.open(EditRbsModalComponent); const modalRef = this.modalService.open(EditRbsModalComponent);
modalRef.componentInstance.member = member; modalRef.componentInstance.member = member;
modalRef.closed.subscribe((updatedMember: Member) => {
if (updatedMember !== undefined) {
member = updatedMember;
}
})
} }
updatePassword(member: Member) { updatePassword(member: Member) {

View File

@ -37,6 +37,8 @@ import { TypeaheadModule } from './typeahead/typeahead.module';
import { AllCollectionsComponent } from './all-collections/all-collections.component'; import { AllCollectionsComponent } from './all-collections/all-collections.component';
import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit-collection-tags.component'; import { EditCollectionTagsComponent } from './_modals/edit-collection-tags/edit-collection-tags.component';
import { RecentlyAddedComponent } from './recently-added/recently-added.component'; import { RecentlyAddedComponent } from './recently-added/recently-added.component';
import { LibraryCardComponent } from './library-card/library-card.component';
import { SeriesCardComponent } from './series-card/series-card.component';
let sentryProviders: any[] = []; let sentryProviders: any[] = [];
@ -100,6 +102,8 @@ if (environment.production) {
AllCollectionsComponent, AllCollectionsComponent,
EditCollectionTagsComponent, EditCollectionTagsComponent,
RecentlyAddedComponent, RecentlyAddedComponent,
LibraryCardComponent,
SeriesCardComponent
], ],
imports: [ imports: [
HttpClientModule, HttpClientModule,

View File

@ -27,7 +27,7 @@
<div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading"> <div class="webtoon-images" *ngIf="readerMode === READER_MODE.WEBTOON && !isLoading">
<app-infinite-scroller [pageNum]="pageNum" [bufferPages]="5" [goToPage]="goToPageEvent" (pageNumberChange)="handleWebtoonPageChange($event)" [totalPages]="maxPages" [urlProvider]="getPageUrl"></app-infinite-scroller> <app-infinite-scroller [pageNum]="pageNum" [bufferPages]="5" [goToPage]="goToPageEvent" (pageNumberChange)="handleWebtoonPageChange($event)" [totalPages]="maxPages" [urlProvider]="getPageUrl"></app-infinite-scroller>
</div> </div>
<ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD"> <!--; else webtoonClickArea; TODO: See if people want this mode WEBTOON_WITH_CLICKS--> <ng-container *ngIf="readerMode === READER_MODE.MANGA_LR || readerMode === READER_MODE.MANGA_UD"> <!--; else webtoonClickArea; See if people want this mode WEBTOON_WITH_CLICKS-->
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'top'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"></div> <div class="{{readerMode === READER_MODE.MANGA_LR ? 'right' : 'top'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, 'right')"></div>
<div class="{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'bottom'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div> <div class="{{readerMode === READER_MODE.MANGA_LR ? 'left' : 'bottom'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, 'left')"></div>
</ng-container> </ng-container>

View File

@ -7,10 +7,9 @@ import { EditSeriesModalComponent } from 'src/app/_modals/edit-series-modal/edit
import { Series } from 'src/app/_models/series'; import { Series } from 'src/app/_models/series';
import { AccountService } from 'src/app/_services/account.service'; import { AccountService } from 'src/app/_services/account.service';
import { ImageService } from 'src/app/_services/image.service'; import { ImageService } from 'src/app/_services/image.service';
import { LibraryService } from 'src/app/_services/library.service';
import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service'; import { ActionFactoryService, Action, ActionItem } from 'src/app/_services/action-factory.service';
import { SeriesService } from 'src/app/_services/series.service'; import { SeriesService } from 'src/app/_services/series.service';
import { ConfirmService } from '../confirm.service'; import { ConfirmService } from '../shared/confirm.service';
@Component({ @Component({
selector: 'app-series-card', selector: 'app-series-card',
@ -30,9 +29,8 @@ export class SeriesCardComponent implements OnInit, OnChanges {
constructor(private accountService: AccountService, private router: Router, constructor(private accountService: AccountService, private router: Router,
private seriesService: SeriesService, private toastr: ToastrService, private seriesService: SeriesService, private toastr: ToastrService,
private libraryService: LibraryService, private modalService: NgbModal, private modalService: NgbModal, private confirmService: ConfirmService,
private confirmService: ConfirmService, public imageService: ImageService, public imageService: ImageService, private actionFactoryService: ActionFactoryService) {
private actionFactoryService: ActionFactoryService) {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => { this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) { if (user) {
this.isAdmin = this.accountService.hasAdminRole(user); this.isAdmin = this.accountService.hasAdminRole(user);

View File

@ -3,14 +3,12 @@ import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { CardItemComponent } from './card-item/card-item.component'; import { CardItemComponent } from './card-item/card-item.component';
import { NgbCollapseModule, NgbDropdownModule, NgbPaginationModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbCollapseModule, NgbDropdownModule, NgbPaginationModule, NgbProgressbarModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
import { LibraryCardComponent } from './library-card/library-card.component';
import { SeriesCardComponent } from './series-card/series-card.component';
import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component'; import { CardDetailsModalComponent } from './_modals/card-details-modal/card-details-modal.component';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { SafeHtmlPipe } from './safe-html.pipe'; import { SafeHtmlPipe } from './safe-html.pipe';
import { LazyLoadImageModule } from 'ng-lazyload-image'; import { LazyLoadImageModule } from 'ng-lazyload-image';
import { CardActionablesComponent } from './card-item/card-actionables/card-actionables.component'; import { CardActionablesComponent } from './card-item/card-actionables/card-actionables.component';
import { RegisterMemberComponent } from './register-member/register-member.component'; import { RegisterMemberComponent } from '../register-member/register-member.component';
import { ReadMoreComponent } from './read-more/read-more.component'; import { ReadMoreComponent } from './read-more/read-more.component';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { DrawerComponent } from './drawer/drawer.component'; import { DrawerComponent } from './drawer/drawer.component';
@ -24,8 +22,6 @@ import { A11yClickDirective } from './a11y-click.directive';
declarations: [ declarations: [
RegisterMemberComponent, RegisterMemberComponent,
CardItemComponent, CardItemComponent,
LibraryCardComponent,
SeriesCardComponent,
CardDetailsModalComponent, CardDetailsModalComponent,
ConfirmDialogComponent, ConfirmDialogComponent,
SafeHtmlPipe, SafeHtmlPipe,
@ -49,10 +45,8 @@ import { A11yClickDirective } from './a11y-click.directive';
NgbPaginationModule // CardDetailLayoutComponent NgbPaginationModule // CardDetailLayoutComponent
], ],
exports: [ exports: [
RegisterMemberComponent, // TODO: Move this out and put in normal app RegisterMemberComponent,
CardItemComponent, CardItemComponent,
LibraryCardComponent, // TODO: Move this out and put in normal app
SeriesCardComponent, // TODO: Move this out and put in normal app
SafeHtmlPipe, SafeHtmlPipe,
CardActionablesComponent, CardActionablesComponent,
ReadMoreComponent, ReadMoreComponent,

View File

@ -27,13 +27,6 @@ export class SelectionModel<T> {
}); });
} }
// __lookupItem(item: T) {
// if (this._propAccessor != '') {
// // TODO: Implement this code to speedup lookups (use a map rather than array)
// }
// const dataItem = this._data.filter(data => data.value == d);
// }
/** /**
* Will toggle if the data item is selected or not. If data option is not tracked, will add it and set state to true. * Will toggle if the data item is selected or not. If data option is not tracked, will add it and set state to true.
* @param data Item to toggle * @param data Item to toggle