diff --git a/API.Tests/Helpers/EntityFactory.cs b/API.Tests/Helpers/EntityFactory.cs index 7b55df108..25b807c32 100644 --- a/API.Tests/Helpers/EntityFactory.cs +++ b/API.Tests/Helpers/EntityFactory.cs @@ -35,7 +35,7 @@ namespace API.Tests.Helpers }; } - public static Chapter CreateChapter(string range, bool isSpecial, List files = null) + public static Chapter CreateChapter(string range, bool isSpecial, List files = null, int pageCount = 0) { return new Chapter() { @@ -43,7 +43,7 @@ namespace API.Tests.Helpers Range = range, Number = API.Parser.Parser.MinimumNumberFromRange(range) + string.Empty, Files = files ?? new List(), - Pages = 0, + Pages = pageCount, }; } diff --git a/API.Tests/Services/ReaderServiceTests.cs b/API.Tests/Services/ReaderServiceTests.cs index 940bc2ebe..d47c891db 100644 --- a/API.Tests/Services/ReaderServiceTests.cs +++ b/API.Tests/Services/ReaderServiceTests.cs @@ -788,27 +788,227 @@ public class ReaderServiceTests #endregion - // #region GetNumberOfPages - // - // [Fact] - // public void GetNumberOfPages_EPUB() - // { - // const string testDirectory = "/manga/"; - // var fileSystem = new MockFileSystem(); - // - // var actualFile = Path.Join(Path.Join(Directory.GetCurrentDirectory(), "../../../Services/Test Data/BookService/EPUB"), "The Golden Harpoon; Or, Lost Among the Floes A Story of the Whaling Grounds.epub") - // fileSystem.File.WriteAllBytes("${testDirectory}test.epub", File.ReadAllBytes(actualFile)); - // - // fileSystem.AddDirectory(CacheDirectory); - // - // var ds = new DirectoryService(Substitute.For>(), fileSystem); - // var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); - // var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); - // - // - // } - // - // - // #endregion + #region GetContinuePoint + + [Fact] + public async Task GetContinuePoint_ShouldReturnFirstNonSpecial() + { + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("1", new List() + { + EntityFactory.CreateChapter("1", false, new List(), 1), + EntityFactory.CreateChapter("2", false, new List(), 1), + }), + EntityFactory.CreateVolume("2", new List() + { + EntityFactory.CreateChapter("21", false, new List(), 1), + EntityFactory.CreateChapter("22", false, new List(), 1), + }), + EntityFactory.CreateVolume("3", new List() + { + EntityFactory.CreateChapter("31", false, new List(), 1), + EntityFactory.CreateChapter("32", false, new List(), 1), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var fileSystem = new MockFileSystem(); + var ds = new DirectoryService(Substitute.For>(), fileSystem); + var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); + + // Save progress on first volume chapters and 1st of second volume + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 1, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 2, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 3, + SeriesId = 1, + VolumeId = 2 + }, 1); + + await _context.SaveChangesAsync(); + + var nextChapter = await readerService.GetContinuePoint(1, 1); + + Assert.Equal("22", nextChapter.Range); + + + } + + [Fact] + public async Task GetContinuePoint_ShouldReturnFirstSpecial() + { + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("1", new List() + { + EntityFactory.CreateChapter("1", false, new List(), 1), + EntityFactory.CreateChapter("2", false, new List(), 1), + }), + EntityFactory.CreateVolume("2", new List() + { + EntityFactory.CreateChapter("21", false, new List(), 1), + }), + EntityFactory.CreateVolume("0", new List() + { + EntityFactory.CreateChapter("31", false, new List(), 1), + EntityFactory.CreateChapter("32", false, new List(), 1), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var fileSystem = new MockFileSystem(); + var ds = new DirectoryService(Substitute.For>(), fileSystem); + var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); + + // Save progress on first volume chapters and 1st of second volume + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 1, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 2, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 3, + SeriesId = 1, + VolumeId = 2 + }, 1); + + await _context.SaveChangesAsync(); + + var nextChapter = await readerService.GetContinuePoint(1, 1); + + Assert.Equal("31", nextChapter.Range); + } + + [Fact] + public async Task GetContinuePoint_ShouldReturnFirstChapter_WhenAllRead() + { + _context.Series.Add(new Series() + { + Name = "Test", + Library = new Library() { + Name = "Test LIb", + Type = LibraryType.Manga, + }, + Volumes = new List() + { + EntityFactory.CreateVolume("1", new List() + { + EntityFactory.CreateChapter("1", false, new List(), 1), + EntityFactory.CreateChapter("2", false, new List(), 1), + }), + EntityFactory.CreateVolume("2", new List() + { + EntityFactory.CreateChapter("21", false, new List(), 1), + }), + } + }); + + _context.AppUser.Add(new AppUser() + { + UserName = "majora2007" + }); + + await _context.SaveChangesAsync(); + + + + var fileSystem = new MockFileSystem(); + var ds = new DirectoryService(Substitute.For>(), fileSystem); + var cs = new CacheService(_logger, _unitOfWork, ds, new MockReadingItemServiceForCacheService(ds)); + var readerService = new ReaderService(_unitOfWork, Substitute.For>(), ds, cs); + + // Save progress on first volume chapters and 1st of second volume + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 1, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 2, + SeriesId = 1, + VolumeId = 1 + }, 1); + await readerService.SaveReadingProgress(new ProgressDto() + { + PageNum = 1, + ChapterId = 3, + SeriesId = 1, + VolumeId = 2 + }, 1); + + await _context.SaveChangesAsync(); + + var nextChapter = await readerService.GetContinuePoint(1, 1); + + Assert.Equal("1", nextChapter.Range); + } + + #endregion + + } diff --git a/API/Controllers/ReaderController.cs b/API/Controllers/ReaderController.cs index 3028d1fee..44d686e22 100644 --- a/API/Controllers/ReaderController.cs +++ b/API/Controllers/ReaderController.cs @@ -356,6 +356,19 @@ namespace API.Controllers return BadRequest("Could not save progress"); } + /// + /// Continue point is the chapter which you should start reading again from. If there is no progress on a series, then the first chapter will be returned (non-special unless only specials). + /// Otherwise, loop through the chapters and volumes in order to find the next chapter which has progress. + /// + /// + [HttpGet("continue-point")] + public async Task> GetContinuePoint(int seriesId) + { + var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername()); + + return Ok(await _readerService.GetContinuePoint(seriesId, userId)); + } + /// /// Returns a list of bookmarked pages for a given Chapter /// diff --git a/API/Services/ReaderService.cs b/API/Services/ReaderService.cs index b9862cf05..aa502cded 100644 --- a/API/Services/ReaderService.cs +++ b/API/Services/ReaderService.cs @@ -22,6 +22,7 @@ public interface IReaderService Task CapPageToChapter(int chapterId, int page); Task GetNextChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); Task GetPrevChapterIdAsync(int seriesId, int volumeId, int currentChapterId, int userId); + Task GetContinuePoint(int seriesId, int userId); } public class ReaderService : IReaderService @@ -305,6 +306,39 @@ public class ReaderService : IReaderService return -1; } + public async Task GetContinuePoint(int seriesId, int userId) + { + // Loop through all chapters that are not in volume 0 + var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId)).ToList(); + + var nonSpecialChapters = volumes + .Where(v => v.Number != 0) + .SelectMany(v => v.Chapters) + .OrderBy(c => float.Parse(c.Number)) + .ToList(); + + var currentlyReadingChapter = nonSpecialChapters.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages); + + + // Check if there are any specials + if (currentlyReadingChapter == null) + { + var volume = volumes.SingleOrDefault(v => v.Number == 0); + if (volume == null) return nonSpecialChapters.First(); + + foreach (var chapter in volume.Chapters.OrderBy(c => float.Parse(c.Number))) + { + if (chapter.PagesRead < chapter.Pages) + { + currentlyReadingChapter = chapter; + break; + } + } + } + + return currentlyReadingChapter ?? nonSpecialChapters.First(); + } + private static int GetNextChapterId(IEnumerable chapters, string currentChapterNumber) { diff --git a/UI/Web/src/app/_services/reader.service.ts b/UI/Web/src/app/_services/reader.service.ts index 16c2ea656..849120250 100644 --- a/UI/Web/src/app/_services/reader.service.ts +++ b/UI/Web/src/app/_services/reader.service.ts @@ -103,33 +103,8 @@ export class ReaderService { return this.httpClient.get(this.baseUrl + 'reader/prev-chapter?seriesId=' + seriesId + '&volumeId=' + volumeId + '¤tChapterId=' + currentChapterId); } - getCurrentChapter(volumes: Array): Chapter { - let currentlyReadingChapter: Chapter | undefined = undefined; - const chapters = volumes.filter(v => v.number !== 0).map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters); - - for (const c of chapters) { - if (c.pagesRead < c.pages) { - currentlyReadingChapter = c; - break; - } - } - - if (currentlyReadingChapter === undefined) { - // Check if there are specials we can load: - const specials = volumes.filter(v => v.number === 0).map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters); - for (const c of specials) { - if (c.pagesRead < c.pages) { - currentlyReadingChapter = c; - break; - } - } - if (currentlyReadingChapter === undefined) { - // Default to first chapter - currentlyReadingChapter = chapters[0]; - } - } - - return currentlyReadingChapter; + getCurrentChapter(seriesId: number) { + return this.httpClient.get(this.baseUrl + 'reader/continue-point?seriesId=' + seriesId); } /** diff --git a/UI/Web/src/app/_services/series.service.ts b/UI/Web/src/app/_services/series.service.ts index af02b8e88..ddd8abf2f 100644 --- a/UI/Web/src/app/_services/series.service.ts +++ b/UI/Web/src/app/_services/series.service.ts @@ -135,9 +135,9 @@ export class SeriesService { })); } - getContinueReading(libraryId: number = 0) { - return this.httpClient.get(this.baseUrl + 'series/continue-reading?libraryId=' + libraryId); - } + // getContinueReading(libraryId: number = 0) { + // return this.httpClient.get(this.baseUrl + 'series/continue-reading?libraryId=' + libraryId); + // } refreshMetadata(series: Series) { return this.httpClient.post(this.baseUrl + 'series/refresh-metadata', {libraryId: series.libraryId, seriesId: series.id}); 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 42319c549..a84f15923 100644 --- a/UI/Web/src/app/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/series-detail.component.ts @@ -354,14 +354,14 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { .filter(action => this.actionFactoryService.filterBookmarksForFormat(action, this.series)); this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this)); this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this)); - - + this.seriesService.getVolumes(this.series.id).subscribe(volumes => { this.chapters = volumes.filter(v => v.number === 0).map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters); this.volumes = volumes.sort(this.utilityService.sortVolumes); - + this.setContinuePoint(); + const vol0 = this.volumes.filter(v => v.number === 0); this.hasSpecials = vol0.map(v => v.chapters || []).flat().sort(this.utilityService.sortChapters).filter(c => c.isSpecial || isNaN(parseInt(c.range, 10))).length > 0 ; if (this.hasSpecials) { @@ -398,7 +398,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { setContinuePoint() { this.hasReadingProgress = this.volumes.filter(v => v.pagesRead > 0).length > 0 || this.chapters.filter(c => c.pagesRead > 0).length > 0; - this.currentlyReadingChapter = this.readerService.getCurrentChapter(this.volumes); + this.readerService.getCurrentChapter(this.series.id).subscribe(chapter => this.currentlyReadingChapter = chapter); } markAsRead(vol: Volume) { @@ -488,7 +488,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy { // If user has progress on the volume, load them where they left off if (volume.pagesRead < volume.pages && volume.pagesRead > 0) { // Find the continue point chapter and load it - this.openChapter(this.readerService.getCurrentChapter([volume])); + this.readerService.getCurrentChapter(this.series.id).subscribe(chapter => this.openChapter(chapter)); return; }