Implemented the ability to allow the admin to change the cover generation size. (#2213)

Changed how covers are merged together. Now a cover image will always be generated for reading list and collections.

Fixed reading list page being a bit laggy due to a missing trackby function.

Reading list page will now show the cover image always. Collection detail page will only hide the image if there is no summary on the collection.
This commit is contained in:
Joe Milazzo 2023-08-14 06:56:09 -05:00 committed by GitHub
parent 19801af6f3
commit d134196470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 221 additions and 87 deletions

View File

@ -42,7 +42,7 @@ internal class MockReadingItemServiceForCacheService : IReadingItemService
return 1; return 1;
} }
public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat) public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{ {
return string.Empty; return string.Empty;
} }

View File

@ -43,7 +43,7 @@ internal class MockReadingItemService : IReadingItemService
return 1; return 1;
} }
public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat) public string GetCoverImage(string fileFilePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{ {
return string.Empty; return string.Empty;
} }

View File

@ -158,11 +158,6 @@ public class ImageController : BaseApiController
private async Task<string> GenerateReadingListCoverImage(int readingListId) private async Task<string> GenerateReadingListCoverImage(int readingListId)
{ {
var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId); var covers = await _unitOfWork.ReadingListRepository.GetRandomCoverImagesAsync(readingListId);
if (covers.Count < 4)
{
return string.Empty;
}
var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory,
ImageService.GetReadingListFormat(readingListId)); ImageService.GetReadingListFormat(readingListId));
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
@ -171,6 +166,7 @@ public class ImageController : BaseApiController
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
ImageService.CreateMergedImage( ImageService.CreateMergedImage(
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
settings.CoverImageSize,
destFile); destFile);
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
} }
@ -178,11 +174,6 @@ public class ImageController : BaseApiController
private async Task<string> GenerateCollectionCoverImage(int collectionId) private async Task<string> GenerateCollectionCoverImage(int collectionId)
{ {
var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId); var covers = await _unitOfWork.CollectionTagRepository.GetRandomCoverImagesAsync(collectionId);
if (covers.Count < 4)
{
return string.Empty;
}
var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory, var destFile = _directoryService.FileSystem.Path.Join(_directoryService.TempDirectory,
ImageService.GetCollectionTagFormat(collectionId)); ImageService.GetCollectionTagFormat(collectionId));
var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
@ -190,6 +181,7 @@ public class ImageController : BaseApiController
if (_directoryService.FileSystem.File.Exists(destFile)) return destFile; if (_directoryService.FileSystem.File.Exists(destFile)) return destFile;
ImageService.CreateMergedImage( ImageService.CreateMergedImage(
covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(), covers.Select(c => _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, c)).ToList(),
settings.CoverImageSize,
destFile); destFile);
return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile; return !_directoryService.FileSystem.File.Exists(destFile) ? string.Empty : destFile;
} }

View File

@ -197,6 +197,12 @@ public class SettingsController : BaseApiController
_unitOfWork.SettingsRepository.Update(setting); _unitOfWork.SettingsRepository.Update(setting);
} }
if (setting.Key == ServerSettingKey.CoverImageSize && updateSettingsDto.CoverImageSize + string.Empty != setting.Value)
{
setting.Value = updateSettingsDto.CoverImageSize + string.Empty;
_unitOfWork.SettingsRepository.Update(setting);
}
if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value) if (setting.Key == ServerSettingKey.TaskScan && updateSettingsDto.TaskScan != setting.Value)
{ {
setting.Value = updateSettingsDto.TaskScan; setting.Value = updateSettingsDto.TaskScan;

View File

@ -84,4 +84,8 @@ public class ServerSettingDto
/// How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically /// How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically
/// </summary> /// </summary>
public int OnDeckUpdateDays { get; set; } public int OnDeckUpdateDays { get; set; }
/// <summary>
/// How large the cover images should be
/// </summary>
public CoverImageSize CoverImageSize { get; set; }
} }

View File

@ -127,7 +127,6 @@ public class CollectionTagRepository : ICollectionTagRepository
.Select(sm => sm.Series.CoverImage) .Select(sm => sm.Series.CoverImage)
.Where(t => !string.IsNullOrEmpty(t)) .Where(t => !string.IsNullOrEmpty(t))
.ToListAsync(); .ToListAsync();
if (data.Count < 4) return new List<string>();
return data return data
.OrderBy(_ => random.Next()) .OrderBy(_ => random.Next())
.Take(4) .Take(4)

View File

@ -101,7 +101,6 @@ public class ReadingListRepository : IReadingListRepository
.SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage)) .SelectMany(r => r.Items.Select(ri => ri.Chapter.CoverImage))
.Where(t => !string.IsNullOrEmpty(t)) .Where(t => !string.IsNullOrEmpty(t))
.ToListAsync(); .ToListAsync();
if (data.Count < 4) return new List<string>();
return data return data
.OrderBy(_ => random.Next()) .OrderBy(_ => random.Next())
.Take(4) .Take(4)

View File

@ -108,8 +108,9 @@ public static class Seed
new() {Key = ServerSettingKey.HostName, Value = string.Empty}, new() {Key = ServerSettingKey.HostName, Value = string.Empty},
new() {Key = ServerSettingKey.EncodeMediaAs, Value = EncodeFormat.PNG.ToString()}, new() {Key = ServerSettingKey.EncodeMediaAs, Value = EncodeFormat.PNG.ToString()},
new() {Key = ServerSettingKey.LicenseKey, Value = string.Empty}, new() {Key = ServerSettingKey.LicenseKey, Value = string.Empty},
new() {Key = ServerSettingKey.OnDeckProgressDays, Value = $"{30}"}, new() {Key = ServerSettingKey.OnDeckProgressDays, Value = "30"},
new() {Key = ServerSettingKey.OnDeckUpdateDays, Value = $"{7}"}, new() {Key = ServerSettingKey.OnDeckUpdateDays, Value = "7"},
new() {Key = ServerSettingKey.CoverImageSize, Value = CoverImageSize.Default.ToString()},
new() { new() {
Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty Key = ServerSettingKey.CacheSize, Value = Configuration.DefaultCacheMemory + string.Empty
}, // Not used from DB, but DB is sync with appSettings.json }, // Not used from DB, but DB is sync with appSettings.json

View File

@ -0,0 +1,36 @@
namespace API.Entities.Enums;
public enum CoverImageSize
{
/// <summary>
/// Default Size: 320x455 (wxh)
/// </summary>
Default = 1,
/// <summary>
/// 640x909
/// </summary>
Medium = 2,
/// <summary>
/// 900x1277
/// </summary>
Large = 3,
/// <summary>
/// 1265x1795
/// </summary>
XLarge = 4
}
public static class CoverImageSizeExtensions
{
public static (int Width, int Height) GetDimensions(this CoverImageSize size)
{
return size switch
{
CoverImageSize.Default => (320, 455),
CoverImageSize.Medium => (640, 909),
CoverImageSize.Large => (900, 1277),
CoverImageSize.XLarge => (1265, 1795),
_ => (320, 455)
};
}
}

View File

@ -143,5 +143,10 @@ public enum ServerSettingKey
/// </summary> /// </summary>
[Description("OnDeckUpdateDays")] [Description("OnDeckUpdateDays")]
OnDeckUpdateDays = 26, OnDeckUpdateDays = 26,
/// <summary>
/// The size of the cover image thumbnail. Defaults to <see cref="CoverImageSize"/>.Default
/// </summary>
[Description("CoverImageSize")]
CoverImageSize = 27
} }

View File

@ -79,6 +79,9 @@ public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>,
case ServerSettingKey.OnDeckUpdateDays: case ServerSettingKey.OnDeckUpdateDays:
destination.OnDeckUpdateDays = int.Parse(row.Value); destination.OnDeckUpdateDays = int.Parse(row.Value);
break; break;
case ServerSettingKey.CoverImageSize:
destination.CoverImageSize = Enum.Parse<CoverImageSize>(row.Value);
break;
} }
} }

View File

@ -22,7 +22,7 @@ public interface IArchiveService
{ {
void ExtractArchive(string archivePath, string extractPath); void ExtractArchive(string archivePath, string extractPath);
int GetNumberOfPagesFromArchive(string archivePath); int GetNumberOfPagesFromArchive(string archivePath);
string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format); string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default);
bool IsValidArchive(string archivePath); bool IsValidArchive(string archivePath);
ComicInfo? GetComicInfo(string archivePath); ComicInfo? GetComicInfo(string archivePath);
ArchiveLibrary CanOpen(string archivePath); ArchiveLibrary CanOpen(string archivePath);
@ -205,7 +205,7 @@ public class ArchiveService : IArchiveService
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param> /// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
/// <param name="format">When saving the file, use encoding</param> /// <param name="format">When saving the file, use encoding</param>
/// <returns></returns> /// <returns></returns>
public string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format) public string GetCoverImage(string archivePath, string fileName, string outputDirectory, EncodeFormat format, CoverImageSize size = CoverImageSize.Default)
{ {
if (archivePath == null || !IsValidArchive(archivePath)) return string.Empty; if (archivePath == null || !IsValidArchive(archivePath)) return string.Empty;
try try
@ -221,7 +221,7 @@ public class ArchiveService : IArchiveService
var entry = archive.Entries.Single(e => e.FullName == entryName); var entry = archive.Entries.Single(e => e.FullName == entryName);
using var stream = entry.Open(); using var stream = entry.Open();
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format); return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size);
} }
case ArchiveLibrary.SharpCompress: case ArchiveLibrary.SharpCompress:
{ {
@ -232,7 +232,7 @@ public class ArchiveService : IArchiveService
var entry = archive.Entries.Single(e => e.Key == entryName); var entry = archive.Entries.Single(e => e.Key == entryName);
using var stream = entry.OpenEntryStream(); using var stream = entry.OpenEntryStream();
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format); return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, format, size);
} }
case ArchiveLibrary.NotSupported: case ArchiveLibrary.NotSupported:
_logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath); _logger.LogWarning("[GetCoverImage] This archive cannot be read: {ArchivePath}. Defaulting to no cover image", archivePath);
@ -246,7 +246,7 @@ public class ArchiveService : IArchiveService
{ {
_logger.LogWarning(ex, "[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath); _logger.LogWarning(ex, "[GetCoverImage] There was an exception when reading archive stream: {ArchivePath}. Defaulting to no cover image", archivePath);
_mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService, _mediaErrorService.ReportMediaIssue(archivePath, MediaErrorProducer.ArchiveService,
"This archive cannot be read or not supported", ex); "This archive cannot be read or not supported", ex); // TODO: Localize this
} }
return string.Empty; return string.Empty;

View File

@ -33,7 +33,7 @@ namespace API.Services;
public interface IBookService public interface IBookService
{ {
int GetNumberOfPages(string filePath); int GetNumberOfPages(string filePath);
string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat); string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
ComicInfo? GetComicInfo(string filePath); ComicInfo? GetComicInfo(string filePath);
ParserInfo? ParseInfo(string filePath); ParserInfo? ParseInfo(string filePath);
/// <summary> /// <summary>
@ -1196,13 +1196,13 @@ public class BookService : IBookService
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param> /// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
/// <param name="encodeFormat">When saving the file, use encoding</param> /// <param name="encodeFormat">When saving the file, use encoding</param>
/// <returns></returns> /// <returns></returns>
public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat) public string GetCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{ {
if (!IsValidFile(fileFilePath)) return string.Empty; if (!IsValidFile(fileFilePath)) return string.Empty;
if (Parser.IsPdf(fileFilePath)) if (Parser.IsPdf(fileFilePath))
{ {
return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, encodeFormat); return GetPdfCoverImage(fileFilePath, fileName, outputDirectory, encodeFormat, size);
} }
using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions); using var epubBook = EpubReader.OpenBook(fileFilePath, BookReaderOptions);
@ -1217,20 +1217,20 @@ public class BookService : IBookService
if (coverImageContent == null) return string.Empty; if (coverImageContent == null) return string.Empty;
using var stream = coverImageContent.GetContentStream(); using var stream = coverImageContent.GetContentStream();
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat); return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size);
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath); _logger.LogWarning(ex, "[BookService] There was a critical error and prevented thumbnail generation on {BookFile}. Defaulting to no cover image", fileFilePath);
_mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService, _mediaErrorService.ReportMediaIssue(fileFilePath, MediaErrorProducer.BookService,
"There was a critical error and prevented thumbnail generation", ex); "There was a critical error and prevented thumbnail generation", ex); // TODO: Localize this
} }
return string.Empty; return string.Empty;
} }
private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat) private string GetPdfCoverImage(string fileFilePath, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size)
{ {
try try
{ {
@ -1240,7 +1240,7 @@ public class BookService : IBookService
using var stream = StreamManager.GetStream("BookService.GetPdfPage"); using var stream = StreamManager.GetStream("BookService.GetPdfPage");
GetPdfPage(docReader, 0, stream); GetPdfPage(docReader, 0, stream);
return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat); return _imageService.WriteCoverThumbnail(stream, fileName, outputDirectory, encodeFormat, size);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -21,7 +21,7 @@ namespace API.Services;
public interface IImageService public interface IImageService
{ {
void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1); void ExtractImages(string fileFilePath, string targetDirectory, int fileCount = 1);
string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat); string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size);
/// <summary> /// <summary>
/// Creates a Thumbnail version of a base64 image /// Creates a Thumbnail version of a base64 image
@ -40,7 +40,7 @@ public interface IImageService
/// <param name="outputDirectory"></param> /// <param name="outputDirectory"></param>
/// <param name="encodeFormat"></param> /// <param name="encodeFormat"></param>
/// <returns></returns> /// <returns></returns>
string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat); string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
/// <summary> /// <summary>
/// Writes out a thumbnail by file path input /// Writes out a thumbnail by file path input
/// </summary> /// </summary>
@ -49,7 +49,7 @@ public interface IImageService
/// <param name="outputDirectory"></param> /// <param name="outputDirectory"></param>
/// <param name="encodeFormat"></param> /// <param name="encodeFormat"></param>
/// <returns></returns> /// <returns></returns>
string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat); string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
/// <summary> /// <summary>
/// Converts the passed image to encoding and outputs it in the same directory /// Converts the passed image to encoding and outputs it in the same directory
/// </summary> /// </summary>
@ -87,6 +87,7 @@ public class ImageService : IImageService
/// </summary> /// </summary>
public const int LibraryThumbnailWidth = 32; public const int LibraryThumbnailWidth = 32;
private static readonly string[] ValidIconRelations = { private static readonly string[] ValidIconRelations = {
"icon", "icon",
"apple-touch-icon", "apple-touch-icon",
@ -124,13 +125,14 @@ public class ImageService : IImageService
} }
} }
public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat) public string GetCoverImage(string path, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size)
{ {
if (string.IsNullOrEmpty(path)) return string.Empty; if (string.IsNullOrEmpty(path)) return string.Empty;
try try
{ {
using var thumbnail = Image.Thumbnail(path, ThumbnailWidth, height: ThumbnailHeight, size: Enums.Size.Force); var dims = size.GetDimensions();
using var thumbnail = Image.Thumbnail(path, dims.Width, height: dims.Height, size: Enums.Size.Force);
var filename = fileName + encodeFormat.GetExtension(); var filename = fileName + encodeFormat.GetExtension();
thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename)); thumbnail.WriteToFile(_directoryService.FileSystem.Path.Join(outputDirectory, filename));
return filename; return filename;
@ -152,9 +154,10 @@ public class ImageService : IImageService
/// <param name="outputDirectory">Where to output the file, defaults to covers directory</param> /// <param name="outputDirectory">Where to output the file, defaults to covers directory</param>
/// <param name="encodeFormat">Export the file as the passed encoding</param> /// <param name="encodeFormat">Export the file as the passed encoding</param>
/// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns> /// <returns>File name with extension of the file. This will always write to <see cref="DirectoryService.CoverImageDirectory"/></returns>
public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat) public string WriteCoverThumbnail(Stream stream, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{ {
using var thumbnail = Image.ThumbnailStream(stream, ThumbnailWidth, height: ThumbnailHeight, size: Enums.Size.Force); var dims = size.GetDimensions();
using var thumbnail = Image.ThumbnailStream(stream, dims.Width, height: dims.Height, size: Enums.Size.Force);
var filename = fileName + encodeFormat.GetExtension(); var filename = fileName + encodeFormat.GetExtension();
_directoryService.ExistOrCreate(outputDirectory); _directoryService.ExistOrCreate(outputDirectory);
try try
@ -165,9 +168,10 @@ public class ImageService : IImageService
return filename; return filename;
} }
public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat) public string WriteCoverThumbnail(string sourceFile, string fileName, string outputDirectory, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{ {
using var thumbnail = Image.Thumbnail(sourceFile, ThumbnailWidth, height: ThumbnailHeight, size: Enums.Size.Force); var dims = size.GetDimensions();
using var thumbnail = Image.Thumbnail(sourceFile, dims.Width, height: dims.Height, size: Enums.Size.Force);
var filename = fileName + encodeFormat.GetExtension(); var filename = fileName + encodeFormat.GetExtension();
_directoryService.ExistOrCreate(outputDirectory); _directoryService.ExistOrCreate(outputDirectory);
try try
@ -420,27 +424,58 @@ public class ImageService : IImageService
} }
public static string CreateMergedImage(IList<string> coverImages, string dest) public static void CreateMergedImage(IList<string> coverImages, CoverImageSize size, string dest)
{ {
var image = Image.Black(ThumbnailWidth, ThumbnailHeight); // 320x455 var dims = size.GetDimensions();
int rows, cols;
var thumbnailWidth = image.Width / 2; if (coverImages.Count == 1)
var thumbnailHeight = image.Height / 2; {
rows = 1;
cols = 1;
}
else if (coverImages.Count == 2)
{
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;
var thumbnailHeight = image.Height / rows;
for (var i = 0; i < coverImages.Count; i++) for (var i = 0; i < coverImages.Count; i++)
{ {
var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential); var tile = Image.NewFromFile(coverImages[i], access: Enums.Access.Sequential);
// Resize the tile to fit the thumbnail size
tile = tile.ThumbnailImage(thumbnailWidth, height: thumbnailHeight); tile = tile.ThumbnailImage(thumbnailWidth, height: thumbnailHeight);
var x = (i % 2) * thumbnailWidth; var row = i / cols;
var y = (i / 2) * thumbnailHeight; var col = i % cols;
var x = col * thumbnailWidth;
var y = row * thumbnailHeight;
if (coverImages.Count == 3 && i == 2)
{
x = (image.Width - thumbnailWidth) / 2;
y = thumbnailHeight;
}
image = image.Insert(tile, x, y); image = image.Insert(tile, x, y);
} }
image.WriteToFile(dest); image.WriteToFile(dest);
return dest;
} }
} }

View File

@ -37,6 +37,7 @@ public class MediaErrorService : IMediaErrorService
public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex) public void ReportMediaIssue(string filename, MediaErrorProducer producer, string errorMessage, Exception ex)
{ {
// TODO: Localize all these messages
// To avoid overhead on commits, do async. We don't need to wait. // To avoid overhead on commits, do async. We don't need to wait.
BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message)); BackgroundJob.Enqueue(() => ReportMediaIssueAsync(filename, producer, errorMessage, ex.Message));
} }

View File

@ -33,7 +33,7 @@ public interface IMetadataService
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param> /// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true); Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true);
Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, bool forceUpdate = false); Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false);
Task RemoveAbandonedMetadataKeys(); Task RemoveAbandonedMetadataKeys();
} }
@ -65,7 +65,7 @@ public class MetadataService : IMetadataService
/// <param name="chapter"></param> /// <param name="chapter"></param>
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param> /// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
/// <param name="encodeFormat">Convert image to Encoding Format when extracting the cover</param> /// <param name="encodeFormat">Convert image to Encoding Format when extracting the cover</param>
private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat) private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize)
{ {
var firstFile = chapter.Files.MinBy(x => x.Chapter); var firstFile = chapter.Files.MinBy(x => x.Chapter);
if (firstFile == null) return Task.FromResult(false); if (firstFile == null) return Task.FromResult(false);
@ -79,7 +79,7 @@ public class MetadataService : IMetadataService
_logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); _logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath);
chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath, chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath,
ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat); ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format, encodeFormat, coverImageSize);
_unitOfWork.ChapterRepository.Update(chapter); _unitOfWork.ChapterRepository.Update(chapter);
_updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter)); _updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter));
return Task.FromResult(true); return Task.FromResult(true);
@ -143,7 +143,7 @@ public class MetadataService : IMetadataService
/// <param name="series"></param> /// <param name="series"></param>
/// <param name="forceUpdate"></param> /// <param name="forceUpdate"></param>
/// <param name="encodeFormat"></param> /// <param name="encodeFormat"></param>
private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat) private async Task ProcessSeriesCoverGen(Series series, bool forceUpdate, EncodeFormat encodeFormat, CoverImageSize coverImageSize)
{ {
_logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName); _logger.LogDebug("[MetadataService] Processing cover image generation for series: {SeriesName}", series.OriginalName);
try try
@ -156,7 +156,7 @@ public class MetadataService : IMetadataService
var index = 0; var index = 0;
foreach (var chapter in volume.Chapters) foreach (var chapter in volume.Chapters)
{ {
var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat); var chapterUpdated = await UpdateChapterCoverImage(chapter, forceUpdate, encodeFormat, coverImageSize);
// If cover was update, either the file has changed or first scan and we should force a metadata update // If cover was update, either the file has changed or first scan and we should force a metadata update
UpdateChapterLastModified(chapter, forceUpdate || chapterUpdated); UpdateChapterLastModified(chapter, forceUpdate || chapterUpdated);
if (index == 0 && chapterUpdated) if (index == 0 && chapterUpdated)
@ -208,7 +208,9 @@ public class MetadataService : IMetadataService
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}")); MessageFactory.CoverUpdateProgressEvent(library.Id, 0F, ProgressEventType.Started, $"Starting {library.Name}"));
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
var encodeFormat = settings.EncodeMediaAs;
var coverImageSize = settings.CoverImageSize;
for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++) for (var chunk = 1; chunk <= chunkInfo.TotalChunks; chunk++)
{ {
@ -238,7 +240,7 @@ public class MetadataService : IMetadataService
try try
{ {
await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat); await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -288,8 +290,10 @@ public class MetadataService : IMetadataService
return; return;
} }
var encodeFormat = (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs; var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
await GenerateCoversForSeries(series, encodeFormat, forceUpdate); var encodeFormat = settings.EncodeMediaAs;
var coverImageSize = settings.CoverImageSize;
await GenerateCoversForSeries(series, encodeFormat, coverImageSize, forceUpdate);
} }
/// <summary> /// <summary>
@ -298,13 +302,13 @@ public class MetadataService : IMetadataService
/// <param name="series">A full Series, with metadata, chapters, etc</param> /// <param name="series">A full Series, with metadata, chapters, etc</param>
/// <param name="encodeFormat">When saving the file, what encoding should be used</param> /// <param name="encodeFormat">When saving the file, what encoding should be used</param>
/// <param name="forceUpdate"></param> /// <param name="forceUpdate"></param>
public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, bool forceUpdate = false) public async Task GenerateCoversForSeries(Series series, EncodeFormat encodeFormat, CoverImageSize coverImageSize, bool forceUpdate = false)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress, await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name)); MessageFactory.CoverUpdateProgressEvent(series.LibraryId, 0F, ProgressEventType.Started, series.Name));
await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat); await ProcessSeriesCoverGen(series, forceUpdate, encodeFormat, coverImageSize);
if (_unitOfWork.HasChanges()) if (_unitOfWork.HasChanges())

