Kavita/UI/Web/src/app/cards/card-detail-layout/card-detail-layout.component.html
Joseph Milazzo 0be0e294aa
Metadata Optimizations (#910)
* Added a tooltip to inform user that format and collection filter selections do not only show for the selected library.

* Refactored a lot of code around when we update chapter cover images. Applied an optimization for when we re-calculate volume/series covers, such that it only occurs when the first chapter's image updates.

* Updated code to ensure only lastmodified gets refreshed in metadata since it always follows a scan

* Optimized how metadata is populated on the series. Instead of re-reading the comicInfos, instead I read the data from the underlying chapter entities. This reduces N additional reads AND enables the ability in the future to show/edit chapter level metadata.

* Spelling mistake

* Fixed a concurency issue by not selecting Genres from DB. Added a test for long paths.

* Fixed a bug in filter where collection tag wasn't populating on load

* Cleaned up the logic for changelog to better compare against the installed verison. For nightly users, show the last stable as installed.

* Removed some demo code

* SplitQuery to allow loading tags much faster for series metadata load.
2022-01-08 06:41:47 -08:00

403 lines
23 KiB
HTML

<div class="container-fluid" style="padding-top: 10px">
<div class="row no-gutters pb-2">
<div class="col mr-auto">
<h2 style="display: inline-block">
<span *ngIf="actions.length > 0" class="">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="header"></app-card-actionables>&nbsp;
</span>{{header}}&nbsp;
<!-- NOTE: On mobile the pill can eat up a lot of space, we can hide it and move to the filter section if user is interested -->
<span class="badge badge-primary badge-pill" attr.aria-label="{{pagination.totalItems}} total items" *ngIf="pagination != undefined">{{pagination.totalItems}}</span>
</h2>
</div>
<button class="btn btn-secondary btn-small" (click)="collapse.toggle()" [attr.aria-expanded]="!filteringCollapsed" placement="left" ngbTooltip="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filteringCollapsed ? 'Open' : 'Close'}} Filtering and Sorting">
<i class="fa fa-filter" aria-hidden="true"></i>
<span class="sr-only">Sort / Filter</span>
</button>
</div>
<div class="phone-hidden">
<div #collapse="ngbCollapse" [(ngbCollapse)]="filteringCollapsed">
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
</div>
</div>
<div class="not-phone-hidden">
<app-drawer #commentDrawer="drawer" [isOpen]="!filteringCollapsed" [style.--drawer-width]="'300px'" [style.--drawer-background-color]="'#010409'" (drawerClosed)="filteringCollapsed = !filteringCollapsed">
<div header>
<h2 style="margin-top: 0.5rem">Book Settings
<button type="button" class="close" aria-label="Close" (click)="commentDrawer.close()">
<span aria-hidden="true">&times;</span>
</button>
</h2>
</div>
<div body class="drawer-body">
<ng-container [ngTemplateOutlet]="filterSection"></ng-container>
</div>
</app-drawer>
</div>
<ng-template #filterSection>
<ng-template #globalFilterTooltip>This is library agnostic</ng-template>
<div class="filter-section mx-auto pb-3">
<div class="row justify-content-center no-gutters">
<div class="col-md-2 mr-3" *ngIf="!filterSettings.formatDisabled">
<div class="form-group">
<label for="format">Format</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
<span class="sr-only" id="filter-global-format-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
<app-typeahead (selectedData)="updateFormatFilters($event)" [settings]="formatSettings" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3"*ngIf="!filterSettings.libraryDisabled">
<div class="form-group">
<label for="libraries">Libraries</label>
<app-typeahead (selectedData)="updateLibraryFilters($event)" [settings]="librarySettings" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="!filterSettings.collectionDisabled">
<div class="form-group">
<label for="collections">Collections</label>&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="globalFilterTooltip" role="button" tabindex="0"></i>
<span class="sr-only" id="filter-global-collections-help"><ng-container [ngTemplateOutlet]="globalFilterTooltip"></ng-container></span>
<app-typeahead (selectedData)="updateCollectionFilters($event)" [settings]="collectionSettings" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="!filterSettings.genresDisabled">
<div class="form-group">
<label for="genres">Genres</label>
<app-typeahead (selectedData)="updateGenreFilters($event)" [settings]="genreSettings" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="!filterSettings.tagsDisabled">
<div class="form-group">
<label for="tags">Tags</label>
<app-typeahead (selectedData)="updateTagFilters($event)" [settings]="tagsSettings" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
</div>
<div class="row justify-content-center no-gutters">
<!-- The People row -->
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.CoverArtist)">
<div class="form-group">
<label for="cover-artist">(Cover) Artists</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.CoverArtist)" [settings]="getPersonsSettings(PersonRole.CoverArtist)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Writer)">
<div class="form-group">
<label for="writers">Writers</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Writer)" [settings]="getPersonsSettings(PersonRole.Writer)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Publisher)">
<div class="form-group">
<label for="publisher">Publisher</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Publisher)" [settings]="getPersonsSettings(PersonRole.Publisher)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Penciller)">
<div class="form-group">
<label for="penciller">Penciller</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Penciller)" [settings]="getPersonsSettings(PersonRole.Penciller)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Letterer)">
<div class="form-group">
<label for="letterer">Letterer</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Letterer)" [settings]="getPersonsSettings(PersonRole.Letterer)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Inker)">
<div class="form-group">
<label for="inker">Inker</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Inker)" [settings]="getPersonsSettings(PersonRole.Inker)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Editor)">
<div class="form-group">
<label for="editor">Editor</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Editor)" [settings]="getPersonsSettings(PersonRole.Editor)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Colorist)">
<div class="form-group">
<label for="colorist">Colorist</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Colorist)" [settings]="getPersonsSettings(PersonRole.Colorist)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Character)">
<div class="form-group">
<label for="character">Character</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Character)" [settings]="getPersonsSettings(PersonRole.Character)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
<div class="col-md-2 mr-3" *ngIf="peopleSettings.hasOwnProperty(PersonRole.Translator)">
<div class="form-group">
<label for="translators">Translators</label>
<app-typeahead (selectedData)="updatePersonFilters($event, PersonRole.Translator)" [settings]="getPersonsSettings(PersonRole.Translator)" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
</div>
</div>
<div class="row justify-content-center no-gutters">
<div class="col-md-2 mr-3" *ngIf="!filterSettings.readProgressDisabled">
<label>Read Progress</label>
<form [formGroup]="readProgressGroup">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="notread" formControlName="notRead">
<label class="form-check-label" for="notread">Unread</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="inprogress" formControlName="inProgress">
<label class="form-check-label" for="inprogress">In Progress</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="read" formControlName="read">
<label class="form-check-label" for="read">Read</label>
</div>
</form>
</div>
<div class="col-md-2 mr-3" *ngIf="!filterSettings.ratingDisabled">
<label for="ratings">Rating</label>
<form class="form-inline">
<ngb-rating class="rating-star" [(rate)]="filter.rating" (rateChange)="updateRating($event)" [resettable]="true">
<ng-template let-fill="fill" let-index="index">
<span class="star" [class.filled]="(index >= (filter.rating - 1)) && filter.rating > 0" [ngbTooltip]="(index + 1) + ' and up'">&#9733;</span>
</ng-template>
</ngb-rating>
</form>
</div>
<div class="col-md-2 mr-3" *ngIf="!filterSettings.ageRatingDisabled">
<label for="age-rating">Age Rating</label>
<app-typeahead (selectedData)="updateAgeRating($event)" [settings]="ageRatingSettings" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
<div class="col-md-2 mr-3" *ngIf="!filterSettings.languageDisabled">
<label for="languages">Language</label>
<app-typeahead (selectedData)="updateLanguageRating($event)" [settings]="languageSettings" [reset]="resetTypeaheads">
<ng-template #badgeItem let-item let-position="idx">
{{item.title}}
</ng-template>
<ng-template #optionItem let-item let-position="idx">
{{item.title}}
</ng-template>
</app-typeahead>
</div>
<div class="col-md-2 mr-3" *ngIf="!filterSettings.sortDisabled">
<form [formGroup]="sortGroup">
<div class="form-group">
<label for="sort-options">Sort By</label>
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
<i class="fa fa-arrow-down" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
<ng-template #descSort>
<i class="fa fa-arrow-up" title="Descending"></i>
</ng-template>
</button>
<select id="sort-options" class="form-control" formControlName="sortField" style="height: 38px;">
<option [value]="SortField.SortName">Sort Name</option>
<option [value]="SortField.Created">Created</option>
<option [value]="SortField.LastModified">Last Modified</option>
</select>
</div>
</form>
</div>
</div>
<div class="row justify-content-center no-gutters">
<div class="col-md-3 mr-3">
<button class="btn btn-secondary btn-block" (click)="clear()">Clear</button>
</div>
<div class="col-md-3">
<button class="btn btn-primary btn-block" (click)="apply()">Apply</button>
</div>
</div>
</div>
</ng-template>
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
<div class="row no-gutters">
<div class="col-auto" *ngFor="let item of items; trackBy:trackByIdentity; index as i">
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
</div>
<p *ngIf="items.length === 0 && !isLoading">
There is no data
</p>
</div>
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'bottom' }"></ng-container>
</div>
<ng-template #paginationTemplate let-id="id">
<div class="d-flex justify-content-center" *ngIf="pagination && items.length > 0">
<ngb-pagination
*ngIf="pagination.totalPages > 1"
[maxSize]="8"
[rotate]="true"
[ellipses]="false"
[(page)]="pagination.currentPage"
[pageSize]="pagination.itemsPerPage"
(pageChange)="onPageChange($event)"
[collectionSize]="pagination.totalItems">
<ng-template ngbPaginationPages let-page let-pages="pages" *ngIf="pagination.totalItems / pagination.itemsPerPage > 20">
<li class="ngb-custom-pages-item" *ngIf="pagination.totalPages > 1">
<div class="form-group d-flex flex-nowrap px-2">
<label
id="paginationInputLabel-{{id}}"
for="paginationInput-{{id}}"
class="col-form-label mr-2 ml-1"
>Page</label>
<input #i
type="text"
inputmode="numeric"
pattern="[0-9]*"
class="form-control custom-pages-input"
id="paginationInput-{{id}}"
[value]="page"
(keyup.enter)="selectPageStr(i.value)"
(blur)="selectPageStr(i.value)"
(input)="formatInput($any($event).target)"
attr.aria-labelledby="paginationInputLabel-{{id}} paginationDescription-{{id}}"
[ngStyle]="{width: (0.5 + pagination.currentPage + '').length + 'rem'} "
/>
<span id="paginationDescription-{{id}}" class="col-form-label text-nowrap px-2">
of {{pagination.totalPages}}</span>
</div>
</li>
</ng-template>
</ngb-pagination>
</div>
</ng-template>