diff --git a/API.Tests/Services/ReadingListServiceTests.cs b/API.Tests/Services/ReadingListServiceTests.cs index 200900bb1..6472f8fb1 100644 --- a/API.Tests/Services/ReadingListServiceTests.cs +++ b/API.Tests/Services/ReadingListServiceTests.cs @@ -329,7 +329,7 @@ public class ReadingListServiceTests Substitute.For()); // Mark 2 as fully read await readerService.MarkChaptersAsRead(user, 1, - await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(new List() {2})); + (await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(new List() {2})).ToList()); await _unitOfWork.CommitAsync(); await _readingListService.RemoveFullyReadItems(1, user); diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 99b95b460..db5db71bb 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -13,6 +13,7 @@ using API.Entities.Enums; using API.Extensions; using API.Services; using API.Services.Tasks; +using API.SignalR; using Hangfire; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -32,12 +33,13 @@ public class ReaderController : BaseApiController private readonly IReaderService _readerService; private readonly IBookmarkService _bookmarkService; private readonly IAccountService _accountService; + private readonly IEventHub _eventHub; /// public ReaderController(ICacheService cacheService, IUnitOfWork unitOfWork, ILogger logger, IReaderService readerService, IBookmarkService bookmarkService, - IAccountService accountService) + IAccountService accountService, IEventHub eventHub) { _cacheService = cacheService; _unitOfWork = unitOfWork; @@ -45,6 +47,7 @@ public class ReaderController : BaseApiController _readerService = readerService; _bookmarkService = bookmarkService; _accountService = accountService; + _eventHub = eventHub; } /// @@ -273,8 +276,6 @@ public class ReaderController : BaseApiController var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); await _readerService.MarkChaptersAsUnread(user, markVolumeReadDto.SeriesId, chapters); - _unitOfWork.UserRepository.Update(user); - if (await _unitOfWork.CommitAsync()) { return Ok(); @@ -295,8 +296,9 @@ public class ReaderController : BaseApiController var chapters = await _unitOfWork.ChapterRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); await _readerService.MarkChaptersAsRead(user, markVolumeReadDto.SeriesId, chapters); - - _unitOfWork.UserRepository.Update(user); + await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, + MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, markVolumeReadDto.SeriesId, + markVolumeReadDto.VolumeId, 0, chapters.Sum(c => c.Pages))); if (await _unitOfWork.CommitAsync()) { @@ -324,15 +326,14 @@ public class ReaderController : BaseApiController chapterIds.Add(chapterId); } var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); - await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters); - - _unitOfWork.UserRepository.Update(user); + await _readerService.MarkChaptersAsRead(user, dto.SeriesId, chapters.ToList()); if (await _unitOfWork.CommitAsync()) { return Ok(); } + return BadRequest("Could not save progress"); } @@ -353,9 +354,7 @@ public class ReaderController : BaseApiController chapterIds.Add(chapterId); } var chapters = await _unitOfWork.ChapterRepository.GetChaptersByIdsAsync(chapterIds); - await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters); - - _unitOfWork.UserRepository.Update(user); + await _readerService.MarkChaptersAsUnread(user, dto.SeriesId, chapters.ToList()); if (await _unitOfWork.CommitAsync()) { @@ -382,8 +381,6 @@ public class ReaderController : BaseApiController await _readerService.MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters); } - _unitOfWork.UserRepository.Update(user); - if (await _unitOfWork.CommitAsync()) { return Ok(); @@ -409,8 +406,6 @@ public class ReaderController : BaseApiController await _readerService.MarkChaptersAsUnread(user, volume.SeriesId, volume.Chapters); } - _unitOfWork.UserRepository.Update(user); - if (await _unitOfWork.CommitAsync()) { return Ok(); diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index 40fe149fa..fcb111d98 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -21,8 +21,8 @@ public interface IReaderService { Task MarkSeriesAsRead(AppUser user, int seriesId); Task MarkSeriesAsUnread(AppUser user, int seriesId); - Task MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable chapters); - Task MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable chapters); + Task MarkChaptersAsRead(AppUser user, int seriesId, IList chapters); + Task MarkChaptersAsUnread(AppUser user, int seriesId, IList chapters); Task SaveReadingProgress(ProgressDto progressDto, int userId); Task CapPageToChapter(int chapterId, int page); int CapPageToChapter(Chapter chapter, int page); @@ -76,8 +76,6 @@ public class ReaderService : IReaderService { await MarkChaptersAsRead(user, seriesId, volume.Chapters); } - - _unitOfWork.UserRepository.Update(user); } /// @@ -93,18 +91,18 @@ public class ReaderService : IReaderService { await MarkChaptersAsUnread(user, seriesId, volume.Chapters); } - - _unitOfWork.UserRepository.Update(user); } /// /// Marks all Chapters as Read by creating or updating UserProgress rows. Does not commit. /// + /// Emits events to the UI for each chapter progress and one for each volume progress /// /// /// - public async Task MarkChaptersAsRead(AppUser user, int seriesId, IEnumerable chapters) + public async Task MarkChaptersAsRead(AppUser user, int seriesId, IList chapters) { + var seenVolume = new Dictionary(); foreach (var chapter in chapters) { var userProgress = GetUserProgressForChapter(user, chapter); @@ -118,19 +116,29 @@ public class ReaderService : IReaderService SeriesId = seriesId, ChapterId = chapter.Id }); - await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, - MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, seriesId, chapter.VolumeId, chapter.Id, chapter.Pages)); } else { userProgress.PagesRead = chapter.Pages; userProgress.SeriesId = seriesId; userProgress.VolumeId = chapter.VolumeId; - - await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, - MessageFactory.UserProgressUpdateEvent(user.Id, user.UserName, userProgress.SeriesId, userProgress.VolumeId, userProgress.ChapterId, chapter.Pages)); } + + await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, + 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); } /// @@ -139,8 +147,9 @@ public class ReaderService : IReaderService /// /// /// - public async Task MarkChaptersAsUnread(AppUser user, int seriesId, IEnumerable chapters) + public async Task MarkChaptersAsUnread(AppUser user, int seriesId, IList chapters) { + var seenVolume = new Dictionary(); foreach (var chapter in chapters) { var userProgress = GetUserProgressForChapter(user, chapter); @@ -153,7 +162,17 @@ public class ReaderService : IReaderService await _eventHub.SendMessageAsync(MessageFactory.UserProgressUpdate, 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); } /// @@ -526,7 +545,7 @@ public class ReaderService : IReaderService var chapters = volume.Chapters .OrderBy(c => float.Parse(c.Number)) .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()); } } diff --git a/API/Services/Tasks/Scanner/ParseScannedFiles.cs b/API/Services/Tasks/Scanner/ParseScannedFiles.cs index b80d2d9b5..dbd23d970 100644 --- a/API/Services/Tasks/Scanner/ParseScannedFiles.cs +++ b/API/Services/Tasks/Scanner/ParseScannedFiles.cs @@ -150,7 +150,7 @@ public class ParseScannedFiles catch (Exception 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; @@ -200,13 +200,13 @@ public class ParseScannedFiles } 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 => ps.Format == info.Format && (ps.NormalizedName.Equals(normalizedSeries) || ps.NormalizedName.Equals(normalizedLocalizedSeries) || 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) { - _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 => (Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedSeries || Parser.Parser.Normalize(p.Key.NormalizedName) == normalizedLocalSeries) && p.Key.Format == info.Format); 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, }).ToList(); await processSeriesInfos.Invoke(new Tuple>(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; } - _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, MessageFactory.FileScanProgressEvent(folder, libraryName, ProgressEventType.Updated)); if (files.Count == 0) @@ -316,7 +316,7 @@ public class ParseScannedFiles catch (Exception 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); } } @@ -339,7 +339,7 @@ public class ParseScannedFiles } catch (ArgumentException ex) { - _logger.LogError(ex, "The directory '{FolderPath}' does not exist", folderPath); + _logger.LogError(ex, "[ScannerService] The directory '{FolderPath}' does not exist", folderPath); } } diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index 8f61452bf..31cca7645 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -29,12 +29,12 @@ public interface IScannerService /// Don't perform optimization checks, defaults to false [Queue(TaskScheduler.ScanQueue)] [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] Task ScanLibrary(int libraryId, bool forceUpdate = false); [Queue(TaskScheduler.ScanQueue)] [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] Task ScanLibraries(); [Queue(TaskScheduler.ScanQueue)] @@ -407,7 +407,7 @@ public class ScannerService : IScannerService [Queue(TaskScheduler.ScanQueue)] [DisableConcurrentExecution(60 * 60 * 60)] - [AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] + [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)] public async Task ScanLibraries() { _logger.LogInformation("Starting Scan of All Libraries"); diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index 618f23e86..9da026262 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -192,7 +192,7 @@ export class AccountService implements OnDestroy { } 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(this.baseUrl + 'account/confirm-password-reset', model, {responseType: 'text' as 'json'}); } resetPassword(username: string, password: string, oldPassword: string) { diff --git a/UI/Web/src/app/_services/device.service.ts b/UI/Web/src/app/_services/device.service.ts index 7996d50bc..d67193e4c 100644 --- a/UI/Web/src/app/_services/device.service.ts +++ b/UI/Web/src/app/_services/device.service.ts @@ -1,9 +1,10 @@ import { HttpClient } from '@angular/common/http'; 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 { Device } from '../_models/device/device'; import { DevicePlatform } from '../_models/device/device-platform'; +import { AccountService } from './account.service'; @Injectable({ providedIn: 'root' @@ -16,9 +17,14 @@ export class DeviceService { public devices$ = this.devicesSource.asObservable().pipe(shareReplay()); - constructor(private httpClient: HttpClient) { - this.httpClient.get(this.baseUrl + 'device', {}).subscribe(data => { - this.devicesSource.next(data); + 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(this.baseUrl + 'device', {}).subscribe(data => { + this.devicesSource.next(data); + }); }); } diff --git a/UI/Web/src/app/admin/edit-user/edit-user.component.ts b/UI/Web/src/app/admin/edit-user/edit-user.component.ts index a57bd6703..8ad8215cd 100644 --- a/UI/Web/src/app/admin/edit-user/edit-user.component.ts +++ b/UI/Web/src/app/admin/edit-user/edit-user.component.ts @@ -34,6 +34,7 @@ export class EditUserComponent implements OnInit { this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required])); this.userForm.get('email')?.disable(); + this.selectedRestriction = this.member.ageRestriction; } updateRoleSelection(roles: Array) { diff --git a/UI/Web/src/app/book-reader/_models/book-black-theme.ts b/UI/Web/src/app/book-reader/_models/book-black-theme.ts index cc588fb76..3ada95a8e 100644 --- a/UI/Web/src/app/book-reader/_models/book-black-theme.ts +++ b/UI/Web/src/app/book-reader/_models/book-black-theme.ts @@ -85,7 +85,6 @@ export const BookBlackTheme = ` --br-actionbar-button-hover-border-color: #6c757d; --br-actionbar-bg-color: black; } -} diff --git a/UI/Web/src/app/book-reader/_models/book-white-theme.ts b/UI/Web/src/app/book-reader/_models/book-white-theme.ts index 6b38dce13..27405b577 100644 --- a/UI/Web/src/app/book-reader/_models/book-white-theme.ts +++ b/UI/Web/src/app/book-reader/_models/book-white-theme.ts @@ -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 export const BookWhiteTheme = ` :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; /* Drawer */ --drawer-pagination-horizontal-rule: inset 0 -1px 0 rgb(0 0 0 / 13%); --drawer-pagination-border: 1px solid rgb(0 0 0 / 13%); - } - .reader-container { - color: black !important; - background-image: none !important; - background-color: white !important; - } +} + +.reader-container { + color: black !important; + background-image: none !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); +} + `; \ No newline at end of file diff --git a/UI/Web/src/app/cards/card-item/card-item.component.ts b/UI/Web/src/app/cards/card-item/card-item.component.ts index 0a4b8d910..10d881636 100644 --- a/UI/Web/src/app/cards/card-item/card-item.component.ts +++ b/UI/Web/src/app/cards/card-item/card-item.component.ts @@ -188,19 +188,27 @@ export class CardItemComponent implements OnInit, OnDestroy { if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return; // 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)) { const v = this.utilityService.asVolume(this.entity); - const chapter = v.chapters.find(c => c.id === updateEvent.chapterId); - if (chapter) { + let sum = 0; + const chapters = v.chapters.filter(c => c.volumeId === updateEvent.volumeId); + chapters.forEach(chapter => { chapter.pagesRead = updateEvent.pagesRead; - } + sum += chapter.pagesRead; + }); + v.pagesRead = sum; + this.read = sum; } else { return; } } - this.read = updateEvent.pagesRead; this.cdRef.detectChanges(); }); diff --git a/UI/Web/src/app/registration/confirm-reset-password/confirm-reset-password.component.ts b/UI/Web/src/app/registration/confirm-reset-password/confirm-reset-password.component.ts index f297ba092..22d7e15ef 100644 --- a/UI/Web/src/app/registration/confirm-reset-password/confirm-reset-password.component.ts +++ b/UI/Web/src/app/registration/confirm-reset-password/confirm-reset-password.component.ts @@ -45,7 +45,7 @@ export class ConfirmResetPasswordComponent { submit() { const model = this.registerForm.getRawValue(); model.token = this.token; - this.accountService.confirmResetPasswordEmail(model).subscribe(() => { + this.accountService.confirmResetPasswordEmail(model).subscribe((response: string) => { this.toastr.success("Password reset"); this.router.navigateByUrl('login'); }, err => { diff --git a/UI/Web/src/app/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/series-detail.component.ts index 46f70b2c9..6909eb7ae 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -640,6 +640,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe } openChapter(chapter: Chapter, incognitoMode = false) { + if (this.bulkSelectionService.hasSelections()) return; if (chapter.pages === 0) { this.toastr.error('There are no pages. Kavita was not able to read this archive.'); return; @@ -648,6 +649,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy, AfterContentChe } openVolume(volume: Volume) { + if (this.bulkSelectionService.hasSelections()) return; if (volume.chapters === undefined || volume.chapters?.length === 0) { this.toastr.error('There are no chapters to this volume. Cannot read.'); return; diff --git a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts index 5b7ef30d8..7364a8995 100644 --- a/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts +++ b/UI/Web/src/app/sidenav/side-nav/side-nav.component.ts @@ -53,12 +53,11 @@ export class SideNavComponent implements OnInit, OnDestroy { ngOnInit(): void { this.accountService.currentUser$.pipe(take(1)).subscribe(user => { - if (user) { - this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => { - this.libraries = libraries; - this.cdRef.markForCheck(); - }); - } + if (!user) return; + this.libraryService.getLibraries().pipe(take(1), shareReplay()).subscribe((libraries: Library[]) => { + this.libraries = libraries; + this.cdRef.markForCheck(); + }); this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this)); this.cdRef.markForCheck(); }); diff --git a/UI/Web/src/app/user-settings/restriction-selector/restriction-selector.component.ts b/UI/Web/src/app/user-settings/restriction-selector/restriction-selector.component.ts index 609af437f..6ca87dd2c 100644 --- a/UI/Web/src/app/user-settings/restriction-selector/restriction-selector.component.ts +++ b/UI/Web/src/app/user-settings/restriction-selector/restriction-selector.component.ts @@ -34,7 +34,7 @@ export class RestrictionSelectorComponent implements OnInit, OnChanges { this.restrictionForm = new FormGroup({ '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, []), }); diff --git a/UI/Web/src/theme/utilities/_global.scss b/UI/Web/src/theme/utilities/_global.scss index 90e221ef2..3ff1fd933 100644 --- a/UI/Web/src/theme/utilities/_global.scss +++ b/UI/Web/src/theme/utilities/_global.scss @@ -33,4 +33,8 @@ hr { .subtitle-with-actionables { margin-left: 32px; +} + +.form-switch .form-check-input:checked { + background-color: var(--primary-color); } \ No newline at end of file