Localization - First Pass (#2174)

* Started designing the backend localization service

* Worked in Transloco for initial PoC

* Worked in Transloco for initial PoC

* Translated the login screen

* translated dashboard screen

* Started work on the backend

* Fixed a logic bug

* translated edit-user screen

* Hooked up the backend for having a locale property.

* Hooked up the ability to view the available locales and switch to them.

* Made the localization service languages be derived from what's in langs/ directory.

* Fixed up localization switching

* Switched when we check for a license on UI bootstrap

* Tweaked some code

* Fixed the bug where dashboard wasn't loading and made it so language switching is working.

* Fixed a bug on dashboard with languagePath

* Converted user-scrobble-history.component.html

* Converted spoiler.component.html

* Converted review-series-modal.component.html

* Converted review-card-modal.component.html

* Updated the readme

* Translated using Weblate (English)

Currently translated at 100.0% (54 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/en/

* Converted review-card.component.html

* Deleted dead component

* Converted want-to-read.component.html

* Added translation using Weblate (Korean)

* Translated using Weblate (Spanish)

Currently translated at 40.7% (22 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/es/

* Translated using Weblate (Korean)

Currently translated at 62.9% (34 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/

* Converted user-preferences.component.html

* Translated using Weblate (Korean)

Currently translated at 92.5% (50 of 54 strings)

Translation: Kavita/ui
Translate-URL: https://hosted.weblate.org/projects/kavita/ui/ko/

* Converted user-holds.component.html

* Converted theme-manager.component.html

* Converted restriction-selector.component.html

* Converted manage-devices.component.html

* Converted edit-device.component.html

* Converted change-password.component.html

* Converted change-email.component.html

* Converted change-age-restriction.component.html

* Converted api-key.component.html

* Converted anilist-key.component.html

* Converted typeahead.component.html

* Converted user-stats-info-cards.component.html

* Converted user-stats.component.html

* Converted top-readers.component.html

* Converted some pipes and ensure translation is loaded before the app.

* Finished all but one pipe for localization

* Converted directory-picker.component.html

* Converted library-access-modal.component.html

* Converted a few components

* Converted a few components

* Converted a few components

* Converted a few components

* Converted a few components

* Merged weblate in

* ... -> … update

* Updated the readme

* Updateded all fonts to be woff2

* Cleaned up some strings to increase re-use

* Removed an old flow (that doesn't exist in backend any longer) from when we introduced emails on Kavita.

* Converted Series detail

* Lots more converted

* Lots more converted & hooked up the ability to flatten during prod build the language files.

* Lots more converted

* Lots more converted & fixed a bunch of broken pipes due to inject()

* Lots more converted

* Lots more converted

* Lots more converted & fixed some bad keys

* Lots more converted

* Fixed some bugs with admin dasbhoard nested tabs not rendering on first load due to not using onpush change detection

* Fixed up some localization errors and fixed forgot password error when the user doesn't have change password permission

* Fixed a stupid build issue again

* Started adding errors for interceptor and backend.

* Finished off manga-reader

* More translations

* Few fixes

* Fixed a bug where character tag badges weren't showing the name on chapter info

* All components are translated

* All toasts are translated

* All confirm/alerts are translated

* Trying something new for the backend

* Migrated the localization strings for the backend into a new file.

* Updated the localization service to be able to do backend localization with fallback to english.

* Cleaned up some external reviews code to reduce looping

* Localized AccountController.cs

* 60% done with controllers

* All controllers are done

* All KavitaExceptions are covered

* Some shakeout fixes

* Prep for initial merge

* Everything is done except options and basic shakeout proves response times are good. Unit tests are broken.

* Fixed up the unit tests

* All unit tests are now working

* Removed some quantifier

* I'm not sure I can support localization for some Volume/Chapter/Book strings within the codebase.

---------

Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
Co-authored-by: majora2007 <kavitareader@gmail.com>
Co-authored-by: expertjun <jtrobin@naver.com>
Co-authored-by: ThePromidius <thepromidiusyt@gmail.com>
This commit is contained in:
Joe Milazzo 2023-08-03 10:33:51 -05:00 committed by GitHub
parent 670bf82c38
commit 3b23d63234
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
389 changed files with 13652 additions and 7925 deletions

View file

@ -1,364 +1,352 @@
<div #companionBar>
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasExtras]="true" [extraDrawer]="extrasDrawer">
<ng-container title>
<h2 class="title text-break">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-v"></app-card-actionables>
<span>{{series.name}}</span>
</h2>
</ng-container>
<ng-container subtitle *ngIf="series.localizedName !== series.name">
<h6 class="subtitle-with-actionables text-break" title="Localized Name">{{series.localizedName}}</h6>
</ng-container>
<ng-container *transloco="let t; read: 'series-detail'">
<div #companionBar>
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasExtras]="true" [extraDrawer]="extrasDrawer">
<ng-container title>
<h2 class="title text-break">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-v"></app-card-actionables>
<span>{{series.name}}</span>
</h2>
</ng-container>
<ng-container subtitle *ngIf="series.localizedName !== series.name">
<h6 class="subtitle-with-actionables text-break" title="Localized Name">{{series.localizedName}}</h6>
</ng-container>
<ng-template #extrasDrawer let-offcanvas>
<div style="margin-top: 56px">
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">Page Settings</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="offcanvas.dismiss()"></button>
</div>
<div class="offcanvas-body">
<form [formGroup]="pageExtrasGroup">
<!-- <div class="row g-0">
<ng-template #extrasDrawer let-offcanvas>
<div style="margin-top: 56px">
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title">{{t('page-settings-title')}}</h4>
<button type="button" class="btn-close" [attr.aria-label]="t('close')" (click)="offcanvas.dismiss()"></button>
</div>
<div class="offcanvas-body">
<form [formGroup]="pageExtrasGroup">
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<label for="settings-book-reading-direction" class="form-label">Sort Order</label>
<button class="btn btn-sm btn-secondary-outline" (click)="updateSortOrder()" style="height: 25px; padding-bottom: 0px;">
<i class="fa fa-arrow-up" title="Ascending" *ngIf="isAscendingSort; else descSort"></i>
<ng-template #descSort>
<i class="fa fa-arrow-down" title="Descending"></i>
</ng-template>
</button>
<label id="list-layout-mode-label" class="form-label">{{t('layout-mode-label')}}</label>
<br/>
<div class="btn-group d-flex justify-content-center" role="group" [attr.aria-label]="t('page-settings-title')">
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-default">{{t('layout-mode-option-card')}}</label>
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="sortingOption">
<option *ngFor="let opt of sortingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
</select>
</div>
</div> -->
<div class="row g-0">
<div class="col-md-12 col-sm-12 pe-2 mb-3">
<label id="list-layout-mode-label" class="form-label">Layout Mode</label>
<br/>
<div class="btn-group d-flex justify-content-center" role="group" aria-label="Layout Mode">
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.Cards" class="btn-check" id="layout-mode-default" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-default">Card</label>
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col1">List</label>
<input type="radio" formControlName="renderMode" [value]="PageLayoutMode.List" class="btn-check" id="layout-mode-col1" autocomplete="off">
<label class="btn btn-outline-primary" for="layout-mode-col1">{{t('layout-mode-option-list')}}</label>
</div>
</div>
</div>
</div>
</form>
</form>
</div>
</div>
</ng-template>
</app-side-nav-companion-bar>
</div>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
<div class="row mb-0 mb-xl-3 info-container">
<div class="image-container col-4 col-sm-6 col-md-4 col-lg-4 col-xl-2 col-xxl-2 d-none d-sm-block mt-2">
<div class="to-read-counter" *ngIf="unreadCount > 0 && unreadCount !== totalCount">
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed" fillStyle="filled">{{unreadCount}}</app-tag-badge>
</div>
<app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image>
<ng-container *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
<div class="progress-banner" ngbTooltip="{{(series.pagesRead / series.pages) | number:'1.0-1'}}% Read">
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
</div>
<div class="under-image">
{{t('continue-from', {title: ContinuePointTitle})}}
</div>
</ng-container>
</div>
</ng-template>
</app-side-nav-companion-bar>
</div>
<div [ngStyle]="{'height': ScrollingBlockHeight}" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
<div class="row mb-0 mb-xl-3 info-container">
<div class="image-container col-4 col-sm-6 col-md-4 col-lg-4 col-xl-2 col-xxl-2 d-none d-sm-block mt-2">
<div class="to-read-counter" *ngIf="unreadCount > 0 && unreadCount !== totalCount">
<app-tag-badge [selectionMode]="TagBadgeCursor.NotAllowed" fillStyle="filled">{{unreadCount}}</app-tag-badge>
</div>
<app-image height="100%" maxHeight="400px" objectFit="contain" background="none" [imageUrl]="seriesImage"></app-image>
<ng-container *ngIf="series.pagesRead < series.pages && hasReadingProgress && currentlyReadingChapter && !currentlyReadingChapter.isSpecial">
<div class="progress-banner" ngbTooltip="{{(series.pagesRead / series.pages) | number:'1.0-1'}}% Read">
<ngb-progressbar type="primary" height="5px" [value]="series.pagesRead" [max]="series.pages"></ngb-progressbar>
</div>
<div class="under-image">
Continue {{ContinuePointTitle}}
</div>
</ng-container>
</div>
<div class="col-xlg-10 col-lg-8 col-md-8 col-xs-8 col-sm-6 mt-2">
<div class="row g-0">
<div class="col-auto">
<div class="btn-group">
<button type="button" class="btn btn-primary" (click)="read()">
<div class="col-xlg-10 col-lg-8 col-md-8 col-xs-8 col-sm-6 mt-2">
<div class="row g-0">
<div class="col-auto">
<div class="btn-group">
<button type="button" class="btn btn-primary" (click)="read()">
<span>
<i class="fa {{showBook ? 'fa-book-open' : 'fa-book'}}" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{(hasReadingProgress) ? 'Continue' : 'Read'}}</span>
<span class="read-btn--text">&nbsp;{{(hasReadingProgress) ? t('continue') : t('read')}}</span>
</span>
</button>
<div class="btn-group" ngbDropdown role="group" display="dynamic" aria-label="Read options">
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<button ngbDropdownItem (click)="read(true)">
</button>
<div class="btn-group" ngbDropdown role="group" display="dynamic" [attr.aria-label]="t('read-options-alt')">
<button type="button" class="btn btn-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<button ngbDropdownItem (click)="read(true)">
<span>
<i class="fa fa-glasses" aria-hidden="true"></i>
<span class="read-btn--text">&nbsp;{{(hasReadingProgress) ? 'Continue' : 'Read'}} Incognito</span>
<span class="read-btn--text">&nbsp;{{(hasReadingProgress) ? t('continue') : t('read')}} Incognito</span>
</span>
</button>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-auto ms-2">
<button class="btn btn-secondary" (click)="toggleWantToRead()" title="{{isWantToRead ? 'Remove from' : 'Add to'}} Want to Read">
<div class="col-auto ms-2">
<button class="btn btn-secondary" (click)="toggleWantToRead()" title="{{isWantToRead ? t('remove-from-want-to-read') : t('add-to-want-to-read')}}">
<span>
<i class="{{isWantToRead ? 'fa-solid' : 'fa-regular'}} fa-star" aria-hidden="true"></i>
</span>
</button>
</div>
<div class="col-auto ms-2" *ngIf="isAdmin">
<button class="btn btn-secondary" (click)="openEditSeriesModal()" title="Edit Series information">
<span><i class="fa fa-pen" aria-hidden="true"></i></span>
</button>
</div>
<div class="col-auto ms-2 d-none d-md-block">
<div class="card-actions">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-h" btnClass="btn-secondary"></app-card-actionables>
</button>
</div>
<div class="col-auto ms-2" *ngIf="isAdmin">
<button class="btn btn-secondary" (click)="openEditSeriesModal()" [title]="t('edit-series-alt')">
<span><i class="fa fa-pen" aria-hidden="true"></i></span>
</button>
</div>
<div class="col-auto ms-2 d-none d-md-block">
<div class="card-actions">
<app-card-actionables (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-h" btnClass="btn-secondary"></app-card-actionables>
</div>
</div>
<div class="col-auto ms-2 d-none d-md-block" *ngIf="isAdmin || hasDownloadingRole">
<button class="btn btn-secondary" (click)="downloadSeries()" title="Download Series" [disabled]="downloadInProgress">
<ng-container *ngIf="downloadInProgress; else notDownloading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="visually-hidden">{{t('downloading-status')}}</span>
</ng-container>
<ng-template #notDownloading>
<i class="fa fa-arrow-alt-circle-down" aria-hidden="true"></i>
</ng-template>
</button>
</div>
</div>
<div class="col-auto ms-2 d-none d-md-block" *ngIf="isAdmin || hasDownloadingRole">
<button class="btn btn-secondary" (click)="downloadSeries()" title="Download Series" [disabled]="downloadInProgress">
<ng-container *ngIf="downloadInProgress; else notDownloading">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span class="visually-hidden">Downloading...</span>
</ng-container>
<ng-template #notDownloading>
<i class="fa fa-arrow-alt-circle-down" aria-hidden="true"></i>
</ng-template>
</button>
<div *ngIf="seriesMetadata" class="mt-2">
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"
[libraryType]="libraryType"
[hasReadingProgress]="hasReadingProgress"></app-series-metadata-detail>
</div>
</div>
<div *ngIf="seriesMetadata" class="mt-2">
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"
[libraryType]="libraryType"
[hasReadingProgress]="hasReadingProgress"></app-series-metadata-detail>
<div class="row pt-4">
<app-carousel-reel [items]="reviews" [alwaysShow]="true" [title]="t('user-reviews-alt')" iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}" [clickableTitle]="true" (sectionClick)="openReviewModal()">
<ng-template #carouselItem let-item let-position="idx">
<app-review-card [review]="item"></app-review-card>
</ng-template>
</app-carousel-reel>
</div>
</div>
<div class="row pt-4">
<app-carousel-reel [items]="reviews" [alwaysShow]="true" title="User Reviews" iconClasses="fa-solid fa-{{getUserReview().length > 0 ? 'pen' : 'plus'}}" [clickableTitle]="true" (sectionClick)="openReviewModal()">
<ng-template #carouselItem let-item let-position="idx">
<app-review-card [review]="item"></app-review-card>
</ng-template>
</app-carousel-reel>
</div>
</div>
<ng-container *ngIf="series">
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)">
<li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (volumes.length > 0 || chapters.length > 0)">
<a ngbNavLink>Storyline</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else storylineListLayout">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
<ng-container *ngIf="!item.isChapter; else chapterCardItem">
<app-card-item class="col-auto mt-2 mb-2" *ngIf="item.volume.number !== 0" [entity]="item.volume" [title]="item.volume.name" (click)="openVolume(item.volume)"
[imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
[read]="item.volume.pagesRead" [total]="item.volume.pages" [actions]="volumeActions"
[count]="item.volume.chapters[0].files.length"
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
</ng-container>
<ng-template #chapterCardItem>
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.chapter.isSpecial" [entity]="item.chapter" [title]="item.chapter.title" (click)="openChapter(item.chapter)"
[imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
[read]="item.chapter.pagesRead" [total]="item.chapter.pages" [actions]="chapterActions"
[count]="item.chapter.files.length"
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, storyChapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
</ng-template>
</ng-container>
</div>
</ng-container>
<ng-template #storylineListLayout>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
<ng-container *ngIf="!item.isChapter; else chapterListItem">
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(item.volume.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="item.volume" *ngIf="item.volume.number !== 0"
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="item.volume.pagesRead" [totalPages]="item.volume.pages" (read)="openVolume(item.volume)"
[blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="item.volume" [seriesName]="series.name" [prioritizeTitleName]="false"></app-entity-title>
</ng-container>
</app-list-item>
</ng-container>
<ng-template #chapterListItem>
<app-list-item [imageUrl]="imageService.getChapterCoverImage(item.chapter.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="item.chapter" *ngIf="!item.chapter.isSpecial"
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="item.chapter.pagesRead" [totalPages]="item.chapter.pages" (read)="openChapter(item.chapter)"
[blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="item.chapter" [seriesName]="series.name" [prioritizeTitleName]="false"></app-entity-title>
</ng-container>
</app-list-item>
</ng-template>
</ng-container>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Volumes" *ngIf="volumes.length > 0">
<a ngbNavLink>{{libraryType === LibraryType.Book ? 'Books': 'Volumes'}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else volumeListLayout">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.name" (click)="openVolume(item)"
[imageUrl]="imageService.getVolumeCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="volumeActions"
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
</app-card-item>
</ng-container>
</div>
</ng-container>
<ng-template #volumeListLayout>
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="volume"
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
[blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="volume" [seriesName]="series.name"></app-entity-title>
</ng-container>
</app-list-item>
</ng-container>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Chapters" *ngIf="chapters.length > 0">
<a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else chapterListLayout">
<div class="card-container row g-0" #container>
<div *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.isSpecial" [entity]="item" [title]="item.title" (click)="openChapter(item)"
[imageUrl]="imageService.getChapterCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
[count]="item.files.length"
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, chapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="item" [seriesName]="series.name" [includeVolume]="true"></app-entity-title>
</ng-container>
</app-card-item>
</div>
</div>
</ng-container>
<ng-template #chapterListLayout>
<div *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="chapter" *ngIf="!chapter.isSpecial"
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
[includeVolume]="true" [blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="chapter" [seriesName]="series.name" [includeVolume]="true" [prioritizeTitleName]="false"></app-entity-title>
</ng-container>
</app-list-item>
</div>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Specials" *ngIf="hasSpecials">
<a ngbNavLink>Specials</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else specialListLayout">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.title || item.range" (click)="openChapter(item)"
[imageUrl]="imageService.getChapterCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
[count]="item.files.length"
(selection)="bulkSelectionService.handleCardSelection('special', scroll.viewPortInfo.startIndexWithBuffer + idx, chapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('special', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
</app-card-item>
</ng-container>
</div>
</ng-container>
<ng-template #specialListLayout>
<ng-container *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="chapter"
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
[blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
{{chapter.title || chapter.range}}
</ng-container>
</app-list-item>
</ng-container>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Related" *ngIf="hasRelations">
<a ngbNavLink>Related</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="relations" [parentScroll]="scrollingBlock">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems let idx = index; trackBy: trackByRelatedSeriesIdentify">
<app-series-card class="col-auto mt-2 mb-2" [data]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
</ng-container>
</div>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Recommendations" *ngIf="hasRecommendations">
<a ngbNavLink>Recommendations</a>
<ng-container *ngIf="series">
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTabId" class="nav nav-tabs mb-2" [destroyOnHide]="false" (navChange)="onNavChange($event)">
<li [ngbNavItem]="TabID.Storyline" *ngIf="libraryType !== LibraryType.Book && (volumes.length > 0 || chapters.length > 0)">
<a ngbNavLink>{{t('storyline-tab')}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="combinedRecs" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else recListLayout">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackBySeriesIdentify">
<ng-container *ngIf="!item.hasOwnProperty('coverUrl'); else externalRec">
<app-series-card class="col-auto mt-2 mb-2" [data]="item" [libraryId]="item.libraryId"></app-series-card>
</ng-container>
<ng-template #externalRec>
<app-external-series-card class="col-auto mt-2 mb-2" [data]="item"></app-external-series-card>
</ng-template>
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else storylineListLayout">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
<ng-container *ngIf="!item.isChapter; else chapterCardItem">
<app-card-item class="col-auto mt-2 mb-2" *ngIf="item.volume.number !== 0" [entity]="item.volume" [title]="item.volume.name" (click)="openVolume(item.volume)"
[imageUrl]="imageService.getVolumeCoverImage(item.volume.id)"
[read]="item.volume.pagesRead" [total]="item.volume.pages" [actions]="volumeActions"
[count]="item.volume.chapters[0].files.length"
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
</ng-container>
</div>
</ng-container>
<ng-template #recListLayout>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackBySeriesIdentify">
<ng-container *ngIf="!item.hasOwnProperty('coverUrl'); else externalRec">
<app-external-list-item [imageUrl]="imageService.getSeriesCoverImage(item.id)" imageWidth="130px" imageHeight="" [summary]="item.summary">
<ng-container title>
<a href="/library/{{item.libraryId}}/series/{{item.id}}">{{item.name}}</a>
</ng-container>
</app-external-list-item>
</ng-container>
<ng-template #externalRec>
<app-external-list-item [imageUrl]="item.coverUrl" imageWidth="130px" imageHeight="" [summary]="item.summary">
<ng-container title>
<a [href]="item.url" target="_blank" rel="noreferrer nofollow">{{item.name}}</a>
</ng-container>
</app-external-list-item>
<ng-template #chapterCardItem>
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.chapter.isSpecial" [entity]="item.chapter" [title]="item.chapter.title" (click)="openChapter(item.chapter)"
[imageUrl]="imageService.getChapterCoverImage(item.chapter.id)"
[read]="item.chapter.pagesRead" [total]="item.chapter.pages" [actions]="chapterActions"
[count]="item.chapter.files.length"
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, storyChapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true"></app-card-item>
</ng-template>
</ng-container>
</ng-template>
</virtual-scroller>
</div>
</ng-container>
<ng-template #storylineListLayout>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByStoryLineIdentity">
<ng-container *ngIf="!item.isChapter; else chapterListItem">
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(item.volume.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="item.volume" *ngIf="item.volume.number !== 0"
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="item.volume.pagesRead" [totalPages]="item.volume.pages" (read)="openVolume(item.volume)"
[blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="item.volume" [seriesName]="series.name" [prioritizeTitleName]="false"></app-entity-title>
</ng-container>
</app-list-item>
</ng-container>
<ng-template #chapterListItem>
<app-list-item [imageUrl]="imageService.getChapterCoverImage(item.chapter.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="item.chapter" *ngIf="!item.chapter.isSpecial"
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="item.chapter.pagesRead" [totalPages]="item.chapter.pages" (read)="openChapter(item.chapter)"
[blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="item.chapter" [seriesName]="series.name" [prioritizeTitleName]="false"></app-entity-title>
</ng-container>
</app-list-item>
</ng-template>
</ng-container>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>
</ng-container>
</li>
<app-loading [loading]="isLoading"></app-loading>
</div>
<li [ngbNavItem]="TabID.Volumes" *ngIf="volumes.length > 0">
<a ngbNavLink>{{libraryType === LibraryType.Book ? t('books-tab') : t('volumes-tab')}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else volumeListLayout">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.name" (click)="openVolume(item)"
[imageUrl]="imageService.getVolumeCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="volumeActions"
(selection)="bulkSelectionService.handleCardSelection('volume', scroll.viewPortInfo.startIndexWithBuffer + idx, volumes.length, $event)"
[selected]="bulkSelectionService.isCardSelected('volume', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
</app-card-item>
</ng-container>
</div>
</ng-container>
<ng-template #volumeListLayout>
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="volume"
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
[blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="volume" [seriesName]="series.name"></app-entity-title>
</ng-container>
</app-list-item>
</ng-container>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Chapters" *ngIf="chapters.length > 0">
<a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else chapterListLayout">
<div class="card-container row g-0" #container>
<div *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!item.isSpecial" [entity]="item" [title]="item.title" (click)="openChapter(item)"
[imageUrl]="imageService.getChapterCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
[count]="item.files.length"
(selection)="bulkSelectionService.handleCardSelection('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx, chapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('chapter', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="item" [seriesName]="series.name" [includeVolume]="true"></app-entity-title>
</ng-container>
</app-card-item>
</div>
</div>
</ng-container>
<ng-template #chapterListLayout>
<div *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="chapter" *ngIf="!chapter.isSpecial"
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
[includeVolume]="true" [blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="chapter" [seriesName]="series.name" [includeVolume]="true" [prioritizeTitleName]="false"></app-entity-title>
</ng-container>
</app-list-item>
</div>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Specials" *ngIf="hasSpecials">
<a ngbNavLink>{{t('specials-tab')}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else specialListLayout">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto mt-2 mb-2" [entity]="item" [title]="item.title || item.range" (click)="openChapter(item)"
[imageUrl]="imageService.getChapterCoverImage(item.id)"
[read]="item.pagesRead" [total]="item.pages" [actions]="chapterActions"
[count]="item.files.length"
(selection)="bulkSelectionService.handleCardSelection('special', scroll.viewPortInfo.startIndexWithBuffer + idx, chapters.length, $event)"
[selected]="bulkSelectionService.isCardSelected('special', scroll.viewPortInfo.startIndexWithBuffer + idx)" [allowSelection]="true">
</app-card-item>
</ng-container>
</div>
</ng-container>
<ng-template #specialListLayout>
<ng-container *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)" [libraryId]="libraryId"
[seriesName]="series.name" [entity]="chapter"
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
[blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title>
{{chapter.title || chapter.range}}
</ng-container>
</app-list-item>
</ng-container>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Related" *ngIf="hasRelations">
<a ngbNavLink>{{t('related-tab')}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="relations" [parentScroll]="scrollingBlock">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems let idx = index; trackBy: trackByRelatedSeriesIdentify">
<app-series-card class="col-auto mt-2 mb-2" [data]="item.series" [libraryId]="item.series.libraryId" [relation]="item.relation"></app-series-card>
</ng-container>
</div>
</virtual-scroller>
</ng-template>
</li>
<li [ngbNavItem]="TabID.Recommendations" *ngIf="hasRecommendations">
<a ngbNavLink>{{t('recommendations-tab')}}</a>
<ng-template ngbNavContent>
<virtual-scroller #scroll [items]="combinedRecs" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else recListLayout">
<div class="card-container row g-0" #container>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackBySeriesIdentify">
<ng-container *ngIf="!item.hasOwnProperty('coverUrl'); else externalRec">
<app-series-card class="col-auto mt-2 mb-2" [data]="item" [libraryId]="item.libraryId"></app-series-card>
</ng-container>
<ng-template #externalRec>
<app-external-series-card class="col-auto mt-2 mb-2" [data]="item"></app-external-series-card>
</ng-template>
</ng-container>
</div>
</ng-container>
<ng-template #recListLayout>
<ng-container *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackBySeriesIdentify">
<ng-container *ngIf="!item.hasOwnProperty('coverUrl'); else externalRec">
<app-external-list-item [imageUrl]="imageService.getSeriesCoverImage(item.id)" imageWidth="130px" imageHeight="" [summary]="item.summary">
<ng-container title>
<a href="/library/{{item.libraryId}}/series/{{item.id}}">{{item.name}}</a>
</ng-container>
</app-external-list-item>
</ng-container>
<ng-template #externalRec>
<app-external-list-item [imageUrl]="item.coverUrl" imageWidth="130px" imageHeight="" [summary]="item.summary">
<ng-container title>
<a [href]="item.url" target="_blank" rel="noreferrer nofollow">{{item.name}}</a>
</ng-container>
</app-external-list-item>
</ng-template>
</ng-container>
</ng-template>
</virtual-scroller>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>
</ng-container>
<app-loading [loading]="isLoading"></app-loading>
</div>
</ng-container>

View file

@ -70,6 +70,7 @@ import { ImageComponent } from '../../../shared/image/image.component';
import { TagBadgeComponent } from '../../../shared/tag-badge/tag-badge.component';
import { CardActionablesComponent } from '../../../cards/card-item/card-actionables/card-actionables.component';
import { SideNavCompanionBarComponent } from '../../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
import {TranslocoModule, TranslocoService} from "@ngneat/transloco";
interface RelatedSeriesPair {
series: Series;
@ -97,7 +98,7 @@ interface StoryLineItem {
styleUrls: ['./series-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe]
imports: [NgIf, SideNavCompanionBarComponent, CardActionablesComponent, ReactiveFormsModule, NgStyle, TagBadgeComponent, ImageComponent, NgbTooltip, NgbProgressbar, NgbDropdown, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, SeriesMetadataDetailComponent, CarouselReelComponent, ReviewCardComponent, BulkOperationsComponent, NgbNav, NgbNavItem, NgbNavLink, NgbNavContent, VirtualScrollerModule, NgFor, CardItemComponent, ListItemComponent, EntityTitleComponent, SeriesCardComponent, ExternalSeriesCardComponent, ExternalListItemComponent, NgbNavOutlet, LoadingComponent, DecimalPipe, TranslocoModule]
})
export class SeriesDetailComponent implements OnInit, AfterContentChecked {
@ -192,7 +193,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
'renderMode': new FormControl(this.renderMode, []),
});
isAscendingSort: boolean = false; // TODO: Get this from User preferences
user: User | undefined;
bulkActionCallback = (action: ActionItem<any>, data: any) => {
@ -295,7 +295,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
public navService: NavService,
private offcanvasService: NgbOffcanvas, @Inject(DOCUMENT) private document: Document,
private cdRef: ChangeDetectorRef, private scrollService: ScrollService,
private deviceService: DeviceService
private deviceService: DeviceService, private translocoService: TranslocoService
) {
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
@ -327,7 +327,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
if (event.event === EVENTS.SeriesRemoved) {
const seriesRemovedEvent = event.payload as SeriesRemovedEvent;
if (seriesRemovedEvent.seriesId === this.seriesId) {
this.toastr.info('This series no longer exists');
this.toastr.info(this.translocoService.translate('errors.series-doesnt-exist'));
this.router.navigateByUrl('/libraries');
}
} else if (event.event === EVENTS.ScanSeries) {
@ -474,7 +474,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
{
const device = (action._extra!.data as Device);
this.deviceService.sendTo([chapter.id], device.id).subscribe(() => {
this.toastr.success('File emailed to ' + device.name);
this.toastr.success(this.translocoService.translate('series-detail.send-to', {deviceName: device.name}));
});
break;
}
@ -704,7 +704,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
openChapter(chapter: Chapter, incognitoMode = false) {
if (this.bulkSelectionService.hasSelections()) return;
if (chapter.pages === 0) {
this.toastr.error('There are no pages. Kavita was not able to read this archive.');
this.toastr.error(this.translocoService.translate('series-detail.no-pages'));
return;
}
this.router.navigate(this.readerService.getNavigationArray(this.libraryId, this.seriesId, chapter.id, chapter.files[0].format), {queryParams: {incognitoMode}});
@ -713,7 +713,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
openVolume(volume: Volume) {
if (this.bulkSelectionService.hasSelections()) return;
if (volume.chapters === undefined || volume.chapters?.length === 0) {
this.toastr.error('There are no chapters to this volume. Cannot read.');
this.toastr.error(this.translocoService.translate('series-detail.no-chapters'));
return;
}
@ -733,10 +733,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
this.openChapter([...volume.chapters].sort(this.utilityService.sortChapters)[0]);
}
isNullOrEmpty(val: string) {
return val === null || val === undefined || val === ''; // TODO: Validate if this code is used
}
openViewInfo(data: Volume | Chapter) {
const drawerRef = this.offcanvasService.open(CardDetailDrawerComponent, {position: 'bottom'});
drawerRef.componentInstance.data = data;
@ -755,7 +751,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
}
if (closeResult.coverImageUpdate) {
this.toastr.info('It can take up to a minute for your browser to refresh the image. Until then, the old image may be shown on some pages.');
this.toastr.info(this.translocoService.translate('series-detail.cover-change'));
}
});
}
@ -785,10 +781,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
});
}
preventClick(event: any) {
event.stopPropagation();
event.preventDefault();
}
performAction(action: ActionItem<any>) {
if (typeof action.callback === 'function') {
@ -807,18 +799,6 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked {
});
}
updateSortOrder() {
this.isAscendingSort = !this.isAscendingSort;
// if (this.filter.sortOptions === null) {
// this.filter.sortOptions = {
// isAscending: this.isAscendingSort,
// sortField: SortField.SortName
// }
// }
// this.filter.sortOptions.isAscending = this.isAscendingSort;
}
toggleWantToRead() {
if (this.isWantToRead) {
this.actionService.removeMultipleSeriesFromWantToReadList([this.series.id]);

View file

@ -1,131 +1,134 @@
<div class="row g-0 mt-2 mb-2">
<ng-container *transloco="let t; read: 'series-metadata-detail'">
<div class="row g-0 mt-2 mb-2">
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
</div>
</div>
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" heading="Ratings">
<ng-template #itemTemplate let-item>
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
</ng-template>
</app-metadata-detail>
<ng-container *ngIf="WebLinks as links">
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" heading="Links">
<app-metadata-detail [tags]="['']" [libraryId]="series.libraryId" heading="Ratings">
<ng-template #itemTemplate let-item>
<a class="col me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item">
<img width="24" height="24" class="lazyload img-placeholder"
[src]="imageService.errorWebLinkImage"
[attr.data-src]="imageService.getWebLinkImage(item)"
(error)="imageService.updateErroredWebLinkImage($event)"
aria-hidden="true" alt="">
</a>
<app-external-rating [seriesId]="series.id" [userRating]="series.userRating" [hasUserRated]="series.hasUserRated" [libraryType]="libraryType"></app-external-rating>
</ng-template>
</app-metadata-detail>
</ng-container>
<app-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Genres" heading="Genres">
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Tags" heading="Tags">
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" heading="Collections">
<ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
{{item.title}}
</app-tag-badge>
</ng-template>
</app-metadata-detail>
<ng-container *ngIf="WebLinks as links">
<app-metadata-detail [tags]="links" [libraryId]="series.libraryId" [heading]="t('links-title')">
<ng-template #itemTemplate let-item>
<a class="col me-1" [href]="item | safeHtml" target="_blank" rel="noopener noreferrer" [title]="item">
<img width="24" height="24" class="lazyload img-placeholder"
[src]="imageService.errorWebLinkImage"
[attr.data-src]="imageService.getWebLinkImage(item)"
(error)="imageService.updateErroredWebLinkImage($event)"
aria-hidden="true" alt="">
</a>
</ng-template>
</app-metadata-detail>
</ng-container>
<app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" heading="Reading Lists">
<ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
<app-metadata-detail [tags]="seriesMetadata.genres" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Genres" [heading]="t('genres-title')">
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.tags" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Tags" [heading]="t('tags-title')">
<ng-template #titleTemplate let-item>{{item.title}}</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.collectionTags" [libraryId]="series.libraryId" [heading]="t('collections-title')">
<ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('collections', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
{{item.title}}
</app-tag-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="readingLists" [libraryId]="series.libraryId" [heading]="t('reading-lists-title')">
<ng-template #itemTemplate let-item>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="navigate('lists', item.id)" [selectionMode]="TagBadgeCursor.Clickable">
<span *ngIf="item.promoted">
<i class="fa fa-angle-double-up" aria-hidden="true"></i>&nbsp;
<span class="visually-hidden">(promoted)</span>
<span class="visually-hidden">({{t('promoted')}})</span>
</span>
{{item.title}}
</app-tag-badge>
</ng-template>
</app-metadata-detail>
{{item.title}}
</app-tag-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Writers" heading="Writers/Authors">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
<app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.CoverArtists" heading="Cover Artists">
<app-metadata-detail [tags]="seriesMetadata.writers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Writers" [heading]="t('writers-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Character" heading="Characters">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Colorist" heading="Colorists">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
<app-metadata-detail [tags]="seriesMetadata.coverArtists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.CoverArtists" [heading]="t('cover-artists-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Editor" heading="Editors">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.characters" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Character" [heading]="t('characters-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Inker" heading="Inkers">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.colorists" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Colorist" [heading]="t('colorists-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Letterer" heading="Letterers">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.editors" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Editor" [heading]="t('editors-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Translator" heading="Translators">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.inkers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Inker" [heading]="t('inkers-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Penciller" heading="Pencillers">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.letterers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Letterer" [heading]="t('letterers-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Publisher" heading="Publishers">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<app-metadata-detail [tags]="seriesMetadata.translators" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Translator" [heading]="t('translators-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
</div>
<app-metadata-detail [tags]="seriesMetadata.pencillers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Penciller" [heading]="t('pencillers-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
<div class="row g-0">
<app-metadata-detail [tags]="seriesMetadata.publishers" [libraryId]="series.libraryId" [queryParam]="FilterQueryParam.Publisher" [heading]="t('publishers-title')">
<ng-template #itemTemplate let-item>
<app-person-badge a11y-click="13,32" class="col-auto" [person]="item"></app-person-badge>
</ng-template>
</app-metadata-detail>
</div>
<div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperties" >
<a [class.hidden]="hasExtendedProperties" *ngIf="hasExtendedProperties"
class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
See {{isCollapsed ? 'More' : 'Less'}}
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
{{isCollapsed ? t('see-more') : t('see-less')}}
</a>
</div>
</div>
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>
</ng-container>

View file

@ -20,6 +20,7 @@ import {NgbCollapse} from "@ng-bootstrap/ng-bootstrap";
import {SeriesInfoCardsComponent} from "../../../cards/series-info-cards/series-info-cards.component";
import {LibraryType} from "../../../_models/library";
import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.component";
import {TranslocoModule} from "@ngneat/transloco";
@Component({
@ -27,7 +28,7 @@ import {MetadataDetailComponent} from "../metadata-detail/metadata-detail.compon
standalone: true,
imports: [CommonModule, TagBadgeComponent, BadgeExpanderComponent, SafeHtmlPipe, ExternalRatingComponent,
ReadMoreComponent, A11yClickDirective, PersonBadgeComponent, NgbCollapse, SeriesInfoCardsComponent,
MetadataDetailComponent],
MetadataDetailComponent, TranslocoModule],
templateUrl: './series-metadata-detail.component.html',
styleUrls: ['./series-metadata-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush