mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Release Shakeout 3 (#1597)
* Fixed a bug where bulk selection on series detail wouldn't allow you to select the whole card, only the checkbox. * Refactored the implementation of MarkChaptersAsRead to streamline it. * Fixed a bug where volume cards weren't properly updating their read state based on events from backend. * Added [ScannerService] to more loggers * Fixed invite user flow * Fixed broken edit user flow * Fixed calling device service on unauthenticated screens causing redirection * Fixed reset password via email not working when success message was sent back * Fixed broken white theme on book reader * Small tweaks to white theme * More fixes * Adjusted AutomaticRetries
This commit is contained in:
parent
b396217e7d
commit
dbe1152d87
@ -329,7 +329,7 @@ public class ReadingListServiceTests
|
|||||||
Substitute.For<IEventHub>());
|
Substitute.For<IEventHub>());
|
||||||
// Mark 2 as fully read
|
// Mark 2 as fully read
|
||||||
await readerService.MarkChaptersAsRead(user, 1,
|
await readerService.MarkChaptersAsRead(user, 1,
|
||||||
await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(new List<int>() {2}));
|
(await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(new List<int>() {2})).ToList());
|
||||||
await _unitOfWork.CommitAsync();
|
await _unitOfWork.CommitAsync();
|
||||||
|
|
||||||
await _readingListService.RemoveFullyReadItems(1, user);
|
await _readingListService.RemoveFullyReadItems(1, user);
|
||||||
|
@ -13,6 +13,7 @@ using API.Entities.Enums;
|
|||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
using API.Services;
|
using API.Services;
|
||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
|
using API.SignalR;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -32,12 +33,13 @@ public class ReaderController : BaseApiController
|
|||||||
private readonly IReaderService _readerService;
|
private readonly IReaderService _readerService;
|
||||||
private readonly IBookmarkService _bookmarkService;
|
private readonly IBookmarkService _bookmarkService;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
|
private readonly IEventHub _eventHub;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ReaderController(ICacheService cacheService,
|
public ReaderController(ICacheService cacheService,
|
||||||
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
IUnitOfWork unitOfWork, ILogger<ReaderController> logger,
|
||||||
IReaderService readerService, IBookmarkService bookmarkService,
|
IReaderService readerService, IBookmarkService bookmarkService,
|
||||||
IAccountService accountService)
|
IAccountService accountService, IEventHub eventHub)
|
||||||
{
|
{
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
@ -45,6 +47,7 @@ public class ReaderController : BaseApiController
|
|||||||
_readerService = readerService;
|
_readerService = readerService;
|
||||||
_bookmarkService = bookmarkService;
|
_bookmarkService = bookmarkService;
|
||||||
_accountService = accountService;
|
_accountService = accountService;
|
||||||
|
_eventHub = eventHub;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -273,8 +276,6 @@ public class ReaderController : BaseApiController
|
|||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||||
await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters);
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
@ -295,8 +296,9 @@ public class ReaderController : BaseApiController
|
|||||||
|
|
||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId);
|
||||||
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters);
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||||
_unitOfWork.UserRepository.Update(user);
|
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markVolumeReadDto.SeriesId,
|
||||||
|
markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages)));
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
@ -324,15 +326,14 @@ public class ReaderController : BaseApiController
|
|||||||
chapterIds.Add(chapterId);
|
chapterIds.Add(chapterId);
|
||||||
}
|
}
|
||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||||
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters);
|
await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList());
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return BadRequest("Could not save progress");
|
return BadRequest("Could not save progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,9 +354,7 @@ public class ReaderController : BaseApiController
|
|||||||
chapterIds.Add(chapterId);
|
chapterIds.Add(chapterId);
|
||||||
}
|
}
|
||||||
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds);
|
||||||
await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters);
|
await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters.ToList());
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
@ -382,8 +381,6 @@ public class ReaderController : BaseApiController
|
|||||||
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
@ -409,8 +406,6 @@ public class ReaderController : BaseApiController
|
|||||||
await _readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
|
await _readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
|
|
||||||
if (await _unitOfWork.CommitAsync())
|
if (await _unitOfWork.CommitAsync())
|
||||||
{
|
{
|
||||||
return Ok();
|
return Ok();
|
||||||
|
@ -21,8 +21,8 @@ public interface IReaderService
|
|||||||
{
|
{
|
||||||
Task MarkSeriesAsRead(AppUser user, int seriesId);
|
Task MarkSeriesAsRead(AppUser user, int seriesId);
|
||||||
Task MarkSeriesAsUnread(AppUser user, int seriesId);
|
Task MarkSeriesAsUnread(AppUser user, int seriesId);
|
||||||
Task MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
Task MarkChaptersAsRead(AppUser user, int seriesId, IList<Chapter> chapters);
|
||||||
Task MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters);
|
Task MarkChaptersAsUnread(AppUser user, int seriesId, IList<Chapter> chapters);
|
||||||
Task<bool> SaveReadingProgress(ProgressDto progressDto, int userId);
|
Task<bool> SaveReadingProgress(ProgressDto progressDto, int userId);
|
||||||
Task<int> CapPageToChapter(int chapterId, int page);
|
Task<int> CapPageToChapter(int chapterId, int page);
|
||||||
int CapPageToChapter(Chapter chapter, int page);
|
int CapPageToChapter(Chapter chapter, int page);
|
||||||
@ -76,8 +76,6 @@ public class ReaderService : IReaderService
|
|||||||
{
|
{
|
||||||
await MarkChaptersAsRead(user, seriesId, volume.Chapters);
|
await MarkChaptersAsRead(user, seriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -93,18 +91,18 @@ public class ReaderService : IReaderService
|
|||||||
{
|
{
|
||||||
await MarkChaptersAsUnread(user, seriesId, volume.Chapters);
|
await MarkChaptersAsUnread(user, seriesId, volume.Chapters);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unitOfWork.UserRepository.Update(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks all Chapters as Read by creating or updating UserProgress rows. Does not commit.
|
/// Marks all Chapters as Read by creating or updating UserProgress rows. Does not commit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>Emits events to the UI for each chapter progress and one for each volume progress</remarks>
|
||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <param name="chapters"></param>
|
/// <param name="chapters"></param>
|
||||||
public async Task MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
|
public async Task MarkChaptersAsRead(AppUser user, int seriesId, IList<Chapter> chapters)
|
||||||
{
|
{
|
||||||
|
var seenVolume = new Dictionary<int, bool>();
|
||||||
foreach (var chapter in chapters)
|
foreach (var chapter in chapters)
|
||||||
{
|
{
|
||||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||||
@ -118,19 +116,29 @@ public class ReaderService : IReaderService
|
|||||||
SeriesId = seriesId,
|
SeriesId = seriesId,
|
||||||
ChapterId = chapter.Id
|
ChapterId = chapter.Id
|
||||||
});
|
});
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
|
||||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages));
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
userProgress.PagesRead = chapter.Pages;
|
userProgress.PagesRead = chapter.Pages;
|
||||||
userProgress.SeriesId = seriesId;
|
userProgress.SeriesId = seriesId;
|
||||||
userProgress.VolumeId = chapter.VolumeId;
|
userProgress.VolumeId = chapter.VolumeId;
|
||||||
|
}
|
||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, chapter.Pages));
|
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages));
|
||||||
|
|
||||||
|
// Send out volume events for each distinct volume
|
||||||
|
if (!seenVolume.ContainsKey(chapter.VolumeId))
|
||||||
|
{
|
||||||
|
seenVolume[chapter.VolumeId] = true;
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||||
|
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId,
|
||||||
|
chapter.VolumeId, 0, chapters.Where(c => c.VolumeId == chapter.VolumeId).Sum(c => c.Pages)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -139,8 +147,9 @@ public class ReaderService : IReaderService
|
|||||||
/// <param name="user"></param>
|
/// <param name="user"></param>
|
||||||
/// <param name="seriesId"></param>
|
/// <param name="seriesId"></param>
|
||||||
/// <param name="chapters"></param>
|
/// <param name="chapters"></param>
|
||||||
public async Task MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable<Chapter> chapters)
|
public async Task MarkChaptersAsUnread(AppUser user, int seriesId, IList<Chapter> chapters)
|
||||||
{
|
{
|
||||||
|
var seenVolume = new Dictionary<int, bool>();
|
||||||
foreach (var chapter in chapters)
|
foreach (var chapter in chapters)
|
||||||
{
|
{
|
||||||
var userProgress = GetUserProgressForChapter(user, chapter);
|
var userProgress = GetUserProgressForChapter(user, chapter);
|
||||||
@ -153,8 +162,18 @@ public class ReaderService : IReaderService
|
|||||||
|
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||||
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, 0));
|
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, 0));
|
||||||
|
|
||||||
|
// Send out volume events for each distinct volume
|
||||||
|
if (!seenVolume.ContainsKey(chapter.VolumeId))
|
||||||
|
{
|
||||||
|
seenVolume[chapter.VolumeId] = true;
|
||||||
|
await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate,
|
||||||
|
MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId,
|
||||||
|
chapter.VolumeId, 0, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_unitOfWork.UserRepository.Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the User Progress for a given Chapter. This will handle any duplicates that might have occured in past versions and will delete them. Does not commit.
|
/// Gets the User Progress for a given Chapter. This will handle any duplicates that might have occured in past versions and will delete them. Does not commit.
|
||||||
@ -526,7 +545,7 @@ public class ReaderService : IReaderService
|
|||||||
var chapters = volume.Chapters
|
var chapters = volume.Chapters
|
||||||
.OrderBy(c => float.Parse(c.Number))
|
.OrderBy(c => float.Parse(c.Number))
|
||||||
.Where(c => !c.IsSpecial && Tasks.Scanner.Parser.Parser.MaxNumberFromRange(c.Range) <= chapterNumber);
|
.Where(c => !c.IsSpecial && Tasks.Scanner.Parser.Parser.MaxNumberFromRange(c.Range) <= chapterNumber);
|
||||||
await MarkChaptersAsRead(user, volume.SeriesId, chapters);
|
await MarkChaptersAsRead(user, volume.SeriesId, chapters.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ public class ParseScannedFiles
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex,
|
_logger.LogError(ex,
|
||||||
"There was an error trying to find and apply .kavitaignores above the Series Folder. Scanning without them present");
|
"[ScannerService] There was an error trying to find and apply .kavitaignores above the Series Folder. Scanning without them present");
|
||||||
}
|
}
|
||||||
|
|
||||||
return seriesMatcher;
|
return seriesMatcher;
|
||||||
@ -200,13 +200,13 @@ public class ParseScannedFiles
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogCritical(ex, "{SeriesName} matches against multiple series in the parsed series. This indicates a critical kavita issue. Key will be skipped", info.Series);
|
_logger.LogCritical(ex, "[ScannerService] {SeriesName} matches against multiple series in the parsed series. This indicates a critical kavita issue. Key will be skipped", info.Series);
|
||||||
foreach (var seriesKey in scannedSeries.Keys.Where(ps =>
|
foreach (var seriesKey in scannedSeries.Keys.Where(ps =>
|
||||||
ps.Format == info.Format && (ps.NormalizedName.Equals(normalizedSeries)
|
ps.Format == info.Format && (ps.NormalizedName.Equals(normalizedSeries)
|
||||||
|| ps.NormalizedName.Equals(normalizedLocalizedSeries)
|
|| ps.NormalizedName.Equals(normalizedLocalizedSeries)
|
||||||
|| ps.NormalizedName.Equals(normalizedSortSeries))))
|
|| ps.NormalizedName.Equals(normalizedSortSeries))))
|
||||||
{
|
{
|
||||||
_logger.LogCritical("Matches: {SeriesName} matches on {SeriesKey}", info.Series, seriesKey.Name);
|
_logger.LogCritical("[ScannerService] Matches: {SeriesName} matches on {SeriesKey}", info.Series, seriesKey.Name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -240,14 +240,14 @@ public class ParseScannedFiles
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogCritical(ex, "Multiple series detected for {SeriesName} ({File})! This is critical to fix! There should only be 1", info.Series, info.FullFilePath);
|
_logger.LogCritical(ex, "[ScannerService] Multiple series detected for {SeriesName} ({File})! This is critical to fix! There should only be 1", info.Series, info.FullFilePath);
|
||||||
var values = scannedSeries.Where(p =>
|
var values = scannedSeries.Where(p =>
|
||||||
(Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries ||
|
(Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries ||
|
||||||
Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) &&
|
Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) &&
|
||||||
p.Key.Format == info.Format);
|
p.Key.Format == info.Format);
|
||||||
foreach (var pair in values)
|
foreach (var pair in values)
|
||||||
{
|
{
|
||||||
_logger.LogCritical("Duplicate Series in DB matches with {SeriesName}: {DuplicateName}", info.Series, pair.Key.Name);
|
_logger.LogCritical("[ScannerService] Duplicate Series in DB matches with {SeriesName}: {DuplicateName}", info.Series, pair.Key.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -285,11 +285,11 @@ public class ParseScannedFiles
|
|||||||
Format = fp.Format,
|
Format = fp.Format,
|
||||||
}).ToList();
|
}).ToList();
|
||||||
await processSeriesInfos.Invoke(new Tuple<bool, IList<ParserInfo>>(true, parsedInfos));
|
await processSeriesInfos.Invoke(new Tuple<bool, IList<ParserInfo>>(true, parsedInfos));
|
||||||
_logger.LogDebug("Skipped File Scan for {Folder} as it hasn't changed since last scan", folder);
|
_logger.LogDebug("[ScannerService] Skipped File Scan for {Folder} as it hasn't changed since last scan", folder);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Found {Count} files for {Folder}", files.Count, folder);
|
_logger.LogDebug("[ScannerService] Found {Count} files for {Folder}", files.Count, folder);
|
||||||
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
await _eventHub.SendMessageAsync(MessageFactory.NotificationProgress,
|
||||||
MessageFactory.FileScanProgressEvent(folder, libraryName, ProgressEventType.Updated));
|
MessageFactory.FileScanProgressEvent(folder, libraryName, ProgressEventType.Updated));
|
||||||
if (files.Count == 0)
|
if (files.Count == 0)
|
||||||
@ -316,7 +316,7 @@ public class ParseScannedFiles
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex,
|
_logger.LogError(ex,
|
||||||
"There was an exception that occurred during tracking {FilePath}. Skipping this file",
|
"[ScannerService] There was an exception that occurred during tracking {FilePath}. Skipping this file",
|
||||||
info.FullFilePath);
|
info.FullFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -339,7 +339,7 @@ public class ParseScannedFiles
|
|||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "The directory '{FolderPath}' does not exist", folderPath);
|
_logger.LogError(ex, "[ScannerService] The directory '{FolderPath}' does not exist", folderPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,12 +29,12 @@ public interface IScannerService
|
|||||||
/// <param name="forceUpdate">Don't perform optimization checks, defaults to false</param>
|
/// <param name="forceUpdate">Don't perform optimization checks, defaults to false</param>
|
||||||
[Queue(TaskScheduler.ScanQueue)]
|
[Queue(TaskScheduler.ScanQueue)]
|
||||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
Task ScanLibrary(int libraryId, bool forceUpdate = false);
|
Task ScanLibrary(int libraryId, bool forceUpdate = false);
|
||||||
|
|
||||||
[Queue(TaskScheduler.ScanQueue)]
|
[Queue(TaskScheduler.ScanQueue)]
|
||||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
Task ScanLibraries();
|
Task ScanLibraries();
|
||||||
|
|
||||||
[Queue(TaskScheduler.ScanQueue)]
|
[Queue(TaskScheduler.ScanQueue)]
|
||||||
@ -407,7 +407,7 @@ public class ScannerService : IScannerService
|
|||||||
|
|
||||||
[Queue(TaskScheduler.ScanQueue)]
|
[Queue(TaskScheduler.ScanQueue)]
|
||||||
[DisableConcurrentExecution(60 * 60 * 60)]
|
[DisableConcurrentExecution(60 * 60 * 60)]
|
||||||
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
|
||||||
public async Task ScanLibraries()
|
public async Task ScanLibraries()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting Scan of All Libraries");
|
_logger.LogInformation("Starting Scan of All Libraries");
|
||||||
|
@ -192,7 +192,7 @@ export class AccountService implements OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirmResetPasswordEmail(model: {email: string, token: string, password: string}) {
|
confirmResetPasswordEmail(model: {email: string, token: string, password: string}) {
|
||||||
return this.httpClient.post(this.baseUrl + 'account/confirm-password-reset', model, {responseType: 'json' as 'text'});
|
return this.httpClient.post<string>(this.baseUrl + 'account/confirm-password-reset', model, {responseType: 'text' as 'json'});
|
||||||
}
|
}
|
||||||
|
|
||||||
resetPassword(username: string, password: string, oldPassword: string) {
|
resetPassword(username: string, password: string, oldPassword: string) {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { ReplaySubject, shareReplay, switchMap, take, tap } from 'rxjs';
|
import { ReplaySubject, shareReplay, take, tap } from 'rxjs';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { Device } from '../_models/device/device';
|
import { Device } from '../_models/device/device';
|
||||||
import { DevicePlatform } from '../_models/device/device-platform';
|
import { DevicePlatform } from '../_models/device/device-platform';
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -16,10 +17,15 @@ export class DeviceService {
|
|||||||
public devices$ = this.devicesSource.asObservable().pipe(shareReplay());
|
public devices$ = this.devicesSource.asObservable().pipe(shareReplay());
|
||||||
|
|
||||||
|
|
||||||
constructor(private httpClient: HttpClient) {
|
constructor(private httpClient: HttpClient, private accountService: AccountService) {
|
||||||
|
// Ensure we are authenticated before we make an authenticated api call.
|
||||||
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
this.httpClient.get<Device[]>(this.baseUrl + 'device', {}).subscribe(data => {
|
this.httpClient.get<Device[]>(this.baseUrl + 'device', {}).subscribe(data => {
|
||||||
this.devicesSource.next(data);
|
this.devicesSource.next(data);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createDevice(name: string, platform: DevicePlatform, emailAddress: string) {
|
createDevice(name: string, platform: DevicePlatform, emailAddress: string) {
|
||||||
|
@ -34,6 +34,7 @@ export class EditUserComponent implements OnInit {
|
|||||||
this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required]));
|
this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required]));
|
||||||
|
|
||||||
this.userForm.get('email')?.disable();
|
this.userForm.get('email')?.disable();
|
||||||
|
this.selectedRestriction = this.member.ageRestriction;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRoleSelection(roles: Array<string>) {
|
updateRoleSelection(roles: Array<string>) {
|
||||||
|
@ -85,7 +85,6 @@ export const BookBlackTheme = `
|
|||||||
--br-actionbar-button-hover-border-color: #6c757d;
|
--br-actionbar-button-hover-border-color: #6c757d;
|
||||||
--br-actionbar-bg-color: black;
|
--br-actionbar-bg-color: black;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,15 +1,140 @@
|
|||||||
// Important note about themes. Must have one section with .reader-container that contains color, background-color and rest of the styles must be scoped to .book-content
|
// Important note about themes. Must have one section with .reader-container that contains color, background-color and rest of the styles must be scoped to .book-content
|
||||||
export const BookWhiteTheme = `
|
export const BookWhiteTheme = `
|
||||||
:root .brtheme-white {
|
:root .brtheme-white {
|
||||||
|
--drawer-text-color: white;
|
||||||
|
--br-actionbar-bg-color: white;
|
||||||
|
--bs-btn-active-color: black;
|
||||||
|
--progress-bg-color: rgb(222, 226, 230);
|
||||||
|
|
||||||
|
/* General */
|
||||||
|
--color-scheme: light;
|
||||||
|
--bs-body-color: black;
|
||||||
|
--hr-color: rgba(239, 239, 239, 0.125);
|
||||||
|
--accent-bg-color: rgba(1, 4, 9, 0.5);
|
||||||
|
--accent-text-color: lightgrey;
|
||||||
|
--body-text-color: black;
|
||||||
|
--btn-icon-filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
|
|
||||||
|
/* Drawer */
|
||||||
|
--drawer-bg-color: white;
|
||||||
|
--drawer-text-color: black;
|
||||||
|
--drawer-pagination-horizontal-rule: inset 0 -1px 0 rgb(255 255 255 / 20%);
|
||||||
|
--drawer-pagination-border: 1px solid rgb(0 0 0 / 13%);
|
||||||
|
|
||||||
|
|
||||||
|
/* Accordion */
|
||||||
|
--accordion-header-text-color: rgba(74, 198, 148, 0.9);
|
||||||
|
--accordion-header-bg-color: rgba(52, 60, 70, 0.5);
|
||||||
|
--accordion-body-bg-color: white;
|
||||||
|
--accordion-body-border-color: rgba(239, 239, 239, 0.125);
|
||||||
|
--accordion-body-text-color: var(--body-text-color);
|
||||||
|
--accordion-header-collapsed-text-color: rgba(74, 198, 148, 0.9);
|
||||||
|
--accordion-header-collapsed-bg-color: white;
|
||||||
|
--accordion-button-focus-border-color: unset;
|
||||||
|
--accordion-button-focus-box-shadow: unset;
|
||||||
|
--accordion-active-body-bg-color: white;
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
--btn-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||||
|
--btn-primary-text-color: white;
|
||||||
|
--btn-primary-bg-color: var(--primary-color);
|
||||||
|
--btn-primary-border-color: var(--primary-color);
|
||||||
|
--btn-primary-hover-text-color: white;
|
||||||
|
--btn-primary-hover-bg-color: var(--primary-color-darker-shade);
|
||||||
|
--btn-primary-hover-border-color: var(--primary-color-darker-shade);
|
||||||
|
--btn-alt-bg-color: #424c72;
|
||||||
|
--btn-alt-border-color: #444f75;
|
||||||
|
--btn-alt-hover-bg-color: #3b4466;
|
||||||
|
--btn-alt-focus-bg-color: #343c59;
|
||||||
|
--btn-alt-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||||
|
--btn-fa-icon-color: black;
|
||||||
|
--btn-disabled-bg-color: #343a40;
|
||||||
|
--btn-disabled-text-color: #efefef;
|
||||||
|
--btn-disabled-border-color: #6c757d;
|
||||||
|
|
||||||
|
/* Inputs */
|
||||||
|
--input-bg-color: white;
|
||||||
|
--input-bg-readonly-color: white;
|
||||||
|
--input-focused-border-color: #ccc;
|
||||||
|
--input-text-color: black;
|
||||||
|
--input-placeholder-color: black;
|
||||||
|
--input-border-color: #ccc;
|
||||||
|
--input-focus-boxshadow-color: rgb(255 255 255 / 50%);
|
||||||
|
|
||||||
|
/* Nav (Tabs) */
|
||||||
|
--nav-tab-border-color: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-text-color: var(--body-text-color);
|
||||||
|
--nav-tab-bg-color: var(--primary-color);
|
||||||
|
--nav-tab-hover-border-color: var(--primary-color);
|
||||||
|
--nav-tab-active-text-color: white;
|
||||||
|
--nav-tab-border-hover-color: transparent;
|
||||||
|
--nav-tab-hover-text-color: var(--body-text-color);
|
||||||
|
--nav-tab-hover-bg-color: transparent;
|
||||||
|
--nav-tab-border-top: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-left: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-bottom: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-border-right: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-top: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-left: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-hover-border-bottom: var(--bs-body-bg);
|
||||||
|
--nav-tab-hover-border-right: rgba(44, 118, 88, 0.7);
|
||||||
|
--nav-tab-active-hover-bg-color: var(--primary-color);
|
||||||
|
--nav-link-bg-color: var(--primary-color);
|
||||||
|
--nav-link-active-text-color: white;
|
||||||
|
--nav-link-text-color: white;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Reading Bar */
|
||||||
|
--br-actionbar-button-text-color: black;
|
||||||
|
--br-actionbar-button-hover-border-color: #6c757d;
|
||||||
--br-actionbar-bg-color: white;
|
--br-actionbar-bg-color: white;
|
||||||
|
|
||||||
/* Drawer */
|
/* Drawer */
|
||||||
--drawer-pagination-horizontal-rule: inset 0 -1px 0 rgb(0 0 0 / 13%);
|
--drawer-pagination-horizontal-rule: inset 0 -1px 0 rgb(0 0 0 / 13%);
|
||||||
--drawer-pagination-border: 1px solid rgb(0 0 0 / 13%);
|
--drawer-pagination-border: 1px solid rgb(0 0 0 / 13%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-container {
|
.reader-container {
|
||||||
color: black !important;
|
color: black !important;
|
||||||
background-image: none !important;
|
background-image: none !important;
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.book-content *:not(input), .book-content *:not(select), .book-content *:not(code), .book-content *:not(:link), .book-content *:not(.ngx-toastr) {
|
||||||
|
color: #dcdcdc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content code {
|
||||||
|
color: #e83e8c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content :link, .book-content a {
|
||||||
|
color: #8db2e5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content img, .book-content img[src] {
|
||||||
|
z-index: 1;
|
||||||
|
filter: brightness(0.85) !important;
|
||||||
|
background-color: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.book-content *:not(code), .book-content *:not(a) {
|
||||||
|
background-color: black;
|
||||||
|
box-shadow: none;
|
||||||
|
text-shadow: none;
|
||||||
|
border-radius: unset;
|
||||||
|
color: #dcdcdc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-content :visited, .book-content :visited *, .book-content :visited *[class] {color: rgb(211, 138, 138) !important}
|
||||||
|
.book-content :link:not(cite), :link .book-content *:not(cite) {color: #8db2e5 !important}
|
||||||
|
|
||||||
|
.btn-check:checked + .btn {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
`;
|
`;
|
@ -188,19 +188,27 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||||
|
|
||||||
// For volume or Series, we can't just take the event
|
// For volume or Series, we can't just take the event
|
||||||
|
if (this.utilityService.isChapter(this.entity)) {
|
||||||
|
const c = this.utilityService.asChapter(this.entity);
|
||||||
|
c.pagesRead = updateEvent.pagesRead;
|
||||||
|
this.read = updateEvent.pagesRead;
|
||||||
|
}
|
||||||
if (this.utilityService.isVolume(this.entity) || this.utilityService.isSeries(this.entity)) {
|
if (this.utilityService.isVolume(this.entity) || this.utilityService.isSeries(this.entity)) {
|
||||||
if (this.utilityService.isVolume(this.entity)) {
|
if (this.utilityService.isVolume(this.entity)) {
|
||||||
const v = this.utilityService.asVolume(this.entity);
|
const v = this.utilityService.asVolume(this.entity);
|
||||||
const chapter = v.chapters.find(c => c.id === updateEvent.chapterId);
|
let sum = 0;
|
||||||
if (chapter) {
|
const chapters = v.chapters.filter(c => c.volumeId === updateEvent.volumeId);
|
||||||
|
chapters.forEach(chapter => {
|
||||||
chapter.pagesRead = updateEvent.pagesRead;
|
chapter.pagesRead = updateEvent.pagesRead;
|
||||||
}
|
sum += chapter.pagesRead;
|
||||||
|
});
|
||||||
|
v.pagesRead = sum;
|
||||||
|
this.read = sum;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.read = updateEvent.pagesRead;
|
|
||||||
this.cdRef.detectChanges();
|
this.cdRef.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ export class ConfirmResetPasswordComponent {
|
|||||||
submit() {
|
submit() {
|
||||||
const model = this.registerForm.getRawValue();
|
const model = this.registerForm.getRawValue();
|
||||||
model.token = this.token;
|
model.token = this.token;
|
||||||
this.accountService.confirmResetPasswordEmail(model).subscribe(() => {
|
this.accountService.confirmResetPasswordEmail(model).subscribe((response: string) => {
|
||||||
this.toastr.success("Password reset");
|
this.toastr.success("Password reset");
|
||||||
this.router.navigateByUrl('login');
|
this.router.navigateByUrl('login');
|
||||||
}, err => {
|
}, err => {
|
||||||
|
@ -640,6 +640,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||||||
}
|
}
|
||||||
|
|
||||||
openChapter(chapter: Chapter, incognitoMode = false) {
|
openChapter(chapter: Chapter, incognitoMode = false) {
|
||||||
|
if (this.bulkSelectionService.hasSelections()) return;
|
||||||
if (chapter.pages === 0) {
|
if (chapter.pages === 0) {
|
||||||
this.toastr.error('There are no pages. Kavita was not able to read this archive.');
|
this.toastr.error('There are no pages. Kavita was not able to read this archive.');
|
||||||
return;
|
return;
|
||||||
@ -648,6 +649,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe
|
|||||||
}
|
}
|
||||||
|
|
||||||
openVolume(volume: Volume) {
|
openVolume(volume: Volume) {
|
||||||
|
if (this.bulkSelectionService.hasSelections()) return;
|
||||||
if (volume.chapters === undefined || volume.chapters?.length === 0) {
|
if (volume.chapters === undefined || volume.chapters?.length === 0) {
|
||||||
this.toastr.error('There are no chapters to this volume. Cannot read.');
|
this.toastr.error('There are no chapters to this volume. Cannot read.');
|
||||||
return;
|
return;
|
||||||
|
@ -53,12 +53,11 @@ export class SideNavComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||||
if (user) {
|
if (!user) return;
|
||||||
this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => {
|
this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => {
|
||||||
this.libraries = libraries;
|
this.libraries = libraries;
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||||
this.cdRef.markForCheck();
|
this.cdRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,7 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
this.restrictionForm = new FormGroup({
|
this.restrictionForm = new FormGroup({
|
||||||
'ageRating': new FormControl(this.member?.ageRestriction.ageRating || AgeRating.NotApplicable || AgeRating.NotApplicable, []),
|
'ageRating': new FormControl(this.member?.ageRestriction.ageRating || AgeRating.NotApplicable || AgeRating.NotApplicable, []),
|
||||||
'ageRestrictionIncludeUnknowns': new FormControl(this.member?.ageRestriction.includeUnknowns, []),
|
'ageRestrictionIncludeUnknowns': new FormControl(this.member?.ageRestriction.includeUnknowns || false, []),
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,3 +34,7 @@ hr {
|
|||||||
.subtitle-with-actionables {
|
.subtitle-with-actionables {
|
||||||
margin-left: 32px;
|
margin-left: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-switch .form-check-input:checked {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user