mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Lots of Bugfixes (#2960)
Co-authored-by: Samuel Martins <s@smartins.ch> Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
97ffdd0975
commit
b50fa0fd1e
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
|
||||
- name: Install Swashbuckle CLI
|
||||
shell: powershell
|
||||
run: dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
run: dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.0.2" />
|
||||
|
@ -74,7 +74,7 @@ public class WordCountAnalysisTests : AbstractDbTest
|
||||
|
||||
var cacheService = new CacheHelper(new FileService());
|
||||
var service = new WordCountAnalyzerService(Substitute.For<ILogger<WordCountAnalyzerService>>(), _unitOfWork,
|
||||
Substitute.For<IEventHub>(), cacheService, _readerService);
|
||||
Substitute.For<IEventHub>(), cacheService, _readerService, Substitute.For<IMediaErrorService>());
|
||||
|
||||
|
||||
await service.ScanSeries(1, 1);
|
||||
@ -126,7 +126,7 @@ public class WordCountAnalysisTests : AbstractDbTest
|
||||
|
||||
var cacheService = new CacheHelper(new FileService());
|
||||
var service = new WordCountAnalyzerService(Substitute.For<ILogger<WordCountAnalyzerService>>(), _unitOfWork,
|
||||
Substitute.For<IEventHub>(), cacheService, _readerService);
|
||||
Substitute.For<IEventHub>(), cacheService, _readerService, Substitute.For<IMediaErrorService>());
|
||||
await service.ScanSeries(1, 1);
|
||||
|
||||
var chapter2 = new ChapterBuilder("2")
|
||||
|
@ -12,9 +12,9 @@
|
||||
<LangVersion>latestmajor</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
|
||||
</Target>
|
||||
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
|
||||
<!-- <Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
|
||||
<!-- </Target>-->
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
@ -95,13 +95,13 @@
|
||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||
<PackageReference Include="SharpCompress" Version="0.37.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.24.0.89429">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.25.0.90414">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.5.2" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="21.0.2" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.4" />
|
||||
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
||||
|
48
API/Data/ManualMigrations/ManualMigrateSwitchToWal.cs
Normal file
48
API/Data/ManualMigrations/ManualMigrateSwitchToWal.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.8.2 switches Default Kavita installs to WAL
|
||||
/// </summary>
|
||||
public static class ManualMigrateSwitchToWal
|
||||
{
|
||||
public static async Task Migrate(DataContext context, ILogger<Program> logger)
|
||||
{
|
||||
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateSwitchToWal"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical("Running ManualMigrateSwitchToWal migration - Please be patient, this may take some time. This is not an error");
|
||||
try
|
||||
{
|
||||
var connection = context.Database.GetDbConnection();
|
||||
await connection.OpenAsync();
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "PRAGMA journal_mode=WAL;";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error setting WAL");
|
||||
/* Swallow */
|
||||
}
|
||||
|
||||
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "ManualMigrateSwitchToWal",
|
||||
ProductVersion = BuildInfo.Version.ToString(),
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
logger.LogCritical("Running ManualMigrateSwitchToWal migration - Completed. This is not an error");
|
||||
}
|
||||
|
||||
}
|
49
API/Data/ManualMigrations/ManualMigrateThemeDescription.cs
Normal file
49
API/Data/ManualMigrations/ManualMigrateThemeDescription.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Entities;
|
||||
using Kavita.Common.EnvironmentInfo;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Data.ManualMigrations;
|
||||
|
||||
/// <summary>
|
||||
/// v0.8.2 introduced Theme repo viewer, this adds Description to existing SiteTheme defaults
|
||||
/// </summary>
|
||||
public static class ManualMigrateThemeDescription
|
||||
{
|
||||
public static async Task Migrate(DataContext context, ILogger<Program> logger)
|
||||
{
|
||||
if (await context.ManualMigrationHistory.AnyAsync(m => m.Name == "ManualMigrateThemeDescription"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogCritical("Running ManualMigrateThemeDescription migration - Please be patient, this may take some time. This is not an error");
|
||||
|
||||
var theme = await context.SiteTheme.FirstOrDefaultAsync(t => t.Name == "Dark");
|
||||
if (theme != null)
|
||||
{
|
||||
theme.Description = Seed.DefaultThemes.First().Description;
|
||||
}
|
||||
|
||||
if (context.ChangeTracker.HasChanges())
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
await context.ManualMigrationHistory.AddAsync(new ManualMigrationHistory()
|
||||
{
|
||||
Name = "ManualMigrateThemeDescription",
|
||||
ProductVersion = BuildInfo.Version.ToString(),
|
||||
RanAt = DateTime.UtcNow
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
logger.LogCritical("Running ManualMigrateThemeDescription migration - Completed. This is not an error");
|
||||
}
|
||||
}
|
@ -1748,12 +1748,12 @@ public class SeriesRepository : ISeriesRepository
|
||||
{
|
||||
// This is due to v0.5.6 introducing bugs where we could have multiple series get duplicated and no way to delete them
|
||||
// This here will delete the 2nd one as the first is the one to likely be used.
|
||||
var sId = _context.Series
|
||||
var sId = await _context.Series
|
||||
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName &&
|
||||
s.LibraryId == libraryId)
|
||||
.Select(s => s.Id)
|
||||
.OrderBy(s => s)
|
||||
.Last();
|
||||
.LastAsync();
|
||||
if (sId > 0)
|
||||
{
|
||||
ids.Add(sId);
|
||||
|
@ -35,6 +35,7 @@ public static class Seed
|
||||
Provider = ThemeProvider.System,
|
||||
FileName = "dark.scss",
|
||||
IsDefault = true,
|
||||
Description = "Default theme shipped with Kavita"
|
||||
}
|
||||
}.ToArray()
|
||||
];
|
||||
|
@ -99,10 +99,13 @@ public class Program
|
||||
// Apply all migrations on startup
|
||||
logger.LogInformation("Running Migrations");
|
||||
|
||||
// v0.7.14
|
||||
try
|
||||
{
|
||||
// v0.7.14
|
||||
await MigrateWantToReadExport.Migrate(context, directoryService, logger);
|
||||
|
||||
// v0.8.2
|
||||
await ManualMigrateSwitchToWal.Migrate(context, logger);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -73,7 +73,8 @@ public class BookService : IBookService
|
||||
{
|
||||
PackageReaderOptions = new PackageReaderOptions
|
||||
{
|
||||
IgnoreMissingToc = true
|
||||
IgnoreMissingToc = true,
|
||||
SkipInvalidManifestItems = true
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using API.Data;
|
||||
using API.DTOs.Reader;
|
||||
@ -51,6 +53,8 @@ public class CacheService : ICacheService
|
||||
private readonly IReadingItemService _readingItemService;
|
||||
private readonly IBookmarkService _bookmarkService;
|
||||
|
||||
private static readonly ConcurrentDictionary<int, SemaphoreSlim> ExtractLocks = new();
|
||||
|
||||
public CacheService(ILogger<CacheService> logger, IUnitOfWork unitOfWork,
|
||||
IDirectoryService directoryService, IReadingItemService readingItemService,
|
||||
IBookmarkService bookmarkService)
|
||||
@ -166,9 +170,17 @@ public class CacheService : ICacheService
|
||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||
var extractPath = GetCachePath(chapterId);
|
||||
|
||||
SemaphoreSlim extractLock = ExtractLocks.GetOrAdd(chapterId, id => new SemaphoreSlim(1,1));
|
||||
|
||||
await extractLock.WaitAsync();
|
||||
try {
|
||||
if(_directoryService.Exists(extractPath)) return chapter;
|
||||
|
||||
var files = chapter?.Files.ToList();
|
||||
ExtractChapterFiles(extractPath, files, extractPdfToImages);
|
||||
} finally {
|
||||
extractLock.Release();
|
||||
}
|
||||
|
||||
return chapter;
|
||||
}
|
||||
@ -190,6 +202,14 @@ public class CacheService : ICacheService
|
||||
var extractDi = _directoryService.FileSystem.DirectoryInfo.New(extractPath);
|
||||
|
||||
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
||||
{
|
||||
// Check if all the files are Images. If so, do a directory copy, else do the normal copy
|
||||
if (files.All(f => f.Format == MangaFormat.Image))
|
||||
{
|
||||
_directoryService.ExistOrCreate(extractPath);
|
||||
_directoryService.CopyFilesToDirectory(files.Select(f => f.FilePath), extractPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
@ -202,6 +222,8 @@ public class CacheService : ICacheService
|
||||
_directoryService.Flatten(extractDi.FullName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (fileCount > 1)
|
||||
|
@ -439,18 +439,13 @@ public class ImageService : IImageService
|
||||
rows = 1;
|
||||
cols = 2;
|
||||
}
|
||||
else if (coverImages.Count == 3)
|
||||
{
|
||||
rows = 2;
|
||||
cols = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to 2x2 layout for more than 3 images
|
||||
rows = 2;
|
||||
cols = 2;
|
||||
}
|
||||
|
||||
|
||||
var image = Image.Black(dims.Width, dims.Height);
|
||||
|
||||
var thumbnailWidth = image.Width / cols;
|
||||
|
@ -60,7 +60,6 @@ public interface IScrobblingService
|
||||
public class ScrobblingService : IScrobblingService
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ILogger<ScrobblingService> _logger;
|
||||
private readonly ILicenseService _licenseService;
|
||||
@ -99,12 +98,10 @@ public class ScrobblingService : IScrobblingService
|
||||
private const string AccessTokenErrorMessage = "Access Token needs to be rotated to continue scrobbling";
|
||||
|
||||
|
||||
public ScrobblingService(IUnitOfWork unitOfWork, ITokenService tokenService,
|
||||
IEventHub eventHub, ILogger<ScrobblingService> logger, ILicenseService licenseService,
|
||||
ILocalizationService localizationService)
|
||||
public ScrobblingService(IUnitOfWork unitOfWork, IEventHub eventHub, ILogger<ScrobblingService> logger,
|
||||
ILicenseService licenseService, ILocalizationService localizationService)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_tokenService = tokenService;
|
||||
_eventHub = eventHub;
|
||||
_logger = logger;
|
||||
_licenseService = licenseService;
|
||||
|
@ -142,10 +142,15 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService
|
||||
// For everything that's not there, link it up for this user.
|
||||
_logger.LogInformation("Starting Sync on {CollectionName} with {SeriesCount} Series", info.Title, info.TotalItems);
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, 0, info.TotalItems, ProgressEventType.Started));
|
||||
|
||||
var missingCount = 0;
|
||||
var missingSeries = new StringBuilder();
|
||||
var counter = -1;
|
||||
foreach (var seriesInfo in info.Series.OrderBy(s => s.SeriesName))
|
||||
{
|
||||
counter++;
|
||||
try
|
||||
{
|
||||
// Normalize series name and localized name
|
||||
@ -164,7 +169,12 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService
|
||||
s.NormalizedLocalizedName == normalizedSeriesName)
|
||||
&& formats.Contains(s.Format));
|
||||
|
||||
if (existingSeries != null) continue;
|
||||
if (existingSeries != null)
|
||||
{
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Series not found in the collection, try to find it in the server
|
||||
var newSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName(seriesInfo.SeriesName,
|
||||
@ -196,6 +206,8 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService
|
||||
missingSeries.Append("<br/>");
|
||||
}
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SmartCollectionProgressEvent(info.Title, seriesInfo.SeriesName, counter, info.TotalItems, ProgressEventType.Updated));
|
||||
}
|
||||
|
||||
// At this point, all series in the info have been checked and added if necessary
|
||||
@ -213,6 +225,9 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService
|
||||
|
||||
await _unitOfWork.CollectionTagRepository.UpdateCollectionAgeRating(collection);
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.SmartCollectionProgressEvent(info.Title, string.Empty, info.TotalItems, info.TotalItems, ProgressEventType.Ended));
|
||||
|
||||
await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated,
|
||||
MessageFactory.CollectionUpdatedEvent(collection.Id), false);
|
||||
|
||||
|
@ -33,17 +33,19 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
||||
private readonly IEventHub _eventHub;
|
||||
private readonly ICacheHelper _cacheHelper;
|
||||
private readonly IReaderService _readerService;
|
||||
private readonly IMediaErrorService _mediaErrorService;
|
||||
|
||||
private const int AverageCharactersPerWord = 5;
|
||||
|
||||
public WordCountAnalyzerService(ILogger<WordCountAnalyzerService> logger, IUnitOfWork unitOfWork, IEventHub eventHub,
|
||||
ICacheHelper cacheHelper, IReaderService readerService)
|
||||
ICacheHelper cacheHelper, IReaderService readerService, IMediaErrorService mediaErrorService)
|
||||
{
|
||||
_logger = logger;
|
||||
_unitOfWork = unitOfWork;
|
||||
_eventHub = eventHub;
|
||||
_cacheHelper = cacheHelper;
|
||||
_readerService = readerService;
|
||||
_mediaErrorService = mediaErrorService;
|
||||
}
|
||||
|
||||
|
||||
@ -188,7 +190,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||
MessageFactory.WordCountAnalyzerProgressEvent(series.LibraryId, progress,
|
||||
ProgressEventType.Updated, useFileName ? filePath : series.Name));
|
||||
sum += await GetWordCountFromHtml(bookPage);
|
||||
sum += await GetWordCountFromHtml(bookPage, filePath);
|
||||
pageCounter++;
|
||||
}
|
||||
|
||||
@ -245,7 +247,9 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
||||
}
|
||||
|
||||
|
||||
private static async Task<int> GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile)
|
||||
private async Task<int> GetWordCountFromHtml(EpubLocalTextContentFileRef bookFile, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(await bookFile.ReadContentAsync());
|
||||
@ -253,5 +257,13 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
||||
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
|
||||
return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) / AverageCharactersPerWord ?? 0;
|
||||
}
|
||||
catch (EpubContentException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error when counting words in epub {EpubPath}", filePath);
|
||||
await _mediaErrorService.ReportMediaIssueAsync(filePath, MediaErrorProducer.BookService,
|
||||
$"Invalid Epub Metadata, {bookFile.FilePath} does not exist", ex.Message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -178,7 +178,7 @@ public class ThemeService : IThemeService
|
||||
themeDtos.Add(dto);
|
||||
}
|
||||
|
||||
_cache.Set(themeDtos, themes, _cacheOptions);
|
||||
_cache.Set(cacheKey, themeDtos, _cacheOptions);
|
||||
|
||||
return themeDtos;
|
||||
}
|
||||
|
@ -134,6 +134,10 @@ public static class MessageFactory
|
||||
/// A Theme was updated and UI should refresh to get the latest version
|
||||
/// </summary>
|
||||
public const string SiteThemeUpdated = "SiteThemeUpdated";
|
||||
/// <summary>
|
||||
/// A Progress event when a smart collection is synchronizing
|
||||
/// </summary>
|
||||
public const string SmartCollectionSync = "SmartCollectionSync";
|
||||
|
||||
public static SignalRMessage DashboardUpdateEvent(int userId)
|
||||
{
|
||||
@ -425,6 +429,31 @@ public static class MessageFactory
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file being scanned by Kavita for processing and grouping
|
||||
/// </summary>
|
||||
/// <remarks>Does not have a progress as it's unknown how many files there are. Instead sends -1 to represent indeterminate</remarks>
|
||||
/// <param name="folderPath"></param>
|
||||
/// <param name="libraryName"></param>
|
||||
/// <param name="eventType"></param>
|
||||
/// <returns></returns>
|
||||
public static SignalRMessage SmartCollectionProgressEvent(string collectionName, string seriesName, int currentItems, int totalItems, string eventType)
|
||||
{
|
||||
return new SignalRMessage()
|
||||
{
|
||||
Name = SmartCollectionSync,
|
||||
Title = $"Synchronizing {collectionName}",
|
||||
SubTitle = seriesName,
|
||||
EventType = eventType,
|
||||
Progress = ProgressType.Determinate,
|
||||
Body = new
|
||||
{
|
||||
Progress = float.Min((currentItems / (totalItems * 1.0f)), 100f),
|
||||
EventTime = DateTime.Now
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This informs the UI with details about what is being processed by the Scanner
|
||||
/// </summary>
|
||||
|
@ -266,6 +266,9 @@ public class Startup
|
||||
// v0.8.1
|
||||
await MigrateLowestSeriesFolderPath.Migrate(dataContext, unitOfWork, logger);
|
||||
|
||||
// v0.8.2
|
||||
await ManualMigrateThemeDescription.Migrate(dataContext, logger);
|
||||
|
||||
// Update the version in the DB after all migrations are run
|
||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||
installVersion.Value = BuildInfo.Version.ToString();
|
||||
|
@ -14,7 +14,7 @@ Setup guides, FAQ, the more information we have on the [wiki](https://wiki.kavit
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher)
|
||||
- .NET 8.0+
|
||||
- dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
- dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
||||
|
||||
### Getting started ###
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.24.0.89429">
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.25.0.90414">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
@ -1,4 +1,4 @@
|
||||
$scrollbarHeight: 34px;
|
||||
$scrollbarHeight: 35px;
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
@ -9,29 +9,31 @@ img {
|
||||
align-items: center;
|
||||
|
||||
&.full-width {
|
||||
height: calc(var(--vh)*100);
|
||||
height: 100dvh;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
&.full-height {
|
||||
height: calc(100vh); // We need to - $scrollbarHeight when there is a horizontal scroll on macos
|
||||
height: calc(100dvh); // We need to - $scrollbarHeight when there is a horizontal scroll on macos
|
||||
display: flex;
|
||||
align-content: center;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
&.original {
|
||||
height: 100vh;
|
||||
height: calc(100dvh);
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
width: auto;
|
||||
margin: auto;
|
||||
max-height: calc(var(--vh)*100);
|
||||
overflow: hidden; // This technically will crop and make it just fit
|
||||
max-height: calc(100dvh);
|
||||
height: calc(100dvh);
|
||||
vertical-align: top;
|
||||
object-fit: cover;
|
||||
&.wide {
|
||||
height: 100vh;
|
||||
height: calc(100dvh);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,12 +48,13 @@ img {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
vertical-align: top;
|
||||
max-width: fit-content;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fit-to-screen.full-width {
|
||||
width: 100%;
|
||||
max-height: calc(var(--vh)*100);
|
||||
max-height: calc(100dvh);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,6 @@ import { Volume } from '../_models/volume';
|
||||
import { AccountService } from './account.service';
|
||||
import { DeviceService } from './device.service';
|
||||
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
|
||||
import {User} from "../_models/user";
|
||||
|
||||
export enum Action {
|
||||
Submenu = -1,
|
||||
|
@ -103,7 +103,11 @@ export enum EVENTS {
|
||||
/**
|
||||
* A Theme was updated and UI should refresh to get the latest version
|
||||
*/
|
||||
SiteThemeUpdated= 'SiteThemeUpdated'
|
||||
SiteThemeUpdated = 'SiteThemeUpdated',
|
||||
/**
|
||||
* A Progress event when a smart collection is synchronizing
|
||||
*/
|
||||
SmartCollectionSync = 'SmartCollectionSync'
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
@ -199,6 +203,13 @@ export class MessageHubService {
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.SmartCollectionSync, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.NotificationProgress,
|
||||
payload: resp.body
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.SiteThemeUpdated, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.SiteThemeUpdated,
|
||||
|
@ -24,11 +24,11 @@
|
||||
<div class="card-footer bg-transparent text-muted">
|
||||
<div>
|
||||
@if (isMyReview) {
|
||||
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
|
||||
<i class="d-md-none fa-solid fa-star me-2" aria-hidden="true" [title]="t('your-review')"></i>
|
||||
<img class="me-2" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
|
||||
{{review.username}}
|
||||
} @else {
|
||||
<img class="me-1" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
||||
<img class="me-2" [ngSrc]="review.provider | providerImage" width="20" height="20" alt="">
|
||||
}
|
||||
|
||||
{{(isMyReview ? '' : review.username | defaultValue:'')}}
|
||||
|
@ -42,4 +42,10 @@
|
||||
max-width: 319px;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
padding: .5rem 0;
|
||||
|
||||
& > * {
|
||||
margin: 0 5px;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<ng-container *transloco="let t; read:'user-scrobble-history'">
|
||||
<h5>{{t('title')}}</h5>
|
||||
<p>{{t('description')}}</p>
|
||||
<p class="fw-bold">{{t('not-read-warning')}}</p>
|
||||
<div class="row g-0 mb-2">
|
||||
<div class="col-md-10">
|
||||
<form [formGroup]="formGroup">
|
||||
@ -67,8 +68,12 @@
|
||||
@switch (item.scrobbleEventType) {
|
||||
@case (ScrobbleEventType.ChapterRead) {
|
||||
@if(item.volumeNumber === LooseLeafOrDefaultNumber) {
|
||||
@if (item.chapterNumber === LooseLeafOrDefaultNumber) {
|
||||
{{t('special')}}
|
||||
} @else {
|
||||
{{t('chapter-num', {num: item.chapterNumber})}}
|
||||
}
|
||||
}
|
||||
@else if (item.chapterNumber === LooseLeafOrDefaultNumber) {
|
||||
{{t('volume-num', {num: item.volumeNumber})}}
|
||||
}
|
||||
|
@ -3,8 +3,12 @@
|
||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||
<h4 id="email-header">{{t('title')}}</h4>
|
||||
|
||||
<p>You must fill out both Host Name and SMTP settings to use email-based functionality within Kavita.</p>
|
||||
|
||||
<p>{{t('setting-description')}}</p>
|
||||
@if (settingsForm.dirty) {
|
||||
<ngb-alert [type]="'warning'">
|
||||
{{t('test-warning')}}
|
||||
</ngb-alert>
|
||||
}
|
||||
<div class="mb-3 pe-2 ps-2 ">
|
||||
<label for="settings-hostname" class="form-label">{{t('host-name-label')}}</label><i class="fa fa-info-circle ms-1" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #hostNameTooltip>{{t('host-name-tooltip')}}</ng-template>
|
||||
|
@ -5,6 +5,7 @@ import {take} from 'rxjs';
|
||||
import {SettingsService} from '../settings.service';
|
||||
import {ServerSettings} from '../_models/server-settings';
|
||||
import {
|
||||
NgbAlert,
|
||||
NgbTooltip
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
|
||||
@ -19,7 +20,7 @@ import {ManageAlertsComponent} from "../manage-alerts/manage-alerts.component";
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe,
|
||||
ManageAlertsComponent, TitleCasePipe]
|
||||
ManageAlertsComponent, TitleCasePipe, NgbAlert]
|
||||
})
|
||||
export class ManageEmailSettingsComponent implements OnInit {
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<!-- TODO: figure a way to get a hover effect -->
|
||||
<div class="card-item-container card clickable" (click)="loadSmartFilter(item)">
|
||||
<div class="overlay">
|
||||
<div class="overlay filter">
|
||||
<div class="card-overlay"></div>
|
||||
<div class="overlay-information overlay-information--centered">
|
||||
<div class="position-relative">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<ng-container *transloco="let t; read: 'series-info-cards'">
|
||||
<div class="row g-0 mt-3">
|
||||
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('release-date-title')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-year-tooltip')">
|
||||
{{seriesMetadata.releaseYear}}
|
||||
</app-icon-and-title>
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
<ng-container *ngIf="seriesMetadata">
|
||||
<ng-container *ngIf="seriesMetadata.ageRating">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('age-rating-title')" [clickable]="true" fontClasses="fas fa-eye" (click)="handleGoTo(FilterField.AgeRating, seriesMetadata.ageRating)" [title]="t('age-rating-title')">
|
||||
{{this.seriesMetadata.ageRating | ageRating}}
|
||||
</app-icon-and-title>
|
||||
@ -20,7 +20,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('language-title')" [clickable]="true" fontClasses="fas fa-language" (click)="handleGoTo(FilterField.Languages, seriesMetadata.language)" [title]="t('language-title')">
|
||||
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
|
||||
</app-icon-and-title>
|
||||
@ -30,7 +30,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
|
||||
<app-icon-and-title [label]="t('publication-status-title')" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === t('ongoing') ? 'empty' : 'end'}}"
|
||||
(click)="handleGoTo(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||
@ -43,7 +43,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="accountService.hasValidLicense$ | async">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('scrobbling-title')" [clickable]="libraryAllowsScrobbling"
|
||||
fontClasses="fa-solid fa-tower-{{(isScrobbling && libraryAllowsScrobbling) ? 'broadcast' : 'observation'}}"
|
||||
(click)="toggleScrobbling($event)"
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
<ng-container *ngIf="series">
|
||||
<ng-container>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('format-title')" [clickable]="true"
|
||||
[fontClasses]="series.format | mangaFormatIcon"
|
||||
(click)="handleGoTo(FilterField.Formats, series.format)" [title]="t('format-title')">
|
||||
@ -73,7 +73,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [title]="t('last-read-title')">
|
||||
{{series.latestReadDate | timeAgo}}
|
||||
</app-icon-and-title>
|
||||
@ -83,7 +83,7 @@
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
|
||||
<ng-container *ngIf="series.wordCount > 0">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{t('words-count', {num: series.wordCount | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
@ -93,7 +93,7 @@
|
||||
|
||||
</ng-container>
|
||||
<ng-template #showPages>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="d-none d-md-block col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{t('pages-count', {num: series.pages | compactNumber})}}
|
||||
</app-icon-and-title>
|
||||
@ -102,7 +102,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title [label]="t('read-time-title')" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
@ -114,7 +114,7 @@
|
||||
|
||||
<ng-container *ngIf="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && readingTimeLeft.avgHours !== 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<div class="col-xl-auto col-lg-auto col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Time Left" [clickable]="false" fontClasses="fa-solid fa-clock">
|
||||
~{{readingTimeLeft.avgHours}} {{readingTimeLeft.avgHours > 1 ? t('hours') : t('hour')}}
|
||||
</app-icon-and-title>
|
||||
|
@ -6,11 +6,13 @@ import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
||||
import {CollectionTagService} from "../../../_services/collection-tag.service";
|
||||
import {MalStack} from "../../../_models/collection/mal-stack";
|
||||
import {UserCollection} from "../../../_models/collection-tag";
|
||||
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
||||
import {ScrobbleProvider, ScrobblingService} from "../../../_services/scrobbling.service";
|
||||
import {forkJoin} from "rxjs";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {DecimalPipe} from "@angular/common";
|
||||
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
||||
import {AccountService} from "../../../_services/account.service";
|
||||
import {ConfirmService} from "../../../shared/confirm.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-import-mal-collection-modal',
|
||||
@ -32,13 +34,25 @@ export class ImportMalCollectionModalComponent {
|
||||
private readonly collectionService = inject(CollectionTagService);
|
||||
private readonly cdRef = inject(ChangeDetectorRef);
|
||||
private readonly toastr = inject(ToastrService);
|
||||
private readonly scrobblingService = inject(ScrobblingService);
|
||||
private readonly confirmService = inject(ConfirmService);
|
||||
|
||||
stacks: Array<MalStack> = [];
|
||||
isLoading = true;
|
||||
collectionMap: {[key: string]: UserCollection | MalStack} = {};
|
||||
|
||||
constructor() {
|
||||
this.scrobblingService.getMalToken().subscribe(async token => {
|
||||
if (token.accessToken === '') {
|
||||
await this.confirmService.alert(translate('toasts.mal-token-required'));
|
||||
this.ngbModal.dismiss();
|
||||
return;
|
||||
}
|
||||
this.setup();
|
||||
});
|
||||
}
|
||||
|
||||
setup() {
|
||||
forkJoin({
|
||||
allCollections: this.collectionService.allCollections(true),
|
||||
malStacks: this.collectionService.getMalStacks()
|
||||
|
@ -3,18 +3,25 @@
|
||||
.image-container {
|
||||
#image-1 {
|
||||
&.double {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-container.full-height {
|
||||
display: inline-block !important;
|
||||
.image-container {
|
||||
&.full-height {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
margin: unset;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
vertical-align: top;
|
||||
max-width: fit-content;
|
||||
|
||||
@ -41,7 +48,7 @@
|
||||
}
|
||||
|
||||
.fit-to-height-double-offset {
|
||||
height: 100vh;
|
||||
height: calc(100dvh);
|
||||
object-fit: scale-down;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
// Overrides for reverse
|
||||
.image-container {
|
||||
height: calc(100vh); // override as on single, we -34px for the potential scrollbar
|
||||
height: calc(100dvh); // override as on single, we -34px for the potential scrollbar
|
||||
|
||||
&.reverse {
|
||||
overflow: unset;
|
||||
@ -29,8 +29,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.image-container.full-height {
|
||||
display: inline-block;
|
||||
.image-container {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
|
||||
|
||||
.full-height {
|
||||
margin: unset;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.full-width {
|
||||
@ -62,7 +70,7 @@
|
||||
}
|
||||
|
||||
.fit-to-height-double-offset {
|
||||
height: 100vh;
|
||||
height: calc(100dvh);
|
||||
object-fit: scale-down;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
@ -41,7 +41,7 @@
|
||||
<app-loading [loading]="isLoading || (!(currentImage$ | async)?.complete && this.readerMode !== ReaderMode.Webtoon)" [absolute]="true"></app-loading>
|
||||
<div class="reading-area"
|
||||
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
|
||||
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : 'calc(var(--vh)*100)'}" #readingArea>
|
||||
[ngStyle]="{'background-color': backgroundColor, 'height': readerMode === ReaderMode.Webtoon ? 'inherit' : '100dvh'}" #readingArea>
|
||||
|
||||
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon; else webtoon">
|
||||
<div (dblclick)="bookmarkPage($event)">
|
||||
@ -56,14 +56,14 @@
|
||||
<!-- Pagination controls and screen hints-->
|
||||
<div class="pagination-area">
|
||||
<div class="{{readerMode === ReaderMode.LeftRight ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, KeyDirection.Left)"
|
||||
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? ImageHeight: '25%'), 'max-height': MaxHeight}">
|
||||
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? MaxHeight: '25%'), 'max-height': MaxHeight}">
|
||||
<div *ngIf="showClickOverlay">
|
||||
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'left' : 'up'}}"
|
||||
[title]="t('prev-page-tooltip')" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="{{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, KeyDirection.Right)"
|
||||
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? ImageHeight: '25%'),
|
||||
[ngStyle]="{'height': (readerMode === ReaderMode.LeftRight ? MaxHeight: '25%'),
|
||||
'left': 'inherit',
|
||||
'right': RightPaginationOffset + 'px',
|
||||
'max-height': MaxHeight}">
|
||||
|
@ -16,7 +16,6 @@ $pointer-offset: 5px;
|
||||
|
||||
|
||||
.reading-area {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
//height: calc(var(--vh)*100); // this needs to be applied on the DOM because it breaks infinite scroller
|
||||
|
@ -432,17 +432,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
// This is for the pagination area
|
||||
get MaxHeight() {
|
||||
if (this.FittingOption === FITTING_OPTION.HEIGHT) {
|
||||
return 'calc(var(--vh) * 100)';
|
||||
}
|
||||
|
||||
const needsScrolling = this.readingArea?.nativeElement?.scrollHeight > this.readingArea?.nativeElement?.clientHeight;
|
||||
if (this.readingArea?.nativeElement?.clientHeight <= this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) {
|
||||
if (needsScrolling) {
|
||||
return Math.min(this.readingArea?.nativeElement?.scrollHeight, this.mangaReaderService.getPageDimensions(this.pageNum)?.height!) + 'px';
|
||||
}
|
||||
}
|
||||
return this.readingArea?.nativeElement?.clientHeight + 'px';
|
||||
return '100dvh';
|
||||
}
|
||||
|
||||
get RightPaginationOffset() {
|
||||
|
@ -150,14 +150,14 @@ export class SingleRendererComponent implements OnInit, ImageRenderer {
|
||||
if (mode !== FITTING_OPTION.HEIGHT) return '';
|
||||
|
||||
const readingArea = this.document.querySelector('.reading-area');
|
||||
if (!readingArea) return 'calc(100vh)';
|
||||
if (!readingArea) return 'calc(100dvh)';
|
||||
|
||||
// If you ever see fit to height and a bit of scrollbar, it's due to currentImage not being ready on first load
|
||||
if (this.currentImage?.width - readingArea.scrollWidth > 0) {
|
||||
// we also need to check if this is FF or Chrome. FF doesn't require the -34px as it doesn't render a scrollbar
|
||||
return 'calc(100vh - 34px)';
|
||||
return 'calc(100dvh)';
|
||||
}
|
||||
return 'calc(100vh)';
|
||||
return 'calc(100dvh)';
|
||||
}),
|
||||
filter(_ => this.isValid())
|
||||
);
|
||||
|
@ -118,7 +118,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
||||
this.cdRef.markForCheck();
|
||||
break;
|
||||
case 'started':
|
||||
// Sometimes we can receive 2 started on long running scans, so better to just treat as a merge then.
|
||||
// Sometimes we can receive 2 started on long-running scans, so better to just treat as a merge then.
|
||||
data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message);
|
||||
this.progressEventsSource.next(data);
|
||||
break;
|
||||
|
@ -3,12 +3,15 @@
|
||||
<h2 title>
|
||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
|
||||
{{readingList?.title}}
|
||||
<span *ngIf="readingList?.promoted" class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
@if (readingList?.promoted) {
|
||||
<span class="ms-1">(<i class="fa fa-angle-double-up" aria-hidden="true"></i>)</span>
|
||||
}
|
||||
</h2>
|
||||
<h6 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h6>
|
||||
|
||||
<ng-template #extrasDrawer let-offcanvas>
|
||||
<div style="margin-top: 56px" *ngIf="readingList">
|
||||
@if (readingList) {
|
||||
<div style="margin-top: 56px">
|
||||
<div class="offcanvas-header">
|
||||
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
|
||||
@ -23,19 +26,24 @@
|
||||
<span class="read-btn--text"> {{t('remove-read')}}</span>
|
||||
</button>
|
||||
|
||||
<div class="col-auto ms-2 mt-2" *ngIf="!(readingList?.promoted && !this.isAdmin)">
|
||||
@if (!(readingList.promoted && !this.isAdmin)) {
|
||||
<div class="col-auto ms-2 mt-2">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="accessibility-mode" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
|
||||
<input class="form-check-input" type="checkbox" id="accessibility-mode" [disabled]="this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet" [value]="accessibilityMode" (change)="updateAccessibilityMode()">
|
||||
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ng-template>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid mt-2" *ngIf="readingList" >
|
||||
|
||||
@if (readingList) {
|
||||
<div class="container-fluid mt-2">
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||
@ -135,5 +143,5 @@
|
||||
</app-draggable-ordered-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
</ng-container>
|
||||
|
@ -3,7 +3,7 @@ import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {ToastrService} from 'ngx-toastr';
|
||||
import {take} from 'rxjs/operators';
|
||||
import {ConfirmService} from 'src/app/shared/confirm.service';
|
||||
import {UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {Breakpoint, UtilityService} from 'src/app/shared/_services/utility.service';
|
||||
import {LibraryType} from 'src/app/_models/library/library';
|
||||
import {MangaFormat} from 'src/app/_models/manga-format';
|
||||
import {ReadingList, ReadingListItem} from 'src/app/_models/reading-list';
|
||||
@ -32,7 +32,7 @@ import {AsyncPipe, DatePipe, DecimalPipe, NgClass, NgIf} from '@angular/common';
|
||||
import {
|
||||
SideNavCompanionBarComponent
|
||||
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
||||
import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
|
||||
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
||||
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
|
||||
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
|
||||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||
@ -53,6 +53,26 @@ import {Title} from "@angular/platform-browser";
|
||||
MetadataDetailComponent]
|
||||
})
|
||||
export class ReadingListDetailComponent implements OnInit {
|
||||
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private readingListService = inject(ReadingListService);
|
||||
private actionService = inject(ActionService);
|
||||
private actionFactoryService = inject(ActionFactoryService);
|
||||
public utilityService = inject(UtilityService);
|
||||
public imageService = inject(ImageService);
|
||||
private accountService = inject(AccountService);
|
||||
private toastr = inject(ToastrService);
|
||||
private confirmService = inject(ConfirmService);
|
||||
private libraryService = inject(LibraryService);
|
||||
private readerService = inject(ReaderService);
|
||||
private cdRef = inject(ChangeDetectorRef);
|
||||
private filterUtilityService = inject(FilterUtilitiesService);
|
||||
private titleService = inject(Title);
|
||||
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
protected readonly Breakpoint = Breakpoint;
|
||||
|
||||
items: Array<ReadingListItem> = [];
|
||||
listId!: number;
|
||||
readingList: ReadingList | undefined;
|
||||
@ -65,15 +85,8 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
libraryTypes: {[key: number]: LibraryType} = {};
|
||||
characters$!: Observable<Person[]>;
|
||||
|
||||
private translocoService = inject(TranslocoService);
|
||||
protected readonly MangaFormat = MangaFormat;
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private readingListService: ReadingListService,
|
||||
private actionService: ActionService, private actionFactoryService: ActionFactoryService, public utilityService: UtilityService,
|
||||
public imageService: ImageService, private accountService: AccountService, private toastr: ToastrService,
|
||||
private confirmService: ConfirmService, private libraryService: LibraryService, private readerService: ReaderService,
|
||||
private readonly cdRef: ChangeDetectorRef, private filterUtilityService: FilterUtilitiesService, private titleService: Title) {
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
const listId = this.route.snapshot.paramMap.get('id');
|
||||
@ -86,6 +99,9 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
this.listId = parseInt(listId, 10);
|
||||
this.characters$ = this.readingListService.getCharacters(this.listId);
|
||||
|
||||
this.accessibilityMode = this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet;
|
||||
this.cdRef.markForCheck();
|
||||
|
||||
forkJoin([
|
||||
this.libraryService.getLibraries(),
|
||||
this.readingListService.getReadingList(this.listId)
|
||||
@ -165,10 +181,10 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
}
|
||||
|
||||
async deleteList(readingList: ReadingList) {
|
||||
if (!await this.confirmService.confirm(this.translocoService.translate('toasts.confirm-delete-reading-list'))) return;
|
||||
if (!await this.confirmService.confirm(translate('toasts.confirm-delete-reading-list'))) return;
|
||||
|
||||
this.readingListService.delete(readingList.id).subscribe(() => {
|
||||
this.toastr.success(this.translocoService.translate('toasts.reading-list-deleted'));
|
||||
this.toastr.success(translate('toasts.reading-list-deleted'));
|
||||
this.router.navigateByUrl('/lists');
|
||||
});
|
||||
}
|
||||
@ -186,7 +202,7 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
this.items.splice(position, 1);
|
||||
this.items = [...this.items];
|
||||
this.cdRef.markForCheck();
|
||||
this.toastr.success(this.translocoService.translate('toasts.item-removed'));
|
||||
this.toastr.success(translate('toasts.item-removed'));
|
||||
});
|
||||
}
|
||||
|
||||
@ -196,7 +212,7 @@ export class ReadingListDetailComponent implements OnInit {
|
||||
this.cdRef.markForCheck();
|
||||
this.readingListService.removeRead(this.readingList.id).subscribe((resp) => {
|
||||
if (resp === 'Nothing to remove') {
|
||||
this.toastr.info(this.translocoService.translate('toasts.nothing-to-remove'));
|
||||
this.toastr.info(translate('toasts.nothing-to-remove'));
|
||||
return;
|
||||
}
|
||||
this.getListItems();
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
<div class="row g-0 theme-container">
|
||||
<div class="col-md-3">
|
||||
<div class="col-lg-3 col-md-5 col-sm-7 col-xs-7 scroller">
|
||||
<div class="pe-2">
|
||||
<ul style="height: 100%" class="list-group list-group-flush">
|
||||
|
||||
@ -23,7 +23,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<div class="col-lg-9 col-md-7 col-sm-4 col-xs-4 ps-3">
|
||||
<div class="card p-3">
|
||||
|
||||
@if (selectedTheme === undefined) {
|
||||
|
||||
<div class="row pb-4">
|
||||
@ -35,7 +37,6 @@
|
||||
} @else {
|
||||
{{t('preview-default')}}
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -91,20 +92,20 @@
|
||||
@if(!selectedTheme.isSiteTheme) {
|
||||
<p>{{selectedTheme.downloadable!.description | defaultValue}}</p>
|
||||
|
||||
<app-carousel-reel [items]="selectedTheme.downloadable!.previewUrls" title="Preview">
|
||||
<app-carousel-reel [items]="selectedTheme.downloadable!.previewUrls" [title]="t('preview-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
|
||||
<app-image [imageUrl]="item" height="100px" width="160px"></app-image>
|
||||
<app-image [imageUrl]="item" height="108px" width="260px"></app-image>
|
||||
</a>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
} @else {
|
||||
<p>{{selectedTheme.site!.description | defaultValue}}</p>
|
||||
|
||||
<app-carousel-reel [items]="selectedTheme.site!.previewUrls" title="Preview">
|
||||
<app-carousel-reel [items]="selectedTheme.site!.previewUrls" [title]="t('preview-title')">
|
||||
<ng-template #carouselItem let-item>
|
||||
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
|
||||
<app-image [imageUrl]="item" height="100px" width="160px"></app-image>
|
||||
<app-image [imageUrl]="item" height="108px" width="260px"></app-image>
|
||||
</a>
|
||||
</ng-template>
|
||||
</app-carousel-reel>
|
||||
@ -113,6 +114,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #themeOption let-item>
|
||||
@if (item !== undefined) {
|
||||
@ -122,12 +124,12 @@
|
||||
<div class="fw-bold">{{item.name | sentenceCase}}</div>
|
||||
|
||||
@if (item.hasOwnProperty('provider')) {
|
||||
{{item.provider | siteThemeProvider}}
|
||||
<span class="pill p-1 me-1 provider">{{item.provider | siteThemeProvider}}</span>
|
||||
} @else if (item.hasOwnProperty('lastCompatibleVersion')) {
|
||||
{{ThemeProvider.Custom | siteThemeProvider}} • v{{item.lastCompatibleVersion}}
|
||||
<span class="pill p-1 me-1 provider">{{ThemeProvider.Custom | siteThemeProvider}}</span><span class="pill p-1 me-1 version">v{{item.lastCompatibleVersion}}</span>
|
||||
}
|
||||
@if (currentTheme && item.name === currentTheme.name) {
|
||||
• {{t('active-theme')}}
|
||||
<span class="pill p-1 active">{{t('active-theme')}}</span>
|
||||
}
|
||||
</div>
|
||||
@if (item.hasOwnProperty('isDefault') && item.isDefault) {
|
||||
|
@ -10,6 +10,25 @@
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
max-height: calc(100dvh - 280px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pill {
|
||||
font-size: .8rem;
|
||||
background-color: var(--card-bg-color);
|
||||
border-radius: 0.375rem;
|
||||
&.active {
|
||||
background-color : var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item, .list-group-item.active {
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
ngx-file-drop ::ng-deep > div {
|
||||
// styling for the outer drop box
|
||||
width: 100%;
|
||||
|
@ -33,6 +33,7 @@
|
||||
"user-scrobble-history": {
|
||||
"title": "Scrobble History",
|
||||
"description": "Here you will find any scrobble events linked with your account. In order for events to exist, you must have an active scrobble provider configured. All events that have been processed will clear after a month. If there are non-processed events, it is likely these cannot form matches upstream. Please reach out to your admin to get them corrected.",
|
||||
"not-read-warning": "Upstream providers will always keep the highest number",
|
||||
"filter-label": "{{common.filter}}",
|
||||
"created-header": "Created",
|
||||
"last-modified-header": "Last Modified",
|
||||
@ -47,7 +48,8 @@
|
||||
"rating": "Rating {{r}}",
|
||||
"not-applicable": "Not Applicable",
|
||||
"processed": "Processed",
|
||||
"not-processed": "Not Processed"
|
||||
"not-processed": "Not Processed",
|
||||
"special": "{{entity-title.special}}"
|
||||
},
|
||||
|
||||
"scrobble-event-type-pipe": {
|
||||
@ -197,7 +199,8 @@
|
||||
"upload": "{{cover-image-chooser.upload}}",
|
||||
"upload-continued": "a css file",
|
||||
"preview-default": "Select a theme first",
|
||||
"preview-default-admin": "Select a theme first or upload one manually"
|
||||
"preview-default-admin": "Select a theme first or upload one manually",
|
||||
"preview-title": "Preview"
|
||||
},
|
||||
|
||||
"theme": {
|
||||
@ -1122,6 +1125,8 @@
|
||||
"manage-email-settings": {
|
||||
"title": "Email Services (SMTP)",
|
||||
"description": "In order to use some functions of Kavita like Forgot Password and Send To Device, an email provider must be setup. Other features like Password change are less secure without Email setup.",
|
||||
"setting-description": "You must fill out both Host Name and SMTP settings to use email-based functionality within Kavita.",
|
||||
"test-warning": "You must save before using Test button.",
|
||||
"send-to-warning": "If you want Send to Device to work you must setup your email settings",
|
||||
"email-url-label": "Email Service URL",
|
||||
"email-url-tooltip": "Use fully qualified URL of the email service. Do not include ending slash.",
|
||||
@ -2199,8 +2204,8 @@
|
||||
"collections-deleted": "Collections deleted",
|
||||
"pdf-book-mode-screen-size": "Screen too small for Book mode",
|
||||
"stack-imported": "Stack Imported",
|
||||
"confirm-delete-theme": "Removing this theme will delete it from the disk. You can grab it from temp directory before removal"
|
||||
|
||||
"confirm-delete-theme": "Removing this theme will delete it from the disk. You can grab it from temp directory before removal",
|
||||
"mal-token-required": "MAL Token is required, set in User Settings"
|
||||
},
|
||||
|
||||
"actionable": {
|
||||
|
@ -15,6 +15,7 @@
|
||||
}
|
||||
|
||||
$image-height: 230px;
|
||||
$image-filter-height: 160px;
|
||||
$image-width: 160px;
|
||||
|
||||
.card-item-container {
|
||||
@ -62,6 +63,14 @@ $image-width: 160px;
|
||||
border-top-right-radius: 4px;
|
||||
z-index: 10;
|
||||
|
||||
&.filter {
|
||||
height: $image-filter-height;
|
||||
|
||||
.card-overlay {
|
||||
height: $image-filter-height;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
visibility: visible;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
"name": "GPL-3.0",
|
||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||
},
|
||||
"version": "0.8.1.3"
|
||||
"version": "0.8.1.4"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user