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:
Joseph Milazzo 2022-05-08 11:15:43 -05:00 committed by GitHub
parent 3334b0ce3f
commit 85f3b620af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 135 additions and 95 deletions

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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)
{

View File

@ -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);
}

View File

@ -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
};

View File

@ -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();

View File

@ -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);
}

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 */});
}
}

View File

@ -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>&nbsp;<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

View File

@ -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];

View File

@ -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>

View File

@ -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;