View File

@ -10,7 +10,7 @@ public interface IReadingItemService
{ {
ComicInfo? GetComicInfo(string filePath); ComicInfo? GetComicInfo(string filePath);
int GetNumberOfPages(string filePath, MangaFormat format); int GetNumberOfPages(string filePath, MangaFormat format);
string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat); string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default);
void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1); void Extract(string fileFilePath, string targetDirectory, MangaFormat format, int imageCount = 1);
ParserInfo? ParseFile(string path, string rootPath, LibraryType type); ParserInfo? ParseFile(string path, string rootPath, LibraryType type);
} }
@ -162,7 +162,7 @@ public class ReadingItemService : IReadingItemService
} }
} }
public string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat) public string GetCoverImage(string filePath, string fileName, MangaFormat format, EncodeFormat encodeFormat, CoverImageSize size = CoverImageSize.Default)
{ {
if (string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(fileName)) if (string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(fileName))
{ {
@ -172,10 +172,10 @@ public class ReadingItemService : IReadingItemService
return format switch return format switch
{ {
MangaFormat.Epub => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat), MangaFormat.Epub => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat, size),
MangaFormat.Archive => _archiveService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat), MangaFormat.Archive => _archiveService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat, size),
MangaFormat.Image => _imageService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat), MangaFormat.Image => _imageService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat, size),
MangaFormat.Pdf => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat), MangaFormat.Pdf => _bookService.GetCoverImage(filePath, fileName, _directoryService.CoverImageDirectory, encodeFormat, size),
_ => string.Empty _ => string.Empty
}; };
} }

View File

@ -231,7 +231,8 @@ public class ProcessSeries : IProcessSeries
_logger.LogError(ex, "[ScannerService] There was an exception updating series for {SeriesName}", series.Name); _logger.LogError(ex, "[ScannerService] There was an exception updating series for {SeriesName}", series.Name);
} }
await _metadataService.GenerateCoversForSeries(series, (await _unitOfWork.SettingsRepository.GetSettingsDtoAsync()).EncodeMediaAs); var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync();
await _metadataService.GenerateCoversForSeries(series, settings.EncodeMediaAs, settings.CoverImageSize);
EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id); EnqueuePostSeriesProcessTasks(series.LibraryId, series.Id);
} }

View File

@ -0,0 +1,14 @@
export enum CoverImageSize {
Default = 1,
Medium = 2,
Large = 3,
XLarge = 4
}
export const CoverImageSizes =
[
{value: CoverImageSize.Default, title: 'cover-image-size.default'},
{value: CoverImageSize.Medium, title: 'cover-image-size.medium'},
{value: CoverImageSize.Large, title: 'cover-image-size.large'},
{value: CoverImageSize.XLarge, title: 'cover-image-size.xlarge'}
];

View File

@ -1,4 +1,5 @@
import { EncodeFormat } from "./encode-format"; import { EncodeFormat } from "./encode-format";
import {CoverImageSize} from "./cover-image-size";
export interface ServerSettings { export interface ServerSettings {
cacheDirectory: string; cacheDirectory: string;
@ -20,4 +21,5 @@ export interface ServerSettings {
cacheSize: number; cacheSize: number;
onDeckProgressDays: number; onDeckProgressDays: number;
onDeckUpdateDays: number; onDeckUpdateDays: number;
coverImageSize: CoverImageSize;
} }

View File

@ -7,7 +7,7 @@
<br/><b>{{t('encode-as-warning')}}</b> <br/><b>{{t('encode-as-warning')}}</b>
</p> </p>
<div *ngIf="settingsForm.get('encodeMediaAs')?.dirty" class="alert alert-danger" role="alert">{{t('media-warning')}}</div> <div *ngIf="settingsForm.get('encodeMediaAs')?.dirty" class="alert alert-danger" role="alert">{{t('media-warning')}}</div>
<div class="col-md-6 col-sm-12 mb-3"> <div class="col-md-6 col-sm-12 mb-3 pe-1">
<label for="settings-media-encodeMediaAs" class="form-label me-1">{{t('encode-as-label')}}</label> <label for="settings-media-encodeMediaAs" class="form-label me-1">{{t('encode-as-label')}}</label>
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="encodeMediaAsTooltip" role="button" tabindex="0"></i> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="encodeMediaAsTooltip" role="button" tabindex="0"></i>
<ng-template #encodeMediaAsTooltip>{{t('encode-as-tooltip')}}</ng-template> <ng-template #encodeMediaAsTooltip>{{t('encode-as-tooltip')}}</ng-template>
@ -16,6 +16,16 @@
<option *ngFor="let format of EncodeFormats" [value]="format.value">{{format.title}}</option> <option *ngFor="let format of EncodeFormats" [value]="format.value">{{format.title}}</option>
</select> </select>
</div> </div>
<div class="col-md-6 col-sm-12 mb-3">
<label for="settings-media-coverImageSize" class="form-label me-1">{{t('cover-image-size-label')}}</label>
<i class="fa fa-info-circle" placement="right" [ngbTooltip]="coverImageSizeTooltip" role="button" tabindex="0"></i>
<ng-template #coverImageSizeTooltip>{{t('cover-image-size-tooltip')}}</ng-template>
<span class="visually-hidden" id="settings-media-coverImageSize-help"><ng-container [ngTemplateOutlet]="coverImageSizeTooltip"></ng-container></span>
<select class="form-select" aria-describedby="settings-media-coverImageSize-help" formControlName="coverImageSize" id="settings-media-coverImageSize">
<option *ngFor="let size of coverImageSizes" [value]="size.value">{{size.title}}</option>
</select>
</div>
</div> </div>
<div class="row g-0"> <div class="row g-0">

View File

@ -21,7 +21,8 @@ import {EncodeFormats} from '../_models/encode-format';
import {ManageScrobbleErrorsComponent} from '../manage-scrobble-errors/manage-scrobble-errors.component'; import {ManageScrobbleErrorsComponent} from '../manage-scrobble-errors/manage-scrobble-errors.component';
import {ManageAlertsComponent} from '../manage-alerts/manage-alerts.component'; import {ManageAlertsComponent} from '../manage-alerts/manage-alerts.component';
import {NgFor, NgIf, NgTemplateOutlet} from '@angular/common'; import {NgFor, NgIf, NgTemplateOutlet} from '@angular/common';
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import { CoverImageSizes } from '../_models/cover-image-size';
@Component({ @Component({
selector: 'app-manage-media-settings', selector: 'app-manage-media-settings',
@ -38,6 +39,11 @@ export class ManageMediaSettingsComponent implements OnInit {
alertCount: number = 0; alertCount: number = 0;
scrobbleCount: number = 0; scrobbleCount: number = 0;
coverImageSizes = CoverImageSizes.map(o => {
const newObj = {...o};
newObj.title = translate(o.title);
return newObj;
})
private readonly translocoService = inject(TranslocoService); private readonly translocoService = inject(TranslocoService);
private readonly cdRef = inject(ChangeDetectorRef); private readonly cdRef = inject(ChangeDetectorRef);
@ -51,6 +57,7 @@ export class ManageMediaSettingsComponent implements OnInit {
this.serverSettings = settings; this.serverSettings = settings;
this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, [Validators.required])); this.settingsForm.addControl('encodeMediaAs', new FormControl(this.serverSettings.encodeMediaAs, [Validators.required]));
this.settingsForm.addControl('bookmarksDirectory', new FormControl(this.serverSettings.bookmarksDirectory, [Validators.required])); this.settingsForm.addControl('bookmarksDirectory', new FormControl(this.serverSettings.bookmarksDirectory, [Validators.required]));
this.settingsForm.addControl('coverImageSize', new FormControl(this.serverSettings.coverImageSize, [Validators.required]));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} }
@ -58,6 +65,7 @@ export class ManageMediaSettingsComponent implements OnInit {
resetForm() { resetForm() {
this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs); this.settingsForm.get('encodeMediaAs')?.setValue(this.serverSettings.encodeMediaAs);
this.settingsForm.get('bookmarksDirectory')?.setValue(this.serverSettings.bookmarksDirectory); this.settingsForm.get('bookmarksDirectory')?.setValue(this.serverSettings.bookmarksDirectory);
this.settingsForm.get('coverImageSize')?.setValue(this.serverSettings.coverImageSize);
this.settingsForm.markAsPristine(); this.settingsForm.markAsPristine();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
} }
@ -66,6 +74,7 @@ export class ManageMediaSettingsComponent implements OnInit {
const modelSettings = Object.assign({}, this.serverSettings); const modelSettings = Object.assign({}, this.serverSettings);
modelSettings.encodeMediaAs = parseInt(this.settingsForm.get('encodeMediaAs')?.value, 10); modelSettings.encodeMediaAs = parseInt(this.settingsForm.get('encodeMediaAs')?.value, 10);
modelSettings.bookmarksDirectory = this.settingsForm.get('bookmarksDirectory')?.value; modelSettings.bookmarksDirectory = this.settingsForm.get('bookmarksDirectory')?.value;
modelSettings.coverImageSize = parseInt(this.settingsForm.get('coverImageSize')?.value, 10);
this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => { this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe(async (settings: ServerSettings) => {
this.serverSettings = settings; this.serverSettings = settings;

View File

@ -11,7 +11,9 @@
[trackByIdentity]="trackByIdentity" [trackByIdentity]="trackByIdentity"
> >
<ng-template #cardItem let-item let-position="idx"> <ng-template #cardItem let-item let-position="idx">
<app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions" [imageUrl]="imageSerivce.getCollectionCoverImage(item.id)" (clicked)="loadCollection(item)"></app-card-item> <app-card-item [title]="item.title" [entity]="item" [actions]="collectionTagActions"
[imageUrl]="imageSerivce.getCollectionCoverImage(item.id)"
(clicked)="loadCollection(item)"></app-card-item>
</ng-template> </ng-template>
<ng-template #noData> <ng-template #noData>

View File

@ -11,9 +11,9 @@
</div> </div>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="collectionTag !== undefined" #scrollingBlock> <div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="collectionTag !== undefined" #scrollingBlock>
<div class="row mb-3" *ngIf="(collectionTag.coverImage !== '' && collectionTag.coverImage !== undefined && collectionTag.coverImage !== null) || summary.length > 0"> <div class="row mb-3" *ngIf="summary.length > 0">
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block" *ngIf="collectionTag.coverImage !== '' && collectionTag.coverImage !== undefined && collectionTag.coverImage !== null"> <div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image maxWidth="481px" [imageUrl]="tagImage"></app-image> <app-image maxWidth="481px" [imageUrl]="imageService.getCollectionCoverImage(collectionTag.id)"></app-image>
</div> </div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2"> <div class="col-md-10 col-xs-8 col-sm-6 mt-2">
<app-read-more [text]="summary" [maxLength]="250"></app-read-more> <app-read-more [text]="summary" [maxLength]="250"></app-read-more>

View File

@ -70,7 +70,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
translocoService = inject(TranslocoService); translocoService = inject(TranslocoService);
collectionTag!: CollectionTag; collectionTag!: CollectionTag;
tagImage: string = '';
isLoading: boolean = true; isLoading: boolean = true;
series: Array<Series> = []; series: Array<Series> = [];
pagination!: Pagination; pagination!: Pagination;
@ -229,9 +228,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
this.collectionTag = matchingTags[0]; this.collectionTag = matchingTags[0];
this.summary = (this.collectionTag.summary === null ? '' : this.collectionTag.summary).replace(/\n/g, '<br>'); this.summary = (this.collectionTag.summary === null ? '' : this.collectionTag.summary).replace(/\n/g, '<br>');
// TODO: This can be changed now that we have app-image and collection cover merge code (can it? Because we still have the case where there is no image)
// I can always tweak merge to allow blank slots and if just one item, just show that item image
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(this.collectionTag.id));
this.titleService.setTitle(this.translocoService.translate('collection-detail.title-alt', {collectionName: this.collectionTag.title})); this.titleService.setTitle(this.translocoService.translate('collection-detail.title-alt', {collectionName: this.collectionTag.title}));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
@ -285,11 +281,6 @@ export class CollectionDetailComponent implements OnInit, AfterContentChecked {
modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => { modalRef.closed.subscribe((results: {success: boolean, coverImageUpdated: boolean}) => {
this.updateTag(this.collectionTag.id); this.updateTag(this.collectionTag.id);
this.loadPage(); this.loadPage();
if (results.coverImageUpdated) {
this.tagImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id));
this.collectionTag.coverImage = this.imageService.randomize(this.imageService.getCollectionCoverImage(collectionTag.id));
this.cdRef.markForCheck();
}
}); });
} }

View File

@ -38,8 +38,8 @@
<div class="container-fluid mt-2" *ngIf="readingList" > <div class="container-fluid mt-2" *ngIf="readingList" >
<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" *ngIf="readingList.coverImage !== '' && readingList.coverImage !== undefined && readingList.coverImage !== null"> <div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
<app-image maxWidth="300px" maxHeight="400px" [imageUrl]="readingListImage"></app-image> <app-image maxWidth="300px" maxHeight="400px" [imageUrl]="imageService.getReadingListCoverImage(readingList.id)"></app-image>
</div> </div>
<div class="col-md-10 col-xs-8 col-sm-6 mt-2"> <div class="col-md-10 col-xs-8 col-sm-6 mt-2">
<div class="row g-0 mb-3"> <div class="row g-0 mb-3">

View File

@ -62,7 +62,6 @@ export class ReadingListDetailComponent implements OnInit {
downloadInProgress: boolean = false; downloadInProgress: boolean = false;
readingListSummary: string = ''; readingListSummary: string = '';
readingListImage: string = '';
libraryTypes: {[key: number]: LibraryType} = {}; libraryTypes: {[key: number]: LibraryType} = {};
characters$!: Observable<Person[]>; characters$!: Observable<Person[]>;
@ -90,7 +89,6 @@ 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.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
forkJoin([ forkJoin([
this.libraryService.getLibraries(), this.libraryService.getLibraries(),
@ -162,7 +160,6 @@ export class ReadingListDetailComponent implements OnInit {
this.readingListService.getReadingList(this.listId).subscribe(rl => { this.readingListService.getReadingList(this.listId).subscribe(rl => {
this.readingList = rl; this.readingList = rl;
this.readingListSummary = (this.readingList.summary === null ? '' : this.readingList.summary).replace(/\n/g, '<br>'); this.readingListSummary = (this.readingList.summary === null ? '' : this.readingList.summary).replace(/\n/g, '<br>');
this.readingListImage = this.imageService.randomize(this.imageService.getReadingListCoverImage(this.listId));
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}) })
}); });

View File

@ -2,7 +2,7 @@
<app-side-nav-companion-bar> <app-side-nav-companion-bar>
<h2 title> <h2 title>
<app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables> <app-card-actionables [actions]="globalActions" (actionHandler)="performGlobalAction($event)"></app-card-actionables>
<span>Reading Lists</span> <span>{{t('title')}}</span>
</h2> </h2>
<h6 subtitle class="subtitle-with-actionables" *ngIf="pagination">{{t('item-count', {num: pagination.totalItems | number})}}</h6> <h6 subtitle class="subtitle-with-actionables" *ngIf="pagination">{{t('item-count', {num: pagination.totalItems | number})}}</h6>
</app-side-nav-companion-bar> </app-side-nav-companion-bar>
@ -13,6 +13,7 @@
[pagination]="pagination" [pagination]="pagination"
[jumpBarKeys]="jumpbarKeys" [jumpBarKeys]="jumpbarKeys"
[filteringDisabled]="true" [filteringDisabled]="true"
[trackByIdentity]="trackByIdentity"
> >
<ng-template #cardItem let-item let-position="idx" > <ng-template #cardItem let-item let-position="idx" >
<app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]" <app-card-item [title]="item.title" [entity]="item" [actions]="actions[item.id]"

View File

@ -19,6 +19,7 @@ import { NgIf, DecimalPipe } from '@angular/common';
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component'; import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {TranslocoDirective, TranslocoService} from "@ngneat/transloco"; import {TranslocoDirective, TranslocoService} from "@ngneat/transloco";
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component"; import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {CollectionTag} from "../../../_models/collection-tag";
@Component({ @Component({
selector: 'app-reading-lists', selector: 'app-reading-lists',
@ -37,6 +38,7 @@ export class ReadingListsComponent implements OnInit {
jumpbarKeys: Array<JumpKey> = []; jumpbarKeys: Array<JumpKey> = [];
actions: {[key: number]: Array<ActionItem<ReadingList>>} = {}; actions: {[key: number]: Array<ActionItem<ReadingList>>} = {};
globalActions: Array<ActionItem<any>> = [{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}]; globalActions: Array<ActionItem<any>> = [{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
trackByIdentity = (index: number, item: ReadingList) => `${item.id}_${item.title}`;
translocoService = inject(TranslocoService); translocoService = inject(TranslocoService);
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService, constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,

View File

@ -1069,7 +1069,16 @@
"save": "{{common.save}}", "save": "{{common.save}}",
"media-issue-title": "Media Issues", "media-issue-title": "Media Issues",
"scrobble-issue-title": "Scrobble Issues" "scrobble-issue-title": "Scrobble Issues",
"cover-image-size-label": "Cover Image Size",
"cover-image-size-tooltip": "How large should cover images generate as. Note: Anything larger than the default will incur longer page load times."
},
"cover-image-size": {
"default": "Default (320x455)",
"medium": "Medium (640x909)",
"large": "Large (900x1277)",
"xlarge": "Extra Large (1265x1795)"
}, },
"manage-scrobble-errors": { "manage-scrobble-errors": {

View File

@ -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.7.7.7" "version": "0.7.7.9"
}, },
"servers": [ "servers": [
{ {
@ -16658,6 +16658,17 @@
"type": "integer", "type": "integer",
"description": "How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically", "description": "How many Days since today in the past for chapter updates, should content be considered for On Deck, before it gets removed automatically",
"format": "int32" "format": "int32"
},
"coverImageSize": {
"enum": [
1,
2,
3,
4
],
"type": "integer",
"description": "How large the cover images should be",
"format": "int32"
} }
}, },
"additionalProperties": false "additionalProperties": false