Jump Bar Testing (#1302)
* Implemented a basic jump bar for the library view. This currently just interacts with existing pagination controls and is not inlined with infinite scroll yet. This is a first pass implementation. * Refactored time estimates into the reading service. * Cleaned up when the jump bar is shown to mimic pagination controls * Cleanup up code in reader service. * Scroll to card when selecting a jump key that is shown on the current page. * Ensure estimated times always has the smaller number on left hand side. * Fixed a bug with a missing vertical rule * Fixed an off by 1 pixel for search overlay
This commit is contained in:
parent
64c0b5a71e
commit
742cfd3293
20 changed files with 319 additions and 120 deletions
|
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
|||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.JumpBar;
|
||||
using API.DTOs.Search;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
|
|
@ -106,6 +107,16 @@ namespace API.Controllers
|
|||
return Ok(await _unitOfWork.LibraryRepository.GetLibraryDtosAsync());
|
||||
}
|
||||
|
||||
[HttpGet("jump-bar")]
|
||||
public async Task<ActionResult<IEnumerable<JumpKeyDto>>> GetJumpBar(int libraryId)
|
||||
{
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
if (!await _unitOfWork.UserRepository.HasAccessToLibrary(libraryId, userId)) return BadRequest("User does not have access to library");
|
||||
|
||||
return Ok(_unitOfWork.LibraryRepository.GetJumpBarAsync(libraryId));
|
||||
}
|
||||
|
||||
|
||||
[Authorize(Policy = "RequireAdminRole")]
|
||||
[HttpPost("grant-access")]
|
||||
public async Task<ActionResult<MemberDto>> UpdateUserLibraries(UpdateLibraryForUserDto updateLibraryForUserDto)
|
||||
|
|
|
|||
|
|
@ -639,25 +639,7 @@ namespace API.Controllers
|
|||
[HttpGet("manual-read-time")]
|
||||
public ActionResult<HourEstimateRangeDto> GetManualReadTime(int wordCount, int pageCount, bool isEpub)
|
||||
{
|
||||
|
||||
if (isEpub)
|
||||
{
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((wordCount / ReaderService.MinWordsPerHour)),
|
||||
MaxHours = (int) Math.Round((wordCount / ReaderService.MaxWordsPerHour)),
|
||||
AvgHours = (int) Math.Round((wordCount / ReaderService.AvgWordsPerHour)),
|
||||
HasProgress = false
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((pageCount / ReaderService.MinPagesPerMinute / 60F)),
|
||||
MaxHours = (int) Math.Round((pageCount / ReaderService.MaxPagesPerMinute / 60F)),
|
||||
AvgHours = (int) Math.Round((pageCount / ReaderService.AvgPagesPerMinute / 60F)),
|
||||
HasProgress = false
|
||||
});
|
||||
return Ok(_readerService.GetTimeEstimate(wordCount, pageCount, isEpub));
|
||||
}
|
||||
|
||||
[HttpGet("read-time")]
|
||||
|
|
@ -667,24 +649,8 @@ namespace API.Controllers
|
|||
var series = await _unitOfWork.SeriesRepository.GetSeriesDtoByIdAsync(seriesId, userId);
|
||||
|
||||
var progress = (await _unitOfWork.AppUserProgressRepository.GetUserProgressForSeriesAsync(seriesId, userId)).ToList();
|
||||
if (series.Format == MangaFormat.Epub)
|
||||
{
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((series.WordCount / ReaderService.MinWordsPerHour)),
|
||||
MaxHours = (int) Math.Round((series.WordCount / ReaderService.MaxWordsPerHour)),
|
||||
AvgHours = (int) Math.Round((series.WordCount / ReaderService.AvgWordsPerHour)),
|
||||
HasProgress = progress.Any()
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((series.Pages / ReaderService.MinPagesPerMinute / 60F)),
|
||||
MaxHours = (int) Math.Round((series.Pages / ReaderService.MaxPagesPerMinute / 60F)),
|
||||
AvgHours = (int) Math.Round((series.Pages / ReaderService.AvgPagesPerMinute / 60F)),
|
||||
HasProgress = progress.Any()
|
||||
});
|
||||
return Ok(_readerService.GetTimeEstimate(series.WordCount, series.Pages, series.Format == MangaFormat.Epub,
|
||||
progress.Any()));
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -709,24 +675,12 @@ namespace API.Controllers
|
|||
// Word count
|
||||
var progressCount = chapters.Sum(c => c.WordCount);
|
||||
var wordsLeft = series.WordCount - progressCount;
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((wordsLeft / ReaderService.MinWordsPerHour)),
|
||||
MaxHours = (int) Math.Round((wordsLeft / ReaderService.MaxWordsPerHour)),
|
||||
AvgHours = (int) Math.Round((wordsLeft / ReaderService.AvgWordsPerHour)),
|
||||
HasProgress = progressCount > 0
|
||||
});
|
||||
return _readerService.GetTimeEstimate(wordsLeft, 0, true, progressCount > 0);
|
||||
}
|
||||
|
||||
var progressPageCount = progress.Sum(p => p.PagesRead);
|
||||
var pagesLeft = series.Pages - progressPageCount;
|
||||
return Ok(new HourEstimateRangeDto()
|
||||
{
|
||||
MinHours = (int) Math.Round((pagesLeft / ReaderService.MinPagesPerMinute / 60F)),
|
||||
MaxHours = (int) Math.Round((pagesLeft / ReaderService.MaxPagesPerMinute / 60F)),
|
||||
AvgHours = (int) Math.Round((pagesLeft / ReaderService.AvgPagesPerMinute / 60F)),
|
||||
HasProgress = progressPageCount > 0
|
||||
});
|
||||
return _readerService.GetTimeEstimate(0, pagesLeft, false, progressPageCount > 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
20
API/DTOs/JumpBar/JumpKeyDto.cs
Normal file
20
API/DTOs/JumpBar/JumpKeyDto.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
namespace API.DTOs.JumpBar;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an individual button in a Jump Bar
|
||||
/// </summary>
|
||||
public class JumpKeyDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of items in this Key
|
||||
/// </summary>
|
||||
public int Size { get; set; }
|
||||
/// <summary>
|
||||
/// Code to use in URL (url encoded)
|
||||
/// </summary>
|
||||
public string Key { get; set; }
|
||||
/// <summary>
|
||||
/// What is visible to user
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace API.DTOs;
|
||||
namespace API.DTOs.SeriesDetail;
|
||||
|
||||
/// <summary>
|
||||
/// This is a special DTO for a UI page in Kavita. This performs sorting and grouping and returns exactly what UI requires for layout.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using API.DTOs;
|
||||
using API.DTOs.JumpBar;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using AutoMapper;
|
||||
|
|
@ -38,6 +40,7 @@ public interface ILibraryRepository
|
|||
Task<LibraryType> GetLibraryTypeAsync(int libraryId);
|
||||
Task<IEnumerable<Library>> GetLibraryForIdsAsync(IList<int> libraryIds);
|
||||
Task<int> GetTotalFiles();
|
||||
IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId);
|
||||
}
|
||||
|
||||
public class LibraryRepository : ILibraryRepository
|
||||
|
|
@ -123,6 +126,37 @@ public class LibraryRepository : ILibraryRepository
|
|||
return await _context.MangaFile.CountAsync();
|
||||
}
|
||||
|
||||
public IEnumerable<JumpKeyDto> GetJumpBarAsync(int libraryId)
|
||||
{
|
||||
var seriesSortCharacters = _context.Series.Where(s => s.LibraryId == libraryId)
|
||||
.Select(s => s.SortName.ToUpper())
|
||||
.OrderBy(s => s)
|
||||
.AsEnumerable()
|
||||
.Select(s => s[0]);
|
||||
|
||||
// Map the title to the number of entities
|
||||
var firstCharacterMap = new Dictionary<char, int>();
|
||||
foreach (var sortChar in seriesSortCharacters)
|
||||
{
|
||||
var c = sortChar;
|
||||
var isAlpha = char.IsLetter(sortChar);
|
||||
if (!isAlpha) c = '#';
|
||||
if (!firstCharacterMap.ContainsKey(c))
|
||||
{
|
||||
firstCharacterMap[c] = 0;
|
||||
}
|
||||
|
||||
firstCharacterMap[c] += 1;
|
||||
}
|
||||
|
||||
return firstCharacterMap.Keys.Select(k => new JumpKeyDto()
|
||||
{
|
||||
Key = k + string.Empty,
|
||||
Size = firstCharacterMap[k],
|
||||
Title = k + string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<LibraryDto>> GetLibraryDtosAsync()
|
||||
{
|
||||
return await _context.Library
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ public interface IUserRepository
|
|||
Task<IEnumerable<AppUser>> GetAllUsers();
|
||||
|
||||
Task<IEnumerable<AppUserPreferences>> GetAllPreferencesByThemeAsync(int themeId);
|
||||
Task<bool> HasAccessToLibrary(int libraryId, int userId);
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
|
|
@ -238,6 +239,13 @@ public class UserRepository : IUserRepository
|
|||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> HasAccessToLibrary(int libraryId, int userId)
|
||||
{
|
||||
return await _context.Library
|
||||
.Include(l => l.AppUsers)
|
||||
.AnyAsync(library => library.AppUsers.Any(user => user.Id == userId));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<AppUser>> GetAdminUsersAsync()
|
||||
{
|
||||
return await _userManager.GetUsersInRoleAsync(PolicyConstants.AdminRole);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using API.Comparators;
|
|||
using API.Data;
|
||||
using API.Data.Repositories;
|
||||
using API.DTOs;
|
||||
using API.DTOs.Reader;
|
||||
using API.Entities;
|
||||
using API.Extensions;
|
||||
using API.SignalR;
|
||||
|
|
@ -28,6 +29,7 @@ public interface IReaderService
|
|||
Task<ChapterDto> GetContinuePoint(int seriesId, int userId);
|
||||
Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber);
|
||||
Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber);
|
||||
HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub, bool hasProgress = false);
|
||||
}
|
||||
|
||||
public class ReaderService : IReaderService
|
||||
|
|
@ -168,7 +170,7 @@ public class ReaderService : IReaderService
|
|||
var progresses = user.Progresses.Where(x => x.ChapterId == chapter.Id && x.AppUserId == user.Id).ToList();
|
||||
if (progresses.Count > 1)
|
||||
{
|
||||
user.Progresses = new List<AppUserProgress>()
|
||||
user.Progresses = new List<AppUserProgress>
|
||||
{
|
||||
user.Progresses.First()
|
||||
};
|
||||
|
|
@ -478,7 +480,7 @@ public class ReaderService : IReaderService
|
|||
/// <param name="chapterNumber"></param>
|
||||
public async Task MarkChaptersUntilAsRead(AppUser user, int seriesId, float chapterNumber)
|
||||
{
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int>() { seriesId }, true);
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { seriesId }, true);
|
||||
foreach (var volume in volumes.OrderBy(v => v.Number))
|
||||
{
|
||||
var chapters = volume.Chapters
|
||||
|
|
@ -490,10 +492,57 @@ public class ReaderService : IReaderService
|
|||
|
||||
public async Task MarkVolumesUntilAsRead(AppUser user, int seriesId, int volumeNumber)
|
||||
{
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int>() { seriesId }, true);
|
||||
var volumes = await _unitOfWork.VolumeRepository.GetVolumesForSeriesAsync(new List<int> { seriesId }, true);
|
||||
foreach (var volume in volumes.OrderBy(v => v.Number).Where(v => v.Number <= volumeNumber && v.Number > 0))
|
||||
{
|
||||
MarkChaptersAsRead(user, volume.SeriesId, volume.Chapters);
|
||||
}
|
||||
}
|
||||
|
||||
public HourEstimateRangeDto GetTimeEstimate(long wordCount, int pageCount, bool isEpub, bool hasProgress = false)
|
||||
{
|
||||
if (isEpub)
|
||||
{
|
||||
var minHours = Math.Max((int) Math.Round((wordCount / MinWordsPerHour)), 1);
|
||||
var maxHours = Math.Max((int) Math.Round((wordCount / MaxWordsPerHour)), 1);
|
||||
if (maxHours < minHours)
|
||||
{
|
||||
return new HourEstimateRangeDto
|
||||
{
|
||||
MinHours = maxHours,
|
||||
MaxHours = minHours,
|
||||
AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour)),
|
||||
HasProgress = hasProgress
|
||||
};
|
||||
}
|
||||
return new HourEstimateRangeDto
|
||||
{
|
||||
MinHours = minHours,
|
||||
MaxHours = maxHours,
|
||||
AvgHours = (int) Math.Round((wordCount / AvgWordsPerHour)),
|
||||
HasProgress = hasProgress
|
||||
};
|
||||
}
|
||||
|
||||
var minHoursPages = Math.Max((int) Math.Round((pageCount / MinPagesPerMinute / 60F)), 1);
|
||||
var maxHoursPages = Math.Max((int) Math.Round((pageCount / MaxPagesPerMinute / 60F)), 1);
|
||||
if (maxHoursPages < minHoursPages)
|
||||
{
|
||||
return new HourEstimateRangeDto
|
||||
{
|
||||
MinHours = maxHoursPages,
|
||||
MaxHours = minHoursPages,
|
||||
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F)),
|
||||
HasProgress = hasProgress
|
||||
};
|
||||
}
|
||||
|
||||
return new HourEstimateRangeDto
|
||||
{
|
||||
MinHours = minHoursPages,
|
||||
MaxHours = maxHoursPages,
|
||||
AvgHours = (int) Math.Round((pageCount / AvgPagesPerMinute / 60F)),
|
||||
HasProgress = hasProgress
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ using API.Data;
|
|||
using API.DTOs;
|
||||
using API.DTOs.CollectionTags;
|
||||
using API.DTOs.Metadata;
|
||||
using API.DTOs.SeriesDetail;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using API.Helpers;
|
||||
using API.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -98,7 +98,7 @@ public class SeriesService : ISeriesService
|
|||
series.Metadata.SummaryLocked = true;
|
||||
}
|
||||
|
||||
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata.Language)
|
||||
if (series.Metadata.Language != updateSeriesMetadataDto.SeriesMetadata?.Language)
|
||||
{
|
||||
series.Metadata.Language = updateSeriesMetadataDto.SeriesMetadata?.Language;
|
||||
series.Metadata.LanguageLocked = true;
|
||||
|
|
@ -112,7 +112,7 @@ public class SeriesService : ISeriesService
|
|||
});
|
||||
|
||||
series.Metadata.Genres ??= new List<Genre>();
|
||||
UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata.Genres, series, allGenres, (genre) =>
|
||||
UpdateGenreList(updateSeriesMetadataDto.SeriesMetadata?.Genres, series, allGenres, (genre) =>
|
||||
{
|
||||
series.Metadata.Genres.Add(genre);
|
||||
}, () => series.Metadata.GenresLocked = true);
|
||||
|
|
@ -521,11 +521,11 @@ public class SeriesService : ISeriesService
|
|||
/// <summary>
|
||||
/// Should we show the given chapter on the UI. We only show non-specials and non-zero chapters.
|
||||
/// </summary>
|
||||
/// <param name="c"></param>
|
||||
/// <param name="chapter"></param>
|
||||
/// <returns></returns>
|
||||
private static bool ShouldIncludeChapter(ChapterDto c)
|
||||
private static bool ShouldIncludeChapter(ChapterDto chapter)
|
||||
{
|
||||
return !c.IsSpecial && !c.Number.Equals(Parser.Parser.DefaultChapter);
|
||||
return !chapter.IsSpecial && !chapter.Number.Equals(Parser.Parser.DefaultChapter);
|
||||
}
|
||||
|
||||
public static void RenameVolumeName(ChapterDto firstChapter, VolumeDto volume, LibraryType libraryType)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue