mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Bugfix polishing (#1245)
* Fixed a bug where volumes that are a range fail to generate series detail * Moved tags closer to genre instead of between different people * Optimized the query for On Deck * Adjusted mime types to map to cbX types instead of their generic compression methods. * Added wiki documentation into invite user flow and register admin user to help users understand email isn't required and they can host their own service. * Refactored the document height to be set and removed on nav service, so the book reader and manga reader aren't broken. * Refactored On Deck to first be completely streamed to UI, without having to do any processing in memory. Rewrote the query so that we sort by progress then chapter added. Progress is 30 days inclusive, chapter added is 7 days. * Fixed an issue where epub date parsing would sometimes fail when it's only a year or not a year at all * Fixed a bug where incognito mode would report progress * Fixed a bug where bulk selection in storyline tab wouldn't properly run the action on the correct chapters (if selecting from volume -> chapter). * Removed a - 1 from total page from card progress bar as the original bug was fixed some time ago * Fixed a bug where the logic for filtering out a progress event for current logged in user didn't check properly when user is logged out. * When a file doesn't exist and we are trying to read, throw a kavita exception to the UI layer and log. * Removed unneeded variable and added some jsdoc
This commit is contained in:
parent
3334b0ce3f
commit
85f3b620af
@ -211,7 +211,6 @@ namespace API.Controllers
|
||||
var chapter = await _cacheService.Ensure(chapterId);
|
||||
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
||||
|
||||
|
||||
using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
|
||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||
|
||||
|
@ -389,12 +389,8 @@ public class OpdsController : BaseApiController
|
||||
var userParams = new UserParams()
|
||||
{
|
||||
PageNumber = pageNumber,
|
||||
PageSize = 20
|
||||
};
|
||||
var results = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
|
||||
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize)
|
||||
.Take(userParams.PageSize).ToList();
|
||||
var pagedList = new PagedList<SeriesDto>(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
|
||||
|
||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||
|
||||
|
@ -243,12 +243,8 @@ namespace API.Controllers
|
||||
[HttpPost("on-deck")]
|
||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
||||
{
|
||||
// NOTE: This has to be done manually like this due to the DistinctBy requirement
|
||||
var userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||
var results = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
|
||||
|
||||
var listResults = results.DistinctBy(s => s.Name).Skip((userParams.PageNumber - 1) * userParams.PageSize).Take(userParams.PageSize).ToList();
|
||||
var pagedList = new PagedList<SeriesDto>(listResults, listResults.Count, userParams.PageNumber, userParams.PageSize);
|
||||
var pagedList = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
|
||||
|
||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
|
||||
|
||||
|
@ -94,7 +94,7 @@ public interface ISeriesRepository
|
||||
/// <returns></returns>
|
||||
Task AddSeriesModifiers(int userId, List<SeriesDto> series);
|
||||
Task<string> GetSeriesCoverImageAsync(int seriesId);
|
||||
Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true);
|
||||
Task<PagedList<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter);
|
||||
Task<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
||||
@ -111,7 +111,6 @@ public interface ISeriesRepository
|
||||
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||
|
||||
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
||||
Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams);
|
||||
Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams);
|
||||
@ -669,50 +668,48 @@ public class SeriesRepository : ISeriesRepository
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Series that the user has some partial progress on. Sorts based on activity. Sort first by User progress, but if a series
|
||||
/// has been updated recently, bump it to the front.
|
||||
/// Returns Series that the user has some partial progress on. Sorts based on activity. Sort first by User progress, then
|
||||
/// by when chapters have been added to series. Restricts progress in the past 30 days and chapters being added to last 7.
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
||||
/// <param name="userParams">Pagination information</param>
|
||||
/// <param name="filter">Optional (default null) filter on query</param>
|
||||
/// <returns></returns>
|
||||
public async Task<IEnumerable<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter, bool cutoffOnDate = true)
|
||||
public async Task<PagedList<SeriesDto>> GetOnDeck(int userId, int libraryId, UserParams userParams, FilterDto filter)
|
||||
{
|
||||
var query = (await CreateFilteredSearchQueryable(userId, libraryId, filter))
|
||||
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
|
||||
new
|
||||
{
|
||||
Series = s,
|
||||
PagesRead = _context.AppUserProgresses.Where(s1 => s1.SeriesId == s.Id && s1.AppUserId == userId)
|
||||
.Sum(s1 => s1.PagesRead),
|
||||
progress.AppUserId,
|
||||
LastReadingProgress = _context.AppUserProgresses
|
||||
.Where(p => p.Id == progress.Id && p.AppUserId == userId)
|
||||
.Max(p => p.LastModified),
|
||||
LastModified = _context.AppUserProgresses.Where(p => p.Id == progress.Id && p.AppUserId == userId).Max(p => p.LastModified),
|
||||
s.LastChapterAdded
|
||||
});
|
||||
if (cutoffOnDate)
|
||||
{
|
||||
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
||||
query = query.Where(d => d.LastReadingProgress >= cutoffProgressPoint || d.LastChapterAdded >= cutoffProgressPoint);
|
||||
}
|
||||
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
||||
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7);
|
||||
|
||||
var retSeries = query.Where(s => s.AppUserId == userId
|
||||
&& s.PagesRead > 0
|
||||
&& s.PagesRead < s.Series.Pages)
|
||||
.OrderByDescending(s => s.LastChapterAdded)
|
||||
.ThenByDescending(s => s.LastReadingProgress)
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
|
||||
|
||||
var query = _context.Series
|
||||
.Where(s => usersSeriesIds.Contains(s.Id))
|
||||
.Select(s => new
|
||||
{
|
||||
Series = s,
|
||||
PagesRead = _context.AppUserProgresses.Where(p => p.SeriesId == s.Id && p.AppUserId == userId)
|
||||
.Sum(s1 => s1.PagesRead),
|
||||
LatestReadDate = _context.AppUserProgresses
|
||||
.Where(p => p.SeriesId == s.Id && p.AppUserId == userId)
|
||||
.Max(p => p.LastModified),
|
||||
s.LastChapterAdded,
|
||||
})
|
||||
.Where(s => s.PagesRead > 0
|
||||
&& s.PagesRead < s.Series.Pages)
|
||||
.Where(d => d.LatestReadDate >= cutoffProgressPoint || d.LastChapterAdded >= cutoffLastAddedPoint).OrderByDescending(s => s.LatestReadDate)
|
||||
.ThenByDescending(s => s.LastChapterAdded)
|
||||
.Select(s => s.Series)
|
||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||
.AsSplitQuery()
|
||||
.AsNoTracking();
|
||||
|
||||
// Pagination does not work for this query as when we pull the data back, we get multiple rows of the same series. See controller for pagination code
|
||||
return await retSeries.ToListAsync();
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
|
||||
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter)
|
||||
{
|
||||
var userLibraries = await GetUserLibraries(libraryId, userId);
|
||||
@ -1044,9 +1041,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
|
||||
var query = _context.Series
|
||||
@ -1061,9 +1056,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||
@ -1110,9 +1103,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithHighRating = _context.AppUserRating
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId) && s.Rating > 4)
|
||||
@ -1131,9 +1122,7 @@ public class SeriesRepository : ISeriesRepository
|
||||
|
||||
public async Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams)
|
||||
{
|
||||
var libraryIds = _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
||||
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||
@ -1152,6 +1141,19 @@ public class SeriesRepository : ISeriesRepository
|
||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all library ids for a user
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="libraryId">0 for no library filter</param>
|
||||
/// <returns></returns>
|
||||
private IQueryable<int> GetLibraryIdsForUser(int userId, int libraryId)
|
||||
{
|
||||
return _context.AppUser
|
||||
.Where(u => u.Id == userId)
|
||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId || libraryId == 0).Select(lib => lib.Id));
|
||||
}
|
||||
|
||||
public async Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId)
|
||||
{
|
||||
var libraryIds = GetLibraryIdsForUser(userId);
|
||||
|
@ -437,6 +437,12 @@ namespace API.Services
|
||||
|
||||
if (Directory.Exists(extractPath)) return;
|
||||
|
||||
if (!_directoryService.FileSystem.File.Exists(archivePath))
|
||||
{
|
||||
_logger.LogError("{Archive} does not exist on disk", archivePath);
|
||||
throw new KavitaException($"{archivePath} does not exist on disk");
|
||||
}
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
|
@ -399,19 +399,36 @@ namespace API.Services
|
||||
{
|
||||
publicationDate = epubBook.Schema.Package.Metadata.Dates.FirstOrDefault()?.Date;
|
||||
}
|
||||
var dateParsed = DateTime.TryParse(publicationDate, out var date);
|
||||
var year = 0;
|
||||
var month = 0;
|
||||
var day = 0;
|
||||
switch (dateParsed)
|
||||
{
|
||||
case true:
|
||||
year = date.Year;
|
||||
month = date.Month;
|
||||
day = date.Day;
|
||||
break;
|
||||
case false when !string.IsNullOrEmpty(publicationDate) && publicationDate.Length == 4:
|
||||
int.TryParse(publicationDate, out year);
|
||||
break;
|
||||
}
|
||||
|
||||
var info = new ComicInfo()
|
||||
{
|
||||
Summary = epubBook.Schema.Package.Metadata.Description,
|
||||
Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.Parser.CleanAuthor(c.Creator))),
|
||||
Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers),
|
||||
Month = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Month : 0,
|
||||
Day = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Day : 0,
|
||||
Year = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Year : 0,
|
||||
Month = month,
|
||||
Day = day,
|
||||
Year = year,
|
||||
Title = epubBook.Title,
|
||||
Genre = string.Join(",", epubBook.Schema.Package.Metadata.Subjects.Select(s => s.ToLower().Trim())),
|
||||
LanguageISO = epubBook.Schema.Package.Metadata.Languages.FirstOrDefault() ?? string.Empty
|
||||
|
||||
};
|
||||
ComicInfo.CleanComicInfo(info);
|
||||
|
||||
// Parse tags not exposed via Library
|
||||
foreach (var metadataItem in epubBook.Schema.Package.Metadata.MetaItems)
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using API.Data;
|
||||
using API.Entities;
|
||||
using API.Entities.Enums;
|
||||
using API.Extensions;
|
||||
using Kavita.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace API.Services
|
||||
@ -145,6 +146,12 @@ namespace API.Services
|
||||
else if (file.Format == MangaFormat.Epub)
|
||||
{
|
||||
removeNonImages = false;
|
||||
if (!_directoryService.FileSystem.File.Exists(files[0].FilePath))
|
||||
{
|
||||
_logger.LogError("{Archive} does not exist on disk", files[0].FilePath);
|
||||
throw new KavitaException($"{files[0].FilePath} does not exist on disk");
|
||||
}
|
||||
|
||||
_directoryService.ExistOrCreate(extractPath);
|
||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||
}
|
||||
|
@ -45,12 +45,16 @@ public class DownloadService : IDownloadService
|
||||
{
|
||||
contentType = Path.GetExtension(filepath).ToLowerInvariant() switch
|
||||
{
|
||||
".cbz" => "application/zip",
|
||||
".cbr" => "application/vnd.rar",
|
||||
".cb7" => "application/x-compressed",
|
||||
".cbz" => "application/x-cbz",
|
||||
".cbr" => "application/x-cbr",
|
||||
".cb7" => "application/x-cb7",
|
||||
".cbt" => "application/x-cbt",
|
||||
".epub" => "application/epub+zip",
|
||||
".7z" => "application/x-7z-compressed",
|
||||
".7zip" => "application/x-7z-compressed",
|
||||
".rar" => "application/vnd.rar",
|
||||
".zip" => "application/zip",
|
||||
".tar.gz" => "application/gzip",
|
||||
".pdf" => "application/pdf",
|
||||
_ => contentType
|
||||
};
|
||||
|
@ -456,7 +456,7 @@ public class SeriesService : ISeriesService
|
||||
|
||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||
.OrderBy(v => float.Parse(v.Name))
|
||||
.OrderBy(v => Parser.Parser.MinimumNumberFromRange(v.Name))
|
||||
.ToList();
|
||||
var chapters = volumes.SelectMany(v => v.Chapters).ToList();
|
||||
|
||||
|
@ -41,6 +41,8 @@ export class NavService {
|
||||
*/
|
||||
showNavBar() {
|
||||
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '56px');
|
||||
this.renderer.setStyle(this.document.querySelector('body'), 'height', 'calc(var(--vh)*100 - 56px)');
|
||||
this.renderer.setStyle(this.document.querySelector('html'), 'height', 'calc(var(--vh)*100 - 56px)');
|
||||
this.navbarVisibleSource.next(true);
|
||||
}
|
||||
|
||||
@ -49,6 +51,8 @@ export class NavService {
|
||||
*/
|
||||
hideNavBar() {
|
||||
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '0px');
|
||||
this.renderer.removeStyle(this.document.querySelector('body'), 'height');
|
||||
this.renderer.removeStyle(this.document.querySelector('html'), 'height');
|
||||
this.navbarVisibleSource.next(false);
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Invite a user to your server. Enter their email in and we will send them an email to create an account. If you do not want to use our email service, you can host your own
|
||||
Invite a user to your server. Enter their email in and we will send them an email to create an account. If you do not want to use our email service, you can <a href="https://wiki.kavitareader.com/en/guides/misc/email" target="_blank" rel="noreferrer">host your own</a>
|
||||
email service or use a fake email (Forgot User will not work). A link will be presented regardless and can be used to setup the email account manually.
|
||||
</p>
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
<app-nav-header></app-nav-header>
|
||||
<div [ngClass]="{'closed' : (navService?.sideNavCollapsed$ | async), 'content-wrapper': navService.sideNavVisibility$ | async}">
|
||||
<a id="content"></a>
|
||||
<app-side-nav *ngIf="navService.sideNavVisibility$ | async"></app-side-nav>
|
||||
<div class="container-fluid">
|
||||
<app-side-nav *ngIf="navService.sideNavVisibility$ | async as sideNavVisibile"></app-side-nav>
|
||||
<div class="container-fluid" [ngClass]="{'g-0': !(navService.sideNavVisibility$ | async)}">
|
||||
<div style="padding: 20px 0;" *ngIf="navService.sideNavVisibility$ | async else noSideNav">
|
||||
<div class="companion-bar" [ngClass]="{'companion-bar-content': !(navService?.sideNavCollapsed$ | async)}">
|
||||
<router-outlet></router-outlet>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image>
|
||||
</ng-container>
|
||||
|
||||
<div class="progress-banner" *ngIf="read < total && total > 0 && read !== (total -1)">
|
||||
<div class="progress-banner" *ngIf="read < total && total > 0 && read !== total">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
||||
|
||||
<span class="download" *ngIf="download$ | async as download">
|
||||
|
@ -5,7 +5,7 @@ $image-height: 230px;
|
||||
$image-width: 160px;
|
||||
|
||||
.error-banner {
|
||||
width: 160px;
|
||||
width: $image-width;
|
||||
height: 18px;
|
||||
background-color: var(--toast-error-bg-color);
|
||||
font-size: 12px;
|
||||
@ -52,7 +52,7 @@ $image-width: 160px;
|
||||
}
|
||||
|
||||
.progress-banner {
|
||||
width: 160px;
|
||||
width: $image-width;
|
||||
height: 5px;
|
||||
|
||||
.progress {
|
||||
@ -163,7 +163,7 @@ $image-width: 160px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 230px;
|
||||
height: $image-height;
|
||||
z-index: 10;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
@ -103,7 +103,15 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
download$: Observable<Download> | null = null;
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
isShiftDown: boolean = false;
|
||||
|
||||
/**
|
||||
* Handles touch events for selection on mobile devices
|
||||
*/
|
||||
prevTouchTime: number = 0;
|
||||
/**
|
||||
* Handles touch events for selection on mobile devices to ensure you are touch scrolling
|
||||
*/
|
||||
prevOffset: number = 0;
|
||||
|
||||
private user: User | undefined;
|
||||
|
||||
@ -157,11 +165,11 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
|
||||
map(evt => evt.payload as UserProgressUpdateEvent), takeUntil(this.onDestroy)).subscribe(updateEvent => {
|
||||
if (this.user !== undefined && this.user.username !== updateEvent.username) return;
|
||||
if (this.user === undefined || this.user.username !== updateEvent.username) return;
|
||||
if (this.utilityService.isChapter(this.entity) && updateEvent.chapterId !== this.entity.id) return;
|
||||
if (this.utilityService.isVolume(this.entity) && updateEvent.volumeId !== this.entity.id) return;
|
||||
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||
|
||||
|
||||
this.read = updateEvent.pagesRead;
|
||||
});
|
||||
}
|
||||
@ -172,8 +180,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
|
||||
prevTouchTime: number = 0;
|
||||
prevOffset: number = 0;
|
||||
@HostListener('touchstart', ['$event'])
|
||||
onTouchStart(event: TouchEvent) {
|
||||
if (!this.allowSelection) return;
|
||||
@ -195,7 +201,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (verticalOffset != this.prevOffset) {
|
||||
this.prevTouchTime = 0;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1188,7 +1188,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
tempPageNum = this.pageNum + 1;
|
||||
}
|
||||
|
||||
if (!this.incognitoMode || !this.bookmarkMode) {
|
||||
if (!this.incognitoMode && !this.bookmarkMode) {
|
||||
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, tempPageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,12 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3" style="width:100%">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||
<label for="email" class="form-label">Email</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #emailTooltip>Email does not have to be valid, it is used for forgot password flow. It is not sent outside the server unless forgot password is used without a custom email service host.</ng-template>
|
||||
<span class="visually-hidden" id="email-help">
|
||||
<ng-container [ngTemplateOutlet]="emailTooltip"></ng-container>
|
||||
</span>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" required aria-describedby="email-help">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||
This field is required
|
||||
|
@ -134,7 +134,11 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
||||
const selectedChapterIndexes = this.bulkSelectionService.getSelectedCardsForSource('chapter');
|
||||
const selectedSpecialIndexes = this.bulkSelectionService.getSelectedCardsForSource('special');
|
||||
|
||||
const selectedChapterIds = this.chapters.filter((_chapter, index: number) => selectedChapterIndexes.includes(index + ''));
|
||||
// NOTE: This needs to check current tab as chapter array will be different
|
||||
let chapterArray = this.storyChapters;
|
||||
if (this.activeTabId === TabID.Chapters) chapterArray = this.chapters;
|
||||
|
||||
const selectedChapterIds = chapterArray.filter((_chapter, index: number) => selectedChapterIndexes.includes(index + ''));
|
||||
const selectedVolumeIds = this.volumes.filter((_volume, index: number) => selectedVolumeIndexes.includes(index + ''));
|
||||
const selectedSpecials = this.specials.filter((_chapter, index: number) => selectedSpecialIndexes.includes(index + ''));
|
||||
const chapters = [...selectedChapterIds, ...selectedSpecials];
|
||||
|
@ -34,6 +34,18 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Tags</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.collectionTags && seriesMetadata.collectionTags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Collections</h5>
|
||||
@ -163,18 +175,6 @@
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Tags</h5>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<app-badge-expander [items]="seriesMetadata.tags">
|
||||
<ng-template #badgeExpanderItem let-item let-position="idx">
|
||||
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
|
||||
</ng-template>
|
||||
</app-badge-expander>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0 mt-1" *ngIf="seriesMetadata.translators && seriesMetadata.translators.length > 0">
|
||||
<div class="col-md-4">
|
||||
<h5>Translators</h5>
|
||||
|
@ -63,10 +63,6 @@ label, select, .clickable {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: calc(var(--vh)*100 - 56px);
|
||||
}
|
||||
|
||||
// Needed for fullscreen
|
||||
app-root {
|
||||
background-color: inherit;
|
||||
|
Loading…
x
Reference in New Issue
Block a user