mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Release Testing Time (#1785)
* Fixed a bug with getting continue point where there was a single volume unread and a later volume with chapters inside it, the chapters were being picked. * Fixed a bug where resuming from jump key wasn't working (develop) * Cleaned up the spacing
This commit is contained in:
parent
0de927dee4
commit
bdd2a0a26a
@ -12,6 +12,6 @@ public class SortComparerZeroLastTests
|
|||||||
[InlineData(new[] {0, 0, 1}, new[] {1, 0, 0})]
|
[InlineData(new[] {0, 0, 1}, new[] {1, 0, 0})]
|
||||||
public void SortComparerZeroLastTest(int[] input, int[] expected)
|
public void SortComparerZeroLastTest(int[] input, int[] expected)
|
||||||
{
|
{
|
||||||
Assert.Equal(expected, input.OrderBy(f => f, new SortComparerZeroLast()).ToArray());
|
Assert.Equal(expected, input.OrderBy(f => f, SortComparerZeroLast.Default).ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2079,10 +2079,84 @@ public class ReaderServiceTests
|
|||||||
_context.Series.Attach(series);
|
_context.Series.Attach(series);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// This tests that if you add a series later to a volume and a loose leaf chapter, we continue from that volume, rather than loose leaf
|
||||||
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||||
Assert.Equal("14.9", nextChapter.Range);
|
Assert.Equal("14.9", nextChapter.Range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetContinuePoint_ShouldReturnUnreadSingleVolume_WhenThereAreSomeSingleVolumesBeforeLooseLeafChapters()
|
||||||
|
{
|
||||||
|
await ResetDb();
|
||||||
|
var readChapter1 = EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1);
|
||||||
|
var readChapter2 = EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1);
|
||||||
|
|
||||||
|
var volume = EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
_context.Series.Add(new Series()
|
||||||
|
{
|
||||||
|
Name = "Test",
|
||||||
|
Library = new Library() {
|
||||||
|
Name = "Test LIb",
|
||||||
|
Type = LibraryType.Manga,
|
||||||
|
},
|
||||||
|
Volumes = new List<Volume>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateVolume("0", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("51", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("52", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("53", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("1", new List<Chapter>()
|
||||||
|
{
|
||||||
|
readChapter1
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("2", new List<Chapter>()
|
||||||
|
{
|
||||||
|
readChapter2
|
||||||
|
}),
|
||||||
|
volume,
|
||||||
|
// 3, 4, and all loose leafs are unread should be unread
|
||||||
|
EntityFactory.CreateVolume("3", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("0", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
EntityFactory.CreateVolume("4", new List<Chapter>()
|
||||||
|
{
|
||||||
|
EntityFactory.CreateChapter("40", false, new List<MangaFile>(), 1),
|
||||||
|
EntityFactory.CreateChapter("41", false, new List<MangaFile>(), 1),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
_context.AppUser.Add(new AppUser()
|
||||||
|
{
|
||||||
|
UserName = "majora2007"
|
||||||
|
});
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var readerService = new ReaderService(_unitOfWork, Substitute.For<ILogger<ReaderService>>(), Substitute.For<IEventHub>());
|
||||||
|
|
||||||
|
// Save progress on first volume chapters and 1st of second volume
|
||||||
|
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(1, AppUserIncludes.Progress);
|
||||||
|
await readerService.MarkChaptersAsRead(user, 1,
|
||||||
|
new List<Chapter>()
|
||||||
|
{
|
||||||
|
readChapter1, readChapter2
|
||||||
|
});
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var nextChapter = await readerService.GetContinuePoint(1, 1);
|
||||||
|
|
||||||
|
Assert.Equal(4, nextChapter.VolumeId);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region MarkChaptersUntilAsRead
|
#region MarkChaptersUntilAsRead
|
||||||
|
@ -62,4 +62,5 @@ public class SortComparerZeroLast : IComparer<double>
|
|||||||
|
|
||||||
return x.CompareTo(y);
|
return x.CompareTo(y);
|
||||||
}
|
}
|
||||||
|
public static readonly SortComparerZeroLast Default = new SortComparerZeroLast();
|
||||||
}
|
}
|
||||||
|
@ -14,4 +14,5 @@ public static class ResponseCacheProfiles
|
|||||||
/// Instant is a very quick cache, because we can't bust based on the query params, but rather body
|
/// Instant is a very quick cache, because we can't bust based on the query params, but rather body
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Instant = "Instant";
|
public const string Instant = "Instant";
|
||||||
|
public const string Month = "Month";
|
||||||
}
|
}
|
||||||
|
@ -370,7 +370,7 @@ public class SeriesController : BaseApiController
|
|||||||
/// <param name="ageRating"></param>
|
/// <param name="ageRating"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <remarks>This is cached for an hour</remarks>
|
/// <remarks>This is cached for an hour</remarks>
|
||||||
[ResponseCache(CacheProfileName = "Hour", VaryByQueryKeys = new [] {"ageRating"})]
|
[ResponseCache(CacheProfileName = "Month", VaryByQueryKeys = new [] {"ageRating"})]
|
||||||
[HttpGet("age-rating")]
|
[HttpGet("age-rating")]
|
||||||
public ActionResult<string> GetAgeRating(int ageRating)
|
public ActionResult<string> GetAgeRating(int ageRating)
|
||||||
{
|
{
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Entities.Interfaces;
|
using API.Entities.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
namespace API.DTOs;
|
namespace API.DTOs;
|
||||||
|
|
||||||
|
@ -482,18 +482,17 @@ public class ReaderService : IReaderService
|
|||||||
var volumeChapters = volumes
|
var volumeChapters = volumes
|
||||||
.Where(v => v.Number != 0)
|
.Where(v => v.Number != 0)
|
||||||
.SelectMany(v => v.Chapters)
|
.SelectMany(v => v.Chapters)
|
||||||
//.OrderBy(c => float.Parse(c.Number))
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// NOTE: If volume 1 has chapter 1 and volume 2 is just chapter 0 due to being a full volume file, then this fails
|
// NOTE: If volume 1 has chapter 1 and volume 2 is just chapter 0 due to being a full volume file, then this fails
|
||||||
// If there are any volumes that have progress, return those. If not, move on.
|
// If there are any volumes that have progress, return those. If not, move on.
|
||||||
var currentlyReadingChapter = volumeChapters
|
var currentlyReadingChapter = volumeChapters
|
||||||
.OrderBy(c => double.Parse(c.Range), _chapterSortComparer)
|
.OrderBy(c => double.Parse(c.Range), _chapterSortComparer)
|
||||||
.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages);
|
.FirstOrDefault(chapter => chapter.PagesRead < chapter.Pages && chapter.PagesRead > 0);
|
||||||
if (currentlyReadingChapter != null) return currentlyReadingChapter;
|
if (currentlyReadingChapter != null) return currentlyReadingChapter;
|
||||||
|
|
||||||
// Order with volume 0 last so we prefer the natural order
|
// Order with volume 0 last so we prefer the natural order
|
||||||
return FindNextReadingChapter(volumes.OrderBy(v => v.Number, new SortComparerZeroLast()).SelectMany(v => v.Chapters).ToList());
|
return FindNextReadingChapter(volumes.OrderBy(v => v.Number, SortComparerZeroLast.Default).SelectMany(v => v.Chapters).ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ChapterDto FindNextReadingChapter(IList<ChapterDto> volumeChapters)
|
private static ChapterDto FindNextReadingChapter(IList<ChapterDto> volumeChapters)
|
||||||
@ -511,7 +510,14 @@ public class ReaderService : IReaderService
|
|||||||
var lastChapter = chaptersWithProgress.ElementAt(last);
|
var lastChapter = chaptersWithProgress.ElementAt(last);
|
||||||
if (lastChapter.PagesRead < lastChapter.Pages)
|
if (lastChapter.PagesRead < lastChapter.Pages)
|
||||||
{
|
{
|
||||||
return chaptersWithProgress.ElementAt(last);
|
return lastChapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the last chapter didn't fit, then we need the next chapter without any progress
|
||||||
|
var firstChapterWithoutProgress = volumeChapters.FirstOrDefault(c => c.PagesRead == 0);
|
||||||
|
if (firstChapterWithoutProgress != null)
|
||||||
|
{
|
||||||
|
return firstChapterWithoutProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
// chaptersWithProgress are all read, then we need to get the next chapter that doesn't have progress
|
// chaptersWithProgress are all read, then we need to get the next chapter that doesn't have progress
|
||||||
|
@ -105,12 +105,6 @@ public class StatisticService : IStatisticService
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|
||||||
// var averageReadingTimePerWeek = _context.AppUserProgresses
|
|
||||||
// .Where(p => p.AppUserId == userId)
|
|
||||||
// .Join(_context.Chapter, p => p.ChapterId, c => c.Id,
|
|
||||||
// (p, c) => (p.PagesRead / (float) c.Pages) * c.AvgHoursToRead)
|
|
||||||
// .Average() / 7.0;
|
|
||||||
|
|
||||||
var averageReadingTimePerWeek = _context.AppUserProgresses
|
var averageReadingTimePerWeek = _context.AppUserProgresses
|
||||||
.Where(p => p.AppUserId == userId)
|
.Where(p => p.AppUserId == userId)
|
||||||
.Join(_context.Chapter, p => p.ChapterId, c => c.Id,
|
.Join(_context.Chapter, p => p.ChapterId, c => c.Id,
|
||||||
@ -168,8 +162,6 @@ public class StatisticService : IStatisticService
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<IEnumerable<StatCount<PublicationStatus>>> GetPublicationCount()
|
public async Task<IEnumerable<StatCount<PublicationStatus>>> GetPublicationCount()
|
||||||
{
|
{
|
||||||
return await _context.SeriesMetadata
|
return await _context.SeriesMetadata
|
||||||
@ -196,7 +188,6 @@ public class StatisticService : IStatisticService
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<ServerStatisticsDto> GetServerStatistics()
|
public async Task<ServerStatisticsDto> GetServerStatistics()
|
||||||
{
|
{
|
||||||
var mostActiveUsers = _context.AppUserProgresses
|
var mostActiveUsers = _context.AppUserProgresses
|
||||||
|
@ -60,10 +60,22 @@ public class Startup
|
|||||||
|
|
||||||
services.AddControllers(options =>
|
services.AddControllers(options =>
|
||||||
{
|
{
|
||||||
options.CacheProfiles.Add(ResponseCacheProfiles.Images,
|
options.CacheProfiles.Add(ResponseCacheProfiles.Instant,
|
||||||
new CacheProfile()
|
new CacheProfile()
|
||||||
{
|
{
|
||||||
Duration = 60,
|
Duration = 30,
|
||||||
|
Location = ResponseCacheLocation.None,
|
||||||
|
});
|
||||||
|
options.CacheProfiles.Add(ResponseCacheProfiles.FiveMinute,
|
||||||
|
new CacheProfile()
|
||||||
|
{
|
||||||
|
Duration = 60 * 5,
|
||||||
|
Location = ResponseCacheLocation.None,
|
||||||
|
});
|
||||||
|
options.CacheProfiles.Add(ResponseCacheProfiles.TenMinute,
|
||||||
|
new CacheProfile()
|
||||||
|
{
|
||||||
|
Duration = 60 * 10,
|
||||||
Location = ResponseCacheLocation.None,
|
Location = ResponseCacheLocation.None,
|
||||||
NoStore = false
|
NoStore = false
|
||||||
});
|
});
|
||||||
@ -74,30 +86,25 @@ public class Startup
|
|||||||
Location = ResponseCacheLocation.None,
|
Location = ResponseCacheLocation.None,
|
||||||
NoStore = false
|
NoStore = false
|
||||||
});
|
});
|
||||||
options.CacheProfiles.Add(ResponseCacheProfiles.TenMinute,
|
|
||||||
new CacheProfile()
|
|
||||||
{
|
|
||||||
Duration = 60 * 10,
|
|
||||||
Location = ResponseCacheLocation.None,
|
|
||||||
NoStore = false
|
|
||||||
});
|
|
||||||
options.CacheProfiles.Add(ResponseCacheProfiles.FiveMinute,
|
|
||||||
new CacheProfile()
|
|
||||||
{
|
|
||||||
Duration = 60 * 5,
|
|
||||||
Location = ResponseCacheLocation.None,
|
|
||||||
});
|
|
||||||
options.CacheProfiles.Add(ResponseCacheProfiles.Statistics,
|
options.CacheProfiles.Add(ResponseCacheProfiles.Statistics,
|
||||||
new CacheProfile()
|
new CacheProfile()
|
||||||
{
|
{
|
||||||
Duration = 60 * 60 * 6,
|
Duration = 60 * 60 * 6,
|
||||||
Location = ResponseCacheLocation.None,
|
Location = ResponseCacheLocation.None,
|
||||||
});
|
});
|
||||||
options.CacheProfiles.Add(ResponseCacheProfiles.Instant,
|
options.CacheProfiles.Add(ResponseCacheProfiles.Images,
|
||||||
new CacheProfile()
|
new CacheProfile()
|
||||||
{
|
{
|
||||||
Duration = 30,
|
Duration = 60,
|
||||||
Location = ResponseCacheLocation.None,
|
Location = ResponseCacheLocation.None,
|
||||||
|
NoStore = false
|
||||||
|
});
|
||||||
|
options.CacheProfiles.Add(ResponseCacheProfiles.Month,
|
||||||
|
new CacheProfile()
|
||||||
|
{
|
||||||
|
Duration = TimeSpan.FromDays(30).Seconds,
|
||||||
|
Location = ResponseCacheLocation.Client,
|
||||||
|
NoStore = false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
services.Configure<ForwardedHeadersOptions>(options =>
|
services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
@ -9,6 +9,8 @@ const keySize = 25; // Height of the JumpBar button
|
|||||||
export class JumpbarService {
|
export class JumpbarService {
|
||||||
|
|
||||||
resumeKeys: {[key: string]: string} = {};
|
resumeKeys: {[key: string]: string} = {};
|
||||||
|
// Used for custom filtered urls
|
||||||
|
resumeScroll: {[key: string]: number} = {};
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@ -18,10 +20,19 @@ export class JumpbarService {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getResumePosition(key: string) {
|
||||||
|
if (this.resumeScroll.hasOwnProperty(key)) return this.resumeScroll[key];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
saveResumeKey(key: string, value: string) {
|
saveResumeKey(key: string, value: string) {
|
||||||
this.resumeKeys[key] = value;
|
this.resumeKeys[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveScrollOffset(key: string, value: number) {
|
||||||
|
this.resumeScroll[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
generateJumpBar(jumpBarKeys: Array<JumpKey>, currentSize: number) {
|
generateJumpBar(jumpBarKeys: Array<JumpKey>, currentSize: number) {
|
||||||
const fullSize = (jumpBarKeys.length * keySize);
|
const fullSize = (jumpBarKeys.length * keySize);
|
||||||
if (currentSize >= fullSize) {
|
if (currentSize >= fullSize) {
|
||||||
|
@ -14,6 +14,7 @@ import { Pagination } from 'src/app/_models/pagination';
|
|||||||
import { FilterEvent, FilterItem, SeriesFilter } from 'src/app/_models/metadata/series-filter';
|
import { FilterEvent, FilterItem, SeriesFilter } from 'src/app/_models/metadata/series-filter';
|
||||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||||
import { JumpbarService } from 'src/app/_services/jumpbar.service';
|
import { JumpbarService } from 'src/app/_services/jumpbar.service';
|
||||||
|
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-card-detail-layout',
|
selector: 'app-card-detail-layout',
|
||||||
@ -74,7 +75,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
|
|
||||||
constructor(private filterUtilitySerivce: FilterUtilitiesService, public utilityService: UtilityService,
|
constructor(private filterUtilitySerivce: FilterUtilitiesService, public utilityService: UtilityService,
|
||||||
@Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef,
|
@Inject(DOCUMENT) private document: Document, private changeDetectionRef: ChangeDetectorRef,
|
||||||
private jumpbarService: JumpbarService, private router: Router) {
|
private jumpbarService: JumpbarService, private router: Router, private scrollService: ScrollService) {
|
||||||
this.filter = this.filterUtilitySerivce.createSeriesFilter();
|
this.filter = this.filterUtilitySerivce.createSeriesFilter();
|
||||||
this.changeDetectionRef.markForCheck();
|
this.changeDetectionRef.markForCheck();
|
||||||
|
|
||||||
@ -117,7 +118,7 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
this.resizeJumpBar();
|
this.resizeJumpBar();
|
||||||
|
|
||||||
// Don't resume jump key when there is a custom sort order, as it won't work
|
// Don't resume jump key when there is a custom sort order, as it won't work
|
||||||
if (this.hasCustomSort()) {
|
if (!this.hasCustomSort()) {
|
||||||
if (!this.hasResumedJumpKey && this.jumpBarKeysToRender.length > 0) {
|
if (!this.hasResumedJumpKey && this.jumpBarKeysToRender.length > 0) {
|
||||||
const resumeKey = this.jumpbarService.getResumeKey(this.router.url);
|
const resumeKey = this.jumpbarService.getResumeKey(this.router.url);
|
||||||
if (resumeKey === '') return;
|
if (resumeKey === '') return;
|
||||||
@ -127,6 +128,13 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
this.hasResumedJumpKey = true;
|
this.hasResumedJumpKey = true;
|
||||||
setTimeout(() => this.scrollTo(keys[0]), 100);
|
setTimeout(() => this.scrollTo(keys[0]), 100);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// I will come back and refactor this to work
|
||||||
|
// const scrollPosition = this.jumpbarService.getResumePosition(this.router.url);
|
||||||
|
// console.log('scroll position: ', scrollPosition);
|
||||||
|
// if (scrollPosition > 0) {
|
||||||
|
// setTimeout(() => this.virtualScroller.scrollToIndex(scrollPosition, true, 0, 1000), 100);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +172,8 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, OnChanges {
|
|||||||
|
|
||||||
this.virtualScroller.scrollToIndex(targetIndex, true, 0, 1000);
|
this.virtualScroller.scrollToIndex(targetIndex, true, 0, 1000);
|
||||||
this.jumpbarService.saveResumeKey(this.router.url, jumpKey.key);
|
this.jumpbarService.saveResumeKey(this.router.url, jumpKey.key);
|
||||||
|
// TODO: This doesn't work, we need the offset from virtual scroller
|
||||||
|
this.jumpbarService.saveScrollOffset(this.router.url, this.scrollService.scrollPosition);
|
||||||
this.changeDetectionRef.markForCheck();
|
this.changeDetectionRef.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user