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
|
- name: Install Swashbuckle CLI
|
||||||
shell: powershell
|
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
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||||
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.0.2" />
|
<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 cacheService = new CacheHelper(new FileService());
|
||||||
var service = new WordCountAnalyzerService(Substitute.For<ILogger<WordCountAnalyzerService>>(), _unitOfWork,
|
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);
|
await service.ScanSeries(1, 1);
|
||||||
@ -126,7 +126,7 @@ public class WordCountAnalysisTests : AbstractDbTest
|
|||||||
|
|
||||||
var cacheService = new CacheHelper(new FileService());
|
var cacheService = new CacheHelper(new FileService());
|
||||||
var service = new WordCountAnalyzerService(Substitute.For<ILogger<WordCountAnalyzerService>>(), _unitOfWork,
|
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);
|
await service.ScanSeries(1, 1);
|
||||||
|
|
||||||
var chapter2 = new ChapterBuilder("2")
|
var chapter2 = new ChapterBuilder("2")
|
||||||
|
@ -12,9 +12,9 @@
|
|||||||
<LangVersion>latestmajor</LangVersion>
|
<LangVersion>latestmajor</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">
|
<!-- <Target Name="PostBuild" AfterTargets="Build" Condition=" '$(Configuration)' == 'Debug' ">-->
|
||||||
<Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />
|
<!-- <Exec Command="swagger tofile --output ../openapi.json bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll v1" />-->
|
||||||
</Target>
|
<!-- </Target>-->
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
@ -95,13 +95,13 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
<PackageReference Include="Serilog.Sinks.SignalR.Core" Version="0.1.2" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.37.2" />
|
<PackageReference Include="SharpCompress" Version="0.37.2" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
<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>
|
<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.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="8.0.2" />
|
<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.IO.Abstractions" Version="21.0.2" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.4" />
|
<PackageReference Include="System.Drawing.Common" Version="8.0.4" />
|
||||||
<PackageReference Include="VersOne.Epub" Version="3.3.1" />
|
<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 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.
|
// 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 &&
|
.Where(s => s.Format == parsedSeries.Format && s.NormalizedName == parsedSeries.NormalizedName &&
|
||||||
s.LibraryId == libraryId)
|
s.LibraryId == libraryId)
|
||||||
.Select(s => s.Id)
|
.Select(s => s.Id)
|
||||||
.OrderBy(s => s)
|
.OrderBy(s => s)
|
||||||
.Last();
|
.LastAsync();
|
||||||
if (sId > 0)
|
if (sId > 0)
|
||||||
{
|
{
|
||||||
ids.Add(sId);
|
ids.Add(sId);
|
||||||
|
@ -35,6 +35,7 @@ public static class Seed
|
|||||||
Provider = ThemeProvider.System,
|
Provider = ThemeProvider.System,
|
||||||
FileName = "dark.scss",
|
FileName = "dark.scss",
|
||||||
IsDefault = true,
|
IsDefault = true,
|
||||||
|
Description = "Default theme shipped with Kavita"
|
||||||
}
|
}
|
||||||
}.ToArray()
|
}.ToArray()
|
||||||
];
|
];
|
||||||
|
@ -99,10 +99,13 @@ public class Program
|
|||||||
// Apply all migrations on startup
|
// Apply all migrations on startup
|
||||||
logger.LogInformation("Running Migrations");
|
logger.LogInformation("Running Migrations");
|
||||||
|
|
||||||
// v0.7.14
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// v0.7.14
|
||||||
await MigrateWantToReadExport.Migrate(context, directoryService, logger);
|
await MigrateWantToReadExport.Migrate(context, directoryService, logger);
|
||||||
|
|
||||||
|
// v0.8.2
|
||||||
|
await ManualMigrateSwitchToWal.Migrate(context, logger);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -73,7 +73,8 @@ public class BookService : IBookService
|
|||||||
{
|
{
|
||||||
PackageReaderOptions = new PackageReaderOptions
|
PackageReaderOptions = new PackageReaderOptions
|
||||||
{
|
{
|
||||||
IgnoreMissingToc = true
|
IgnoreMissingToc = true,
|
||||||
|
SkipInvalidManifestItems = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using API.Data;
|
using API.Data;
|
||||||
using API.DTOs.Reader;
|
using API.DTOs.Reader;
|
||||||
@ -51,6 +53,8 @@ public class CacheService : ICacheService
|
|||||||
private readonly IReadingItemService _readingItemService;
|
private readonly IReadingItemService _readingItemService;
|
||||||
private readonly IBookmarkService _bookmarkService;
|
private readonly IBookmarkService _bookmarkService;
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<int, SemaphoreSlim> ExtractLocks = new();
|
||||||
|
|
||||||
public CacheService(ILogger<CacheService> logger, IUnitOfWork unitOfWork,
|
public CacheService(ILogger<CacheService> logger, IUnitOfWork unitOfWork,
|
||||||
IDirectoryService directoryService, IReadingItemService readingItemService,
|
IDirectoryService directoryService, IReadingItemService readingItemService,
|
||||||
IBookmarkService bookmarkService)
|
IBookmarkService bookmarkService)
|
||||||
@ -166,9 +170,17 @@ public class CacheService : ICacheService
|
|||||||
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
var chapter = await _unitOfWork.ChapterRepository.GetChapterAsync(chapterId);
|
||||||
var extractPath = GetCachePath(chapterId);
|
var extractPath = GetCachePath(chapterId);
|
||||||
|
|
||||||
if (_directoryService.Exists(extractPath)) return chapter;
|
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();
|
var files = chapter?.Files.ToList();
|
||||||
ExtractChapterFiles(extractPath, files, extractPdfToImages);
|
ExtractChapterFiles(extractPath, files, extractPdfToImages);
|
||||||
|
} finally {
|
||||||
|
extractLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
return chapter;
|
return chapter;
|
||||||
}
|
}
|
||||||
@ -190,6 +202,14 @@ public class CacheService : ICacheService
|
|||||||
var extractDi = _directoryService.FileSystem.DirectoryInfo.New(extractPath);
|
var extractDi = _directoryService.FileSystem.DirectoryInfo.New(extractPath);
|
||||||
|
|
||||||
if (files.Count > 0 && files[0].Format == MangaFormat.Image)
|
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)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
@ -202,6 +222,8 @@ public class CacheService : ICacheService
|
|||||||
_directoryService.Flatten(extractDi.FullName);
|
_directoryService.Flatten(extractDi.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
if (fileCount > 1)
|
if (fileCount > 1)
|
||||||
|
@ -439,18 +439,13 @@ public class ImageService : IImageService
|
|||||||
rows = 1;
|
rows = 1;
|
||||||
cols = 2;
|
cols = 2;
|
||||||
}
|
}
|
||||||
else if (coverImages.Count == 3)
|
|
||||||
{
|
|
||||||
rows = 2;
|
|
||||||
cols = 2;
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Default to 2x2 layout for more than 3 images
|
|
||||||
rows = 2;
|
rows = 2;
|
||||||
cols = 2;
|
cols = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var image = Image.Black(dims.Width, dims.Height);
|
var image = Image.Black(dims.Width, dims.Height);
|
||||||
|
|
||||||
var thumbnailWidth = image.Width / cols;
|
var thumbnailWidth = image.Width / cols;
|
||||||
|
@ -60,7 +60,6 @@ public interface IScrobblingService
|
|||||||
public class ScrobblingService : IScrobblingService
|
public class ScrobblingService : IScrobblingService
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _unitOfWork;
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
private readonly ITokenService _tokenService;
|
|
||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
private readonly ILogger<ScrobblingService> _logger;
|
private readonly ILogger<ScrobblingService> _logger;
|
||||||
private readonly ILicenseService _licenseService;
|
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";
|
private const string AccessTokenErrorMessage = "Access Token needs to be rotated to continue scrobbling";
|
||||||
|
|
||||||
|
|
||||||
public ScrobblingService(IUnitOfWork unitOfWork, ITokenService tokenService,
|
public ScrobblingService(IUnitOfWork unitOfWork, IEventHub eventHub, ILogger<ScrobblingService> logger,
|
||||||
IEventHub eventHub, ILogger<ScrobblingService> logger, ILicenseService licenseService,
|
ILicenseService licenseService, ILocalizationService localizationService)
|
||||||
ILocalizationService localizationService)
|
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_tokenService = tokenService;
|
|
||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_licenseService = licenseService;
|
_licenseService = licenseService;
|
||||||
|
@ -142,10 +142,15 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService
|
|||||||
// For everything that's not there, link it up for this user.
|
// 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);
|
_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 missingCount = 0;
|
||||||
var missingSeries = new StringBuilder();
|
var missingSeries = new StringBuilder();
|
||||||
|
var counter = -1;
|
||||||
foreach (var seriesInfo in info.Series.OrderBy(s => s.SeriesName))
|
foreach (var seriesInfo in info.Series.OrderBy(s => s.SeriesName))
|
||||||
{
|
{
|
||||||
|
counter++;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Normalize series name and localized name
|
// Normalize series name and localized name
|
||||||
@ -164,7 +169,12 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService
|
|||||||
s.NormalizedLocalizedName == normalizedSeriesName)
|
s.NormalizedLocalizedName == normalizedSeriesName)
|
||||||
&& formats.Contains(s.Format));
|
&& 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
|
// Series not found in the collection, try to find it in the server
|
||||||
var newSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName(seriesInfo.SeriesName,
|
var newSeries = await _unitOfWork.SeriesRepository.GetSeriesByAnyName(seriesInfo.SeriesName,
|
||||||
@ -196,6 +206,8 @@ public class SmartCollectionSyncService : ISmartCollectionSyncService
|
|||||||
missingSeries.Append("<br/>");
|
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
|
// 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 _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,
|
await _eventHub.SendMessageAsync(MessageFactory.CollectionUpdated,
|
||||||
MessageFactory.CollectionUpdatedEvent(collection.Id), false);
|
MessageFactory.CollectionUpdatedEvent(collection.Id), false);
|
||||||
|
|
||||||
|
@ -33,17 +33,19 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
private readonly IEventHub _eventHub;
|
private readonly IEventHub _eventHub;
|
||||||
private readonly ICacheHelper _cacheHelper;
|
private readonly ICacheHelper _cacheHelper;
|
||||||
private readonly IReaderService _readerService;
|
private readonly IReaderService _readerService;
|
||||||
|
private readonly IMediaErrorService _mediaErrorService;
|
||||||
|
|
||||||
private const int AverageCharactersPerWord = 5;
|
private const int AverageCharactersPerWord = 5;
|
||||||
|
|
||||||
public WordCountAnalyzerService(ILogger<WordCountAnalyzerService> logger, IUnitOfWork unitOfWork, IEventHub eventHub,
|
public WordCountAnalyzerService(ILogger<WordCountAnalyzerService> logger, IUnitOfWork unitOfWork, IEventHub eventHub,
|
||||||
ICacheHelper cacheHelper, IReaderService readerService)
|
ICacheHelper cacheHelper, IReaderService readerService, IMediaErrorService mediaErrorService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_eventHub = eventHub;
|
_eventHub = eventHub;
|
||||||
_cacheHelper = cacheHelper;
|
_cacheHelper = cacheHelper;
|
||||||
_readerService = readerService;
|
_readerService = readerService;
|
||||||
|
_mediaErrorService = mediaErrorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -188,7 +190,7 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.WordCountAnalyzerProgressEvent(series.LibraryId, progress,
|
MessageFactory.WordCountAnalyzerProgressEvent(series.LibraryId, progress,
|
||||||
ProgressEventType.Updated, useFileName ? filePath : series.Name));
|
ProgressEventType.Updated, useFileName ? filePath : series.Name));
|
||||||
sum += await GetWordCountFromHtml(bookPage);
|
sum += await GetWordCountFromHtml(bookPage, filePath);
|
||||||
pageCounter++;
|
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();
|
var doc = new HtmlDocument();
|
||||||
doc.LoadHtml(await bookFile.ReadContentAsync());
|
doc.LoadHtml(await bookFile.ReadContentAsync());
|
||||||
@ -253,5 +257,13 @@ public class WordCountAnalyzerService : IWordCountAnalyzerService
|
|||||||
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
|
var textNodes = doc.DocumentNode.SelectNodes("//body//text()[not(parent::script)]");
|
||||||
return textNodes?.Sum(node => node.InnerText.Count(char.IsLetter)) / AverageCharactersPerWord ?? 0;
|
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);
|
themeDtos.Add(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
_cache.Set(themeDtos, themes, _cacheOptions);
|
_cache.Set(cacheKey, themeDtos, _cacheOptions);
|
||||||
|
|
||||||
return themeDtos;
|
return themeDtos;
|
||||||
}
|
}
|
||||||
|
@ -134,6 +134,10 @@ public static class MessageFactory
|
|||||||
/// A Theme was updated and UI should refresh to get the latest version
|
/// A Theme was updated and UI should refresh to get the latest version
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SiteThemeUpdated = "SiteThemeUpdated";
|
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)
|
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>
|
/// <summary>
|
||||||
/// This informs the UI with details about what is being processed by the Scanner
|
/// This informs the UI with details about what is being processed by the Scanner
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -266,6 +266,9 @@ public class Startup
|
|||||||
// v0.8.1
|
// v0.8.1
|
||||||
await MigrateLowestSeriesFolderPath.Migrate(dataContext, unitOfWork, logger);
|
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
|
// Update the version in the DB after all migrations are run
|
||||||
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion);
|
||||||
installVersion.Value = BuildInfo.Version.ToString();
|
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)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher)
|
- [NodeJS](https://nodejs.org/en/download/) (Node 18.13.X or higher)
|
||||||
- .NET 8.0+
|
- .NET 8.0+
|
||||||
- dotnet tool install -g --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
- dotnet tool install -g Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
### Getting started ###
|
### Getting started ###
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" 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>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
$scrollbarHeight: 34px;
|
$scrollbarHeight: 35px;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -9,29 +9,31 @@ img {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&.full-width {
|
&.full-width {
|
||||||
height: calc(var(--vh)*100);
|
height: 100dvh;
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.full-height {
|
&.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;
|
display: flex;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.original {
|
&.original {
|
||||||
height: 100vh;
|
height: calc(100dvh);
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-height {
|
.full-height {
|
||||||
width: auto;
|
width: auto;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-height: calc(var(--vh)*100);
|
max-height: calc(100dvh);
|
||||||
overflow: hidden; // This technically will crop and make it just fit
|
height: calc(100dvh);
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
object-fit: cover;
|
||||||
&.wide {
|
&.wide {
|
||||||
height: 100vh;
|
height: calc(100dvh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,12 +48,13 @@ img {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
max-width: fit-content;
|
object-fit: contain;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fit-to-screen.full-width {
|
.fit-to-screen.full-width {
|
||||||
width: 100%;
|
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 { AccountService } from './account.service';
|
||||||
import { DeviceService } from './device.service';
|
import { DeviceService } from './device.service';
|
||||||
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
|
import {SideNavStream} from "../_models/sidenav/sidenav-stream";
|
||||||
import {User} from "../_models/user";
|
|
||||||
|
|
||||||
export enum Action {
|
export enum Action {
|
||||||
Submenu = -1,
|
Submenu = -1,
|
||||||
|
@ -103,7 +103,11 @@ export enum EVENTS {
|
|||||||
/**
|
/**
|
||||||
* A Theme was updated and UI should refresh to get the latest version
|
* 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> {
|
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.hubConnection.on(EVENTS.SiteThemeUpdated, resp => {
|
||||||
this.messagesSource.next({
|
this.messagesSource.next({
|
||||||
event: EVENTS.SiteThemeUpdated,
|
event: EVENTS.SiteThemeUpdated,
|
||||||
|
@ -24,11 +24,11 @@
|
|||||||
<div class="card-footer bg-transparent text-muted">
|
<div class="card-footer bg-transparent text-muted">
|
||||||
<div>
|
<div>
|
||||||
@if (isMyReview) {
|
@if (isMyReview) {
|
||||||
<i class="d-md-none fa-solid fa-star me-1" aria-hidden="true" [title]="t('your-review')"></i>
|
<i class="d-md-none fa-solid fa-star me-2" aria-hidden="true" [title]="t('your-review')"></i>
|
||||||
<img class="me-1" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
|
<img class="me-2" [ngSrc]="ScrobbleProvider.Kavita | providerImage" width="20" height="20" alt="">
|
||||||
{{review.username}}
|
{{review.username}}
|
||||||
} @else {
|
} @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:'')}}
|
{{(isMyReview ? '' : review.username | defaultValue:'')}}
|
||||||
|
@ -42,4 +42,10 @@
|
|||||||
max-width: 319px;
|
max-width: 319px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin: 0 auto;
|
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'">
|
<ng-container *transloco="let t; read:'user-scrobble-history'">
|
||||||
<h5>{{t('title')}}</h5>
|
<h5>{{t('title')}}</h5>
|
||||||
<p>{{t('description')}}</p>
|
<p>{{t('description')}}</p>
|
||||||
|
<p class="fw-bold">{{t('not-read-warning')}}</p>
|
||||||
<div class="row g-0 mb-2">
|
<div class="row g-0 mb-2">
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<form [formGroup]="formGroup">
|
<form [formGroup]="formGroup">
|
||||||
@ -67,8 +68,12 @@
|
|||||||
@switch (item.scrobbleEventType) {
|
@switch (item.scrobbleEventType) {
|
||||||
@case (ScrobbleEventType.ChapterRead) {
|
@case (ScrobbleEventType.ChapterRead) {
|
||||||
@if(item.volumeNumber === LooseLeafOrDefaultNumber) {
|
@if(item.volumeNumber === LooseLeafOrDefaultNumber) {
|
||||||
|
@if (item.chapterNumber === LooseLeafOrDefaultNumber) {
|
||||||
|
{{t('special')}}
|
||||||
|
} @else {
|
||||||
{{t('chapter-num', {num: item.chapterNumber})}}
|
{{t('chapter-num', {num: item.chapterNumber})}}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@else if (item.chapterNumber === LooseLeafOrDefaultNumber) {
|
@else if (item.chapterNumber === LooseLeafOrDefaultNumber) {
|
||||||
{{t('volume-num', {num: item.volumeNumber})}}
|
{{t('volume-num', {num: item.volumeNumber})}}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,12 @@
|
|||||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
||||||
<h4 id="email-header">{{t('title')}}</h4>
|
<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 ">
|
<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>
|
<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>
|
<ng-template #hostNameTooltip>{{t('host-name-tooltip')}}</ng-template>
|
||||||
|
@ -5,6 +5,7 @@ import {take} from 'rxjs';
|
|||||||
import {SettingsService} from '../settings.service';
|
import {SettingsService} from '../settings.service';
|
||||||
import {ServerSettings} from '../_models/server-settings';
|
import {ServerSettings} from '../_models/server-settings';
|
||||||
import {
|
import {
|
||||||
|
NgbAlert,
|
||||||
NgbTooltip
|
NgbTooltip
|
||||||
} from '@ng-bootstrap/ng-bootstrap';
|
} from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
|
import {NgIf, NgTemplateOutlet, TitleCasePipe} from '@angular/common';
|
||||||
@ -19,7 +20,7 @@ import {ManageAlertsComponent} from "../manage-alerts/manage-alerts.component";
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe,
|
imports: [NgIf, ReactiveFormsModule, NgbTooltip, NgTemplateOutlet, TranslocoModule, SafeHtmlPipe,
|
||||||
ManageAlertsComponent, TitleCasePipe]
|
ManageAlertsComponent, TitleCasePipe, NgbAlert]
|
||||||
})
|
})
|
||||||
export class ManageEmailSettingsComponent implements OnInit {
|
export class ManageEmailSettingsComponent implements OnInit {
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<ng-template #cardItem let-item let-position="idx">
|
<ng-template #cardItem let-item let-position="idx">
|
||||||
<!-- TODO: figure a way to get a hover effect -->
|
<!-- TODO: figure a way to get a hover effect -->
|
||||||
<div class="card-item-container card clickable" (click)="loadSmartFilter(item)">
|
<div class="card-item-container card clickable" (click)="loadSmartFilter(item)">
|
||||||
<div class="overlay">
|
<div class="overlay filter">
|
||||||
<div class="card-overlay"></div>
|
<div class="card-overlay"></div>
|
||||||
<div class="overlay-information overlay-information--centered">
|
<div class="overlay-information overlay-information--centered">
|
||||||
<div class="position-relative">
|
<div class="position-relative">
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<ng-container *transloco="let t; read: 'series-info-cards'">
|
<ng-container *transloco="let t; read: 'series-info-cards'">
|
||||||
<div class="row g-0 mt-3">
|
<div class="row g-0 mt-3">
|
||||||
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
|
<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')">
|
<app-icon-and-title [label]="t('release-date-title')" [clickable]="false" fontClasses="fa-regular fa-calendar" [title]="t('release-year-tooltip')">
|
||||||
{{seriesMetadata.releaseYear}}
|
{{seriesMetadata.releaseYear}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="seriesMetadata">
|
<ng-container *ngIf="seriesMetadata">
|
||||||
<ng-container *ngIf="seriesMetadata.ageRating">
|
<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')">
|
<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}}
|
{{this.seriesMetadata.ageRating | ageRating}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
@ -20,7 +20,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''">
|
<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')">
|
<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}}
|
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<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">
|
<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'}}"
|
<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)"
|
(click)="handleGoTo(FilterField.PublicationStatus, seriesMetadata.publicationStatus)"
|
||||||
@ -43,7 +43,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="accountService.hasValidLicense$ | async">
|
<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"
|
<app-icon-and-title [label]="t('scrobbling-title')" [clickable]="libraryAllowsScrobbling"
|
||||||
fontClasses="fa-solid fa-tower-{{(isScrobbling && libraryAllowsScrobbling) ? 'broadcast' : 'observation'}}"
|
fontClasses="fa-solid fa-tower-{{(isScrobbling && libraryAllowsScrobbling) ? 'broadcast' : 'observation'}}"
|
||||||
(click)="toggleScrobbling($event)"
|
(click)="toggleScrobbling($event)"
|
||||||
@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="series">
|
<ng-container *ngIf="series">
|
||||||
<ng-container>
|
<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"
|
<app-icon-and-title [label]="t('format-title')" [clickable]="true"
|
||||||
[fontClasses]="series.format | mangaFormatIcon"
|
[fontClasses]="series.format | mangaFormatIcon"
|
||||||
(click)="handleGoTo(FilterField.Formats, series.format)" [title]="t('format-title')">
|
(click)="handleGoTo(FilterField.Formats, series.format)" [title]="t('format-title')">
|
||||||
@ -73,7 +73,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
|
<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')">
|
<app-icon-and-title [label]="t('last-read-title')" [clickable]="false" fontClasses="fa-regular fa-clock" [title]="t('last-read-title')">
|
||||||
{{series.latestReadDate | timeAgo}}
|
{{series.latestReadDate | timeAgo}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
@ -83,7 +83,7 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
|
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
|
||||||
<ng-container *ngIf="series.wordCount > 0">
|
<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">
|
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||||
{{t('words-count', {num: series.wordCount | compactNumber})}}
|
{{t('words-count', {num: series.wordCount | compactNumber})}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
@ -93,7 +93,7 @@
|
|||||||
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #showPages>
|
<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">
|
<app-icon-and-title [label]="t('length-title')" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||||
{{t('pages-count', {num: series.pages | compactNumber})}}
|
{{t('pages-count', {num: series.pages | compactNumber})}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
@ -102,7 +102,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
<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">
|
<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-container *ngIf="readingTime.maxHours === 0 || readingTime.minHours === 0; else normalReadTime">{{t('less-than-hour')}}</ng-container>
|
||||||
<ng-template #normalReadTime>
|
<ng-template #normalReadTime>
|
||||||
@ -114,7 +114,7 @@
|
|||||||
|
|
||||||
<ng-container *ngIf="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && readingTimeLeft.avgHours !== 0">
|
<ng-container *ngIf="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && readingTimeLeft.avgHours !== 0">
|
||||||
<div class="vr d-none d-lg-block m-2"></div>
|
<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">
|
<app-icon-and-title label="Time Left" [clickable]="false" fontClasses="fa-solid fa-clock">
|
||||||
~{{readingTimeLeft.avgHours}} {{readingTimeLeft.avgHours > 1 ? t('hours') : t('hour')}}
|
~{{readingTimeLeft.avgHours}} {{readingTimeLeft.avgHours > 1 ? t('hours') : t('hour')}}
|
||||||
</app-icon-and-title>
|
</app-icon-and-title>
|
||||||
|
@ -6,11 +6,13 @@ import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap";
|
|||||||
import {CollectionTagService} from "../../../_services/collection-tag.service";
|
import {CollectionTagService} from "../../../_services/collection-tag.service";
|
||||||
import {MalStack} from "../../../_models/collection/mal-stack";
|
import {MalStack} from "../../../_models/collection/mal-stack";
|
||||||
import {UserCollection} from "../../../_models/collection-tag";
|
import {UserCollection} from "../../../_models/collection-tag";
|
||||||
import {ScrobbleProvider} from "../../../_services/scrobbling.service";
|
import {ScrobbleProvider, ScrobblingService} from "../../../_services/scrobbling.service";
|
||||||
import {forkJoin} from "rxjs";
|
import {forkJoin} from "rxjs";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
import {DecimalPipe} from "@angular/common";
|
import {DecimalPipe} from "@angular/common";
|
||||||
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
import {LoadingComponent} from "../../../shared/loading/loading.component";
|
||||||
|
import {AccountService} from "../../../_services/account.service";
|
||||||
|
import {ConfirmService} from "../../../shared/confirm.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-import-mal-collection-modal',
|
selector: 'app-import-mal-collection-modal',
|
||||||
@ -32,13 +34,25 @@ export class ImportMalCollectionModalComponent {
|
|||||||
private readonly collectionService = inject(CollectionTagService);
|
private readonly collectionService = inject(CollectionTagService);
|
||||||
private readonly cdRef = inject(ChangeDetectorRef);
|
private readonly cdRef = inject(ChangeDetectorRef);
|
||||||
private readonly toastr = inject(ToastrService);
|
private readonly toastr = inject(ToastrService);
|
||||||
|
private readonly scrobblingService = inject(ScrobblingService);
|
||||||
|
private readonly confirmService = inject(ConfirmService);
|
||||||
|
|
||||||
stacks: Array<MalStack> = [];
|
stacks: Array<MalStack> = [];
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
collectionMap: {[key: string]: UserCollection | MalStack} = {};
|
collectionMap: {[key: string]: UserCollection | MalStack} = {};
|
||||||
|
|
||||||
constructor() {
|
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({
|
forkJoin({
|
||||||
allCollections: this.collectionService.allCollections(true),
|
allCollections: this.collectionService.allCollections(true),
|
||||||
malStacks: this.collectionService.getMalStacks()
|
malStacks: this.collectionService.getMalStacks()
|
||||||
|
@ -3,18 +3,25 @@
|
|||||||
.image-container {
|
.image-container {
|
||||||
#image-1 {
|
#image-1 {
|
||||||
&.double {
|
&.double {
|
||||||
margin: 0 0 0 auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-container.full-height {
|
.image-container {
|
||||||
display: inline-block !important;
|
&.full-height {
|
||||||
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-height {
|
||||||
|
margin: unset;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width {
|
.full-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
max-width: fit-content;
|
max-width: fit-content;
|
||||||
|
|
||||||
@ -41,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fit-to-height-double-offset {
|
.fit-to-height-double-offset {
|
||||||
height: 100vh;
|
height: calc(100dvh);
|
||||||
object-fit: scale-down;
|
object-fit: scale-down;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
// Overrides for reverse
|
// Overrides for reverse
|
||||||
.image-container {
|
.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 {
|
&.reverse {
|
||||||
overflow: unset;
|
overflow: unset;
|
||||||
@ -29,8 +29,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-container.full-height {
|
.image-container {
|
||||||
display: inline-block;
|
display: flex;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
|
||||||
|
.full-height {
|
||||||
|
margin: unset;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-width {
|
.full-width {
|
||||||
@ -62,7 +70,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fit-to-height-double-offset {
|
.fit-to-height-double-offset {
|
||||||
height: 100vh;
|
height: calc(100dvh);
|
||||||
object-fit: scale-down;
|
object-fit: scale-down;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<app-loading [loading]="isLoading || (!(currentImage$ | async)?.complete && this.readerMode !== ReaderMode.Webtoon)" [absolute]="true"></app-loading>
|
<app-loading [loading]="isLoading || (!(currentImage$ | async)?.complete && this.readerMode !== ReaderMode.Webtoon)" [absolute]="true"></app-loading>
|
||||||
<div class="reading-area"
|
<div class="reading-area"
|
||||||
ngSwipe (swipeEnd)="onSwipeEnd($event)" (swipeMove)="onSwipeMove($event)"
|
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">
|
<ng-container *ngIf="readerMode !== ReaderMode.Webtoon; else webtoon">
|
||||||
<div (dblclick)="bookmarkPage($event)">
|
<div (dblclick)="bookmarkPage($event)">
|
||||||
@ -56,14 +56,14 @@
|
|||||||
<!-- Pagination controls and screen hints-->
|
<!-- Pagination controls and screen hints-->
|
||||||
<div class="pagination-area">
|
<div class="pagination-area">
|
||||||
<div class="{{readerMode === ReaderMode.LeftRight ? 'left' : 'top'}} {{clickOverlayClass('left')}}" (click)="handlePageChange($event, KeyDirection.Left)"
|
<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">
|
<div *ngIf="showClickOverlay">
|
||||||
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'left' : 'up'}}"
|
<i class="fa fa-angle-{{readingDirection === ReadingDirection.RightToLeft ? 'double-' : ''}}{{readerMode === ReaderMode.LeftRight ? 'left' : 'up'}}"
|
||||||
[title]="t('prev-page-tooltip')" aria-hidden="true"></i>
|
[title]="t('prev-page-tooltip')" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="{{readerMode === ReaderMode.LeftRight ? 'right' : 'bottom'}} {{clickOverlayClass('right')}}" (click)="handlePageChange($event, KeyDirection.Right)"
|
<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',
|
'left': 'inherit',
|
||||||
'right': RightPaginationOffset + 'px',
|
'right': RightPaginationOffset + 'px',
|
||||||
'max-height': MaxHeight}">
|
'max-height': MaxHeight}">
|
||||||
|
@ -16,7 +16,6 @@ $pointer-offset: 5px;
|
|||||||
|
|
||||||
|
|
||||||
.reading-area {
|
.reading-area {
|
||||||
position: relative;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
//height: calc(var(--vh)*100); // this needs to be applied on the DOM because it breaks infinite scroller
|
//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
|
// This is for the pagination area
|
||||||
get MaxHeight() {
|
get MaxHeight() {
|
||||||
if (this.FittingOption === FITTING_OPTION.HEIGHT) {
|
return '100dvh';
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get RightPaginationOffset() {
|
get RightPaginationOffset() {
|
||||||
|
@ -150,14 +150,14 @@ export class SingleRendererComponent implements OnInit, ImageRenderer {
|
|||||||
if (mode !== FITTING_OPTION.HEIGHT) return '';
|
if (mode !== FITTING_OPTION.HEIGHT) return '';
|
||||||
|
|
||||||
const readingArea = this.document.querySelector('.reading-area');
|
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 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) {
|
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
|
// 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())
|
filter(_ => this.isValid())
|
||||||
);
|
);
|
||||||
|
@ -118,7 +118,7 @@ export class EventsWidgetComponent implements OnInit, OnDestroy {
|
|||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
break;
|
break;
|
||||||
case 'started':
|
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);
|
data = this.mergeOrUpdate(this.progressEventsSource.getValue(), message);
|
||||||
this.progressEventsSource.next(data);
|
this.progressEventsSource.next(data);
|
||||||
break;
|
break;
|
||||||
|
@ -3,12 +3,15 @@
|
|||||||
<h2 title>
|
<h2 title>
|
||||||
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
|
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [attr.aria-labelledby]="readingList?.title" *ngIf="actions.length > 0"></app-card-actionables>
|
||||||
{{readingList?.title}}
|
{{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>
|
</h2>
|
||||||
<h6 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h6>
|
<h6 subtitle class="subtitle-with-actionables">{{t('item-count', {num: items.length | number})}}</h6>
|
||||||
|
|
||||||
<ng-template #extrasDrawer let-offcanvas>
|
<ng-template #extrasDrawer let-offcanvas>
|
||||||
<div style="margin-top: 56px" *ngIf="readingList">
|
@if (readingList) {
|
||||||
|
<div style="margin-top: 56px">
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
|
<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>
|
<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>
|
<span class="read-btn--text"> {{t('remove-read')}}</span>
|
||||||
</button>
|
</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">
|
<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>
|
<label class="form-check-label" for="accessibility-mode">{{t('order-numbers-label')}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-side-nav-companion-bar>
|
</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="row mb-2">
|
||||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||||
@ -135,5 +143,5 @@
|
|||||||
</app-draggable-ordered-list>
|
</app-draggable-ordered-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -3,7 +3,7 @@ import {ActivatedRoute, Router} from '@angular/router';
|
|||||||
import {ToastrService} from 'ngx-toastr';
|
import {ToastrService} from 'ngx-toastr';
|
||||||
import {take} from 'rxjs/operators';
|
import {take} from 'rxjs/operators';
|
||||||
import {ConfirmService} from 'src/app/shared/confirm.service';
|
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 {LibraryType} from 'src/app/_models/library/library';
|
||||||
import {MangaFormat} from 'src/app/_models/manga-format';
|
import {MangaFormat} from 'src/app/_models/manga-format';
|
||||||
import {ReadingList, ReadingListItem} from 'src/app/_models/reading-list';
|
import {ReadingList, ReadingListItem} from 'src/app/_models/reading-list';
|
||||||
@ -32,7 +32,7 @@ import {AsyncPipe, DatePipe, DecimalPipe, NgClass, NgIf} from '@angular/common';
|
|||||||
import {
|
import {
|
||||||
SideNavCompanionBarComponent
|
SideNavCompanionBarComponent
|
||||||
} from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
} 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 {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
|
||||||
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
|
import {FilterUtilitiesService} from "../../../shared/_services/filter-utilities.service";
|
||||||
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
import {FilterField} from "../../../_models/metadata/v2/filter-field";
|
||||||
@ -53,6 +53,26 @@ import {Title} from "@angular/platform-browser";
|
|||||||
MetadataDetailComponent]
|
MetadataDetailComponent]
|
||||||
})
|
})
|
||||||
export class ReadingListDetailComponent implements OnInit {
|
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> = [];
|
items: Array<ReadingListItem> = [];
|
||||||
listId!: number;
|
listId!: number;
|
||||||
readingList: ReadingList | undefined;
|
readingList: ReadingList | undefined;
|
||||||
@ -65,15 +85,8 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
libraryTypes: {[key: number]: LibraryType} = {};
|
libraryTypes: {[key: number]: LibraryType} = {};
|
||||||
characters$!: Observable<Person[]>;
|
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 {
|
ngOnInit(): void {
|
||||||
const listId = this.route.snapshot.paramMap.get('id');
|
const listId = this.route.snapshot.paramMap.get('id');
|
||||||
@ -86,6 +99,9 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
this.listId = parseInt(listId, 10);
|
this.listId = parseInt(listId, 10);
|
||||||
this.characters$ = this.readingListService.getCharacters(this.listId);
|
this.characters$ = this.readingListService.getCharacters(this.listId);
|
||||||
|
|
||||||
|
this.accessibilityMode = this.utilityService.getActiveBreakpoint() < Breakpoint.Tablet;
|
||||||
|
this.cdRef.markForCheck();
|
||||||
|
|
||||||
forkJoin([
|
forkJoin([
|
||||||
this.libraryService.getLibraries(),
|
this.libraryService.getLibraries(),
|
||||||
this.readingListService.getReadingList(this.listId)
|
this.readingListService.getReadingList(this.listId)
|
||||||
@ -165,10 +181,10 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteList(readingList: ReadingList) {
|
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.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');
|
this.router.navigateByUrl('/lists');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -186,7 +202,7 @@ export class ReadingListDetailComponent implements OnInit {
|
|||||||
this.items.splice(position, 1);
|
this.items.splice(position, 1);
|
||||||
this.items = [...this.items];
|
this.items = [...this.items];
|
||||||
this.cdRef.markForCheck();
|
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.cdRef.markForCheck();
|
||||||
this.readingListService.removeRead(this.readingList.id).subscribe((resp) => {
|
this.readingListService.removeRead(this.readingList.id).subscribe((resp) => {
|
||||||
if (resp === 'Nothing to remove') {
|
if (resp === 'Nothing to remove') {
|
||||||
this.toastr.info(this.translocoService.translate('toasts.nothing-to-remove'));
|
this.toastr.info(translate('toasts.nothing-to-remove'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.getListItems();
|
this.getListItems();
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="row g-0 theme-container">
|
<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">
|
<div class="pe-2">
|
||||||
<ul style="height: 100%" class="list-group list-group-flush">
|
<ul style="height: 100%" class="list-group list-group-flush">
|
||||||
|
|
||||||
@ -23,7 +23,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</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) {
|
@if (selectedTheme === undefined) {
|
||||||
|
|
||||||
<div class="row pb-4">
|
<div class="row pb-4">
|
||||||
@ -35,7 +37,6 @@
|
|||||||
} @else {
|
} @else {
|
||||||
{{t('preview-default')}}
|
{{t('preview-default')}}
|
||||||
}
|
}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -91,20 +92,20 @@
|
|||||||
@if(!selectedTheme.isSiteTheme) {
|
@if(!selectedTheme.isSiteTheme) {
|
||||||
<p>{{selectedTheme.downloadable!.description | defaultValue}}</p>
|
<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>
|
<ng-template #carouselItem let-item>
|
||||||
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
|
<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>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
} @else {
|
} @else {
|
||||||
<p>{{selectedTheme.site!.description | defaultValue}}</p>
|
<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>
|
<ng-template #carouselItem let-item>
|
||||||
<a [href]="item | safeUrl" target="_blank" rel="noopener noreferrer">
|
<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>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</app-carousel-reel>
|
</app-carousel-reel>
|
||||||
@ -113,6 +114,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ng-template #themeOption let-item>
|
<ng-template #themeOption let-item>
|
||||||
@if (item !== undefined) {
|
@if (item !== undefined) {
|
||||||
@ -122,12 +124,12 @@
|
|||||||
<div class="fw-bold">{{item.name | sentenceCase}}</div>
|
<div class="fw-bold">{{item.name | sentenceCase}}</div>
|
||||||
|
|
||||||
@if (item.hasOwnProperty('provider')) {
|
@if (item.hasOwnProperty('provider')) {
|
||||||
{{item.provider | siteThemeProvider}}
|
<span class="pill p-1 me-1 provider">{{item.provider | siteThemeProvider}}</span>
|
||||||
} @else if (item.hasOwnProperty('lastCompatibleVersion')) {
|
} @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) {
|
@if (currentTheme && item.name === currentTheme.name) {
|
||||||
• {{t('active-theme')}}
|
<span class="pill p-1 active">{{t('active-theme')}}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (item.hasOwnProperty('isDefault') && item.isDefault) {
|
@if (item.hasOwnProperty('isDefault') && item.isDefault) {
|
||||||
|
@ -10,6 +10,25 @@
|
|||||||
justify-content: space-around;
|
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 {
|
ngx-file-drop ::ng-deep > div {
|
||||||
// styling for the outer drop box
|
// styling for the outer drop box
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
"user-scrobble-history": {
|
"user-scrobble-history": {
|
||||||
"title": "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.",
|
"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}}",
|
"filter-label": "{{common.filter}}",
|
||||||
"created-header": "Created",
|
"created-header": "Created",
|
||||||
"last-modified-header": "Last Modified",
|
"last-modified-header": "Last Modified",
|
||||||
@ -47,7 +48,8 @@
|
|||||||
"rating": "Rating {{r}}",
|
"rating": "Rating {{r}}",
|
||||||
"not-applicable": "Not Applicable",
|
"not-applicable": "Not Applicable",
|
||||||
"processed": "Processed",
|
"processed": "Processed",
|
||||||
"not-processed": "Not Processed"
|
"not-processed": "Not Processed",
|
||||||
|
"special": "{{entity-title.special}}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"scrobble-event-type-pipe": {
|
"scrobble-event-type-pipe": {
|
||||||
@ -197,7 +199,8 @@
|
|||||||
"upload": "{{cover-image-chooser.upload}}",
|
"upload": "{{cover-image-chooser.upload}}",
|
||||||
"upload-continued": "a css file",
|
"upload-continued": "a css file",
|
||||||
"preview-default": "Select a theme first",
|
"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": {
|
"theme": {
|
||||||
@ -1122,6 +1125,8 @@
|
|||||||
"manage-email-settings": {
|
"manage-email-settings": {
|
||||||
"title": "Email Services (SMTP)",
|
"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.",
|
"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",
|
"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-label": "Email Service URL",
|
||||||
"email-url-tooltip": "Use fully qualified URL of the email service. Do not include ending slash.",
|
"email-url-tooltip": "Use fully qualified URL of the email service. Do not include ending slash.",
|
||||||
@ -2199,8 +2204,8 @@
|
|||||||
"collections-deleted": "Collections deleted",
|
"collections-deleted": "Collections deleted",
|
||||||
"pdf-book-mode-screen-size": "Screen too small for Book mode",
|
"pdf-book-mode-screen-size": "Screen too small for Book mode",
|
||||||
"stack-imported": "Stack Imported",
|
"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": {
|
"actionable": {
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$image-height: 230px;
|
$image-height: 230px;
|
||||||
|
$image-filter-height: 160px;
|
||||||
$image-width: 160px;
|
$image-width: 160px;
|
||||||
|
|
||||||
.card-item-container {
|
.card-item-container {
|
||||||
@ -62,6 +63,14 @@ $image-width: 160px;
|
|||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
|
&.filter {
|
||||||
|
height: $image-filter-height;
|
||||||
|
|
||||||
|
.card-overlay {
|
||||||
|
height: $image-filter-height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
"name": "GPL-3.0",
|
"name": "GPL-3.0",
|
||||||
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
|
||||||
},
|
},
|
||||||
"version": "0.8.1.3"
|
"version": "0.8.1.4"
|
||||||
},
|
},
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user