mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-31 04:04:19 -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 chapter = await _cacheService.Ensure(chapterId);
|
||||||
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
var path = _cacheService.GetCachedEpubFile(chapter.Id, chapter);
|
||||||
|
|
||||||
|
|
||||||
using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
|
using var book = await EpubReader.OpenBookAsync(path, BookService.BookReaderOptions);
|
||||||
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
var mappings = await _bookService.CreateKeyToPageMappingAsync(book);
|
||||||
|
|
||||||
|
@ -389,12 +389,8 @@ public class OpdsController : BaseApiController
|
|||||||
var userParams = new UserParams()
|
var userParams = new UserParams()
|
||||||
{
|
{
|
||||||
PageNumber = pageNumber,
|
PageNumber = pageNumber,
|
||||||
PageSize = 20
|
|
||||||
};
|
};
|
||||||
var results = await _unitOfWork.SeriesRepository.GetOnDeck(userId, 0, userParams, _filterDto);
|
var pagedList = 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);
|
|
||||||
|
|
||||||
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
Response.AddPaginationHeader(pagedList.CurrentPage, pagedList.PageSize, pagedList.TotalCount, pagedList.TotalPages);
|
||||||
|
|
||||||
|
@ -243,12 +243,8 @@ namespace API.Controllers
|
|||||||
[HttpPost("on-deck")]
|
[HttpPost("on-deck")]
|
||||||
public async Task<ActionResult<IEnumerable<SeriesDto>>> GetOnDeck(FilterDto filterDto, [FromQuery] UserParams userParams, [FromQuery] int libraryId = 0)
|
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 userId = await _unitOfWork.UserRepository.GetUserIdByUsernameAsync(User.GetUsername());
|
||||||
var results = await _unitOfWork.SeriesRepository.GetOnDeck(userId, libraryId, userParams, filterDto);
|
var pagedList = 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);
|
|
||||||
|
|
||||||
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
|
await _unitOfWork.SeriesRepository.AddSeriesModifiers(userId, pagedList);
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ public interface ISeriesRepository
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task AddSeriesModifiers(int userId, List<SeriesDto> series);
|
Task AddSeriesModifiers(int userId, List<SeriesDto> series);
|
||||||
Task<string> GetSeriesCoverImageAsync(int seriesId);
|
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<PagedList<SeriesDto>> GetRecentlyAdded(int libraryId, int userId, UserParams userParams, FilterDto filter);
|
||||||
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
Task<SeriesMetadataDto> GetSeriesMetadata(int seriesId);
|
||||||
Task<PagedList<SeriesDto>> GetSeriesDtoForCollectionAsync(int collectionId, int userId, UserParams userParams);
|
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
|
IEnumerable<PublicationStatusDto> GetAllPublicationStatusesDtosForLibrariesAsync(List<int> libraryIds); // TODO: Move to LibraryRepository
|
||||||
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
Task<IEnumerable<GroupedSeriesDto>> GetRecentlyUpdatedSeries(int userId, int pageSize = 30);
|
||||||
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId);
|
||||||
|
|
||||||
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
Task<IEnumerable<SeriesDto>> GetSeriesForRelationKind(int userId, int seriesId, RelationKind kind);
|
||||||
Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams);
|
Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams);
|
||||||
Task<PagedList<SeriesDto>> GetHighlyRated(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>
|
/// <summary>
|
||||||
/// Returns Series that the user has some partial progress on. Sorts based on activity. Sort first by User progress, but if a series
|
/// Returns Series that the user has some partial progress on. Sorts based on activity. Sort first by User progress, then
|
||||||
/// has been updated recently, bump it to the front.
|
/// by when chapters have been added to series. Restricts progress in the past 30 days and chapters being added to last 7.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
/// <param name="libraryId">Library to restrict to, if 0, will apply to all libraries</param>
|
||||||
/// <param name="userParams">Pagination information</param>
|
/// <param name="userParams">Pagination information</param>
|
||||||
/// <param name="filter">Optional (default null) filter on query</param>
|
/// <param name="filter">Optional (default null) filter on query</param>
|
||||||
/// <returns></returns>
|
/// <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))
|
var cutoffProgressPoint = DateTime.Now - TimeSpan.FromDays(30);
|
||||||
.Join(_context.AppUserProgresses, s => s.Id, progress => progress.SeriesId, (s, progress) =>
|
var cutoffLastAddedPoint = DateTime.Now - TimeSpan.FromDays(7);
|
||||||
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 retSeries = query.Where(s => s.AppUserId == userId
|
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||||
&& s.PagesRead > 0
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
&& s.PagesRead < s.Series.Pages)
|
|
||||||
.OrderByDescending(s => s.LastChapterAdded)
|
|
||||||
.ThenByDescending(s => s.LastReadingProgress)
|
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)
|
.Select(s => s.Series)
|
||||||
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
.ProjectTo<SeriesDto>(_mapper.ConfigurationProvider)
|
||||||
.AsSplitQuery()
|
.AsSplitQuery()
|
||||||
.AsNoTracking();
|
.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 PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
||||||
return await retSeries.ToListAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter)
|
private async Task<IQueryable<Series>> CreateFilteredSearchQueryable(int userId, int libraryId, FilterDto filter)
|
||||||
{
|
{
|
||||||
var userLibraries = await GetUserLibraries(libraryId, userId);
|
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)
|
public async Task<PagedList<SeriesDto>> GetMoreIn(int userId, int libraryId, int genreId, UserParams userParams)
|
||||||
{
|
{
|
||||||
var libraryIds = _context.AppUser
|
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||||
.Where(u => u.Id == userId)
|
|
||||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
|
||||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
|
|
||||||
var query = _context.Series
|
var query = _context.Series
|
||||||
@ -1061,9 +1056,7 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
|
|
||||||
public async Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams)
|
public async Task<PagedList<SeriesDto>> GetRediscover(int userId, int libraryId, UserParams userParams)
|
||||||
{
|
{
|
||||||
var libraryIds = _context.AppUser
|
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||||
.Where(u => u.Id == userId)
|
|
||||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
|
||||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
.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)
|
public async Task<PagedList<SeriesDto>> GetHighlyRated(int userId, int libraryId, UserParams userParams)
|
||||||
{
|
{
|
||||||
var libraryIds = _context.AppUser
|
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||||
.Where(u => u.Id == userId)
|
|
||||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
|
||||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
var distinctSeriesIdsWithHighRating = _context.AppUserRating
|
var distinctSeriesIdsWithHighRating = _context.AppUserRating
|
||||||
.Where(s => usersSeriesIds.Contains(s.SeriesId) && s.Rating > 4)
|
.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)
|
public async Task<PagedList<SeriesDto>> GetQuickReads(int userId, int libraryId, UserParams userParams)
|
||||||
{
|
{
|
||||||
var libraryIds = _context.AppUser
|
var libraryIds = GetLibraryIdsForUser(userId, libraryId);
|
||||||
.Where(u => u.Id == userId)
|
|
||||||
.SelectMany(l => l.Libraries.Where(l => l.Id == libraryId).Select(lib => lib.Id));
|
|
||||||
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
var usersSeriesIds = GetSeriesIdsForLibraryIds(libraryIds);
|
||||||
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
var distinctSeriesIdsWithProgress = _context.AppUserProgresses
|
||||||
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
.Where(s => usersSeriesIds.Contains(s.SeriesId))
|
||||||
@ -1152,6 +1141,19 @@ public class SeriesRepository : ISeriesRepository
|
|||||||
return await PagedList<SeriesDto>.CreateAsync(query, userParams.PageNumber, userParams.PageSize);
|
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)
|
public async Task<RelatedSeriesDto> GetRelatedSeries(int userId, int seriesId)
|
||||||
{
|
{
|
||||||
var libraryIds = GetLibraryIdsForUser(userId);
|
var libraryIds = GetLibraryIdsForUser(userId);
|
||||||
|
@ -437,6 +437,12 @@ namespace API.Services
|
|||||||
|
|
||||||
if (Directory.Exists(extractPath)) return;
|
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();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -399,19 +399,36 @@ namespace API.Services
|
|||||||
{
|
{
|
||||||
publicationDate = epubBook.Schema.Package.Metadata.Dates.FirstOrDefault()?.Date;
|
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()
|
var info = new ComicInfo()
|
||||||
{
|
{
|
||||||
Summary = epubBook.Schema.Package.Metadata.Description,
|
Summary = epubBook.Schema.Package.Metadata.Description,
|
||||||
Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.Parser.CleanAuthor(c.Creator))),
|
Writer = string.Join(",", epubBook.Schema.Package.Metadata.Creators.Select(c => Parser.Parser.CleanAuthor(c.Creator))),
|
||||||
Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers),
|
Publisher = string.Join(",", epubBook.Schema.Package.Metadata.Publishers),
|
||||||
Month = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Month : 0,
|
Month = month,
|
||||||
Day = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Day : 0,
|
Day = day,
|
||||||
Year = !string.IsNullOrEmpty(publicationDate) ? DateTime.Parse(publicationDate).Year : 0,
|
Year = year,
|
||||||
Title = epubBook.Title,
|
Title = epubBook.Title,
|
||||||
Genre = string.Join(",", epubBook.Schema.Package.Metadata.Subjects.Select(s => s.ToLower().Trim())),
|
Genre = string.Join(",", epubBook.Schema.Package.Metadata.Subjects.Select(s => s.ToLower().Trim())),
|
||||||
LanguageISO = epubBook.Schema.Package.Metadata.Languages.FirstOrDefault() ?? string.Empty
|
LanguageISO = epubBook.Schema.Package.Metadata.Languages.FirstOrDefault() ?? string.Empty
|
||||||
|
|
||||||
};
|
};
|
||||||
|
ComicInfo.CleanComicInfo(info);
|
||||||
|
|
||||||
// Parse tags not exposed via Library
|
// Parse tags not exposed via Library
|
||||||
foreach (var metadataItem in epubBook.Schema.Package.Metadata.MetaItems)
|
foreach (var metadataItem in epubBook.Schema.Package.Metadata.MetaItems)
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,7 @@ using API.Data;
|
|||||||
using API.Entities;
|
using API.Entities;
|
||||||
using API.Entities.Enums;
|
using API.Entities.Enums;
|
||||||
using API.Extensions;
|
using API.Extensions;
|
||||||
|
using Kavita.Common;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace API.Services
|
namespace API.Services
|
||||||
@ -145,6 +146,12 @@ namespace API.Services
|
|||||||
else if (file.Format == MangaFormat.Epub)
|
else if (file.Format == MangaFormat.Epub)
|
||||||
{
|
{
|
||||||
removeNonImages = false;
|
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.ExistOrCreate(extractPath);
|
||||||
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
_directoryService.CopyFileToDirectory(files[0].FilePath, extractPath);
|
||||||
}
|
}
|
||||||
|
@ -45,12 +45,16 @@ public class DownloadService : IDownloadService
|
|||||||
{
|
{
|
||||||
contentType = Path.GetExtension(filepath).ToLowerInvariant() switch
|
contentType = Path.GetExtension(filepath).ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
".cbz" => "application/zip",
|
".cbz" => "application/x-cbz",
|
||||||
".cbr" => "application/vnd.rar",
|
".cbr" => "application/x-cbr",
|
||||||
".cb7" => "application/x-compressed",
|
".cb7" => "application/x-cb7",
|
||||||
|
".cbt" => "application/x-cbt",
|
||||||
".epub" => "application/epub+zip",
|
".epub" => "application/epub+zip",
|
||||||
".7z" => "application/x-7z-compressed",
|
".7z" => "application/x-7z-compressed",
|
||||||
".7zip" => "application/x-7z-compressed",
|
".7zip" => "application/x-7z-compressed",
|
||||||
|
".rar" => "application/vnd.rar",
|
||||||
|
".zip" => "application/zip",
|
||||||
|
".tar.gz" => "application/gzip",
|
||||||
".pdf" => "application/pdf",
|
".pdf" => "application/pdf",
|
||||||
_ => contentType
|
_ => contentType
|
||||||
};
|
};
|
||||||
|
@ -456,7 +456,7 @@ public class SeriesService : ISeriesService
|
|||||||
|
|
||||||
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
var libraryType = await _unitOfWork.LibraryRepository.GetLibraryTypeAsync(series.LibraryId);
|
||||||
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
var volumes = (await _unitOfWork.VolumeRepository.GetVolumesDtoAsync(seriesId, userId))
|
||||||
.OrderBy(v => float.Parse(v.Name))
|
.OrderBy(v => Parser.Parser.MinimumNumberFromRange(v.Name))
|
||||||
.ToList();
|
.ToList();
|
||||||
var chapters = volumes.SelectMany(v => v.Chapters).ToList();
|
var chapters = volumes.SelectMany(v => v.Chapters).ToList();
|
||||||
|
|
||||||
|
@ -41,6 +41,8 @@ export class NavService {
|
|||||||
*/
|
*/
|
||||||
showNavBar() {
|
showNavBar() {
|
||||||
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '56px');
|
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);
|
this.navbarVisibleSource.next(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +51,8 @@ export class NavService {
|
|||||||
*/
|
*/
|
||||||
hideNavBar() {
|
hideNavBar() {
|
||||||
this.renderer.setStyle(this.document.querySelector('body'), 'margin-top', '0px');
|
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);
|
this.navbarVisibleSource.next(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>
|
<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.
|
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>
|
</p>
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<app-nav-header></app-nav-header>
|
<app-nav-header></app-nav-header>
|
||||||
<div [ngClass]="{'closed' : (navService?.sideNavCollapsed$ | async), 'content-wrapper': navService.sideNavVisibility$ | async}">
|
<div [ngClass]="{'closed' : (navService?.sideNavCollapsed$ | async), 'content-wrapper': navService.sideNavVisibility$ | async}">
|
||||||
<a id="content"></a>
|
<a id="content"></a>
|
||||||
<app-side-nav *ngIf="navService.sideNavVisibility$ | async"></app-side-nav>
|
<app-side-nav *ngIf="navService.sideNavVisibility$ | async as sideNavVisibile"></app-side-nav>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid" [ngClass]="{'g-0': !(navService.sideNavVisibility$ | async)}">
|
||||||
<div style="padding: 20px 0;" *ngIf="navService.sideNavVisibility$ | async else noSideNav">
|
<div style="padding: 20px 0;" *ngIf="navService.sideNavVisibility$ | async else noSideNav">
|
||||||
<div class="companion-bar" [ngClass]="{'companion-bar-content': !(navService?.sideNavCollapsed$ | async)}">
|
<div class="companion-bar" [ngClass]="{'companion-bar-content': !(navService?.sideNavCollapsed$ | async)}">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image>
|
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image>
|
||||||
</ng-container>
|
</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>
|
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
||||||
|
|
||||||
<span class="download" *ngIf="download$ | async as download">
|
<span class="download" *ngIf="download$ | async as download">
|
||||||
|
@ -5,7 +5,7 @@ $image-height: 230px;
|
|||||||
$image-width: 160px;
|
$image-width: 160px;
|
||||||
|
|
||||||
.error-banner {
|
.error-banner {
|
||||||
width: 160px;
|
width: $image-width;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
background-color: var(--toast-error-bg-color);
|
background-color: var(--toast-error-bg-color);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -52,7 +52,7 @@ $image-width: 160px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress-banner {
|
.progress-banner {
|
||||||
width: 160px;
|
width: $image-width;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
@ -163,7 +163,7 @@ $image-width: 160px;
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 230px;
|
height: $image-height;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,15 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
download$: Observable<Download> | null = null;
|
download$: Observable<Download> | null = null;
|
||||||
downloadInProgress: boolean = false;
|
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;
|
private user: User | undefined;
|
||||||
|
|
||||||
@ -157,11 +165,11 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
|
this.messageHub.messages$.pipe(filter(event => event.event === EVENTS.UserProgressUpdate),
|
||||||
map(evt => evt.payload as UserProgressUpdateEvent), takeUntil(this.onDestroy)).subscribe(updateEvent => {
|
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.isChapter(this.entity) && updateEvent.chapterId !== this.entity.id) return;
|
||||||
if (this.utilityService.isVolume(this.entity) && updateEvent.volumeId !== 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;
|
if (this.utilityService.isSeries(this.entity) && updateEvent.seriesId !== this.entity.id) return;
|
||||||
|
|
||||||
this.read = updateEvent.pagesRead;
|
this.read = updateEvent.pagesRead;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -172,8 +180,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
prevTouchTime: number = 0;
|
|
||||||
prevOffset: number = 0;
|
|
||||||
@HostListener('touchstart', ['$event'])
|
@HostListener('touchstart', ['$event'])
|
||||||
onTouchStart(event: TouchEvent) {
|
onTouchStart(event: TouchEvent) {
|
||||||
if (!this.allowSelection) return;
|
if (!this.allowSelection) return;
|
||||||
@ -195,7 +201,6 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (verticalOffset != this.prevOffset) {
|
if (verticalOffset != this.prevOffset) {
|
||||||
this.prevTouchTime = 0;
|
this.prevTouchTime = 0;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1188,7 +1188,7 @@ export class MangaReaderComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
tempPageNum = this.pageNum + 1;
|
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 */});
|
this.readerService.saveProgress(this.seriesId, this.volumeId, this.chapterId, tempPageNum).pipe(take(1)).subscribe(() => {/* No operation */});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3" style="width:100%">
|
<div class="mb-3" style="width:100%">
|
||||||
<label for="email" class="form-label">Email</label>
|
<label for="email" class="form-label">Email</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="emailTooltip" role="button" tabindex="0"></i>
|
||||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
<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 id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||||
This field is required
|
This field is required
|
||||||
|
@ -134,7 +134,11 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||||||
const selectedChapterIndexes = this.bulkSelectionService.getSelectedCardsForSource('chapter');
|
const selectedChapterIndexes = this.bulkSelectionService.getSelectedCardsForSource('chapter');
|
||||||
const selectedSpecialIndexes = this.bulkSelectionService.getSelectedCardsForSource('special');
|
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 selectedVolumeIds = this.volumes.filter((_volume, index: number) => selectedVolumeIndexes.includes(index + ''));
|
||||||
const selectedSpecials = this.specials.filter((_chapter, index: number) => selectedSpecialIndexes.includes(index + ''));
|
const selectedSpecials = this.specials.filter((_chapter, index: number) => selectedSpecialIndexes.includes(index + ''));
|
||||||
const chapters = [...selectedChapterIds, ...selectedSpecials];
|
const chapters = [...selectedChapterIds, ...selectedSpecials];
|
||||||
|
@ -34,6 +34,18 @@
|
|||||||
</app-badge-expander>
|
</app-badge-expander>
|
||||||
</div>
|
</div>
|
||||||
</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="row g-0 mt-1" *ngIf="seriesMetadata.collectionTags && seriesMetadata.collectionTags.length > 0">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h5>Collections</h5>
|
<h5>Collections</h5>
|
||||||
@ -163,18 +175,6 @@
|
|||||||
</app-badge-expander>
|
</app-badge-expander>
|
||||||
</div>
|
</div>
|
||||||
</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="row g-0 mt-1" *ngIf="seriesMetadata.translators && seriesMetadata.translators.length > 0">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h5>Translators</h5>
|
<h5>Translators</h5>
|
||||||
|
@ -63,10 +63,6 @@ label, select, .clickable {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: calc(var(--vh)*100 - 56px);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Needed for fullscreen
|
// Needed for fullscreen
|
||||||
app-root {
|
app-root {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user