Infinite Scroll + List View + Cover Upload Redesign (#1319)
* Started with the redesign of the cover image chooser redesign to be less click intensive for volume/chapter images. Made some headings bold in card detail drawer. * Tweaked the styles * Moved where the info cards show * Added an ability to open a page settings drawer * Cleaned up some old code that isn't needed anymore. * Started implementing a list view. Refactored some title code to a dedicated component * List view implemented but way too many API calls. Either need caching or adjusting the SeriesDetail api. * Fixed a bug where if the progress bar didn't render on a card item while a download was in progress, the download indicator would be removed. * Large refactor to move a lot of the needed fields to the chapter and volume dtos for series detail. All fields are noted when only used in series detail. * Implemented cards for other tabs (except related) * Fixed the unit test which needed a mocked reader service call. * More cleanup around age rating and removing old code from the refactor. Commented out sorting till i feel motivated to work on that. * Some cleanup and restored cards as initial layout. Time to test this out and see if there is value add. * Added ability for Chapters tab to show the volume chapters belong to (if applicable) * Adding style fixes * Cover image updates, don't allow the first image (which is what is currently set) to respond to cover changes. Hide the ID field on list item for series detail. * Refactored the title for list item to be injectable * Cleaned up the selection code to make it less finicky on mobile when tap scrolling. * Refactored chapter tab to show volume as well on list view. * Ensure word count shows for Volumes * Started adding virtual scrolling, pushing up so Robbie can mess around * Started adding virtual scrolling, pushing up so Robbie can mess around * Fixed a bug where all chapters would come under specials * Show title data as accent if set. * Style fixes for virtual scroller * Restyling scroll * Implemented a way to show storyline with virtual scrolling * Show Word Count for chapters and cleaned up some logics. * I might have card layout working with virtual scroll code. * Some cleanup to hide more system like properties from info bar on series detail page. Fixed some missing time estimate info on storyline chapters. * Fixed a regression on series service when I integrated VolumeTitle. * Refactored read time to the backend. Added WordCount to the volume itself so we don't need to calculate on frontend. When asking to analyze files from a series, force the calculation. * Fixed SeriesDetail api code * Fixed up the code in the drawer to better update list/card mode * Basic infinite scroll implemented, however due to how we are updating the list to render, we are re-rending cards that haven't been touched. * Updated how we render and layout data for infinite scroll on library detail. It's almost there. * Started laying foundation for loading pages backwards. Removed lazy loading of images since we are now using virtual paging. * Hooked in some basic code to allow user to load a prev page with infinite scroll. * Fixed up series detail api and undid the non-lazy loaded images. Changed the router to help with this infinite loading on Firefox issue. * Fixed up some naming issues with Series Detail and added a new test. * This is an infinite scroll without pagination implementation. It is not fully done, but off to a good start. Virtual scroller with jump bar is working pretty well, def needs more polishing and tweaking. There are hacks in this implementation that need to be revisited. * Refactored code so that we don't use any pagination and load all results by default. * Misc code cleanup from build warnings. * Cleaned up some logic for how to display titles in list view. * More title cleanup for specials * Hooked up page layout to user preferences and renamed an existing user pref name to match the dto. * Swapped out everything but storyline with virtual-scroller over CDK * Removed CDK from series detail. * Default value for migration on page layout * Updating card layout for library detail page * fixing height for mobile * Moved scrollbar * Tweaked some styling for layouts when there is no data * Refactored the series cards into their own component to make it re-usable. * More tweaks on series info cards layout and enhanced a few pages with trackby functions. * Removed some dead code * Added download on series detail to actionables to fit in with new scroll strategy. * Fixed language not being updated and sent to the backend for series update. * Fixed a bad migration (if you ran any prior migration in this branch, you need to undo before you use this commit) * Adding sticky tabs * fixed mobile gap on sticky tab * Enhanced the card title for books to show number up front. * Adjusted the gutters on admin dashboard * Removed debug code * Removing duplicate book title * Cleaned up old references to cdk scroller * Implemented a basic jump bar scaling algorithm. Not perfect, but works pretty well. * Code smells Co-authored-by: Robbie Davis <robbie@therobbiedavis.com>
This commit is contained in:
parent
f0f0e23e88
commit
bbc48a5f5b
122 changed files with 7863 additions and 2097 deletions
|
|
@ -1,4 +1,4 @@
|
|||
<app-side-nav-companion-bar *ngIf="series !== undefined">
|
||||
<app-side-nav-companion-bar *ngIf="series !== undefined" [hasExtras]="true" [extraDrawer]="extrasDrawer">
|
||||
<ng-container title>
|
||||
<h2 style="margin-bottom: 0px">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="seriesActions" [labelBy]="series.name" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
|
|
@ -8,9 +8,52 @@
|
|||
<ng-container subtitle *ngIf="series?.localizedName !== series?.name">
|
||||
<h6 class="subtitle-with-actionables" title="Localized Name">{{series?.localizedName}}</h6>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #extrasDrawer let-offcanvas>
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid pt-2" *ngIf="series !== undefined">
|
||||
<div class="row mb-3">
|
||||
|
||||
|
||||
<div (scroll)="onScroll()" class="main-container container-fluid pt-2" *ngIf="series !== undefined" #scrollingBlock>
|
||||
<div class="row mb-3 info-container">
|
||||
<div class="col-md-2 col-xs-4 col-sm-6 d-none d-sm-block">
|
||||
<app-image maxWidth="300px" [imageUrl]="seriesImage"></app-image>
|
||||
<!-- NOTE: We can put continue point here as Vol X Ch Y or just Ch Y or Book Z ?-->
|
||||
|
|
@ -37,7 +80,7 @@
|
|||
<app-card-actionables [disabled]="actionInProgress" (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" *ngIf="isAdmin || hasDownloadingRole">
|
||||
<button class="btn btn-secondary" (click)="downloadSeries()" title="Download Series" [disabled]="downloadInProgress">
|
||||
<ng-container *ngIf="downloadInProgress; else notDownloading">
|
||||
|
|
@ -59,82 +102,173 @@
|
|||
<app-read-more class="user-review {{userReview ? 'mt-1' : ''}}" [text]="series?.userReview || ''" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
<div *ngIf="seriesMetadata" class="mt-2">
|
||||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series"></app-series-metadata-detail>
|
||||
<app-series-metadata-detail [seriesMetadata]="seriesMetadata" [readingLists]="readingLists" [series]="series" [hasReadingProgress]="hasReadingProgress"></app-series-metadata-detail>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container>
|
||||
<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>
|
||||
<div class="card-container row g-0">
|
||||
<ng-container *ngFor="let volume of volumes; let idx = index; trackBy: trackByVolumeIdentity">
|
||||
<app-card-item class="col-auto mt-2 mb-2" *ngIf="volume.number != 0" [entity]="volume" [title]="volume.name" (click)="openVolume(volume)"
|
||||
[imageUrl]="imageService.getVolumeCoverImage(volume.id)"
|
||||
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item>
|
||||
<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" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', 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" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, storyChapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let chapter of storyChapters; let idx = index; trackBy: trackByChapterIdentity">
|
||||
<app-card-item class="col-auto mt-2 mb-2" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="chapter.title" (click)="openChapter(chapter)"
|
||||
[imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
||||
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, storyChapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
|
||||
</ng-container>
|
||||
</div>
|
||||
<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)"
|
||||
[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)">
|
||||
<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)"
|
||||
[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)">
|
||||
<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>
|
||||
<div class="card-container row g-0">
|
||||
<ng-container *ngFor="let item of volumes; 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', idx, volumes.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true">
|
||||
</app-card-item>
|
||||
<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', idx, volumes.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true">
|
||||
</app-card-item>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #volumeListLayout>
|
||||
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
|
||||
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)"
|
||||
[seriesName]="series.name" [entity]="volume" *ngIf="volume.number != 0"
|
||||
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)">
|
||||
<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>
|
||||
|
||||
<li [ngbNavItem]="TabID.Chapters" *ngIf="chapters.length > 0">
|
||||
<a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="card-container row g-0">
|
||||
<ng-container *ngFor="let item of chapters; 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"
|
||||
(selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
|
||||
<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"
|
||||
(selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('chapter', 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>
|
||||
</div>
|
||||
<ng-template #chapterListLayout>
|
||||
<div *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
|
||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
||||
[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">
|
||||
<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>
|
||||
<div class="card-container row g-0">
|
||||
<ng-container *ngFor="let item of specials; 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"
|
||||
(selection)="bulkSelectionService.handleCardSelection('special', idx, chapters.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('special', idx)" [allowSelection]="true"></app-card-item>
|
||||
<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"
|
||||
(selection)="bulkSelectionService.handleCardSelection('special', idx, chapters.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('special', idx)" [allowSelection]="true">
|
||||
</app-card-item>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #specialListLayout>
|
||||
<ng-container *ngFor="let chapter of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
|
||||
<app-list-item [imageUrl]="imageService.getChapterCoverImage(chapter.id)"
|
||||
[seriesName]="series.name" [entity]="chapter"
|
||||
[actions]="chapterActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
|
||||
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)">
|
||||
<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>
|
||||
<div class="card-container row g-0">
|
||||
<ng-container *ngFor="let item of relations; let idx = index; trackBy: trackByRelatedSeriesIdentiy">
|
||||
<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 #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: trackByRelatedSeriesIdentiy">
|
||||
<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>
|
||||
</ul>
|
||||
|
|
@ -147,3 +281,4 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,3 +13,24 @@
|
|||
grid-gap: 0.5rem;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.virtual-scroller, virtual-scroller {
|
||||
width: 100%;
|
||||
//height: 10000px;
|
||||
height: calc(100vh - 85px);
|
||||
max-height: calc(var(--vh)*100 - 170px);
|
||||
}
|
||||
|
||||
// This is responsible for ensuring we scroll down and only tabs and companion bar is visible
|
||||
.main-container {
|
||||
height: calc(var(--vh)*100 - 117px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-tabs.fixed {
|
||||
position: fixed;
|
||||
top: 114px;
|
||||
z-index: 100;
|
||||
background-color: var(--bs-body-bg);
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModal, NgbNavChangeEvent, NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { forkJoin, Subject } from 'rxjs';
|
||||
import { finalize, mergeMap, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
|
||||
import { BulkSelectionService } from '../cards/bulk-selection.service';
|
||||
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
|
||||
import { EditSeriesModalComponent } from '../cards/_modals/edit-series-modal/edit-series-modal.component';
|
||||
import { ConfirmConfig } from '../shared/confirm-dialog/_models/confirm-config';
|
||||
import { ConfirmService } from '../shared/confirm.service';
|
||||
|
|
@ -36,6 +35,9 @@ import { NavService } from '../_services/nav.service';
|
|||
import { RelatedSeries } from '../_models/series-detail/related-series';
|
||||
import { RelationKind } from '../_models/series-detail/relation-kind';
|
||||
import { CardDetailDrawerComponent } from '../cards/card-detail-drawer/card-detail-drawer.component';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { PageLayoutMode } from '../_models/page-layout-mode';
|
||||
import { VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
|
||||
|
||||
interface RelatedSeris {
|
||||
series: Series;
|
||||
|
|
@ -50,6 +52,12 @@ enum TabID {
|
|||
Chapters = 4
|
||||
}
|
||||
|
||||
interface StoryLineItem {
|
||||
chapter?: Chapter;
|
||||
volume?: Volume;
|
||||
isChapter: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-detail',
|
||||
templateUrl: './series-detail.component.html',
|
||||
|
|
@ -57,6 +65,8 @@ enum TabID {
|
|||
})
|
||||
export class SeriesDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild(VirtualScrollerComponent) private virtualScroller!: VirtualScrollerComponent;
|
||||
|
||||
/**
|
||||
* Series Id. Set at load before UI renders
|
||||
*/
|
||||
|
|
@ -65,6 +75,7 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
volumes: Volume[] = [];
|
||||
chapters: Chapter[] = [];
|
||||
storyChapters: Chapter[] = [];
|
||||
storylineItems: StoryLineItem[] = [];
|
||||
libraryId = 0;
|
||||
isAdmin = false;
|
||||
hasDownloadingRole = false;
|
||||
|
|
@ -101,6 +112,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
*/
|
||||
actionInProgress: boolean = false;
|
||||
|
||||
itemSize: number = 10; // when 10 done, 16 loads
|
||||
|
||||
/**
|
||||
* Track by function for Volume to tell when to refresh card data
|
||||
*/
|
||||
|
|
@ -110,6 +123,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
*/
|
||||
trackByChapterIdentity = (index: number, item: Chapter) => `${item.title}_${item.number}_${item.pagesRead}`;
|
||||
trackByRelatedSeriesIdentiy = (index: number, item: RelatedSeris) => `${item.series.name}_${item.series.libraryId}_${item.series.pagesRead}_${item.relation}`;
|
||||
trackByStoryLineIdentity = (index: number, item: StoryLineItem) => {
|
||||
if (item.isChapter) {
|
||||
return this.trackByChapterIdentity(index, item!.chapter!)
|
||||
}
|
||||
return this.trackByVolumeIdentity(index, item!.volume!);
|
||||
};
|
||||
|
||||
/**
|
||||
* Are there any related series
|
||||
|
|
@ -120,6 +139,20 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
*/
|
||||
relations: Array<RelatedSeris> = [];
|
||||
|
||||
sortingOptions: Array<{value: string, text: string}> = [
|
||||
{value: 'Storyline', text: 'Storyline'},
|
||||
{value: 'Release', text: 'Release'},
|
||||
{value: 'Added', text: 'Added'},
|
||||
];
|
||||
renderMode: PageLayoutMode = PageLayoutMode.Cards;
|
||||
|
||||
pageExtrasGroup = new FormGroup({
|
||||
'sortingOption': new FormControl(this.sortingOptions[0].value, []),
|
||||
'renderMode': new FormControl(this.renderMode, []),
|
||||
});
|
||||
|
||||
isAscendingSort: boolean = false; // TODO: Get this from User preferences
|
||||
|
||||
bulkActionCallback = (action: Action, data: any) => {
|
||||
if (this.series === undefined) {
|
||||
return;
|
||||
|
|
@ -167,22 +200,27 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
|
||||
get LibraryType(): typeof LibraryType {
|
||||
get LibraryType() {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
get MangaFormat(): typeof MangaFormat {
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
}
|
||||
|
||||
get TagBadgeCursor(): typeof TagBadgeCursor {
|
||||
get TagBadgeCursor() {
|
||||
return TagBadgeCursor;
|
||||
}
|
||||
|
||||
get TabID(): typeof TabID {
|
||||
get TabID() {
|
||||
return TabID;
|
||||
}
|
||||
|
||||
get PageLayoutMode() {
|
||||
return PageLayoutMode;
|
||||
}
|
||||
|
||||
|
||||
constructor(private route: ActivatedRoute, private seriesService: SeriesService,
|
||||
private router: Router, public bulkSelectionService: BulkSelectionService,
|
||||
private modalService: NgbModal, public readerService: ReaderService,
|
||||
|
|
@ -200,10 +238,28 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
if (user) {
|
||||
this.isAdmin = this.accountService.hasAdminRole(user);
|
||||
this.hasDownloadingRole = this.accountService.hasDownloadRole(user);
|
||||
this.renderMode = user.preferences.globalPageLayoutMode;
|
||||
this.pageExtrasGroup.get('renderMode')?.setValue(this.renderMode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onScroll(): void {
|
||||
const tabs = document.querySelector('.nav-tabs') as HTMLElement | null;
|
||||
const main = document.querySelector('.main-container') as HTMLElement | null;
|
||||
const content = document.querySelector('.tab-content') as HTMLElement | null;
|
||||
let mainOffset = main!.offsetTop;
|
||||
let tabOffset = tabs!.offsetTop;
|
||||
let contentOffset = content!.offsetTop;
|
||||
let mainScrollPos = main!.scrollTop;
|
||||
|
||||
if (!document.querySelector('.nav-tabs.fixed') && (tabOffset - mainOffset) <= mainScrollPos) {
|
||||
tabs!.classList.add("fixed");
|
||||
} else if (document.querySelector('.nav-tabs.fixed') && mainScrollPos <= (contentOffset - mainOffset)) {
|
||||
tabs!.classList.remove("fixed");
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const routeId = this.route.snapshot.paramMap.get('seriesId');
|
||||
const libraryId = this.route.snapshot.paramMap.get('libraryId');
|
||||
|
|
@ -223,7 +279,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
const seriesCoverUpdatedEvent = event.payload as ScanSeriesEvent;
|
||||
if (seriesCoverUpdatedEvent.seriesId === this.seriesId) {
|
||||
this.loadSeries(this.seriesId);
|
||||
this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.seriesId)); // NOTE: Is this needed as cover update will update the image for us
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -232,6 +287,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
this.libraryId = parseInt(libraryId, 10);
|
||||
this.seriesImage = this.imageService.getSeriesCoverImage(this.seriesId);
|
||||
this.loadSeries(this.seriesId);
|
||||
|
||||
this.pageExtrasGroup.get('renderMode')?.valueChanges.pipe(takeUntil(this.onDestroy)).subscribe((val: PageLayoutMode) => {
|
||||
this.renderMode = val;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
|
@ -290,6 +349,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
case (Action.AnalyzeFiles):
|
||||
this.actionService.analyzeFilesForSeries(series, () => this.actionInProgress = false);
|
||||
break;
|
||||
case (Action.Download):
|
||||
if (this.downloadInProgress) return;
|
||||
this.downloadSeries();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -358,12 +421,13 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
this.setContinuePoint();
|
||||
|
||||
forkJoin([
|
||||
this.libraryService.getLibraryType(this.libraryId),
|
||||
this.seriesService.getSeries(seriesId)
|
||||
]).subscribe(results => {
|
||||
this.libraryType = results[0];
|
||||
this.series = results[1];
|
||||
forkJoin({
|
||||
libType: this.libraryService.getLibraryType(this.libraryId),
|
||||
series: this.seriesService.getSeries(seriesId)
|
||||
}).subscribe(results => {
|
||||
this.libraryType = results.libType;
|
||||
console.log('library type: ', this.libraryType);
|
||||
this.series = results.series;
|
||||
|
||||
this.createHTML();
|
||||
|
||||
|
|
@ -371,6 +435,8 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.seriesActions = this.actionFactoryService.getSeriesActions(this.handleSeriesActionCallback.bind(this))
|
||||
.filter(action => action.action !== Action.Edit);
|
||||
this.seriesActions.push({action: Action.Download, callback: this.seriesActions[0].callback, requiresAdmin: false, title: 'Download'});
|
||||
|
||||
this.volumeActions = this.actionFactoryService.getVolumeActions(this.handleVolumeActionCallback.bind(this));
|
||||
this.chapterActions = this.actionFactoryService.getChapterActions(this.handleChapterActionCallback.bind(this));
|
||||
|
||||
|
|
@ -401,6 +467,16 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
this.chapters = detail.chapters;
|
||||
this.volumes = detail.volumes;
|
||||
this.storyChapters = detail.storylineChapters;
|
||||
this.storylineItems = [];
|
||||
const v = this.volumes.map(v => {
|
||||
return {volume: v, chapter: undefined, isChapter: false} as StoryLineItem;
|
||||
});
|
||||
this.storylineItems.push(...v);
|
||||
const c = this.storyChapters.map(c => {
|
||||
return {volume: undefined, chapter: c, isChapter: true} as StoryLineItem;
|
||||
});
|
||||
this.storylineItems.push(...c);
|
||||
|
||||
|
||||
this.updateSelectedTab();
|
||||
this.isLoading = false;
|
||||
|
|
@ -530,9 +606,6 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
this.toastr.error('There are no chapters to this volume. Cannot read.');
|
||||
return;
|
||||
}
|
||||
// NOTE: When selecting a volume, we might want to ask the user which chapter they want or an "Automatic" option. For Volumes
|
||||
// made up of lots of chapter files, it makes it more versitile. The modal can have pages read / pages with colored background
|
||||
// to help the user make a good choice.
|
||||
|
||||
// If user has progress on the volume, load them where they left off
|
||||
if (volume.pagesRead < volume.pages && volume.pagesRead > 0) {
|
||||
|
|
@ -573,15 +646,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
|
||||
this.loadSeries(this.seriesId);
|
||||
if (closeResult.coverImageUpdate) {
|
||||
// Random triggers a load change without any problems with API
|
||||
this.seriesImage = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.seriesId));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async promptToReview() {
|
||||
// TODO: After a review has been set, we might just want to show an edit icon next to star rating which opens the review, instead of prompting each time.
|
||||
const shouldPrompt = this.isNullOrEmpty(this.series.userReview);
|
||||
const config = new ConfirmConfig();
|
||||
config.header = 'Confirm';
|
||||
|
|
@ -631,15 +701,15 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
formatChapterTitle(chapter: Chapter) {
|
||||
return this.utilityService.formatChapterName(this.libraryType, true, true) + chapter.range;
|
||||
}
|
||||
updateSortOrder() {
|
||||
this.isAscendingSort = !this.isAscendingSort;
|
||||
// if (this.filter.sortOptions === null) {
|
||||
// this.filter.sortOptions = {
|
||||
// isAscending: this.isAscendingSort,
|
||||
// sortField: SortField.SortName
|
||||
// }
|
||||
// }
|
||||
|
||||
formatVolumeTitle(volume: Volume) {
|
||||
if (this.libraryType === LibraryType.Book) {
|
||||
return volume.name;
|
||||
}
|
||||
|
||||
return 'Volume ' + volume.name;
|
||||
// this.filter.sortOptions.isAscending = this.isAscendingSort;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SeriesDetailRoutingModule } from './series-detail-routing.module';
|
||||
import { NgbCollapseModule, NgbNavModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbCollapseModule, NgbNavModule, NgbRatingModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SeriesDetailComponent } from './series-detail.component';
|
||||
import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-metadata-detail.component';
|
||||
import { ReviewSeriesModalComponent } from './review-series-modal/review-series-modal.component';
|
||||
|
|
@ -17,7 +17,7 @@ import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-n
|
|||
declarations: [
|
||||
SeriesDetailComponent,
|
||||
ReviewSeriesModalComponent,
|
||||
SeriesMetadataDetailComponent
|
||||
SeriesMetadataDetailComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
@ -26,6 +26,7 @@ import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-n
|
|||
NgbCollapseModule, // Series Metadata
|
||||
NgbNavModule,
|
||||
NgbRatingModule,
|
||||
NgbTooltipModule, // Series Detail, Extras Drawer
|
||||
|
||||
TypeaheadModule,
|
||||
PipeModule,
|
||||
|
|
|
|||
|
|
@ -2,108 +2,7 @@
|
|||
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
|
||||
<!-- This first row will have random information about the series-->
|
||||
<div class="row g-0 mb-4 mt-3">
|
||||
<ng-container *ngIf="seriesMetadata.ageRating">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-3">
|
||||
<app-icon-and-title label="Age Rating" [clickable]="true" fontClasses="fas fa-eye" (click)="goTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" title="Age Rating">
|
||||
{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="series">
|
||||
<ng-container *ngIf="seriesMetadata.releaseYear > 0">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-3">
|
||||
<app-icon-and-title label="Release Year" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release Year">
|
||||
{{seriesMetadata.releaseYear}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="seriesMetadata.language !== null">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-3">
|
||||
<app-icon-and-title label="Language" [clickable]="true" fontClasses="fas fa-language" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" title="Language">
|
||||
{{seriesMetadata.language | defaultValue:'en' | languageName | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<ng-container *ngIf="seriesMetadata.publicationStatus | publicationStatus as pubStatus">
|
||||
<app-icon-and-title label="Publication" [clickable]="true" fontClasses="fa-solid fa-hourglass-{{pubStatus === 'Ongoing' ? 'empty' : 'end'}}" (click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)" title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})">
|
||||
{{pubStatus}}
|
||||
</app-icon-and-title>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="vr m-2 d-none d-lg-block"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Format" [clickable]="true" [fontClasses]="'fa ' + utilityService.mangaFormatIcon(series.format)" (click)="goTo(FilterQueryParam.Format, series.format)" title="Format">
|
||||
{{utilityService.mangaFormat(series.format)}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Last Read" [clickable]="false" fontClasses="fa-regular fa-clock" title="Last Read">
|
||||
{{series.latestReadDate | date:'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB; else showPages">
|
||||
<ng-container *ngIf="series.wordCount > 0">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Word Count" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{series.wordCount | compactNumber}} Words
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
<ng-template #showPages>
|
||||
<div class="d-none d-md-block col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Print Length" [clickable]="false" fontClasses="fa-regular fa-file-lines">
|
||||
{{series.pages | number:''}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-template>
|
||||
|
||||
|
||||
|
||||
<ng-container *ngIf="series.format === MangaFormat.EPUB && series.wordCount > 0 || series.format !== MangaFormat.EPUB">
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Read Time" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngIf="readingTimeLeft.hasProgress && readingTimeLeft.avgHours !== 0 ">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-lg-1 col-md-4 col-sm-4 col-4 mb-2">
|
||||
<app-icon-and-title label="Read Left" [clickable]="false" fontClasses="fa-solid fa-clock">
|
||||
~{{readingTimeLeft.avgHours}} Hour{{readingTimeLeft.avgHours > 1 ? 's' : ''}} Left
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="row g-0" *ngIf="seriesMetadata.genres && seriesMetadata.genres.length > 0">
|
||||
<div class="col-md-4">
|
||||
|
|
@ -300,4 +199,7 @@
|
|||
<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}} me-1" aria-controls="extended-series-metadata"></i>
|
||||
See {{isCollapsed ? 'More' : 'Less'}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- This first row will have random information about the series-->
|
||||
<app-series-info-cards [series]="series" [seriesMetadata]="seriesMetadata" (goTo)="handleGoTo($event)" [hasReadingProgress]="hasReadingProgress"></app-series-info-cards>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { HourEstimateRange } from 'src/app/_models/hour-estimate-range';
|
||||
import { MAX_WORDS_PER_HOUR, MIN_WORDS_PER_HOUR, MIN_PAGES_PER_MINUTE, MAX_PAGES_PER_MINUTE, ReaderService } from 'src/app/_services/reader.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { TagBadgeCursor } from '../../shared/tag-badge/tag-badge.component';
|
||||
import { FilterQueryParam } from '../../shared/_services/filter-utilities.service';
|
||||
import { UtilityService } from '../../shared/_services/utility.service';
|
||||
|
|
@ -20,6 +20,7 @@ import { MetadataService } from '../../_services/metadata.service';
|
|||
export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input() seriesMetadata!: SeriesMetadata;
|
||||
@Input() hasReadingProgress: boolean = false;
|
||||
/**
|
||||
* Reading lists with a connection to the Series
|
||||
*/
|
||||
|
|
@ -29,8 +30,8 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
|||
isCollapsed: boolean = true;
|
||||
hasExtendedProperites: boolean = false;
|
||||
|
||||
readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1, hasProgress: false};
|
||||
readingTimeLeft: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1, hasProgress: false};
|
||||
// readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1};
|
||||
// readingTimeLeft: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1};
|
||||
|
||||
/**
|
||||
* Html representation of Series Summary
|
||||
|
|
@ -67,11 +68,6 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
|||
if (this.seriesMetadata !== null) {
|
||||
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
if (this.series !== null) {
|
||||
this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => this.readingTimeLeft = timeLeft);
|
||||
this.readerService.getTimeToRead(this.series.id).subscribe((time) => this.readingTime = time);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -81,6 +77,10 @@ export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
|
|||
this.isCollapsed = !this.isCollapsed;
|
||||
}
|
||||
|
||||
handleGoTo(event: {queryParamName: FilterQueryParam, filter: any}) {
|
||||
this.goTo(event.queryParamName, event.filter);
|
||||
}
|
||||
|
||||
goTo(queryParamName: FilterQueryParam, filter: any) {
|
||||
let params: any = {};
|
||||
params[queryParamName] = filter;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue