Merge branch 'develop' of https://github.com/Kareadita/Kavita into feature/comic-parser-enhancements
This commit is contained in:
commit
cf6195722a
60 changed files with 414 additions and 561 deletions
|
|
@ -1,6 +1,4 @@
|
||||||
using System;
|
using System.IO;
|
||||||
using System.IO;
|
|
||||||
using API.Data;
|
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
|
|
@ -57,8 +55,8 @@ namespace API.Benchmark
|
||||||
Title = "A Town Where You Live",
|
Title = "A Town Where You Live",
|
||||||
Volumes = "1"
|
Volumes = "1"
|
||||||
};
|
};
|
||||||
var parsedSeries = _parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new string[] {libraryPath},
|
_parseScannedFiles.ScanLibrariesForSeries(LibraryType.Manga, new [] {libraryPath},
|
||||||
out var totalFiles, out var scanElapsedTime);
|
out _, out _);
|
||||||
_parseScannedFiles.MergeName(p1);
|
_parseScannedFiles.MergeName(p1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ namespace API.Benchmark
|
||||||
|
|
||||||
private static void NormalizeNew(string name)
|
private static void NormalizeNew(string name)
|
||||||
{
|
{
|
||||||
NormalizeRegex.Replace(name, string.Empty).ToLower();
|
// ReSharper disable once UnusedVariable
|
||||||
|
var ret = NormalizeRegex.Replace(name, string.Empty).ToLower();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace API.Benchmark
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
static void Main(string[] args)
|
private static void Main(string[] args)
|
||||||
{
|
{
|
||||||
//BenchmarkRunner.Run<ParseScannedFilesBenchmarks>();
|
//BenchmarkRunner.Run<ParseScannedFilesBenchmarks>();
|
||||||
//BenchmarkRunner.Run<TestBenchmark>();
|
//BenchmarkRunner.Run<TestBenchmark>();
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using API.Archive;
|
using API.Archive;
|
||||||
|
using API.Data.Metadata;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -216,8 +217,30 @@ namespace API.Tests.Services
|
||||||
var archive = Path.Join(testDirectory, "file in folder.zip");
|
var archive = Path.Join(testDirectory, "file in folder.zip");
|
||||||
var summaryInfo = "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?";
|
var summaryInfo = "By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?";
|
||||||
|
|
||||||
Assert.Equal(summaryInfo, _archiveService.GetSummaryInfo(archive));
|
Assert.Equal(summaryInfo, _archiveService.GetComicInfo(archive).Summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanParseComicInfo()
|
||||||
|
{
|
||||||
|
var testDirectory = Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/ArchiveService/ComicInfos");
|
||||||
|
var archive = Path.Join(testDirectory, "ComicInfo.zip");
|
||||||
|
var actual = _archiveService.GetComicInfo(archive);
|
||||||
|
var expected = new ComicInfo()
|
||||||
|
{
|
||||||
|
Publisher = "Yen Press",
|
||||||
|
Genre = "Manga, Movies & TV",
|
||||||
|
Summary =
|
||||||
|
"By all counts, Ryouta Sakamoto is a loser when he's not holed up in his room, bombing things into oblivion in his favorite online action RPG. But his very own uneventful life is blown to pieces when he's abducted and taken to an uninhabited island, where he soon learns the hard way that he's being pitted against others just like him in a explosives-riddled death match! How could this be happening? Who's putting them up to this? And why!? The name, not to mention the objective, of this very real survival game is eerily familiar to Ryouta, who has mastered its virtual counterpart-BTOOOM! Can Ryouta still come out on top when he's playing for his life!?",
|
||||||
|
PageCount = 194,
|
||||||
|
LanguageISO = "en",
|
||||||
|
Notes = "Scraped metadata from Comixology [CMXDB450184]",
|
||||||
|
Series = "BTOOOM!",
|
||||||
|
Title = "v01",
|
||||||
|
Web = "https://www.comixology.com/BTOOOM/digital-comic/450184"
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.NotStrictEqual(expected, actual);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ namespace API.Tests.Services
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(new string[] {"C:/Manga/"}, new string[] {"C:/Manga/Love Hina/Vol. 01.cbz"}, "C:/Manga/Love Hina")]
|
[InlineData(new [] {"C:/Manga/"}, new [] {"C:/Manga/Love Hina/Vol. 01.cbz"}, "C:/Manga/Love Hina")]
|
||||||
public void FindHighestDirectoriesFromFilesTest(string[] rootDirectories, string[] folders, string expectedDirectory)
|
public void FindHighestDirectoriesFromFilesTest(string[] rootDirectories, string[] folders, string expectedDirectory)
|
||||||
{
|
{
|
||||||
var actual = DirectoryService.FindHighestDirectoriesFromFiles(rootDirectories, folders);
|
var actual = DirectoryService.FindHighestDirectoriesFromFiles(rootDirectories, folders);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Interfaces;
|
|
||||||
using API.Interfaces.Services;
|
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.SignalR;
|
|
||||||
using Microsoft.AspNetCore.SignalR;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace API.Tests.Services
|
namespace API.Tests.Services
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -41,35 +41,34 @@
|
||||||
<PackageReference Include="ExCSS" Version="4.1.0" />
|
<PackageReference Include="ExCSS" Version="4.1.0" />
|
||||||
<PackageReference Include="Flurl" Version="3.0.2" />
|
<PackageReference Include="Flurl" Version="3.0.2" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
<PackageReference Include="Flurl.Http" Version="3.2.0" />
|
||||||
<PackageReference Include="Hangfire" Version="1.7.24" />
|
<PackageReference Include="Hangfire" Version="1.7.25" />
|
||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.24" />
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.25" />
|
||||||
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
|
||||||
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.35" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.37" />
|
||||||
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.8" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.8">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.10">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.1.3" />
|
||||||
<PackageReference Include="NetVips" Version="2.0.1" />
|
<PackageReference Include="NetVips" Version="2.0.1" />
|
||||||
<PackageReference Include="NetVips.Native" Version="8.11.0" />
|
<PackageReference Include="NetVips.Native" Version="8.11.4" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.1.2" />
|
<PackageReference Include="NReco.Logging.File" Version="1.1.2" />
|
||||||
<PackageReference Include="Sentry.AspNetCore" Version="3.8.3" />
|
<PackageReference Include="SharpCompress" Version="0.30.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.29.0.36737">
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.27.0.35380">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.5" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.2" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.12.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.12.2" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
<PackageReference Include="VersOne.Epub" Version="3.0.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
using System;
|
using System.IO;
|
||||||
using System.IO;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
using API.Constants;
|
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
using API.DTOs.OPDS;
|
using API.DTOs.OPDS;
|
||||||
|
|
@ -16,7 +15,6 @@ using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ 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.DTOs;
|
using API.DTOs.Settings;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Helpers.Converters;
|
using API.Helpers.Converters;
|
||||||
|
|
@ -85,6 +85,17 @@ namespace API.Controllers
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value)
|
||||||
|
{
|
||||||
|
var path = !updateSettingsDto.BaseUrl.StartsWith("/")
|
||||||
|
? $"/{updateSettingsDto.BaseUrl}"
|
||||||
|
: updateSettingsDto.BaseUrl;
|
||||||
|
setting.Value = path;
|
||||||
|
// BaseUrl is managed in appSetting.json
|
||||||
|
Configuration.BaseUrl = updateSettingsDto.BaseUrl;
|
||||||
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
|
}
|
||||||
|
|
||||||
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ using API.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NetVips;
|
|
||||||
|
|
||||||
namespace API.Controllers
|
namespace API.Controllers
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ namespace API.DTOs.OPDS
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
[XmlAttribute("count", Namespace = "http://vaemendis.net/opds-pse/ns")]
|
[XmlAttribute("count", Namespace = "http://vaemendis.net/opds-pse/ns")]
|
||||||
public int TotalPages { get; set; } = 0;
|
public int TotalPages { get; set; }
|
||||||
|
|
||||||
public bool ShouldSerializeTotalPages()
|
public bool ShouldSerializeTotalPages()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace API.DTOs.Reader
|
namespace API.DTOs.Reader
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
namespace API.DTOs
|
namespace API.DTOs.Settings
|
||||||
{
|
{
|
||||||
public class ServerSettingDto
|
public class ServerSettingDto
|
||||||
{
|
{
|
||||||
|
|
@ -26,5 +26,9 @@
|
||||||
/// Enables Authentication on the server. Defaults to true.
|
/// Enables Authentication on the server. Defaults to true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableAuthentication { get; set; }
|
public bool EnableAuthentication { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Base Url for the kavita. Defaults to "/". Managed in appsettings.json.Requires restart to take effect.
|
||||||
|
/// </summary>
|
||||||
|
public string BaseUrl { get; set; } = "/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
API/Data/Metadata/ComicInfo.cs
Normal file
51
API/Data/Metadata/ComicInfo.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
namespace API.Data.Metadata
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A representation of a ComicInfo.xml file
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>See reference of the loose spec here: https://github.com/Kussie/ComicInfoStandard/blob/main/ComicInfo.xsd</remarks>
|
||||||
|
public class ComicInfo
|
||||||
|
{
|
||||||
|
public string Summary { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Series { get; set; }
|
||||||
|
public string Number { get; set; }
|
||||||
|
public string Volume { get; set; }
|
||||||
|
public string Notes { get; set; }
|
||||||
|
public string Genre { get; set; }
|
||||||
|
public int PageCount { get; set; }
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
public string LanguageISO { get; set; }
|
||||||
|
public string Web { get; set; }
|
||||||
|
public int Month { get; set; }
|
||||||
|
public int Year { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Rating based on the content. Think PG-13, R for movies
|
||||||
|
/// </summary>
|
||||||
|
public string AgeRating { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// User's rating of the content
|
||||||
|
/// </summary>
|
||||||
|
public float UserRating { get; set; }
|
||||||
|
|
||||||
|
public string AlternateSeries { get; set; }
|
||||||
|
public string StoryArc { get; set; }
|
||||||
|
public string SeriesGroup { get; set; }
|
||||||
|
public string AlternativeSeries { get; set; }
|
||||||
|
public string AlternativeNumber { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is the Author. For Books, we map creator tag in OPF to this field. Comma separated if multiple.
|
||||||
|
/// </summary>
|
||||||
|
public string Writer { get; set; } // TODO: Validate if we should make this a list of writers
|
||||||
|
public string Penciller { get; set; }
|
||||||
|
public string Inker { get; set; }
|
||||||
|
public string Colorist { get; set; }
|
||||||
|
public string Letterer { get; set; }
|
||||||
|
public string CoverArtist { get; set; }
|
||||||
|
public string Editor { get; set; }
|
||||||
|
public string Publisher { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Comparators;
|
|
||||||
using API.Data.Scanner;
|
using API.Data.Scanner;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.Filtering;
|
using API.DTOs.Filtering;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs.Settings;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Interfaces.Repositories;
|
using API.Interfaces.Repositories;
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ namespace API.Data
|
||||||
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"},
|
||||||
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||||
new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
||||||
|
new () {Key = ServerSettingKey.BaseUrl, Value = ""},// Not used from DB, but DB is sync with appSettings.json
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var defaultSetting in defaultSettings)
|
foreach (var defaultSetting in defaultSettings)
|
||||||
|
|
@ -63,11 +64,20 @@ namespace API.Data
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(Configuration.BaseUrl))
|
||||||
|
{
|
||||||
|
Configuration.BaseUrl = "/";
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
|
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
|
||||||
Configuration.Port + string.Empty;
|
Configuration.Port + string.Empty;
|
||||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value =
|
context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value =
|
||||||
Configuration.LogLevel + string.Empty;
|
Configuration.LogLevel + string.Empty;
|
||||||
|
context.ServerSetting.First(s => s.Key == ServerSettingKey.BaseUrl).Value =
|
||||||
|
Configuration.BaseUrl;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ namespace API.Entities.Enums
|
||||||
[Description("EnableOpds")]
|
[Description("EnableOpds")]
|
||||||
EnableOpds = 7,
|
EnableOpds = 7,
|
||||||
[Description("EnableAuthentication")]
|
[Description("EnableAuthentication")]
|
||||||
EnableAuthentication = 8
|
EnableAuthentication = 8,
|
||||||
|
[Description("BaseUrl")]
|
||||||
|
BaseUrl = 9
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
namespace API.Entities
|
namespace API.Entities
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
using API.DTOs.ReadingLists;
|
using API.DTOs.ReadingLists;
|
||||||
|
using API.DTOs.Settings;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Helpers.Converters;
|
using API.Helpers.Converters;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.DTOs;
|
using API.DTOs.Settings;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
|
@ -39,6 +39,9 @@ namespace API.Helpers.Converters
|
||||||
case ServerSettingKey.EnableAuthentication:
|
case ServerSettingKey.EnableAuthentication:
|
||||||
destination.EnableAuthentication = bool.Parse(row.Value);
|
destination.EnableAuthentication = bool.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.BaseUrl:
|
||||||
|
destination.BaseUrl = row.Value;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data.Scanner;
|
using API.Data.Scanner;
|
||||||
using API.DTOs;
|
using API.DTOs;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.DTOs;
|
using API.DTOs.Settings;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Archive;
|
using API.Archive;
|
||||||
|
using API.Data.Metadata;
|
||||||
|
|
||||||
namespace API.Interfaces.Services
|
namespace API.Interfaces.Services
|
||||||
{
|
{
|
||||||
|
|
@ -12,7 +13,7 @@ namespace API.Interfaces.Services
|
||||||
int GetNumberOfPagesFromArchive(string archivePath);
|
int GetNumberOfPagesFromArchive(string archivePath);
|
||||||
string GetCoverImage(string archivePath, string fileName);
|
string GetCoverImage(string archivePath, string fileName);
|
||||||
bool IsValidArchive(string archivePath);
|
bool IsValidArchive(string archivePath);
|
||||||
string GetSummaryInfo(string archivePath);
|
ComicInfo GetComicInfo(string archivePath);
|
||||||
ArchiveLibrary CanOpen(string archivePath);
|
ArchiveLibrary CanOpen(string archivePath);
|
||||||
bool ArchiveNeedsFlattening(ZipArchive archive);
|
bool ArchiveNeedsFlattening(ZipArchive archive);
|
||||||
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
Task<Tuple<byte[], string>> CreateZipForDownload(IEnumerable<string> files, string tempFolder);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Data.Metadata;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
using VersOne.Epub;
|
using VersOne.Epub;
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ namespace API.Interfaces.Services
|
||||||
/// <param name="book">Book Reference, needed for if you expect Import statements</param>
|
/// <param name="book">Book Reference, needed for if you expect Import statements</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<string> ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book);
|
Task<string> ScopeStyles(string stylesheetHtml, string apiBase, string filename, EpubBookRef book);
|
||||||
string GetSummaryInfo(string filePath);
|
ComicInfo GetComicInfo(string filePath);
|
||||||
ParserInfo ParseInfo(string filePath);
|
ParserInfo ParseInfo(string filePath);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts a PDF file's pages as images to an target directory
|
/// Extracts a PDF file's pages as images to an target directory
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Channels;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Helpers;
|
|
||||||
using API.Interfaces;
|
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
|
@ -21,9 +13,6 @@ using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.IO;
|
|
||||||
using NetVips;
|
|
||||||
using Sentry;
|
|
||||||
|
|
||||||
namespace API
|
namespace API
|
||||||
{
|
{
|
||||||
|
|
@ -103,62 +92,6 @@ namespace API
|
||||||
opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; });
|
opts.ListenAnyIP(HttpPort, options => { options.Protocols = HttpProtocols.Http1AndHttp2; });
|
||||||
});
|
});
|
||||||
|
|
||||||
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
|
||||||
if (environment != Environments.Development)
|
|
||||||
{
|
|
||||||
webBuilder.UseSentry(options =>
|
|
||||||
{
|
|
||||||
options.Dsn = "https://40f4e7b49c094172a6f99d61efb2740f@o641015.ingest.sentry.io/5757423";
|
|
||||||
options.MaxBreadcrumbs = 200;
|
|
||||||
options.AttachStacktrace = true;
|
|
||||||
options.Debug = false;
|
|
||||||
options.SendDefaultPii = false;
|
|
||||||
options.DiagnosticLevel = SentryLevel.Debug;
|
|
||||||
options.ShutdownTimeout = TimeSpan.FromSeconds(5);
|
|
||||||
options.Release = BuildInfo.Version.ToString();
|
|
||||||
options.AddExceptionFilterForType<OutOfMemoryException>();
|
|
||||||
options.AddExceptionFilterForType<NetVips.VipsException>();
|
|
||||||
options.AddExceptionFilterForType<InvalidDataException>();
|
|
||||||
options.AddExceptionFilterForType<KavitaException>();
|
|
||||||
|
|
||||||
options.BeforeSend = sentryEvent =>
|
|
||||||
{
|
|
||||||
if (sentryEvent.Exception != null
|
|
||||||
&& sentryEvent.Exception.Message.StartsWith("[GetCoverImage]")
|
|
||||||
&& sentryEvent.Exception.Message.StartsWith("[BookService]")
|
|
||||||
&& sentryEvent.Exception.Message.StartsWith("[ExtractArchive]")
|
|
||||||
&& sentryEvent.Exception.Message.StartsWith("[GetSummaryInfo]")
|
|
||||||
&& sentryEvent.Exception.Message.StartsWith("[GetSummaryInfo]")
|
|
||||||
&& sentryEvent.Exception.Message.StartsWith("[GetNumberOfPagesFromArchive]")
|
|
||||||
&& sentryEvent.Exception.Message.Contains("EPUB parsing error")
|
|
||||||
&& sentryEvent.Exception.Message.Contains("Unsupported EPUB version")
|
|
||||||
&& sentryEvent.Exception.Message.Contains("Incorrect EPUB")
|
|
||||||
&& sentryEvent.Exception.Message.Contains("Access is Denied"))
|
|
||||||
{
|
|
||||||
return null; // Don't send this event to Sentry
|
|
||||||
}
|
|
||||||
|
|
||||||
sentryEvent.ServerName = null; // Never send Server Name to Sentry
|
|
||||||
return sentryEvent;
|
|
||||||
};
|
|
||||||
|
|
||||||
options.ConfigureScope(scope =>
|
|
||||||
{
|
|
||||||
scope.User = new User()
|
|
||||||
{
|
|
||||||
Id = HashUtil.AnonymousToken()
|
|
||||||
};
|
|
||||||
scope.Contexts.App.Name = BuildInfo.AppName;
|
|
||||||
scope.Contexts.App.Version = BuildInfo.Version.ToString();
|
|
||||||
scope.Contexts.App.StartTime = DateTime.UtcNow;
|
|
||||||
scope.Contexts.App.Hash = HashUtil.AnonymousToken();
|
|
||||||
scope.Contexts.App.Build = BuildInfo.Release;
|
|
||||||
scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name);
|
|
||||||
scope.SetTag("branch", BuildInfo.Branch);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
webBuilder.UseStartup<Startup>();
|
webBuilder.UseStartup<Startup>();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using API.Archive;
|
using API.Archive;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
|
using API.Data.Metadata;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
|
@ -293,15 +294,13 @@ namespace API.Services
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetSummaryInfo(string archivePath)
|
public ComicInfo GetComicInfo(string archivePath)
|
||||||
{
|
{
|
||||||
var summary = string.Empty;
|
if (!IsValidArchive(archivePath)) return null;
|
||||||
if (!IsValidArchive(archivePath)) return summary;
|
|
||||||
|
|
||||||
ComicInfo info = null;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(archivePath)) return summary;
|
if (!File.Exists(archivePath)) return null;
|
||||||
|
|
||||||
var libraryHandler = CanOpen(archivePath);
|
var libraryHandler = CanOpen(archivePath);
|
||||||
switch (libraryHandler)
|
switch (libraryHandler)
|
||||||
|
|
@ -309,48 +308,55 @@ namespace API.Services
|
||||||
case ArchiveLibrary.Default:
|
case ArchiveLibrary.Default:
|
||||||
{
|
{
|
||||||
using var archive = ZipFile.OpenRead(archivePath);
|
using var archive = ZipFile.OpenRead(archivePath);
|
||||||
var entry = archive.Entries.SingleOrDefault(x => !Parser.Parser.HasBlacklistedFolderInPath(x.FullName)
|
var entry = archive.Entries.SingleOrDefault(x =>
|
||||||
|
!Parser.Parser.HasBlacklistedFolderInPath(x.FullName)
|
||||||
&& Path.GetFileNameWithoutExtension(x.Name)?.ToLower() == ComicInfoFilename
|
&& Path.GetFileNameWithoutExtension(x.Name)?.ToLower() == ComicInfoFilename
|
||||||
&& !Path.GetFileNameWithoutExtension(x.Name).StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)
|
&& !Path.GetFileNameWithoutExtension(x.Name)
|
||||||
|
.StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)
|
||||||
&& Parser.Parser.IsXml(x.FullName));
|
&& Parser.Parser.IsXml(x.FullName));
|
||||||
if (entry != null)
|
if (entry != null)
|
||||||
{
|
{
|
||||||
using var stream = entry.Open();
|
using var stream = entry.Open();
|
||||||
var serializer = new XmlSerializer(typeof(ComicInfo));
|
var serializer = new XmlSerializer(typeof(ComicInfo));
|
||||||
info = (ComicInfo) serializer.Deserialize(stream);
|
return (ComicInfo) serializer.Deserialize(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ArchiveLibrary.SharpCompress:
|
case ArchiveLibrary.SharpCompress:
|
||||||
{
|
{
|
||||||
using var archive = ArchiveFactory.Open(archivePath);
|
using var archive = ArchiveFactory.Open(archivePath);
|
||||||
info = FindComicInfoXml(archive.Entries.Where(entry => !entry.IsDirectory
|
return FindComicInfoXml(archive.Entries.Where(entry => !entry.IsDirectory
|
||||||
&& !Parser.Parser.HasBlacklistedFolderInPath(Path.GetDirectoryName(entry.Key) ?? string.Empty)
|
&& !Parser.Parser
|
||||||
&& !Path.GetFileNameWithoutExtension(entry.Key).StartsWith(Parser.Parser.MacOsMetadataFileStartsWith)
|
.HasBlacklistedFolderInPath(
|
||||||
|
Path.GetDirectoryName(
|
||||||
|
entry.Key) ?? string.Empty)
|
||||||
|
&& !Path
|
||||||
|
.GetFileNameWithoutExtension(
|
||||||
|
entry.Key).StartsWith(Parser
|
||||||
|
.Parser
|
||||||
|
.MacOsMetadataFileStartsWith)
|
||||||
&& Parser.Parser.IsXml(entry.Key)));
|
&& Parser.Parser.IsXml(entry.Key)));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case ArchiveLibrary.NotSupported:
|
case ArchiveLibrary.NotSupported:
|
||||||
_logger.LogWarning("[GetSummaryInfo] This archive cannot be read: {ArchivePath}", archivePath);
|
_logger.LogWarning("[GetComicInfo] This archive cannot be read: {ArchivePath}", archivePath);
|
||||||
return summary;
|
return null;
|
||||||
default:
|
default:
|
||||||
_logger.LogWarning("[GetSummaryInfo] There was an exception when reading archive stream: {ArchivePath}", archivePath);
|
_logger.LogWarning(
|
||||||
return summary;
|
"[GetComicInfo] There was an exception when reading archive stream: {ArchivePath}",
|
||||||
}
|
archivePath);
|
||||||
|
return null;
|
||||||
if (info != null)
|
|
||||||
{
|
|
||||||
return info.Summary;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "[GetSummaryInfo] There was an exception when reading archive stream: {Filepath}", archivePath);
|
_logger.LogWarning(ex, "[GetComicInfo] There was an exception when reading archive stream: {Filepath}", archivePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void ExtractArchiveEntities(IEnumerable<IArchiveEntry> entries, string extractPath)
|
private static void ExtractArchiveEntities(IEnumerable<IArchiveEntry> entries, string extractPath)
|
||||||
{
|
{
|
||||||
DirectoryService.ExistOrCreate(extractPath);
|
DirectoryService.ExistOrCreate(extractPath);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ using System.Drawing;
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
using API.Data.Metadata;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using API.Parser;
|
using API.Parser;
|
||||||
|
|
@ -165,22 +165,43 @@ namespace API.Services
|
||||||
return RemoveWhiteSpaceFromStylesheets(stylesheet.ToCss());
|
return RemoveWhiteSpaceFromStylesheets(stylesheet.ToCss());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetSummaryInfo(string filePath)
|
public ComicInfo GetComicInfo(string filePath)
|
||||||
{
|
{
|
||||||
if (!IsValidFile(filePath) || Parser.Parser.IsPdf(filePath)) return string.Empty;
|
if (!IsValidFile(filePath) || Parser.Parser.IsPdf(filePath)) return null;
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var epubBook = EpubReader.OpenBook(filePath);
|
using var epubBook = EpubReader.OpenBook(filePath);
|
||||||
return epubBook.Schema.Package.Metadata.Description;
|
var publicationDate =
|
||||||
|
epubBook.Schema.Package.Metadata.Dates.FirstOrDefault(date => date.Event == "publication")?.Date;
|
||||||
|
|
||||||
|
var info = new ComicInfo()
|
||||||
|
{
|
||||||
|
Summary = epubBook.Schema.Package.Metadata.Description,
|
||||||
|
Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators),
|
||||||
|
Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers),
|
||||||
|
Month = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Month : 0,
|
||||||
|
Year = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Year : 0,
|
||||||
|
};
|
||||||
|
// Parse tags not exposed via Library
|
||||||
|
foreach (var metadataItem in epubBook.Schema.Package.Metadata.MetaItems)
|
||||||
|
{
|
||||||
|
switch (metadataItem.Name)
|
||||||
|
{
|
||||||
|
case "calibre:rating":
|
||||||
|
info.UserRating = float.Parse(metadataItem.Content);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "[BookService] There was an exception getting summary, defaulting to empty string");
|
_logger.LogWarning(ex, "[GetComicInfo] There was an exception getting metadata");
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsValidFile(string filePath)
|
private bool IsValidFile(string filePath)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
namespace API.Services
|
|
||||||
{
|
|
||||||
public class ComicInfo
|
|
||||||
{
|
|
||||||
public string Summary { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Series { get; set; }
|
|
||||||
public string Notes { get; set; }
|
|
||||||
public string Publisher { get; set; }
|
|
||||||
public string Genre { get; set; }
|
|
||||||
public int PageCount { get; set; }
|
|
||||||
// ReSharper disable once InconsistentNaming
|
|
||||||
public string LanguageISO { get; set; }
|
|
||||||
public string Web { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -47,8 +47,6 @@ namespace API.Services
|
||||||
var firstImage = _directoryService.GetFilesWithExtension(directory, Parser.Parser.ImageFileExtensions)
|
var firstImage = _directoryService.GetFilesWithExtension(directory, Parser.Parser.ImageFileExtensions)
|
||||||
.OrderBy(f => f, new NaturalSortComparer()).FirstOrDefault();
|
.OrderBy(f => f, new NaturalSortComparer()).FirstOrDefault();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return firstImage;
|
return firstImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,7 +93,7 @@ namespace API.Services
|
||||||
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
|
||||||
public static string WriteCoverThumbnail(Stream stream, string fileName)
|
public static string WriteCoverThumbnail(Stream stream, string fileName)
|
||||||
{
|
{
|
||||||
using var thumbnail = NetVips.Image.ThumbnailStream(stream, ThumbnailWidth);
|
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth);
|
||||||
var filename = fileName + ".png";
|
var filename = fileName + ".png";
|
||||||
thumbnail.WriteToFile(Path.Join(DirectoryService.CoverImageDirectory, fileName + ".png"));
|
thumbnail.WriteToFile(Path.Join(DirectoryService.CoverImageDirectory, fileName + ".png"));
|
||||||
return filename;
|
return filename;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Comparators;
|
using API.Comparators;
|
||||||
|
using API.Data.Metadata;
|
||||||
using API.Data.Repositories;
|
using API.Data.Repositories;
|
||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
|
|
@ -171,6 +171,9 @@ namespace API.Services
|
||||||
|
|
||||||
private bool UpdateSeriesSummary(Series series, bool forceUpdate)
|
private bool UpdateSeriesSummary(Series series, bool forceUpdate)
|
||||||
{
|
{
|
||||||
|
// NOTE: This can be problematic when the file changes and a summary already exists, but it is likely
|
||||||
|
// better to let the user kick off a refresh metadata on an individual Series than having overhead of
|
||||||
|
// checking File last write time.
|
||||||
if (!string.IsNullOrEmpty(series.Summary) && !forceUpdate) return false;
|
if (!string.IsNullOrEmpty(series.Summary) && !forceUpdate) return false;
|
||||||
|
|
||||||
var isBook = series.Library.Type == LibraryType.Book;
|
var isBook = series.Library.Type == LibraryType.Book;
|
||||||
|
|
@ -181,16 +184,21 @@ namespace API.Services
|
||||||
if (firstFile == null || (!forceUpdate && !firstFile.HasFileBeenModified())) return false;
|
if (firstFile == null || (!forceUpdate && !firstFile.HasFileBeenModified())) return false;
|
||||||
if (Parser.Parser.IsPdf(firstFile.FilePath)) return false;
|
if (Parser.Parser.IsPdf(firstFile.FilePath)) return false;
|
||||||
|
|
||||||
if (series.Format is MangaFormat.Archive or MangaFormat.Epub)
|
var comicInfo = GetComicInfo(series.Format, firstFile);
|
||||||
{
|
if (string.IsNullOrEmpty(comicInfo?.Summary)) return false;
|
||||||
var summary = Parser.Parser.IsEpub(firstFile.FilePath) ? _bookService.GetSummaryInfo(firstFile.FilePath) : _archiveService.GetSummaryInfo(firstFile.FilePath);
|
|
||||||
if (!string.IsNullOrEmpty(series.Summary))
|
series.Summary = comicInfo.Summary;
|
||||||
{
|
|
||||||
series.Summary = summary;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ComicInfo GetComicInfo(MangaFormat format, MangaFile firstFile)
|
||||||
|
{
|
||||||
|
if (format is MangaFormat.Archive or MangaFormat.Epub)
|
||||||
|
{
|
||||||
|
return Parser.Parser.IsEpub(firstFile.FilePath) ? _bookService.GetComicInfo(firstFile.FilePath) : _archiveService.GetComicInfo(firstFile.FilePath);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ namespace API.Services.Tasks
|
||||||
_directoryService.CopyFilesToDirectory(
|
_directoryService.CopyFilesToDirectory(
|
||||||
chapterImages.Select(s => Path.Join(DirectoryService.CoverImageDirectory, s)), outputTempDir);
|
chapterImages.Select(s => Path.Join(DirectoryService.CoverImageDirectory, s)), outputTempDir);
|
||||||
}
|
}
|
||||||
catch (IOException e)
|
catch (IOException)
|
||||||
{
|
{
|
||||||
// Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file.
|
// Swallow exception. This can be a duplicate cover being copied as chapter and volumes can share same file.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Interfaces;
|
using API.Interfaces;
|
||||||
using API.Interfaces.Services;
|
using API.Interfaces.Services;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NetVips;
|
|
||||||
|
|
||||||
namespace API.Services.Tasks
|
namespace API.Services.Tasks
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ namespace API.Services.Tasks
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_unitOfWork.SeriesRepository.Remove(series);
|
_unitOfWork.SeriesRepository.Remove(series);
|
||||||
await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds);
|
await CommitAndSend(totalFiles, parsedSeries, sw, scanElapsedTime, series);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -121,7 +121,7 @@ namespace API.Services.Tasks
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
UpdateSeries(series, parsedSeries);
|
UpdateSeries(series, parsedSeries);
|
||||||
await CommitAndSend(libraryId, seriesId, totalFiles, parsedSeries, sw, scanElapsedTime, series, chapterIds);
|
await CommitAndSend(totalFiles, parsedSeries, sw, scanElapsedTime, series);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -131,6 +131,9 @@ namespace API.Services.Tasks
|
||||||
// Tell UI that this series is done
|
// Tell UI that this series is done
|
||||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.ScanSeriesEvent(seriesId, series.Name),
|
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanSeries, MessageFactory.ScanSeriesEvent(seriesId, series.Name),
|
||||||
cancellationToken: token);
|
cancellationToken: token);
|
||||||
|
await CleanupDbEntities();
|
||||||
|
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
||||||
|
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, series.Id, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
|
private static void RemoveParsedInfosNotForSeries(Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Series series)
|
||||||
|
|
@ -143,8 +146,8 @@ namespace API.Services.Tasks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CommitAndSend(int libraryId, int seriesId, int totalFiles,
|
private async Task CommitAndSend(int totalFiles,
|
||||||
Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Stopwatch sw, long scanElapsedTime, Series series, int[] chapterIds)
|
Dictionary<ParsedSeries, List<ParserInfo>> parsedSeries, Stopwatch sw, long scanElapsedTime, Series series)
|
||||||
{
|
{
|
||||||
if (_unitOfWork.HasChanges())
|
if (_unitOfWork.HasChanges())
|
||||||
{
|
{
|
||||||
|
|
@ -152,10 +155,6 @@ namespace API.Services.Tasks
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
|
"Processed {TotalFiles} files and {ParsedSeriesCount} series in {ElapsedScanTime} milliseconds for {SeriesName}",
|
||||||
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
|
totalFiles, parsedSeries.Keys.Count, sw.ElapsedMilliseconds + scanElapsedTime, series.Name);
|
||||||
|
|
||||||
await CleanupDbEntities();
|
|
||||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, false));
|
|
||||||
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,7 +224,7 @@ namespace API.Services.Tasks
|
||||||
"[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan");
|
"[ScannerService] There was a critical error that resulted in a failed scan. Please check logs and rescan");
|
||||||
}
|
}
|
||||||
|
|
||||||
await CleanupAbandonedChapters();
|
await CleanupDbEntities();
|
||||||
|
|
||||||
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
|
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false));
|
||||||
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress,
|
||||||
|
|
|
||||||
|
|
@ -160,11 +160,23 @@ namespace API
|
||||||
|
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(Configuration.BaseUrl))
|
||||||
|
{
|
||||||
|
var path = !Configuration.BaseUrl.StartsWith("/")
|
||||||
|
? $"/{Configuration.BaseUrl}"
|
||||||
|
: Configuration.BaseUrl;
|
||||||
|
app.UsePathBase(path);
|
||||||
|
Console.WriteLine("Starting with base url as " + path);
|
||||||
|
}
|
||||||
|
|
||||||
app.UseStaticFiles(new StaticFileOptions
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
{
|
{
|
||||||
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
context.Response.GetTypedHeaders().CacheControl =
|
context.Response.GetTypedHeaders().CacheControl =
|
||||||
|
|
|
||||||
|
|
@ -18,5 +18,7 @@
|
||||||
"MaxRollingFiles": 5
|
"MaxRollingFiles": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Port": 5000
|
"Port": 5000,
|
||||||
|
"BaseUrl": "/"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace Kavita.Common
|
||||||
{
|
{
|
||||||
public static class Configuration
|
public static class Configuration
|
||||||
{
|
{
|
||||||
private static readonly string AppSettingsFilename = GetAppSettingFilename();
|
private static string AppSettingsFilename = GetAppSettingFilename();
|
||||||
public static string Branch
|
public static string Branch
|
||||||
{
|
{
|
||||||
get => GetBranch(GetAppSettingFilename());
|
get => GetBranch(GetAppSettingFilename());
|
||||||
|
|
@ -33,6 +33,12 @@ namespace Kavita.Common
|
||||||
set => SetLogLevel(GetAppSettingFilename(), value);
|
set => SetLogLevel(GetAppSettingFilename(), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string BaseUrl
|
||||||
|
{
|
||||||
|
get => GetBaseUrl(GetAppSettingFilename());
|
||||||
|
set => SetBaseUrl(GetAppSettingFilename(), value);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetAppSettingFilename()
|
private static string GetAppSettingFilename()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(AppSettingsFilename))
|
if (!string.IsNullOrEmpty(AppSettingsFilename))
|
||||||
|
|
@ -151,6 +157,55 @@ namespace Kavita.Common
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region BaseUrl
|
||||||
|
private static string GetBaseUrl(string filePath)
|
||||||
|
{
|
||||||
|
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
|
||||||
|
{
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
|
||||||
|
const string key = "BaseUrl";
|
||||||
|
|
||||||
|
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
|
||||||
|
{
|
||||||
|
return tokenElement.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Error reading app settings: " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetBaseUrl(string filePath, string value)
|
||||||
|
{
|
||||||
|
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentBaseUrl = GetBaseUrl(filePath);
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
if (!json.Contains("BaseUrl"))
|
||||||
|
{
|
||||||
|
var lastBracket = json.LastIndexOf("}", StringComparison.Ordinal) - 1;
|
||||||
|
json = (json.Substring(0, lastBracket) + (",\n \"BaseUrl\": " + currentBaseUrl) + "}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
json = json.Replace("\"BaseUrl\": " + currentBaseUrl, "\"BaseUrl\": " + value);
|
||||||
|
}
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region LogLevel
|
#region LogLevel
|
||||||
|
|
||||||
private static void SetLogLevel(string filePath, string logLevel)
|
private static void SetLogLevel(string filePath, string logLevel)
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,14 @@
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<Company>kavitareader.com</Company>
|
<Company>kavitareader.com</Company>
|
||||||
<Product>Kavita</Product>
|
<Product>Kavita</Product>
|
||||||
<AssemblyVersion>0.4.6.14</AssemblyVersion>
|
<AssemblyVersion>0.4.6.19</AssemblyVersion>
|
||||||
<NeutralLanguage>en</NeutralLanguage>
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<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="Microsoft.Extensions.Hosting" Version="5.0.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.8.3" />
|
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.29.0.36737">
|
||||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.27.0.35380">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
||||||
150
UI/Web/package-lock.json
generated
150
UI/Web/package-lock.json
generated
|
|
@ -2679,135 +2679,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sentry/angular": {
|
|
||||||
"version": "6.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-6.10.0.tgz",
|
|
||||||
"integrity": "sha512-SSnsz4sVu9LJh7RM+z9FopWytl2yYNZQ2nK/zv/6iQKIBOqvnCqUIPjVjq1rFYXOe0jOJKsn0QlQLKp4MajYMg==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/browser": "6.10.0",
|
|
||||||
"@sentry/types": "6.10.0",
|
|
||||||
"@sentry/utils": "6.10.0",
|
|
||||||
"rxjs": "^6.6.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sentry/browser": {
|
|
||||||
"version": "6.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.10.0.tgz",
|
|
||||||
"integrity": "sha512-H0Blgp8f8bomebkkGWIgxHVjabtQAlsKJDiFXBg7gIc75YcarRxwH0R3hMog1/h8mmv4CGGUsy5ljYW6jsNnvA==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/core": "6.10.0",
|
|
||||||
"@sentry/types": "6.10.0",
|
|
||||||
"@sentry/utils": "6.10.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sentry/core": {
|
|
||||||
"version": "6.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.10.0.tgz",
|
|
||||||
"integrity": "sha512-5KlxHJlbD7AMo+b9pMGkjxUOfMILtsqCtGgI7DMvZNfEkdohO8QgUY+hPqr540kmwArFS91ipQYWhqzGaOhM3Q==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/hub": "6.10.0",
|
|
||||||
"@sentry/minimal": "6.10.0",
|
|
||||||
"@sentry/types": "6.10.0",
|
|
||||||
"@sentry/utils": "6.10.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sentry/hub": {
|
|
||||||
"version": "6.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.10.0.tgz",
|
|
||||||
"integrity": "sha512-MV8wjhWiFAXZAhmj7Ef5QdBr2IF93u8xXiIo2J+dRZ7eVa4/ZszoUiDbhUcl/TPxczaw4oW2a6tINBNFLzXiig==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/types": "6.10.0",
|
|
||||||
"@sentry/utils": "6.10.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sentry/integrations": {
|
|
||||||
"version": "6.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-6.10.0.tgz",
|
|
||||||
"integrity": "sha512-NMtB0jjFYFZRxyjYu2dWLThk9YPIwqhi4hYywmWkbv4/ILzi5Rwnh+aqNW6yrj8qG4b9itNMh3YvEzmf0aqauw==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/types": "6.10.0",
|
|
||||||
"@sentry/utils": "6.10.0",
|
|
||||||
"localforage": "^1.8.1",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sentry/minimal": {
|
|
||||||
"version": "6.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.10.0.tgz",
|
|
||||||
"integrity": "sha512-yarm046UgUFIBoxqnBan2+BEgaO9KZCrLzsIsmALiQvpfW92K1lHurSawl5W6SR7wCYBnNn7CPvPE/BHFdy4YA==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/hub": "6.10.0",
|
|
||||||
"@sentry/types": "6.10.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sentry/types": {
|
|
||||||
"version": "6.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.10.0.tgz",
|
|
||||||
"integrity": "sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw=="
|
|
||||||
},
|
|
||||||
"@sentry/utils": {
|
|
||||||
"version": "6.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.10.0.tgz",
|
|
||||||
"integrity": "sha512-F9OczOcZMFtazYVZ6LfRIe65/eOfQbiAedIKS0li4npuMz0jKYRbxrjd/U7oLiNQkPAp4/BujU4m1ZIwq6a+tg==",
|
|
||||||
"requires": {
|
|
||||||
"@sentry/types": "6.10.0",
|
|
||||||
"tslib": "^1.9.3"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@sinonjs/commons": {
|
"@sinonjs/commons": {
|
||||||
"version": "1.8.2",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz",
|
||||||
|
|
@ -7521,7 +7392,8 @@
|
||||||
"immediate": {
|
"immediate": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
|
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"import-fresh": {
|
"import-fresh": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|
@ -10032,24 +9904,6 @@
|
||||||
"json5": "^2.1.2"
|
"json5": "^2.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"localforage": {
|
|
||||||
"version": "1.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz",
|
|
||||||
"integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==",
|
|
||||||
"requires": {
|
|
||||||
"lie": "3.1.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"lie": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
|
||||||
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
|
|
||||||
"requires": {
|
|
||||||
"immediate": "~3.0.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"locate-path": {
|
"locate-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,6 @@
|
||||||
"@ng-bootstrap/ng-bootstrap": "^9.1.0",
|
"@ng-bootstrap/ng-bootstrap": "^9.1.0",
|
||||||
"@ngx-lite/nav-drawer": "^0.4.6",
|
"@ngx-lite/nav-drawer": "^0.4.6",
|
||||||
"@ngx-lite/util": "0.0.0",
|
"@ngx-lite/util": "0.0.0",
|
||||||
"@sentry/angular": "^6.10.0",
|
|
||||||
"@sentry/integrations": "^6.10.0",
|
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"angular-ng-autocomplete": "^2.0.5",
|
"angular-ng-autocomplete": "^2.0.5",
|
||||||
"bootstrap": "^4.5.0",
|
"bootstrap": "^4.5.0",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { map, takeUntil } from 'rxjs/operators';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { Preferences } from '../_models/preferences/preferences';
|
import { Preferences } from '../_models/preferences/preferences';
|
||||||
import { User } from '../_models/user';
|
import { User } from '../_models/user';
|
||||||
import * as Sentry from "@sentry/angular";
|
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { MessageHubService } from './message-hub.service';
|
import { MessageHubService } from './message-hub.service';
|
||||||
|
|
||||||
|
|
@ -63,12 +62,6 @@ export class AccountService implements OnDestroy {
|
||||||
user.roles = [];
|
user.roles = [];
|
||||||
const roles = this.getDecodedToken(user.token).role;
|
const roles = this.getDecodedToken(user.token).role;
|
||||||
Array.isArray(roles) ? user.roles = roles : user.roles.push(roles);
|
Array.isArray(roles) ? user.roles = roles : user.roles.push(roles);
|
||||||
Sentry.setContext('admin', {'admin': this.hasAdminRole(user)});
|
|
||||||
Sentry.configureScope(scope => {
|
|
||||||
scope.setUser({
|
|
||||||
username: user.username
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem(this.userKey, JSON.stringify(user));
|
localStorage.setItem(this.userKey, JSON.stringify(user));
|
||||||
localStorage.setItem(this.lastLoginKey, user.username);
|
localStorage.setItem(this.lastLoginKey, user.username);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { EventEmitter, Injectable } from '@angular/core';
|
import { EventEmitter, Injectable } from '@angular/core';
|
||||||
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
|
||||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { User } from '@sentry/angular';
|
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
@ -10,6 +9,7 @@ import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event';
|
||||||
import { ScanLibraryProgressEvent } from '../_models/events/scan-library-progress-event';
|
import { ScanLibraryProgressEvent } from '../_models/events/scan-library-progress-event';
|
||||||
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
|
||||||
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
import { SeriesAddedEvent } from '../_models/events/series-added-event';
|
||||||
|
import { User } from '../_models/user';
|
||||||
|
|
||||||
export enum EVENTS {
|
export enum EVENTS {
|
||||||
UpdateAvailable = 'UpdateAvailable',
|
UpdateAvailable = 'UpdateAvailable',
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ export interface ServerSettings {
|
||||||
allowStatCollection: boolean;
|
allowStatCollection: boolean;
|
||||||
enableOpds: boolean;
|
enableOpds: boolean;
|
||||||
enableAuthentication: boolean;
|
enableAuthentication: boolean;
|
||||||
|
baseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { take, takeUntil, takeWhile } from 'rxjs/operators';
|
import { take, takeUntil } from 'rxjs/operators';
|
||||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||||
import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
|
import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
|
||||||
import { Library, LibraryType } from 'src/app/_models/library';
|
import { Library, LibraryType } from 'src/app/_models/library';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||||
<p class="text-warning pt-2">Port and Logging Level require a manual restart of Kavita to take effect.</p>
|
<p class="text-warning pt-2">Port, Base Url, and Logging Level require a manual restart of Kavita to take effect.</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="settings-cachedir">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
<label for="settings-cachedir">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #cacheDirectoryTooltip>Where the server place temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
<ng-template #cacheDirectoryTooltip>Where the server place temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
<input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text">
|
<input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="settings-baseurl">Base Url</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
|
||||||
|
<ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</ng-template>
|
||||||
|
<span class="sr-only" id="settings-baseurl-help">Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</span>
|
||||||
|
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="settings-port">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
<label for="settings-port">Port</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
|
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required]));
|
this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required]));
|
||||||
this.settingsForm.addControl('enableOpds', new FormControl(this.serverSettings.enableOpds, [Validators.required]));
|
this.settingsForm.addControl('enableOpds', new FormControl(this.serverSettings.enableOpds, [Validators.required]));
|
||||||
this.settingsForm.addControl('enableAuthentication', new FormControl(this.serverSettings.enableAuthentication, [Validators.required]));
|
this.settingsForm.addControl('enableAuthentication', new FormControl(this.serverSettings.enableAuthentication, [Validators.required]));
|
||||||
|
this.settingsForm.addControl('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.required]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +50,7 @@ export class ManageSettingsComponent implements OnInit {
|
||||||
this.settingsForm.get('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection);
|
this.settingsForm.get('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection);
|
||||||
this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds);
|
this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds);
|
||||||
this.settingsForm.get('enableAuthentication')?.setValue(this.serverSettings.enableAuthentication);
|
this.settingsForm.get('enableAuthentication')?.setValue(this.serverSettings.enableAuthentication);
|
||||||
|
this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { BrowserModule, Title } from '@angular/platform-browser';
|
import { BrowserModule, Title } from '@angular/platform-browser';
|
||||||
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
|
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
||||||
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
||||||
|
|
@ -21,67 +22,15 @@ import { AutocompleteLibModule } from 'angular-ng-autocomplete';
|
||||||
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
|
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
|
||||||
import { CarouselModule } from './carousel/carousel.module';
|
import { CarouselModule } from './carousel/carousel.module';
|
||||||
|
|
||||||
|
|
||||||
import * as Sentry from '@sentry/angular';
|
|
||||||
import { environment } from 'src/environments/environment';
|
|
||||||
import { version } from 'package.json';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { RewriteFrames as RewriteFramesIntegration } from '@sentry/integrations';
|
|
||||||
import { Dedupe as DedupeIntegration } from '@sentry/integrations';
|
|
||||||
import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
import { PersonBadgeComponent } from './person-badge/person-badge.component';
|
||||||
import { TypeaheadModule } from './typeahead/typeahead.module';
|
import { TypeaheadModule } from './typeahead/typeahead.module';
|
||||||
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
||||||
|
import { InProgressComponent } from './in-progress/in-progress.component';
|
||||||
|
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||||
import { CardsModule } from './cards/cards.module';
|
import { CardsModule } from './cards/cards.module';
|
||||||
import { CollectionsModule } from './collections/collections.module';
|
import { CollectionsModule } from './collections/collections.module';
|
||||||
import { InProgressComponent } from './in-progress/in-progress.component';
|
|
||||||
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
|
||||||
import { ReadingListModule } from './reading-list/reading-list.module';
|
import { ReadingListModule } from './reading-list/reading-list.module';
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
||||||
|
|
||||||
let sentryProviders: any[] = [];
|
|
||||||
|
|
||||||
if (environment.production) {
|
|
||||||
Sentry.init({
|
|
||||||
dsn: 'https://db1a1f6445994b13a6f479512aecdd48@o641015.ingest.sentry.io/5757426',
|
|
||||||
environment: environment.production ? 'prod' : 'dev',
|
|
||||||
release: version,
|
|
||||||
integrations: [
|
|
||||||
new Sentry.Integrations.GlobalHandlers({
|
|
||||||
onunhandledrejection: true,
|
|
||||||
onerror: true
|
|
||||||
}),
|
|
||||||
new DedupeIntegration(),
|
|
||||||
new RewriteFramesIntegration(),
|
|
||||||
],
|
|
||||||
ignoreErrors: [new RegExp(/\/api\/admin/)],
|
|
||||||
tracesSampleRate: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
Sentry.configureScope(scope => {
|
|
||||||
scope.setUser({
|
|
||||||
username: 'Not authorized'
|
|
||||||
});
|
|
||||||
scope.setTag('production', environment.production);
|
|
||||||
scope.setTag('version', version);
|
|
||||||
});
|
|
||||||
|
|
||||||
sentryProviders = [{
|
|
||||||
provide: ErrorHandler,
|
|
||||||
useValue: Sentry.createErrorHandler({
|
|
||||||
showDialog: false,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: Sentry.TraceService,
|
|
||||||
deps: [Router],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: () => () => {},
|
|
||||||
deps: [Sentry.TraceService],
|
|
||||||
multi: true,
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|
@ -134,7 +83,7 @@ if (environment.production) {
|
||||||
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
||||||
Title,
|
Title,
|
||||||
{provide: SAVER, useFactory: getSaver},
|
{provide: SAVER, useFactory: getSaver},
|
||||||
...sentryProviders,
|
{ provide: APP_BASE_HREF, useValue: window['_app_base' as keyof Window] || '/' },
|
||||||
],
|
],
|
||||||
entryComponents: [],
|
entryComponents: [],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
|
|
||||||
|
|
@ -58,17 +58,16 @@
|
||||||
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i><span *ngIf="darkMode"> {{clickToPaginate ? 'On' : 'Off'}}</span></button>
|
<button (click)="toggleClickToPaginate()" class="btn btn-icon" aria-labelledby="tap-pagination"><i class="fa fa-arrows-alt-h {{clickToPaginate ? 'icon-primary-color' : ''}}" aria-hidden="true"></i><span *ngIf="darkMode"> {{clickToPaginate ? 'On' : 'Off'}}</span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters justify-content-between">
|
<div class="row no-gutters justify-content-between">
|
||||||
<button (click)="resetSettings()" class="btn btn-secondary col">Reset</button>
|
<button (click)="resetSettings()" class="btn btn-secondary col">Reset to Defaults</button>
|
||||||
<button (click)="saveSettings()" class="btn btn-primary col" style="margin-left:10px;">Save</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" [disabled]="prevChapterDisabled" (click)="loadPrevChapter()" title="Prev Chapter/Volume"><i class="fa fa-fast-backward" aria-hidden="true"></i></button>
|
||||||
<div class="col-1" style="margin-top: 6px">{{pageNum}}</div>
|
<div class="col-1 page-stub">{{pageNum}}</div>
|
||||||
<div class="col-8" style="margin-top: 15px">
|
<div class="col-8" style="margin-top: 15px;padding-right:10px">
|
||||||
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
<ngb-progressbar style="cursor: pointer" title="Go to page" (click)="goToPage()" type="primary" height="5px" [value]="pageNum" [max]="maxPages - 1"></ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-1 btn-icon" style="margin-top: 6px" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
<div class="col-1 btn-icon page-stub" (click)="goToPage(maxPages - 1)" title="Go to last page">{{maxPages - 1}}</div>
|
||||||
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
<button class="btn btn-small btn-icon col-1" [disabled]="nextChapterDisabled" (click)="loadNextChapter()" title="Next Chapter/Volume"><i class="fa fa-fast-forward" aria-hidden="true"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-of-contents">
|
<div class="table-of-contents">
|
||||||
|
|
@ -100,15 +99,6 @@
|
||||||
</app-drawer>
|
</app-drawer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- This pushes down the page. Need to overlay
|
|
||||||
<ng-container *ngIf="isLoading">
|
|
||||||
<div class="d-flex justify-content-center m-5">
|
|
||||||
<div class="spinner-border text-secondary loading" role="status">
|
|
||||||
<span class="invisible">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container> -->
|
|
||||||
|
|
||||||
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}" [@isLoading]="isLoading ? true : false" (click)="handleReaderClick($event)">
|
<div #readingSection class="reading-section" [ngStyle]="{'padding-top': topOffset + 20 + 'px'}" [@isLoading]="isLoading ? true : false" (click)="handleReaderClick($event)">
|
||||||
<div #readingHtml [innerHtml]="page" *ngIf="page !== undefined"></div>
|
<div #readingHtml [innerHtml]="page" *ngIf="page !== undefined"></div>
|
||||||
|
|
||||||
|
|
@ -138,7 +128,7 @@
|
||||||
[disabled]="IsNextDisabled"
|
[disabled]="IsNextDisabled"
|
||||||
(click)="nextPage()" title="{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} Page">
|
(click)="nextPage()" title="{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} Page">
|
||||||
<span class="phone-hidden">{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} </span>
|
<span class="phone-hidden">{{readingDirection === ReadingDirection.LeftToRight ? 'Next' : 'Previous'}} </span>
|
||||||
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? pageNum + 1 >= maxPages - 1 : pageNum === 0) ? 'fa-angle-double-right' : 'fa-angle-right'}}" aria-hidden="true"></i>
|
<i class="fa {{(readingDirection === ReadingDirection.LeftToRight ? pageNum + 1 > maxPages - 1 : pageNum === 0) ? 'fa-angle-double-right' : 'fa-angle-right'}}" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
src: url(../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype");
|
src: url(../../../assets/fonts/RocknRoll_One/RocknRollOne-Regular.ttf) format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$dark-form-background-no-opacity: rgb(1, 4, 9);
|
||||||
$primary-color: #0062cc;
|
$primary-color: #0062cc;
|
||||||
|
|
||||||
.control-container {
|
.control-container {
|
||||||
|
|
@ -42,6 +43,11 @@ $primary-color: #0062cc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-stub {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.dark-mode {
|
.dark-mode {
|
||||||
|
|
||||||
|
|
@ -73,60 +79,12 @@ $primary-color: #0062cc;
|
||||||
color: #8db2e5 !important;
|
color: #8db2e5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coppied
|
|
||||||
// html, body {
|
|
||||||
// color: #dcdcdc !important;
|
|
||||||
// background-image: none !important;
|
|
||||||
// background-color: #292929 !important;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// html::before, body::before {
|
|
||||||
// background-image: none !important;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// html *:not(input) {color: #dcdcdc !important}
|
|
||||||
// html * {background-color: rgb(41, 41, 41, 0.90) !important}
|
|
||||||
|
|
||||||
// html *, html *[id], html *[class] {
|
|
||||||
// box-shadow: none !important;
|
|
||||||
// text-shadow: none !important;
|
|
||||||
// border-radius: unset !important;
|
|
||||||
// border-color: #555555 !important;
|
|
||||||
// outline-color: #555555 !important;
|
|
||||||
// }
|
|
||||||
|
|
||||||
img, img[src] {
|
img, img[src] {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
filter: brightness(0.85) !important;
|
filter: brightness(0.85) !important;
|
||||||
background-color: initial !important;
|
background-color: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// video, video[src] {
|
|
||||||
// z-index: 1;
|
|
||||||
// background-color: transparent !important;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// input:not([type='button']):not([type='submit']) {
|
|
||||||
// color: #dcdcdc !important;
|
|
||||||
// background-image: none !important;
|
|
||||||
// background-color: #333333 !important;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// textarea, textarea[class], input[type='text'], input[type='text'][class] {
|
|
||||||
// color: #dcdcdc !important;
|
|
||||||
// background-color: #555555 !important;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// svg:not([fill]) {fill: #7d7d7d !important}
|
|
||||||
// li, select {background-image: none !important}
|
|
||||||
// input[type='text'], input[type='search'] {text-indent: 10px}
|
|
||||||
// a {background-color: rgba(255, 255, 255, 0.01) !important}
|
|
||||||
// html cite, html cite *, html cite *[class] {color: #029833 !important}
|
|
||||||
// svg[fill], button, input[type='button'], input[type='submit'] {opacity: 0.85 !important}
|
|
||||||
|
|
||||||
// :before {color: #dcdcdc !important}
|
|
||||||
// :link:not(cite), :link *:not(cite) {color: #8db2e5 !important}
|
|
||||||
// :visited, :visited *, :visited *[class] {color: rgb(211, 138, 138) !important}
|
|
||||||
:visited, :visited *, :visited *[class] {color: rgb(211, 138, 138) !important}
|
:visited, :visited *, :visited *[class] {color: rgb(211, 138, 138) !important}
|
||||||
:link:not(cite), :link *:not(cite) {color: #8db2e5 !important}
|
:link:not(cite), :link *:not(cite) {color: #8db2e5 !important}
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +94,21 @@ $primary-color: #0062cc;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark-mode {
|
||||||
|
.reading-bar, .book-title, .drawer-body, .drawer-container {
|
||||||
|
background-color: $dark-form-background-no-opacity;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: $dark-form-background-no-opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .dark-mode .drawer-container {
|
||||||
|
.header, body, *:not(.progress-bar) {
|
||||||
|
background-color: $dark-form-background-no-opacity !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media(max-width: 875px) {
|
@media(max-width: 875px) {
|
||||||
.book-title {
|
.book-title {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,10 @@ import { SeriesService } from 'src/app/_services/series.service';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { BookService } from '../book.service';
|
import { BookService } from '../book.service';
|
||||||
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
import { KEY_CODES, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||||
import { BookChapterItem } from '../_models/book-chapter-item';
|
import { BookChapterItem } from '../_models/book-chapter-item';
|
||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||||
import { Stack } from 'src/app/shared/data-structures/stack';
|
import { Stack } from 'src/app/shared/data-structures/stack';
|
||||||
import { Preferences } from 'src/app/_models/preferences/preferences';
|
|
||||||
import { MemberService } from 'src/app/_services/member.service';
|
import { MemberService } from 'src/app/_services/member.service';
|
||||||
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
|
import { ReadingDirection } from 'src/app/_models/preferences/reading-direction';
|
||||||
import { ScrollService } from 'src/app/scroll.service';
|
import { ScrollService } from 'src/app/scroll.service';
|
||||||
|
|
@ -166,7 +165,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
pageAnchors: {[n: string]: number } = {};
|
pageAnchors: {[n: string]: number } = {};
|
||||||
currentPageAnchor: string = '';
|
currentPageAnchor: string = '';
|
||||||
intersectionObserver: IntersectionObserver = new IntersectionObserver((entries) => this.handleIntersection(entries), { threshold: [1] });
|
|
||||||
/**
|
/**
|
||||||
* Last seen progress part path
|
* Last seen progress part path
|
||||||
*/
|
*/
|
||||||
|
|
@ -186,10 +184,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
color: #e83e8c !important;
|
color: #e83e8c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// .btn-icon {
|
|
||||||
// background-color: transparent;
|
|
||||||
// }
|
|
||||||
|
|
||||||
:link, a {
|
:link, a {
|
||||||
color: #8db2e5 !important;
|
color: #8db2e5 !important;
|
||||||
}
|
}
|
||||||
|
|
@ -205,25 +199,31 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return ReadingDirection;
|
return ReadingDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
get IsPrevDisabled() {
|
get IsPrevDisabled(): boolean {
|
||||||
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
||||||
|
// Acting as Previous button
|
||||||
return this.prevPageDisabled && this.pageNum === 0;
|
return this.prevPageDisabled && this.pageNum === 0;
|
||||||
|
} else {
|
||||||
|
// Acting as a Next button
|
||||||
|
return this.nextPageDisabled && this.pageNum + 1 > this.maxPages - 1;
|
||||||
}
|
}
|
||||||
return this.nextPageDisabled && this.pageNum + 1 >= this.maxPages - 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get IsNextDisabled() {
|
get IsNextDisabled(): boolean {
|
||||||
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
if (this.readingDirection === ReadingDirection.LeftToRight) {
|
||||||
this.nextPageDisabled && this.pageNum + 1 >= this.maxPages - 1;
|
// Acting as Next button
|
||||||
}
|
return this.nextPageDisabled && this.pageNum + 1 > this.maxPages - 1;
|
||||||
|
} else {
|
||||||
|
// Acting as Previous button
|
||||||
return this.prevPageDisabled && this.pageNum === 0;
|
return this.prevPageDisabled && this.pageNum === 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService,
|
||||||
private seriesService: SeriesService, private readerService: ReaderService, private location: Location,
|
private seriesService: SeriesService, private readerService: ReaderService, private location: Location,
|
||||||
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
|
private renderer: Renderer2, private navService: NavService, private toastr: ToastrService,
|
||||||
private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService,
|
private domSanitizer: DomSanitizer, private bookService: BookService, private memberService: MemberService,
|
||||||
private scrollService: ScrollService) {
|
private scrollService: ScrollService, private utilityService: UtilityService) {
|
||||||
this.navService.hideNavBar();
|
this.navService.hideNavBar();
|
||||||
|
|
||||||
this.darkModeStyleElem = this.renderer.createElement('style');
|
this.darkModeStyleElem = this.renderer.createElement('style');
|
||||||
|
|
@ -296,6 +296,36 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Find the element that is on screen to bookmark against
|
||||||
|
const intersectingEntries = Array.from(this.readingSectionElemRef.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span'))
|
||||||
|
.filter(element => !element.classList.contains('no-observe'))
|
||||||
|
.filter(entry => {
|
||||||
|
return this.utilityService.isInViewport(entry, this.topOffset);
|
||||||
|
});
|
||||||
|
|
||||||
|
intersectingEntries.sort((a: Element, b: Element) => {
|
||||||
|
const aTop = a.getBoundingClientRect().top;
|
||||||
|
const bTop = b.getBoundingClientRect().top;
|
||||||
|
if (aTop < bTop) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (aTop > bTop) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (intersectingEntries.length > 0) {
|
||||||
|
let path = this.getXPathTo(intersectingEntries[0]);
|
||||||
|
if (path === '') { return; }
|
||||||
|
if (!path.startsWith('id')) {
|
||||||
|
path = '//html[1]/' + path;
|
||||||
|
}
|
||||||
|
this.lastSeenScrollPartPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastSeenScrollPartPath !== '' && !this.incognitoMode) {
|
if (this.lastSeenScrollPartPath !== '' && !this.incognitoMode) {
|
||||||
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */});
|
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum, this.lastSeenScrollPartPath).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +356,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
this.onDestroy.next();
|
this.onDestroy.next();
|
||||||
this.onDestroy.complete();
|
this.onDestroy.complete();
|
||||||
this.intersectionObserver.disconnect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
@ -443,11 +472,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleIntersection(entries: IntersectionObserverEntry[]) {
|
sortElements(a: Element, b: Element) {
|
||||||
let intersectingEntries = Array.from(entries)
|
|
||||||
.filter(entry => entry.isIntersecting)
|
|
||||||
.map(entry => entry.target)
|
|
||||||
intersectingEntries.sort((a: Element, b: Element) => {
|
|
||||||
const aTop = a.getBoundingClientRect().top;
|
const aTop = a.getBoundingClientRect().top;
|
||||||
const bTop = b.getBoundingClientRect().top;
|
const bTop = b.getBoundingClientRect().top;
|
||||||
if (aTop < bTop) {
|
if (aTop < bTop) {
|
||||||
|
|
@ -458,17 +483,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (intersectingEntries.length > 0) {
|
|
||||||
let path = this.getXPathTo(intersectingEntries[0]);
|
|
||||||
if (path === '') { return; }
|
|
||||||
if (!path.startsWith('id')) {
|
|
||||||
path = '//html[1]/' + path;
|
|
||||||
}
|
|
||||||
this.lastSeenScrollPartPath = path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadNextChapter() {
|
loadNextChapter() {
|
||||||
|
|
@ -541,16 +555,13 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetSettings(afterSave: boolean = false) {
|
resetSettings() {
|
||||||
const windowWidth = window.innerWidth
|
const windowWidth = window.innerWidth
|
||||||
|| document.documentElement.clientWidth
|
|| document.documentElement.clientWidth
|
||||||
|| document.body.clientWidth;
|
|| document.body.clientWidth;
|
||||||
|
|
||||||
let margin = '15%';
|
let margin = '15%';
|
||||||
if (windowWidth <= 700) {
|
if (windowWidth <= 700) {
|
||||||
if (afterSave && this.user.preferences.bookReaderMargin !== 0) {
|
|
||||||
this.toastr.info('Margin will be reset to 0% on mobile. You do not have to save for settings to take effect.');
|
|
||||||
}
|
|
||||||
margin = '0%';
|
margin = '0%';
|
||||||
}
|
}
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
|
|
@ -558,11 +569,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
margin = this.user.preferences.bookReaderMargin + '%';
|
margin = this.user.preferences.bookReaderMargin + '%';
|
||||||
}
|
}
|
||||||
this.pageStyles = {'font-family': this.user.preferences.bookReaderFontFamily, 'font-size': this.user.preferences.bookReaderFontSize + '%', 'margin-left': margin, 'margin-right': margin, 'line-height': this.user.preferences.bookReaderLineSpacing + '%'};
|
this.pageStyles = {'font-family': this.user.preferences.bookReaderFontFamily, 'font-size': this.user.preferences.bookReaderFontSize + '%', 'margin-left': margin, 'margin-right': margin, 'line-height': this.user.preferences.bookReaderLineSpacing + '%'};
|
||||||
if (!afterSave) {
|
|
||||||
if (this.user.preferences.siteDarkMode && !this.user.preferences.bookReaderDarkMode) {
|
|
||||||
this.user.preferences.bookReaderDarkMode = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggleDarkMode(this.user.preferences.bookReaderDarkMode);
|
this.toggleDarkMode(this.user.preferences.bookReaderDarkMode);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -657,12 +663,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPageAnchors() {
|
setupPageAnchors() {
|
||||||
this.readingSectionElemRef.nativeElement.querySelectorAll('div,o,p,ul,li,a,img,h1,h2,h3,h4,h5,h6,span').forEach(elem => {
|
|
||||||
if (!elem.classList.contains('no-observe')) {
|
|
||||||
this.intersectionObserver.observe(elem);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pageAnchors = {};
|
this.pageAnchors = {};
|
||||||
this.currentPageAnchor = '';
|
this.currentPageAnchor = '';
|
||||||
const ids = this.chapters.map(item => item.children).flat().filter(item => item.page === this.pageNum).map(item => item.part).filter(item => item.length > 0);
|
const ids = this.chapters.map(item => item.children).flat().filter(item => item.page === this.pageNum).map(item => item.part).filter(item => item.length > 0);
|
||||||
|
|
@ -875,7 +875,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
getDarkModeBackgroundColor() {
|
getDarkModeBackgroundColor() {
|
||||||
return this.darkMode ? '#292929' : '#fff';
|
return this.darkMode ? '#010409' : '#fff';
|
||||||
}
|
}
|
||||||
|
|
||||||
setOverrideStyles() {
|
setOverrideStyles() {
|
||||||
|
|
@ -896,33 +896,6 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveSettings() {
|
|
||||||
if (this.user === undefined) return;
|
|
||||||
const modelSettings = this.settingsForm.value;
|
|
||||||
const data: Preferences = {
|
|
||||||
readingDirection: this.user.preferences.readingDirection,
|
|
||||||
scalingOption: this.user.preferences.scalingOption,
|
|
||||||
pageSplitOption: this.user.preferences.pageSplitOption,
|
|
||||||
autoCloseMenu: this.user.preferences.autoCloseMenu,
|
|
||||||
readerMode: this.user.preferences.readerMode,
|
|
||||||
bookReaderDarkMode: this.darkMode,
|
|
||||||
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
|
||||||
bookReaderFontSize: parseInt(this.pageStyles['font-size'].substr(0, this.pageStyles['font-size'].length - 1), 10),
|
|
||||||
bookReaderLineSpacing: parseInt(this.pageStyles['line-height'].replace('!important', '').trim(), 10),
|
|
||||||
bookReaderMargin: parseInt(this.pageStyles['margin-left'].replace('%', '').replace('!important', '').trim(), 10),
|
|
||||||
bookReaderTapToPaginate: this.clickToPaginate,
|
|
||||||
bookReaderReadingDirection: this.readingDirection,
|
|
||||||
siteDarkMode: this.user.preferences.siteDarkMode,
|
|
||||||
};
|
|
||||||
this.accountService.updatePreferences(data).pipe(take(1)).subscribe((updatedPrefs) => {
|
|
||||||
this.toastr.success('User settings updated');
|
|
||||||
if (this.user) {
|
|
||||||
this.user.preferences = updatedPrefs;
|
|
||||||
}
|
|
||||||
this.resetSettings(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDrawer() {
|
toggleDrawer() {
|
||||||
this.topOffset = this.stickyTopElemRef.nativeElement?.offsetHeight;
|
this.topOffset = this.stickyTopElemRef.nativeElement?.offsetHeight;
|
||||||
this.drawerOpen = !this.drawerOpen;
|
this.drawerOpen = !this.drawerOpen;
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ export class InfiniteScrollerComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
return document.documentElement.offsetHeight + document.documentElement.scrollTop;
|
return document.documentElement.offsetHeight + document.documentElement.scrollTop;
|
||||||
}
|
}
|
||||||
getScrollTop() {
|
getScrollTop() {
|
||||||
return document.documentElement.scrollTop
|
return document.documentElement.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIfShouldTriggerContinuousReader() {
|
checkIfShouldTriggerContinuousReader() {
|
||||||
|
|
|
||||||
|
|
@ -127,4 +127,14 @@ export class UtilityService {
|
||||||
return Breakpoint.Desktop;
|
return Breakpoint.Desktop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInViewport(element: Element, additionalTopOffset: number = 0) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.top >= additionalTopOffset &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,9 @@
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||||
</body>
|
</body>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
window['_app_base'] = '/' + window.location.pathname.split('/')[1];
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue