diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 9b112e5da..c5ba062d1 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -172,7 +172,7 @@ namespace API.Controllers } /// - /// Marks a Chapter as Unread (progress) + /// Marks a Series as Unread (progress) /// /// /// @@ -206,6 +206,50 @@ namespace API.Controllers return BadRequest("There was an issue saving progress"); } + /// + /// Marks all chapters within a volume as unread + /// + /// + /// + [HttpPost("mark-volume-unread")] + public async Task MarkVolumeAsUnread(MarkVolumeReadDto markVolumeReadDto) + { + var user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(User.GetUsername()); + + var chapters = await _unitOfWork.VolumeRepository.GetChaptersAsync(markVolumeReadDto.VolumeId); + foreach (var chapter in chapters) + { + user.Progresses ??= new List(); + var userProgress = user.Progresses.FirstOrDefault(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id); + + if (userProgress == null) + { + user.Progresses.Add(new AppUserProgress + { + PagesRead = 0, + VolumeId = markVolumeReadDto.VolumeId, + SeriesId = markVolumeReadDto.SeriesId, + ChapterId = chapter.Id + }); + } + else + { + userProgress.PagesRead = 0; + userProgress.SeriesId = markVolumeReadDto.SeriesId; + userProgress.VolumeId = markVolumeReadDto.VolumeId; + } + } + + _unitOfWork.UserRepository.Update(user); + + if (await _unitOfWork.CommitAsync()) + { + return Ok(); + } + + return BadRequest("Could not save progress"); + } + /// /// Marks all chapters within a volume as Read /// diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts index 178e72400..1640644da 100644 --- a/UI/Web/src/app/_services/action-factory.service.ts +++ b/UI/Web/src/app/_services/action-factory.service.ts @@ -16,7 +16,8 @@ export enum Action { Info = 5, RefreshMetadata = 6, Download = 7, - Bookmarks = 8 + Bookmarks = 8, + IncognitoRead = 9 } export interface ActionItem { @@ -203,6 +204,12 @@ export class ActionFactoryService { callback: this.dummyCallback, requiresAdmin: false }, + { + action: Action.IncognitoRead, + title: 'Read in Incognito', + callback: this.dummyCallback, + requiresAdmin: false + }, { action: Action.Edit, title: 'Info', @@ -223,7 +230,13 @@ export class ActionFactoryService { title: 'Mark as Unread', callback: this.dummyCallback, requiresAdmin: false - } + }, + { + action: Action.IncognitoRead, + title: 'Read in Incognito', + callback: this.dummyCallback, + requiresAdmin: false + }, ]; } } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 3e400a80b..b336e5733 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -156,7 +156,7 @@ export class ActionService implements OnDestroy { * @param callback Optional callback to perform actions after API completes */ markVolumeAsUnread(seriesId: number, volume: Volume, callback?: VolumeActionCallback) { - this.readerService.markVolumeRead(seriesId, volume.id).subscribe(() => { + this.readerService.markVolumeUnread(seriesId, volume.id).subscribe(() => { volume.pagesRead = 0; volume.chapters?.forEach(c => c.pagesRead = 0); this.toastr.success('Marked as Unread'); diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 042eab4a0..450c89a8f 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -1,5 +1,6 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; import { ChapterInfo } from '../manga-reader/_models/chapter-info'; import { UtilityService } from '../shared/_services/utility.service'; @@ -68,6 +69,10 @@ export class ReaderService { return this.httpClient.post(this.baseUrl + 'reader/mark-volume-read', {seriesId, volumeId}); } + markVolumeUnread(seriesId: number, volumeId: number) { + return this.httpClient.post(this.baseUrl + 'reader/mark-volume-unread', {seriesId, volumeId}); + } + getNextChapter(seriesId: number, volumeId: number, currentChapterId: number) { return this.httpClient.get(this.baseUrl + 'reader/next-chapter?seriesId=' + seriesId + '&volumeId=' + volumeId + '¤tChapterId=' + currentChapterId); } diff --git a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts index 0b6e35163..51d5fbfeb 100644 --- a/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts +++ b/UI/Web/src/app/book-reader/book-reader/book-reader.component.ts @@ -64,6 +64,10 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { volumeId!: number; chapterId!: number; chapter!: Chapter; + /** + * If we should save progress or not + */ + incognitoMode: boolean = false; chapters: Array = []; @@ -268,6 +272,7 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.libraryId = parseInt(libraryId, 10); this.seriesId = parseInt(seriesId, 10); this.chapterId = parseInt(chapterId, 10); + this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true'; this.memberService.hasReadingProgress(this.libraryId).pipe(take(1)).subscribe(hasProgress => { if (!hasProgress) { @@ -292,7 +297,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { if (this.pageNum >= this.maxPages) { this.pageNum = this.maxPages - 1; - this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); + if (!this.incognitoMode) { + this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); + } } // Check if user progress has part, if so load it so we scroll to it @@ -483,7 +490,9 @@ export class BookReaderComponent implements OnInit, AfterViewInit, OnDestroy { loadPage(part?: string | undefined, scrollTop?: number | undefined) { this.isLoading = true; - this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); + if (!this.incognitoMode) { + this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); + } this.bookService.getBookPage(this.chapterId, this.pageNum).pipe(take(1)).subscribe(content => { this.page = this.domSanitizer.bypassSecurityTrustHtml(content); diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.html b/UI/Web/src/app/manga-reader/manga-reader.component.html index 1dd55b6f3..6b3122388 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.html +++ b/UI/Web/src/app/manga-reader/manga-reader.component.html @@ -7,7 +7,7 @@
-
{{title}}
+
{{title}} (Incognito Mode)
{{subtitle}}
diff --git a/UI/Web/src/app/manga-reader/manga-reader.component.ts b/UI/Web/src/app/manga-reader/manga-reader.component.ts index 8e6d8c9b3..42fc7777c 100644 --- a/UI/Web/src/app/manga-reader/manga-reader.component.ts +++ b/UI/Web/src/app/manga-reader/manga-reader.component.ts @@ -66,6 +66,11 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { volumeId!: number; chapterId!: number; + /** + * If this is true, no progress will be saved. + */ + incognitoMode: boolean = false; + /** * The current page. UI will show this number + 1. */ @@ -259,6 +264,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.libraryId = parseInt(libraryId, 10); this.seriesId = parseInt(seriesId, 10); this.chapterId = parseInt(chapterId, 10); + this.incognitoMode = this.route.snapshot.queryParamMap.get('incognitoMode') === 'true'; this.continuousChaptersStack.push(this.chapterId); @@ -706,7 +712,10 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { this.continuousChaptersStack.push(chapterId); // Load chapter Id onto route but don't reload const lastSlashIndex = this.router.url.lastIndexOf('/'); - const newRoute = this.router.url.substring(0, lastSlashIndex + 1) + this.chapterId + ''; + let newRoute = this.router.url.substring(0, lastSlashIndex + 1) + this.chapterId + ''; + if (this.incognitoMode) { + newRoute += '?incognitoMode=true'; + } window.history.replaceState({}, '', newRoute); this.init(); } else { @@ -777,8 +786,9 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { pageNum = this.pageNum + 1; } - - this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); + if (!this.incognitoMode) { + this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); + } this.isLoading = true; this.canvasImage = this.cachedImages.current(); @@ -929,6 +939,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy { handleWebtoonPageChange(updatedPageNum: number) { this.setPageNum(updatedPageNum); + if (this.incognitoMode) return; this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, this.pageNum).pipe(take(1)).subscribe(() => {/* No operation */}); } 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 56b562fec..969f6f3e9 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -175,6 +175,11 @@ export class SeriesDetailComponent implements OnInit { case(Action.Edit): this.openViewInfo(volume); break; + case(Action.IncognitoRead): + if (volume.chapters != undefined && volume.chapters?.length >= 1) { + this.openChapter(volume.chapters[0], true); + } + break; default: break; } @@ -191,6 +196,9 @@ export class SeriesDetailComponent implements OnInit { case(Action.Edit): this.openViewInfo(chapter); break; + case(Action.IncognitoRead): + this.openChapter(chapter, true); + break; default: break; } @@ -348,16 +356,16 @@ export class SeriesDetailComponent implements OnInit { }); } - openChapter(chapter: Chapter) { + openChapter(chapter: Chapter, incognitoMode = false) { if (chapter.pages === 0) { this.toastr.error('There are no pages. Kavita was not able to read this archive.'); return; } if (chapter.files.length > 0 && chapter.files[0].format === MangaFormat.EPUB) { - this.router.navigate(['library', this.libraryId, 'series', this.series?.id, 'book', chapter.id]); + this.router.navigate(['library', this.libraryId, 'series', this.series?.id, 'book', chapter.id], {queryParams: {incognitoMode}}); } else { - this.router.navigate(['library', this.libraryId, 'series', this.series?.id, 'manga', chapter.id]); + this.router.navigate(['library', this.libraryId, 'series', this.series?.id, 'manga', chapter.id], {queryParams: {incognitoMode}}); } }