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
1859
UI/Web/package-lock.json
generated
1859
UI/Web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -27,6 +27,7 @@
|
|||
"@angular/platform-browser-dynamic": "~13.2.2",
|
||||
"@angular/router": "~13.2.2",
|
||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||
"@iharbeck/ngx-virtual-scroller": "^13.0.4",
|
||||
"@microsoft/signalr": "^6.0.2",
|
||||
"@ng-bootstrap/ng-bootstrap": "^12.1.2",
|
||||
"@popperjs/core": "^2.11.2",
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { HourEstimateRange } from './hour-estimate-range';
|
||||
import { MangaFile } from './manga-file';
|
||||
import { AgeRating } from './metadata/age-rating';
|
||||
import { AgeRatingDto } from './metadata/age-rating-dto';
|
||||
|
||||
/**
|
||||
* Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields.
|
||||
|
@ -23,4 +26,19 @@ export interface Chapter {
|
|||
* Actual name of the Chapter if populated in underlying metadata
|
||||
*/
|
||||
titleName: string;
|
||||
/**
|
||||
* Summary for the chapter
|
||||
*/
|
||||
summary?: string;
|
||||
minHoursToRead: number;
|
||||
maxHoursToRead: number;
|
||||
avgHoursToRead: number;
|
||||
|
||||
ageRating: AgeRating;
|
||||
releaseDate: string;
|
||||
wordCount: number;
|
||||
/**
|
||||
* 'Volume number'. Only available for SeriesDetail
|
||||
*/
|
||||
volumeTitle?: string;
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@ export interface HourEstimateRange{
|
|||
minHours: number;
|
||||
maxHours: number;
|
||||
avgHours: number;
|
||||
hasProgress: boolean;
|
||||
//hasProgress: boolean;
|
||||
}
|
10
UI/Web/src/app/_models/page-layout-mode.ts
Normal file
10
UI/Web/src/app/_models/page-layout-mode.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export enum PageLayoutMode {
|
||||
/**
|
||||
* Use Cards for laying out data
|
||||
*/
|
||||
Cards = 0,
|
||||
/**
|
||||
* Use list style for laying out items
|
||||
*/
|
||||
List = 1
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode';
|
||||
import { BookPageLayoutMode } from '../book-page-layout-mode';
|
||||
import { PageLayoutMode } from '../page-layout-mode';
|
||||
import { PageSplitOption } from './page-split-option';
|
||||
import { ReaderMode } from './reader-mode';
|
||||
import { ReadingDirection } from './reading-direction';
|
||||
|
@ -31,6 +32,7 @@ export interface Preferences {
|
|||
|
||||
// Global
|
||||
theme: SiteTheme;
|
||||
globalPageLayoutMode: PageLayoutMode;
|
||||
}
|
||||
|
||||
export const readingDirections = [{text: 'Left to Right', value: ReadingDirection.LeftToRight}, {text: 'Right to Left', value: ReadingDirection.RightToLeft}];
|
||||
|
@ -39,3 +41,4 @@ export const pageSplitOptions = [{text: 'Fit to Screen', value: PageSplitOption.
|
|||
export const readingModes = [{text: 'Left to Right', value: ReaderMode.LeftRight}, {text: 'Up to Down', value: ReaderMode.UpDown}, {text: 'Webtoon', value: ReaderMode.Webtoon}];
|
||||
export const layoutModes = [{text: 'Single', value: LayoutMode.Single}, {text: 'Double', value: LayoutMode.Double}, {text: 'Double (Manga)', value: LayoutMode.DoubleReversed}];
|
||||
export const bookLayoutModes = [{text: 'Default', value: BookPageLayoutMode.Default}, {text: '1 Column', value: BookPageLayoutMode.Column1}, {text: '2 Column', value: BookPageLayoutMode.Column2}];
|
||||
export const pageLayoutModes = [{text: 'Cards', value: PageLayoutMode.Cards}, {text: 'List', value: PageLayoutMode.List}];
|
||||
|
|
|
@ -52,4 +52,7 @@ export interface Series {
|
|||
* Number of words in the series
|
||||
*/
|
||||
wordCount: number;
|
||||
minHoursToRead: number;
|
||||
maxHoursToRead: number;
|
||||
avgHoursToRead: number;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Chapter } from './chapter';
|
||||
import { HourEstimateRange } from './hour-estimate-range';
|
||||
|
||||
export interface Volume {
|
||||
id: number;
|
||||
|
@ -8,5 +9,12 @@ export interface Volume {
|
|||
lastModified: string;
|
||||
pages: number;
|
||||
pagesRead: number;
|
||||
chapters: Array<Chapter>; // TODO: Validate any cases where this is undefined
|
||||
chapters: Array<Chapter>;
|
||||
/**
|
||||
* This is only available on the object when fetched for SeriesDetail
|
||||
*/
|
||||
timeEstimate?: HourEstimateRange;
|
||||
minHoursToRead: number;
|
||||
maxHoursToRead: number;
|
||||
avgHoursToRead: number;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Injectable, OnDestroy } from '@angular/core';
|
|||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { Subject } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { finalize, take, takeWhile } from 'rxjs/operators';
|
||||
import { BulkAddToCollectionComponent } from '../cards/_modals/bulk-add-to-collection/bulk-add-to-collection.component';
|
||||
import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component';
|
||||
import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component';
|
||||
|
@ -527,5 +527,4 @@ export class ActionService implements OnDestroy {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -67,6 +67,10 @@ export enum EVENTS {
|
|||
* When bulk bookmarks are being converted
|
||||
*/
|
||||
ConvertBookmarksProgress = 'ConvertBookmarksProgress',
|
||||
/**
|
||||
* When files are being scanned to calculate word count
|
||||
*/
|
||||
WordCountAnalyzerProgress = 'WordCountAnalyzerProgress'
|
||||
}
|
||||
|
||||
export interface Message<T> {
|
||||
|
@ -155,6 +159,13 @@ export class MessageHubService {
|
|||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.WordCountAnalyzerProgress, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.WordCountAnalyzerProgress,
|
||||
payload: resp.body
|
||||
});
|
||||
});
|
||||
|
||||
this.hubConnection.on(EVENTS.LibraryModified, resp => {
|
||||
this.messagesSource.next({
|
||||
event: EVENTS.LibraryModified,
|
||||
|
|
|
@ -22,7 +22,7 @@ export class MetadataService {
|
|||
private ageRatingTypes: {[key: number]: string} | undefined = undefined;
|
||||
private validLanguages: Array<Language> = [];
|
||||
|
||||
constructor(private httpClient: HttpClient, private utilityService: UtilityService) { }
|
||||
constructor(private httpClient: HttpClient) { }
|
||||
|
||||
getAgeRating(ageRating: AgeRating) {
|
||||
if (this.ageRatingTypes != undefined && this.ageRatingTypes.hasOwnProperty(ageRating)) {
|
||||
|
|
|
@ -9,11 +9,6 @@ import { BookmarkInfo } from '../_models/manga-reader/bookmark-info';
|
|||
import { PageBookmark } from '../_models/page-bookmark';
|
||||
import { ProgressBookmark } from '../_models/progress-bookmark';
|
||||
|
||||
export const MAX_WORDS_PER_HOUR = 30_000;
|
||||
export const MIN_WORDS_PER_HOUR = 10_260;
|
||||
export const MAX_PAGES_PER_MINUTE = 2.75;
|
||||
export const MIN_PAGES_PER_MINUTE = 3.33;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
|
@ -129,18 +124,11 @@ export class ReaderService {
|
|||
return this.httpClient.get<Chapter>(this.baseUrl + 'reader/continue-point?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
// TODO: Cache this information
|
||||
getTimeLeft(seriesId: number) {
|
||||
return this.httpClient.get<HourEstimateRange>(this.baseUrl + 'reader/time-left?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
getTimeToRead(seriesId: number) {
|
||||
return this.httpClient.get<HourEstimateRange>(this.baseUrl + 'reader/read-time?seriesId=' + seriesId);
|
||||
}
|
||||
|
||||
getManualTimeToRead(wordCount: number, pageCount: number, isEpub: boolean) {
|
||||
return this.httpClient.get<HourEstimateRange>(this.baseUrl + 'reader/manual-read-time?wordCount=' + wordCount + '&pageCount=' + pageCount + '&isEpub=' + isEpub);
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures current body color and forces background color to be black. Call @see resetOverrideStyles() on destroy of component to revert changes
|
||||
*/
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
Admin Dashboard
|
||||
</h2>
|
||||
</app-side-nav-companion-bar>
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid g-0">
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav nav-tabs">
|
||||
<li *ngFor="let tab of tabs" [ngbNavItem]="tab">
|
||||
<a ngbNavLink routerLink="." [fragment]="tab.fragment">{{ tab.title | sentenceCase }}</a>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<app-card-detail-layout
|
||||
[isLoading]="loadingSeries"
|
||||
[items]="series"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[filterOpen]="filterOpen"
|
||||
|
|
|
@ -131,5 +131,5 @@ export class AllSeriesComponent implements OnInit, OnDestroy {
|
|||
this.loadPage();
|
||||
}
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.originalName}_${item.localizedName}_${item.pagesRead}`;
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||
}
|
||||
|
|
|
@ -71,8 +71,9 @@ const routes: Routes = [
|
|||
]
|
||||
},
|
||||
{path: 'login', loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)},
|
||||
//{path: '', pathMatch: 'full', redirectTo: 'login'}, // This shouldn't be needed
|
||||
{path: '**', pathMatch: 'full', redirectTo: 'libraries'},
|
||||
{path: '', pathMatch: 'full', redirectTo: 'login'},
|
||||
{path: '**', pathMatch: 'prefix', redirectTo: 'libraries'},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -33,6 +33,7 @@ export class AppComponent implements OnInit {
|
|||
this.ngbModal.dismissAll();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
|
||||
<input type="radio" formControlName="layoutMode" [value]="BookPageLayoutMode.Column2" class="btn-check" id="layout-mode-col2" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="layout-mode-col2">2 Column</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
<app-bulk-operations [actionCallback]="bulkActionCallback"></app-bulk-operations>
|
||||
<app-card-detail-layout
|
||||
[isLoading]="loadingBookmarks"
|
||||
[items]="series">
|
||||
[items]="series"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-card-item [entity]="item" (reload)="loadBookmarks()" [title]="item.name" [imageUrl]="imageService.getSeriesCoverImage(item.id)"
|
||||
[supressArchiveWarning]="true" (clicked)="viewBookmarks(item)" [count]="seriesIds[item.id]" [allowSelection]="true"
|
||||
|
|
|
@ -28,6 +28,8 @@ export class BookmarksComponent implements OnInit, OnDestroy {
|
|||
clearingSeries: {[id: number]: boolean} = {};
|
||||
actions: ActionItem<Series>[] = [];
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||
|
||||
private onDestroy: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(private readerService: ReaderService, private seriesService: SeriesService,
|
||||
|
|
|
@ -338,11 +338,13 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
return a.isoCode == b.isoCode;
|
||||
}
|
||||
|
||||
if (this.metadata.language) {
|
||||
const l = this.validLanguages.find(l => l.isoCode === this.metadata.language);
|
||||
if (l !== undefined) {
|
||||
this.languageSettings.savedData = l;
|
||||
}
|
||||
if (this.metadata.language === undefined || this.metadata.language === null || this.metadata.language === '') {
|
||||
this.metadata.language = 'en';
|
||||
}
|
||||
|
||||
const l = this.validLanguages.find(l => l.isoCode === this.metadata.language);
|
||||
if (l !== undefined) {
|
||||
this.languageSettings.savedData = l;
|
||||
}
|
||||
return of(true);
|
||||
}
|
||||
|
@ -428,6 +430,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
model.nameLocked = this.series.nameLocked;
|
||||
model.sortNameLocked = this.series.sortNameLocked;
|
||||
model.localizedNameLocked = this.series.localizedNameLocked;
|
||||
model.language = this.metadata.language;
|
||||
apis.push(this.seriesService.updateSeries(model));
|
||||
}
|
||||
|
||||
|
@ -459,8 +462,12 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
|
|||
this.metadata.genres = genres;
|
||||
}
|
||||
|
||||
updateLanguage(language: Language) {
|
||||
this.metadata.language = language.isoCode;
|
||||
updateLanguage(language: Array<Language>) {
|
||||
if (language.length === 0) {
|
||||
this.metadata.language = '';
|
||||
return;
|
||||
}
|
||||
this.metadata.language = language[0].isoCode;
|
||||
}
|
||||
|
||||
updatePerson(persons: Person[], role: PersonRole) {
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<div class="card" *ngIf="bookmark != undefined">
|
||||
<app-image height="230px" width="170px" [imageUrl]="imageService.getBookmarkedImage(bookmark.chapterId, bookmark.page)"></app-image>
|
||||
|
||||
<div class="card-body" *ngIf="bookmark.page >= 0">
|
||||
<div class="header-row">
|
||||
<span class="card-title" tabindex="0">
|
||||
Page {{bookmark.page + 1}}
|
||||
</span>
|
||||
<span class="card-actions float-end" *ngIf="series != undefined">
|
||||
<button attr.aria-labelledby="series--{{series.name}}" class="btn btn-danger btn-sm" (click)="removeBookmark()"
|
||||
[disabled]="isClearing" placement="top" ngbTooltip="Remove Bookmark" attr.aria-label="Remove Bookmark">
|
||||
<ng-container *ngIf="isClearing; else notClearing">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</ng-container>
|
||||
<ng-template #notClearing>
|
||||
<i class="fa fa-trash-alt" aria-hidden="true"></i>
|
||||
</ng-template>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<a *ngIf="series != undefined" class="title-overflow library" href="/library/{{series.libraryId}}/series/{{series.id}}"
|
||||
placement="top" id="bookmark_card_{{series.name}}_{{bookmark.id}}" [ngbTooltip]="series.name | titlecase">{{series.name | titlecase}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,25 +0,0 @@
|
|||
.card-body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.title-overflow {
|
||||
font-size: 13px;
|
||||
width: 130px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 0px;
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
import { PageBookmark } from '../../_models/page-bookmark';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bookmark',
|
||||
templateUrl: './bookmark.component.html',
|
||||
styleUrls: ['./bookmark.component.scss']
|
||||
})
|
||||
export class BookmarkComponent implements OnInit {
|
||||
|
||||
@Input() bookmark: PageBookmark | undefined;
|
||||
@Output() bookmarkRemoved: EventEmitter<PageBookmark> = new EventEmitter<PageBookmark>();
|
||||
series: Series | undefined;
|
||||
|
||||
isClearing: boolean = false;
|
||||
isDownloading: boolean = false;
|
||||
|
||||
constructor(public imageService: ImageService, private seriesService: SeriesService, private readerService: ReaderService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.bookmark) {
|
||||
this.seriesService.getSeries(this.bookmark.seriesId).subscribe(series => {
|
||||
this.series = series;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleClick(event: any) {
|
||||
|
||||
}
|
||||
|
||||
removeBookmark() {
|
||||
if (this.bookmark === undefined) return;
|
||||
this.readerService.unbookmark(this.bookmark.seriesId, this.bookmark.volumeId, this.bookmark.chapterId, this.bookmark.page).subscribe(res => {
|
||||
this.bookmarkRemoved.emit(this.bookmark);
|
||||
this.bookmark = undefined;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ export class BulkOperationsComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
const navBar = document.querySelector('.navbar');
|
||||
if (navBar) {
|
||||
this.topOffset = Math.ceil(navBar.getBoundingClientRect().height);
|
||||
this.topOffset = Math.ceil(navBar.getBoundingClientRect().height); // TODO: We can make this fixed 63px
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,32 +1,8 @@
|
|||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">
|
||||
<ng-container [ngSwitch]="libraryType">
|
||||
<ng-container *ngSwitchCase="LibraryType.Comic">
|
||||
<span class="modal-title" id="modal-basic-title">
|
||||
<ng-container *ngIf="chapter.titleName != ''; else fullComicTitle">
|
||||
{{chapter.titleName}}
|
||||
</ng-container>
|
||||
<ng-template #fullComicTitle>
|
||||
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Chapter ' : 'Volume ') + data.number : 'Special'}}
|
||||
</ng-template>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Manga">
|
||||
<span class="modal-title" id="modal-basic-title">
|
||||
<ng-container *ngIf="chapter.titleName != ''; else fullMangaTitle">
|
||||
{{chapter.titleName}}
|
||||
</ng-container>
|
||||
<ng-template #fullMangaTitle>
|
||||
{{parentName}} - {{data.number != 0 ? (isChapter ? 'Issue #' : 'Volume ') + data.number : 'Special'}}
|
||||
</ng-template>
|
||||
</span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Book">
|
||||
<span class="modal-title" id="modal-basic-title">
|
||||
{{chapter.titleName}}
|
||||
</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<span class="modal-title" id="modal-basic-title">
|
||||
<app-entity-title [libraryType]="libraryType" [entity]="data" [seriesName]="parentName"></app-entity-title>
|
||||
</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close text-reset" aria-label="Close" (click)="activeOffcanvas.dismiss()"></button>
|
||||
</div>
|
||||
|
@ -44,7 +20,7 @@
|
|||
<app-image class="me-2" width="74px" [imageUrl]="chapter.coverImage"></app-image>
|
||||
</div>
|
||||
<div class="col-md-10 col-lg-11">
|
||||
<ng-container *ngIf="summary$ | async as summary; else noSummary">
|
||||
<ng-container *ngIf="summary.length > 0; else noSummary">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</ng-container>
|
||||
<ng-template #noSummary>
|
||||
|
@ -53,73 +29,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0 mt-4 mb-3">
|
||||
<ng-container *ngIf="totalPages > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Print Length" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Pages">
|
||||
{{totalPages | number:''}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
<app-entity-info-cards [entity]="data"></app-entity-info-cards>
|
||||
|
||||
<ng-container *ngIf="chapterMetadata !== undefined && chapterMetadata.releaseDate && (chapterMetadata.releaseDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Release Date" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release">
|
||||
{{chapterMetadata.releaseDate | date:'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && chapterMetadata !== undefined && chapterMetadata.wordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||
<div class="col-auto 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="chapter.files[0].format === MangaFormat.EPUB && chapterMetadata !== undefined && chapterMetadata.wordCount > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Word Count" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{chapterMetadata.wordCount | compactNumber}} Words
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapterMetadata !== undefined">
|
||||
<ng-container *ngIf="ageRating !== '' && ageRating !== 'Unknown'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Age Rating" [clickable]="false" fontClasses="fas fa-eye" title="Age Rating">
|
||||
{{ageRating}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.created && chapter.created !== '' && (chapter.created | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Date Added" [clickable]="false" fontClasses="fa-solid fa-file-import" title="Date Added">
|
||||
{{chapter.created | date:'short' || '-'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ID" [clickable]="false" fontClasses="fa-solid fa-fingerprint" title="ID">
|
||||
{{data.id}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- 2 rows to show some tags-->
|
||||
<ng-container *ngIf="chapterMetadata !== undefined">
|
||||
|
@ -187,18 +98,13 @@
|
|||
<li [ngbNavItem]="tabs[TabID.Cover]">
|
||||
<a ngbNavLink>{{tabs[TabID.Cover].title}}</a>
|
||||
<ng-template ngbNavContent>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls" (imageSelected)="updateSelectedIndex($event)" (selectedBase64Url)="updateSelectedImage($event)" [showReset]="chapter.coverImageLocked" (resetClicked)="handleReset()"></app-cover-image-chooser>
|
||||
<div class="row g-0">
|
||||
<button class="btn btn-primary flex-end mb-2" [disabled]="coverImageSaveLoading" (click)="saveCoverImage()">
|
||||
<ng-container *ngIf="coverImageSaveLoading; else notSaving">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</ng-container>
|
||||
<ng-template #notSaving>
|
||||
Save
|
||||
</ng-template>
|
||||
</button>
|
||||
</div>
|
||||
<app-cover-image-chooser [(imageUrls)]="imageUrls"
|
||||
[showReset]="chapter.coverImageLocked"
|
||||
[showApplyButton]="true"
|
||||
(applyCover)="applyCoverImage($event)"
|
||||
(resetCover)="resetCoverImage()"
|
||||
>
|
||||
</app-cover-image-chooser>
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
|
|
|
@ -14,3 +14,7 @@
|
|||
overflow: auto;
|
||||
height: calc(40vh - 63px); // drawer height - offcanvas heading height
|
||||
}
|
||||
|
||||
.h6 {
|
||||
font-weight: 600;
|
||||
}
|
|
@ -2,7 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
|
|||
import { Router } from '@angular/router';
|
||||
import { NgbActiveOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { finalize, Observable, of, take, takeWhile, tap } from 'rxjs';
|
||||
import { finalize, Observable, of, take, takeWhile } from 'rxjs';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
|
@ -50,21 +50,6 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
isChapter = false;
|
||||
chapters: Chapter[] = [];
|
||||
|
||||
|
||||
/**
|
||||
* If a cover image update occured.
|
||||
*/
|
||||
coverImageUpdate: boolean = false;
|
||||
coverImageIndex: number = 0;
|
||||
/**
|
||||
* Url of the selected cover
|
||||
*/
|
||||
selectedCover: string = '';
|
||||
coverImageLocked: boolean = false;
|
||||
/**
|
||||
* When the API is doing work
|
||||
*/
|
||||
coverImageSaveLoading: boolean = false;
|
||||
imageUrls: Array<string> = [];
|
||||
|
||||
|
||||
|
@ -77,16 +62,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
active = this.tabs[0];
|
||||
|
||||
chapterMetadata!: ChapterMetadata;
|
||||
ageRating!: string;
|
||||
|
||||
summary$: Observable<string> = of('');
|
||||
readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1, hasProgress: false};
|
||||
minHoursToRead: number = 1;
|
||||
maxHoursToRead: number = 1;
|
||||
/**
|
||||
* We use a separate variable because if this is a volume, we need a sum of all chapters
|
||||
*/
|
||||
totalPages: number = 0;
|
||||
summary: string = '';
|
||||
|
||||
download$: Observable<Download> | null = null;
|
||||
downloadInProgress: boolean = false;
|
||||
|
@ -129,25 +105,14 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
|
||||
this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
this.chapterMetadata = metadata;
|
||||
|
||||
this.metadataService.getAgeRating(this.chapterMetadata.ageRating).subscribe(ageRating => this.ageRating = ageRating);
|
||||
|
||||
this.totalPages = this.chapter.pages;
|
||||
if (!this.isChapter) {
|
||||
// Need to account for multiple chapters if this is a volume
|
||||
this.totalPages = this.utilityService.asVolume(this.data).chapters.map(c => c.pages).reduce((sum, d) => sum + d);
|
||||
}
|
||||
|
||||
this.readerService.getManualTimeToRead(this.chapterMetadata.wordCount, this.totalPages, this.chapter.files[0].format === MangaFormat.EPUB).subscribe((time) => this.readingTime = time);
|
||||
});
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.summary$ = this.metadataService.getChapterSummary(this.data.id);
|
||||
this.summary = this.utilityService.asChapter(this.data).summary || '';
|
||||
} else {
|
||||
this.summary$ = this.metadataService.getChapterSummary(this.utilityService.asVolume(this.data).chapters[0].id);
|
||||
this.summary = this.utilityService.asVolume(this.data).chapters[0].summary || '';
|
||||
}
|
||||
|
||||
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
|
||||
if (user) {
|
||||
|
@ -180,7 +145,7 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
}
|
||||
|
||||
close() {
|
||||
this.activeOffcanvas.close({coverImageUpdate: this.coverImageUpdate});
|
||||
this.activeOffcanvas.close();
|
||||
}
|
||||
|
||||
formatChapterNumber(chapter: Chapter) {
|
||||
|
@ -196,36 +161,14 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
updateSelectedIndex(index: number) {
|
||||
this.coverImageIndex = index;
|
||||
applyCoverImage(coverUrl: string) {
|
||||
this.uploadService.updateChapterCoverImage(this.chapter.id, coverUrl).subscribe(() => {});
|
||||
}
|
||||
|
||||
updateSelectedImage(url: string) {
|
||||
this.selectedCover = url;
|
||||
}
|
||||
|
||||
handleReset() {
|
||||
this.coverImageLocked = false;
|
||||
}
|
||||
|
||||
saveCoverImage() {
|
||||
this.coverImageSaveLoading = true;
|
||||
const selectedIndex = this.coverImageIndex || 0;
|
||||
if (selectedIndex > 0) {
|
||||
this.uploadService.updateChapterCoverImage(this.chapter.id, this.selectedCover).subscribe(() => {
|
||||
if (this.coverImageIndex > 0) {
|
||||
this.chapter.coverImageLocked = true;
|
||||
this.coverImageUpdate = true;
|
||||
}
|
||||
this.coverImageSaveLoading = false;
|
||||
}, err => this.coverImageSaveLoading = false);
|
||||
} else if (this.coverImageLocked === false) {
|
||||
this.uploadService.resetChapterCoverLock(this.chapter.id).subscribe(() => {
|
||||
this.toastr.info('Cover image reset');
|
||||
this.coverImageSaveLoading = false;
|
||||
this.coverImageUpdate = true;
|
||||
});
|
||||
}
|
||||
resetCoverImage() {
|
||||
this.uploadService.resetChapterCoverLock(this.chapter.id).subscribe(() => {
|
||||
this.toastr.info('A job has been enqueued to regenerate the cover image');
|
||||
});
|
||||
}
|
||||
|
||||
markChapterAsRead(chapter: Chapter) {
|
||||
|
@ -292,13 +235,10 @@ export class CardDetailDrawerComponent implements OnInit {
|
|||
|
||||
this.downloadService.downloadChapterSize(chapter.id).pipe(take(1)).subscribe(async (size) => {
|
||||
const wantToDownload = await this.downloadService.confirmSize(size, 'chapter');
|
||||
console.log('want to download: ', wantToDownload);
|
||||
if (!wantToDownload) { return; }
|
||||
|
||||
this.downloadInProgress = true;
|
||||
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
||||
tap(val => {
|
||||
console.log(val);
|
||||
}),
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
}),
|
||||
|
|
|
@ -13,29 +13,39 @@
|
|||
</div>
|
||||
</div>
|
||||
<app-metadata-filter [filterSettings]="filterSettings" [filterOpen]="filterOpen" (applyFilter)="applyMetadataFilter($event)"></app-metadata-filter>
|
||||
<div class="viewport-container">
|
||||
<div class="viewport-container" #scrollingBlock>
|
||||
<div class="content-container">
|
||||
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'top' }"></ng-container>
|
||||
|
||||
<div class="card-container mt-2 mb-2">
|
||||
<ng-container [ngTemplateOutlet]="cardTemplate"></ng-container>
|
||||
<virtual-scroller #scroll [items]="items" (vsEnd)="fetchMore($event)" [bufferAmount]="1">
|
||||
<div class="grid row g-0" #container>
|
||||
<div class="card col-auto mt-2 mb-2" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
|
||||
|
||||
<p *ngIf="items.length === 0 && !isLoading">
|
||||
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ng-container [ngTemplateOutlet]="paginationTemplate" [ngTemplateOutletContext]="{ id: 'bottom' }"></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="pagination && items.length > 0 && pagination.totalPages > 1" [ngTemplateOutlet]="jumpBar" [ngTemplateOutletContext]="{ id: 'jumpbar' }"></ng-container>
|
||||
<ng-container [ngTemplateOutlet]="jumpBar" [ngTemplateOutletContext]="{ id: 'jumpbar' }"></ng-container>
|
||||
</div>
|
||||
<ng-template #cardTemplate>
|
||||
|
||||
<div class="grid row g-0" >
|
||||
<div class="card col-auto mt-2 mb-2" *ngFor="let item of items; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
<virtual-scroller #scroll [items]="items" [bufferAmount]="1">
|
||||
<div class="grid row g-0" #container>
|
||||
<div class="card col-auto mt-2 mb-2" *ngFor="let item of scroll.viewPortItems; trackBy:trackByIdentity; index as i" id="jumpbar-index--{{i}}" [attr.jumpbar-index]="i">
|
||||
<ng-container [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item, idx: i }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</virtual-scroller>
|
||||
|
||||
<div class="mx-auto" *ngIf="items.length === 0 && !isLoading" style="width: 200px;">
|
||||
<p><ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container></p>
|
||||
</div>
|
||||
<p *ngIf="items.length === 0 && !isLoading">
|
||||
<ng-container [ngTemplateOutlet]="noDataTemplate"></ng-container>
|
||||
</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #paginationTemplate let-id="id">
|
||||
|
@ -93,8 +103,8 @@
|
|||
|
||||
<ng-template #jumpBar>
|
||||
<div class="jump-bar">
|
||||
<ng-container *ngFor="let jumpKey of jumpBarKeys; let i = index;">
|
||||
<button class="btn btn-link {{i % 2 !== 0 ? 'd-lg-flex' : 'd-md-flex'}}" (click)="scrollTo(jumpKey)">
|
||||
<ng-container *ngFor="let jumpKey of jumpBarKeysToRender; let i = index;">
|
||||
<button class="btn btn-link" (click)="scrollTo(jumpKey)">
|
||||
{{jumpKey.title}}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
.card-container {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
//overflow-y: auto;
|
||||
}
|
||||
|
||||
.grid {
|
||||
|
@ -73,3 +73,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-scroller, virtual-scroller {
|
||||
width: 100%;
|
||||
//height: calc(100vh - 160px); // 64 is a random number, 523 for me.
|
||||
height: calc(var(--vh) * 100 - 160px);
|
||||
//height: calc(100vh - 160px);
|
||||
//background-color: red;
|
||||
//max-height: calc(var(--vh)*100 - 170px);
|
||||
}
|
||||
|
|
|
@ -1,28 +1,33 @@
|
|||
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { AfterViewInit, Component, ContentChild, EventEmitter, HostListener, Inject, Input, OnDestroy, OnInit, Output, Renderer2, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { from, Subject } from 'rxjs';
|
||||
import { AfterViewInit, Component, ContentChild, ElementRef, EventEmitter, HostListener, Inject, Input, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
|
||||
import { IPageInfo, VirtualScrollerComponent } from '@iharbeck/ngx-virtual-scroller';
|
||||
import { filter, from, map, pairwise, Subject, tap, throttleTime } from 'rxjs';
|
||||
import { FilterSettings } from 'src/app/metadata-filter/filter-settings';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { JumpKey } from 'src/app/_models/jumpbar/jump-key';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { Pagination } from 'src/app/_models/pagination';
|
||||
import { PaginatedResult, Pagination } from 'src/app/_models/pagination';
|
||||
import { FilterEvent, FilterItem, SeriesFilter } from 'src/app/_models/series-filter';
|
||||
import { ActionItem } from 'src/app/_services/action-factory.service';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
|
||||
const FILTER_PAG_REGEX = /[^0-9]/g;
|
||||
const SCROLL_BREAKPOINT = 300;
|
||||
const keySize = 24;
|
||||
|
||||
@Component({
|
||||
selector: 'app-card-detail-layout',
|
||||
templateUrl: './card-detail-layout.component.html',
|
||||
styleUrls: ['./card-detail-layout.component.scss']
|
||||
})
|
||||
export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewInit, OnChanges {
|
||||
|
||||
@Input() header: string = '';
|
||||
@Input() isLoading: boolean = false;
|
||||
@Input() items: any[] = [];
|
||||
// ?! we need to have chunks to render in, because if we scroll down, then up, then down, we don't want to trigger a duplicate call
|
||||
@Input() paginatedItems: PaginatedResult<any> | undefined;
|
||||
@Input() pagination!: Pagination;
|
||||
|
||||
// Filter Code
|
||||
|
@ -35,56 +40,119 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
* Any actions to exist on the header for the parent collection (library, collection)
|
||||
*/
|
||||
@Input() actions: ActionItem<any>[] = [];
|
||||
@Input() trackByIdentity!: (index: number, item: any) => string;
|
||||
@Input() trackByIdentity!: TrackByFunction<any>; //(index: number, item: any) => string
|
||||
@Input() filterSettings!: FilterSettings;
|
||||
|
||||
|
||||
@Input() jumpBarKeys: Array<JumpKey> = []; // This is aprox 784 pixels wide
|
||||
jumpBarKeysToRender: Array<JumpKey> = []; // Original
|
||||
|
||||
@Output() itemClicked: EventEmitter<any> = new EventEmitter();
|
||||
@Output() pageChange: EventEmitter<Pagination> = new EventEmitter();
|
||||
@Output() pageChangeWithDirection: EventEmitter<0 | 1> = new EventEmitter();
|
||||
@Output() applyFilter: EventEmitter<FilterEvent> = new EventEmitter();
|
||||
|
||||
@ContentChild('cardItem') itemTemplate!: TemplateRef<any>;
|
||||
@ContentChild('noData') noDataTemplate!: TemplateRef<any>;
|
||||
@ViewChild('.jump-bar') jumpBar!: ElementRef<HTMLDivElement>;
|
||||
@ViewChild('scroller') scroller!: CdkVirtualScrollViewport;
|
||||
|
||||
@ViewChild(VirtualScrollerComponent) private virtualScroller!: VirtualScrollerComponent;
|
||||
|
||||
itemSize: number = 100; // Idk what this actually does. Less results in more items rendering, 5 works well with pagination. 230 is technically what a card is height wise
|
||||
|
||||
filter!: SeriesFilter;
|
||||
libraries: Array<FilterItem<Library>> = [];
|
||||
|
||||
updateApplied: number = 0;
|
||||
|
||||
intersectionObserver: IntersectionObserver = new IntersectionObserver((entries) => this.handleIntersection(entries), { threshold: 0.01 });
|
||||
|
||||
|
||||
private onDestory: Subject<void> = new Subject();
|
||||
|
||||
get Breakpoint() {
|
||||
return Breakpoint;
|
||||
}
|
||||
|
||||
constructor(private seriesService: SeriesService, public utilityService: UtilityService, @Inject(DOCUMENT) private document: Document,
|
||||
private scrollService: ScrollService) {
|
||||
constructor(private seriesService: SeriesService, public utilityService: UtilityService,
|
||||
@Inject(DOCUMENT) private document: Document, private ngZone: NgZone) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@HostListener('window:orientationchange', ['$event'])
|
||||
resizeJumpBar() {
|
||||
console.log('resizing jump bar');
|
||||
//const breakpoint = this.utilityService.getActiveBreakpoint();
|
||||
// if (window.innerWidth < 784) {
|
||||
// // We need to remove a few sections of keys
|
||||
// const len = this.jumpBarKeys.length;
|
||||
// if (this.jumpBarKeys.length <= 8) return;
|
||||
// this.jumpBarKeys = this.jumpBarKeys.filter((item, index) => {
|
||||
// return index % 2 === 0;
|
||||
// });
|
||||
// }
|
||||
// TODO: Debounce this
|
||||
|
||||
const fullSize = (this.jumpBarKeys.length * keySize) - 20;
|
||||
const currentSize = (this.document.querySelector('.jump-bar')?.getBoundingClientRect().height || fullSize + 20) - 20;
|
||||
if (currentSize >= fullSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNumberOfKeys = parseInt(Math.round(currentSize / keySize) + '', 10);
|
||||
const removeCount = this.jumpBarKeys.length - targetNumberOfKeys - 3;
|
||||
if (removeCount <= 0) return;
|
||||
|
||||
|
||||
this.jumpBarKeysToRender = [];
|
||||
|
||||
|
||||
const midPoint = this.jumpBarKeys.length / 2;
|
||||
this.jumpBarKeysToRender.push(this.jumpBarKeys[0]);
|
||||
this.removeFirstPartOfJumpBar(midPoint, removeCount / 2);
|
||||
this.jumpBarKeysToRender.push(this.jumpBarKeys[midPoint]);
|
||||
this.removeSecondPartOfJumpBar(midPoint, removeCount / 2);
|
||||
this.jumpBarKeysToRender.push(this.jumpBarKeys[this.jumpBarKeys.length - 1]);
|
||||
|
||||
//console.log('End product: ', this.jumpBarKeysToRender);
|
||||
// console.log('End key size: ', this.jumpBarKeysToRender.length);
|
||||
}
|
||||
|
||||
removeSecondPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1) {
|
||||
const removedIndexes: Array<number> = [];
|
||||
for(let removal = 0; removal < numberOfRemovals; removal++) {
|
||||
let min = 100000000;
|
||||
let minIndex = -1;
|
||||
for(let i = midPoint + 1; i < this.jumpBarKeys.length - 2; i++) {
|
||||
if (this.jumpBarKeys[i].size < min && !removedIndexes.includes(i)) {
|
||||
min = this.jumpBarKeys[i].size;
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
removedIndexes.push(minIndex);
|
||||
}
|
||||
// console.log('second: removing ', removedIndexes);
|
||||
for(let i = midPoint + 1; i < this.jumpBarKeys.length - 2; i++) {
|
||||
if (!removedIndexes.includes(i)) this.jumpBarKeysToRender.push(this.jumpBarKeys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
removeFirstPartOfJumpBar(midPoint: number, numberOfRemovals: number = 1) {
|
||||
const removedIndexes: Array<number> = [];
|
||||
for(let removal = 0; removal < numberOfRemovals; removal++) {
|
||||
let min = 100000000;
|
||||
let minIndex = -1;
|
||||
for(let i = 1; i < midPoint; i++) {
|
||||
if (this.jumpBarKeys[i].size < min && !removedIndexes.includes(i)) {
|
||||
min = this.jumpBarKeys[i].size;
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
removedIndexes.push(minIndex);
|
||||
}
|
||||
|
||||
// console.log('first: removing ', removedIndexes);
|
||||
for(let i = 1; i < midPoint; i++) {
|
||||
if (!removedIndexes.includes(i)) this.jumpBarKeysToRender.push(this.jumpBarKeys[i]);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.pagination?.currentPage}_${this.updateApplied}_${item?.libraryId}`;
|
||||
if (this.trackByIdentity === undefined) {
|
||||
this.trackByIdentity = (index: number, item: any) => `${this.header}_${this.updateApplied}_${item?.libraryId}`; // ${this.pagination?.currentPage}_
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (this.filterSettings === undefined) {
|
||||
|
@ -96,27 +164,49 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.jumpBarKeysToRender = [...this.jumpBarKeys];
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.resizeJumpBar();
|
||||
// this.scroller.elementScrolled().pipe(
|
||||
// map(() => this.scroller.measureScrollOffset('bottom')),
|
||||
// pairwise(),
|
||||
// filter(([y1, y2]) => ((y2 < y1 && y2 < SCROLL_BREAKPOINT))), // 140
|
||||
// throttleTime(200)
|
||||
// ).subscribe(([y1, y2]) => {
|
||||
// const movingForward = y2 < y1;
|
||||
// if (this.pagination.currentPage === this.pagination.totalPages || this.pagination.currentPage === 1 && !movingForward) return;
|
||||
// this.ngZone.run(() => {
|
||||
// console.log('Load next pages');
|
||||
|
||||
const parent = this.document.querySelector('.card-container');
|
||||
if (parent == null) return;
|
||||
console.log('card divs', this.document.querySelectorAll('div[id^="jumpbar-index--"]'));
|
||||
console.log('cards: ', this.document.querySelectorAll('.card'));
|
||||
// this.pagination.currentPage = this.pagination.currentPage + 1;
|
||||
// this.pageChangeWithDirection.emit(1);
|
||||
// });
|
||||
// });
|
||||
|
||||
Array.from(this.document.querySelectorAll('div')).forEach(elem => this.intersectionObserver.observe(elem));
|
||||
// this.scroller.elementScrolled().pipe(
|
||||
// map(() => this.scroller.measureScrollOffset('top')),
|
||||
// pairwise(),
|
||||
// filter(([y1, y2]) => y2 >= y1 && y2 < SCROLL_BREAKPOINT),
|
||||
// throttleTime(200)
|
||||
// ).subscribe(([y1, y2]) => {
|
||||
// if (this.pagination.currentPage === 1) return;
|
||||
// this.ngZone.run(() => {
|
||||
// console.log('Load prev pages');
|
||||
|
||||
// this.pagination.currentPage = this.pagination.currentPage - 1;
|
||||
// this.pageChangeWithDirection.emit(0);
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.intersectionObserver.disconnect();
|
||||
this.onDestory.next();
|
||||
this.onDestory.complete();
|
||||
}
|
||||
|
||||
handleIntersection(entries: IntersectionObserverEntry[]) {
|
||||
console.log('interception: ', entries.filter(e => e.target.hasAttribute('no-observe')));
|
||||
|
||||
|
||||
}
|
||||
|
||||
onPageChange(page: number) {
|
||||
this.pageChange.emit(this.pagination);
|
||||
|
@ -142,18 +232,21 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
this.updateApplied++;
|
||||
}
|
||||
|
||||
// onScroll() {
|
||||
loading: boolean = false;
|
||||
fetchMore(event: IPageInfo) {
|
||||
if (event.endIndex !== this.items.length - 1) return;
|
||||
if (event.startIndex < 0) return;
|
||||
console.log('Requesting next page ', (this.pagination.currentPage + 1), 'of data', event);
|
||||
this.loading = true;
|
||||
|
||||
// }
|
||||
// this.pagination.currentPage = this.pagination.currentPage + 1;
|
||||
// this.pageChangeWithDirection.emit(1);
|
||||
|
||||
// onScrollDown() {
|
||||
// console.log('scrolled down');
|
||||
// }
|
||||
// onScrollUp() {
|
||||
// console.log('scrolled up');
|
||||
// }
|
||||
|
||||
|
||||
// this.fetchNextChunk(this.items.length, 10).then(chunk => {
|
||||
// this.items = this.items.concat(chunk);
|
||||
// this.loading = false;
|
||||
// }, () => this.loading = false);
|
||||
}
|
||||
|
||||
scrollTo(jumpKey: JumpKey) {
|
||||
// TODO: Figure out how to do this
|
||||
|
@ -165,6 +258,10 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
}
|
||||
//console.log('scrolling to card that starts with ', jumpKey.key, + ' with index of ', targetIndex);
|
||||
|
||||
// Infinite scroll
|
||||
this.virtualScroller.scrollToIndex(targetIndex, true, undefined, 1000);
|
||||
return;
|
||||
|
||||
// Basic implementation based on itemsPerPage being the same.
|
||||
//var minIndex = this.pagination.currentPage * this.pagination.itemsPerPage;
|
||||
var targetPage = Math.max(Math.ceil(targetIndex / this.pagination.itemsPerPage), 1);
|
||||
|
@ -173,14 +270,18 @@ export class CardDetailLayoutComponent implements OnInit, OnDestroy, AfterViewIn
|
|||
// Scroll to the element
|
||||
const elem = this.document.querySelector(`div[id="jumpbar-index--${targetIndex}"`);
|
||||
if (elem !== null) {
|
||||
elem.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
this.virtualScroller.scrollToIndex(targetIndex);
|
||||
// elem.scrollIntoView({
|
||||
// behavior: 'smooth'
|
||||
// });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// With infinite scroll, we can't just jump to a random place, because then our list of items would be out of sync.
|
||||
this.selectPageStr(targetPage + '');
|
||||
//this.pageChangeWithDirection.emit(1);
|
||||
|
||||
// if (minIndex > targetIndex) {
|
||||
// // We need to scroll forward (potentially to another page)
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
<app-image borderRadius=".25rem .25rem 0 0" height="230px" width="158px" [imageUrl]="imageService.errorImage"></app-image>
|
||||
</ng-container>
|
||||
|
||||
<div class="progress-banner" *ngIf="read < total && total > 0 && read !== total">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
||||
<div class="progress-banner">
|
||||
<p *ngIf="read < total && total > 0 && read !== total"><ngb-progressbar type="primary" height="5px" [value]="read" [max]="total"></ngb-progressbar></p>
|
||||
|
||||
<span class="download" *ngIf="download$ | async as download">
|
||||
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
|||
import { ImageService } from 'src/app/_services/image.service';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
import { EVENTS, MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
import { ScrollService } from 'src/app/_services/scroll.service';
|
||||
import { BulkSelectionService } from '../bulk-selection.service';
|
||||
|
||||
@Component({
|
||||
|
@ -129,7 +130,7 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
constructor(public imageService: ImageService, private libraryService: LibraryService,
|
||||
public utilityService: UtilityService, private downloadService: DownloadService,
|
||||
private toastr: ToastrService, public bulkSelectionService: BulkSelectionService,
|
||||
private messageHub: MessageHubService, private accountService: AccountService) {}
|
||||
private messageHub: MessageHubService, private accountService: AccountService, private scrollService: ScrollService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.entity.hasOwnProperty('promoted') && this.entity.hasOwnProperty('title')) {
|
||||
|
@ -182,28 +183,18 @@ export class CardItemComponent implements OnInit, OnDestroy {
|
|||
@HostListener('touchstart', ['$event'])
|
||||
onTouchStart(event: TouchEvent) {
|
||||
if (!this.allowSelection) return;
|
||||
const verticalOffset = (window.pageYOffset
|
||||
|| document.documentElement.scrollTop
|
||||
|| document.body.scrollTop || 0);
|
||||
|
||||
this.prevTouchTime = event.timeStamp;
|
||||
this.prevOffset = verticalOffset;
|
||||
this.prevOffset = this.scrollService.scrollPosition;
|
||||
}
|
||||
|
||||
@HostListener('touchend', ['$event'])
|
||||
onTouchEnd(event: TouchEvent) {
|
||||
if (!this.allowSelection) return;
|
||||
const delta = event.timeStamp - this.prevTouchTime;
|
||||
const verticalOffset = (window.pageYOffset
|
||||
|| document.documentElement.scrollTop
|
||||
|| document.body.scrollTop || 0);
|
||||
const verticalOffset = this.scrollService.scrollPosition;
|
||||
|
||||
if (verticalOffset != this.prevOffset) {
|
||||
this.prevTouchTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (delta >= 300 && delta <= 1000) {
|
||||
if (delta >= 300 && delta <= 1000 && (verticalOffset === this.prevOffset)) {
|
||||
this.handleSelection();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
|
|
@ -23,6 +23,12 @@ import { FileInfoComponent } from './file-info/file-info.component';
|
|||
import { MetadataFilterModule } from '../metadata-filter/metadata-filter.module';
|
||||
import { EditSeriesRelationComponent } from './edit-series-relation/edit-series-relation.component';
|
||||
import { CardDetailDrawerComponent } from './card-detail-drawer/card-detail-drawer.component';
|
||||
import { EntityTitleComponent } from './entity-title/entity-title.component';
|
||||
import { EntityInfoCardsComponent } from './entity-info-cards/entity-info-cards.component';
|
||||
import { ListItemComponent } from './list-item/list-item.component';
|
||||
import { VirtualScrollerModule } from '@iharbeck/ngx-virtual-scroller';
|
||||
import { SeriesInfoCardsComponent } from './series-info-cards/series-info-cards.component';
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -43,6 +49,10 @@ import { CardDetailDrawerComponent } from './card-detail-drawer/card-detail-draw
|
|||
FileInfoComponent,
|
||||
EditSeriesRelationComponent,
|
||||
CardDetailDrawerComponent,
|
||||
EntityTitleComponent,
|
||||
EntityInfoCardsComponent,
|
||||
ListItemComponent,
|
||||
SeriesInfoCardsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -60,8 +70,7 @@ import { CardDetailDrawerComponent } from './card-detail-drawer/card-detail-draw
|
|||
NgbCollapseModule,
|
||||
NgbRatingModule,
|
||||
|
||||
//ScrollingModule,
|
||||
//InfiniteScrollModule,
|
||||
VirtualScrollerModule,
|
||||
|
||||
|
||||
NgbOffcanvasModule, // Series Detail, action of cards
|
||||
|
@ -93,7 +102,16 @@ import { CardDetailDrawerComponent } from './card-detail-drawer/card-detail-draw
|
|||
ChapterMetadataDetailComponent,
|
||||
EditSeriesRelationComponent,
|
||||
|
||||
NgbOffcanvasModule
|
||||
EntityTitleComponent,
|
||||
EntityInfoCardsComponent,
|
||||
ListItemComponent,
|
||||
|
||||
NgbOffcanvasModule,
|
||||
|
||||
VirtualScrollerModule,
|
||||
SeriesInfoCardsComponent
|
||||
|
||||
|
||||
]
|
||||
})
|
||||
export class CardsModule { }
|
||||
|
|
|
@ -48,11 +48,27 @@
|
|||
</form>
|
||||
|
||||
<div class="row g-0 chooser" style="padding-top: 10px">
|
||||
<div class="image-card col-auto {{selectedIndex === idx ? 'selected' : ''}}" *ngFor="let url of imageUrls; let idx = index;" tabindex="0" attr.aria-label="Image {{idx + 1}}" (click)="selectImage(idx)">
|
||||
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url"></app-image>
|
||||
<div class="image-card col-auto"
|
||||
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" attr.aria-label="Image {{idx + 1}}" (click)="selectImage(idx)"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
|
||||
<app-image class="card-img-top" height="230px" width="158px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button class="btn btn-primary" style="width: 100%;" aria-label="Apply for uploaded image"
|
||||
(click)="applyImage(idx)">
|
||||
{{appliedIndex === idx ? 'Applied' : 'Apply'}}
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="image-card col-auto {{selectedIndex === -1 ? 'selected' : ''}}" *ngIf="showReset" tabindex="0" attr.aria-label="Reset cover image" (click)="reset()">
|
||||
<div class="image-card col-auto"
|
||||
*ngIf="showReset" tabindex="0" attr.aria-label="Reset cover image" (click)="reset()"
|
||||
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
|
||||
<app-image class="card-img-top" title="Reset Cover Image" height="230px" width="158px" [imageUrl]="imageService.resetCoverImage"></app-image>
|
||||
<ng-container *ngIf="showApplyButton">
|
||||
<br>
|
||||
<button style="width: 100%;" class="btn btn-secondary" aria-label="Reset to generated image" (click)="resetImage()">Reset</button>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import { KEY_CODES } from 'src/app/shared/_services/utility.service';
|
|||
import { UploadService } from 'src/app/_services/upload.service';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
export type SelectCoverFunction = (selectedCover: string) => void;
|
||||
|
||||
@Component({
|
||||
selector: 'app-cover-image-chooser',
|
||||
templateUrl: './cover-image-chooser.component.html',
|
||||
|
@ -16,6 +18,19 @@ import { DOCUMENT } from '@angular/common';
|
|||
})
|
||||
export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* If buttons show under images to allow immediate selection of cover images.
|
||||
*/
|
||||
@Input() showApplyButton: boolean = false;
|
||||
/**
|
||||
* When a cover image is selected, this will be called with a base url representation of the file.
|
||||
*/
|
||||
@Output() applyCover: EventEmitter<string> = new EventEmitter<string>();
|
||||
/**
|
||||
* When a cover image is reset, this will be called.
|
||||
*/
|
||||
@Output() resetCover: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
@Input() imageUrls: Array<string> = [];
|
||||
@Output() imageUrlsChange: EventEmitter<Array<string>> = new EventEmitter<Array<string>>();
|
||||
|
||||
|
@ -37,6 +52,10 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
|
||||
|
||||
selectedIndex: number = 0;
|
||||
/**
|
||||
* Only applies for showApplyButton. Used to track which image is applied.
|
||||
*/
|
||||
appliedIndex: number = 0;
|
||||
form!: FormGroup;
|
||||
files: NgxFileDropEntry[] = [];
|
||||
|
||||
|
@ -78,6 +97,19 @@ export class CoverImageChooserComponent implements OnInit, OnDestroy {
|
|||
this.selectedBase64Url.emit(this.imageUrls[this.selectedIndex]);
|
||||
}
|
||||
|
||||
applyImage(index: number) {
|
||||
if (this.showApplyButton) {
|
||||
this.applyCover.emit(this.imageUrls[index]);
|
||||
this.appliedIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
resetImage() {
|
||||
if (this.showApplyButton) {
|
||||
this.resetCover.emit();
|
||||
}
|
||||
}
|
||||
|
||||
loadImage() {
|
||||
const url = this.form.get('coverImageUrl')?.value.trim();
|
||||
if (url && url != '') {
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<div class="row g-0 mt-4 mb-3">
|
||||
<ng-container *ngIf="totalPages > 0">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Print Length" [clickable]="false" fontClasses="fa-regular fa-file-lines" title="Pages">
|
||||
{{totalPages | number:''}} Pages
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter !== undefined && chapter.releaseDate && (chapter.releaseDate | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Release Date" [clickable]="false" fontClasses="fa-regular fa-calendar" title="Release">
|
||||
{{chapter.releaseDate | date:'shortDate'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0 || chapter.files[0].format !== MangaFormat.EPUB">
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Read Time" [clickable]="false" fontClasses="fa-regular fa-clock">
|
||||
<ng-container *ngIf="readingTime.maxHours === 0; else normalReadTime"><1 Hour</ng-container>
|
||||
<ng-template #normalReadTime>
|
||||
{{readingTime.minHours}}{{readingTime.maxHours !== readingTime.minHours ? ('-' + readingTime.maxHours) : ''}} Hour{{readingTime.minHours > 1 ? 's' : ''}}
|
||||
</ng-template>
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.files[0].format === MangaFormat.EPUB && totalWordCount > 0">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto mb-2">
|
||||
<app-icon-and-title label="Word Count" [clickable]="false" fontClasses="fa-solid fa-book-open">
|
||||
{{totalWordCount | compactNumber}} Words
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="chapter.ageRating !== AgeRating.Unknown">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Age Rating" [clickable]="false" fontClasses="fas fa-eye" title="Age Rating">
|
||||
{{chapter.ageRating | ageRating | async}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties && chapter.created && chapter.created !== '' && (chapter.created | date: 'shortDate') !== '1/1/01'">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="Date Added" [clickable]="false" fontClasses="fa-solid fa-file-import" title="Date Added">
|
||||
{{chapter.created | date:'short' || '-'}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="showExtendedProperties">
|
||||
<div class="vr d-none d-lg-block m-2"></div>
|
||||
<div class="col-auto">
|
||||
<app-icon-and-title label="ID" [clickable]="false" fontClasses="fa-solid fa-fingerprint" title="ID">
|
||||
{{entity.id}}
|
||||
</app-icon-and-title>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,96 @@
|
|||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { ChapterMetadata } from 'src/app/_models/chapter-metadata';
|
||||
import { HourEstimateRange } from 'src/app/_models/hour-estimate-range';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { AgeRating } from 'src/app/_models/metadata/age-rating';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { SeriesService } from 'src/app/_services/series.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-entity-info-cards',
|
||||
templateUrl: './entity-info-cards.component.html',
|
||||
styleUrls: ['./entity-info-cards.component.scss']
|
||||
})
|
||||
export class EntityInfoCardsComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() entity!: Volume | Chapter;
|
||||
/**
|
||||
* This will pull extra information
|
||||
*/
|
||||
@Input() includeMetadata: boolean = false;
|
||||
|
||||
/**
|
||||
* Hide more system based fields, like Id or Date Added
|
||||
*/
|
||||
@Input() showExtendedProperties: boolean = true;
|
||||
|
||||
isChapter = false;
|
||||
chapter!: Chapter;
|
||||
|
||||
chapterMetadata!: ChapterMetadata;
|
||||
ageRating!: string;
|
||||
totalPages: number = 0;
|
||||
totalWordCount: number = 0;
|
||||
readingTime: HourEstimateRange = {maxHours: 1, minHours: 1, avgHours: 1};
|
||||
|
||||
private readonly onDestroy: Subject<void> = new Subject();
|
||||
|
||||
get LibraryType() {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
}
|
||||
|
||||
get AgeRating() {
|
||||
return AgeRating;
|
||||
}
|
||||
|
||||
constructor(private utilityService: UtilityService, private seriesService: SeriesService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
|
||||
this.chapter = this.utilityService.isChapter(this.entity) ? (this.entity as Chapter) : (this.entity as Volume).chapters[0];
|
||||
|
||||
if (this.includeMetadata) {
|
||||
this.seriesService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
|
||||
this.chapterMetadata = metadata;
|
||||
});
|
||||
}
|
||||
|
||||
this.totalPages = this.chapter.pages;
|
||||
if (!this.isChapter) {
|
||||
this.totalPages = this.utilityService.asVolume(this.entity).pages;
|
||||
}
|
||||
|
||||
this.totalWordCount = this.chapter.wordCount;
|
||||
if (!this.isChapter) {
|
||||
this.totalWordCount = this.utilityService.asVolume(this.entity).chapters.map(c => c.wordCount).reduce((sum, d) => sum + d);
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.isChapter) {
|
||||
this.readingTime.minHours = this.chapter.minHoursToRead;
|
||||
this.readingTime.maxHours = this.chapter.maxHoursToRead;
|
||||
this.readingTime.avgHours = this.chapter.avgHoursToRead;
|
||||
} else {
|
||||
const vol = this.utilityService.asVolume(this.entity);
|
||||
this.readingTime.minHours = vol.minHoursToRead;
|
||||
this.readingTime.maxHours = vol.maxHoursToRead;
|
||||
this.readingTime.avgHours = vol.avgHoursToRead;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.onDestroy.next();
|
||||
this.onDestroy.complete();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<ng-container [ngSwitch]="libraryType">
|
||||
<ng-container *ngSwitchCase="LibraryType.Comic">
|
||||
<ng-container *ngIf="titleName != '' && prioritizeTitleName; else fullComicTitle">
|
||||
{{titleName}}
|
||||
</ng-container>
|
||||
<ng-template #fullComicTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle != ''">
|
||||
{{entity.number != 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</ng-container>
|
||||
{{entity.number != 0 ? (isChapter ? 'Issue #' + entity.number : volumeTitle) : 'Special'}}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Manga">
|
||||
<ng-container *ngIf="titleName != '' && prioritizeTitleName; else fullMangaTitle">
|
||||
{{titleName}}
|
||||
</ng-container>
|
||||
<ng-template #fullMangaTitle>
|
||||
{{seriesName.length > 0 ? seriesName + ' - ' : ''}}
|
||||
<ng-container *ngIf="includeVolume && volumeTitle != ''">
|
||||
{{entity.number != 0 ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
|
||||
</ng-container>
|
||||
{{entity.number != 0 ? (isChapter ? 'Chapter ' + entity.number : volumeTitle) : 'Special'}}
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="LibraryType.Book">
|
||||
{{volumeTitle}}
|
||||
</ng-container>
|
||||
</ng-container>
|
57
UI/Web/src/app/cards/entity-title/entity-title.component.ts
Normal file
57
UI/Web/src/app/cards/entity-title/entity-title.component.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
|
||||
@Component({
|
||||
selector: 'app-entity-title',
|
||||
templateUrl: './entity-title.component.html',
|
||||
styleUrls: ['./entity-title.component.scss']
|
||||
})
|
||||
export class EntityTitleComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Library type for which the entity belongs
|
||||
*/
|
||||
@Input() libraryType: LibraryType = LibraryType.Manga;
|
||||
@Input() seriesName: string = '';
|
||||
@Input() entity!: Volume | Chapter;
|
||||
/**
|
||||
* When generating the title, should this prepend 'Volume number' before the Chapter wording
|
||||
*/
|
||||
@Input() includeVolume: boolean = false;
|
||||
/**
|
||||
* When a titleName (aka a title) is avaliable on the entity, show it over Volume X Chapter Y
|
||||
*/
|
||||
@Input() prioritizeTitleName: boolean = true;
|
||||
|
||||
isChapter = false;
|
||||
chapter!: Chapter;
|
||||
titleName: string = '';
|
||||
volumeTitle: string = '';
|
||||
|
||||
|
||||
get LibraryType() {
|
||||
return LibraryType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
|
||||
if (this.isChapter) {
|
||||
const c = (this.entity as Chapter);
|
||||
this.volumeTitle = c.volumeTitle || '';
|
||||
this.titleName = c.titleName || '';
|
||||
} else {
|
||||
const v = this.utilityService.asVolume(this.entity);
|
||||
this.volumeTitle = v.name || '';
|
||||
this.titleName = v.chapters[0].titleName || '';
|
||||
}
|
||||
}
|
||||
}
|
40
UI/Web/src/app/cards/list-item/list-item.component.html
Normal file
40
UI/Web/src/app/cards/list-item/list-item.component.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div class="list-item-container d-flex flex-row g-0 mb-2 p-2">
|
||||
<div class="pe-2">
|
||||
<app-image [imageUrl]="imageUrl" [height]="imageHeight" [width]="imageWidth"></app-image>
|
||||
|
||||
<span class="download" *ngIf="download$ | async as download">
|
||||
<app-circular-loader [currentValue]="download.progress"></app-circular-loader>
|
||||
<span class="visually-hidden" role="status">
|
||||
{{download.progress}}% downloaded
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="progress-banner" *ngIf="pagesRead < totalPages && totalPages > 0 && pagesRead !== totalPages">
|
||||
<p><ngb-progressbar type="primary" height="5px" [value]="pagesRead" [max]="totalPages"></ngb-progressbar></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="g-0">
|
||||
<h5 style="margin-bottom: 0px">
|
||||
<app-card-actionables [disabled]="actionInProgress" (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="seriesName" iconClass="fa-ellipsis-v"></app-card-actionables>
|
||||
<ng-content select="[title]"></ng-content>
|
||||
<button class="btn btn-primary float-end" (click)="read.emit()">
|
||||
<span>
|
||||
<i class="fa fa-book me-1" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="d-none d-sm-inline-block">Read</span>
|
||||
</button>
|
||||
</h5>
|
||||
<!-- This isn't perfect, but it might work. TODO: Polish this-->
|
||||
<h6 class="text-muted" [ngClass]="{'subtitle-with-actionables' : actions.length > 0}" style="font-size: 0.75rem" *ngIf="Title != '' && showTitle">{{Title}}</h6>
|
||||
<ng-container *ngIf="summary.length > 0">
|
||||
<div class="mt-2 ps-2">
|
||||
<app-read-more [text]="summary" [maxLength]="250"></app-read-more>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="ps-2 d-none d-md-inline-block">
|
||||
<app-entity-info-cards [entity]="entity" [showExtendedProperties]="false"></app-entity-info-cards>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
26
UI/Web/src/app/cards/list-item/list-item.component.scss
Normal file
26
UI/Web/src/app/cards/list-item/list-item.component.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
$image-height: 230px;
|
||||
$image-width: 160px;
|
||||
|
||||
.download {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
top: 55px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.progress-banner {
|
||||
height: 5px;
|
||||
|
||||
.progress {
|
||||
color: var(--card-progress-bar-color);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item-container {
|
||||
background: rgb(0,0,0);
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.15) 0%, rgba(0,0,0,0.15) 1%, rgba(0,0,0,0) 100%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
}
|
140
UI/Web/src/app/cards/list-item/list-item.component.ts
Normal file
140
UI/Web/src/app/cards/list-item/list-item.component.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { finalize, Observable, of, take, takeWhile } from 'rxjs';
|
||||
import { Download } from 'src/app/shared/_models/download';
|
||||
import { DownloadService } from 'src/app/shared/_services/download.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { Chapter } from 'src/app/_models/chapter';
|
||||
import { LibraryType } from 'src/app/_models/library';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { RelationKind } from 'src/app/_models/series-detail/relation-kind';
|
||||
import { Volume } from 'src/app/_models/volume';
|
||||
import { Action, ActionItem } from 'src/app/_services/action-factory.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-list-item',
|
||||
templateUrl: './list-item.component.html',
|
||||
styleUrls: ['./list-item.component.scss']
|
||||
})
|
||||
export class ListItemComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Volume or Chapter to render
|
||||
*/
|
||||
@Input() entity!: Volume | Chapter;
|
||||
/**
|
||||
* Image to show
|
||||
*/
|
||||
@Input() imageUrl: string = '';
|
||||
/**
|
||||
* Actions to show
|
||||
*/
|
||||
@Input() actions: ActionItem<any>[] = []; // Volume | Chapter
|
||||
/**
|
||||
* Library type to help with formatting title
|
||||
*/
|
||||
@Input() libraryType: LibraryType = LibraryType.Manga;
|
||||
/**
|
||||
* Name of the Series to show under the title
|
||||
*/
|
||||
@Input() seriesName: string = '';
|
||||
|
||||
/**
|
||||
* Size of the Image Height. Defaults to 230px.
|
||||
*/
|
||||
@Input() imageHeight: string = '230px';
|
||||
/**
|
||||
* Size of the Image Width Defaults to 158px.
|
||||
*/
|
||||
@Input() imageWidth: string = '158px';
|
||||
@Input() seriesLink: string = '';
|
||||
|
||||
@Input() pagesRead: number = 0;
|
||||
@Input() totalPages: number = 0;
|
||||
|
||||
@Input() relation: RelationKind | undefined = undefined;
|
||||
|
||||
/**
|
||||
* When generating the title, should this prepend 'Volume number' before the Chapter wording
|
||||
*/
|
||||
@Input() includeVolume: boolean = false;
|
||||
/**
|
||||
* Show's the title if avaible on entity
|
||||
*/
|
||||
@Input() showTitle: boolean = true;
|
||||
|
||||
@Output() read: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
actionInProgress: boolean = false;
|
||||
summary$: Observable<string> = of('');
|
||||
summary: string = '';
|
||||
isChapter: boolean = false;
|
||||
|
||||
|
||||
download$: Observable<Download> | null = null;
|
||||
downloadInProgress: boolean = false;
|
||||
|
||||
get Title() {
|
||||
if (this.isChapter) return (this.entity as Chapter).titleName;
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
constructor(private utilityService: UtilityService, private downloadService: DownloadService, private toastr: ToastrService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.isChapter = this.utilityService.isChapter(this.entity);
|
||||
if (this.isChapter) {
|
||||
this.summary = this.utilityService.asChapter(this.entity).summary || '';
|
||||
} else {
|
||||
this.summary = this.utilityService.asVolume(this.entity).chapters[0].summary || '';
|
||||
}
|
||||
}
|
||||
|
||||
performAction(action: ActionItem<any>) {
|
||||
if (action.action == Action.Download) {
|
||||
if (this.downloadInProgress === true) {
|
||||
this.toastr.info('Download is already in progress. Please wait.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.utilityService.isVolume(this.entity)) {
|
||||
const volume = this.utilityService.asVolume(this.entity);
|
||||
this.downloadService.downloadVolumeSize(volume.id).pipe(take(1)).subscribe(async (size) => {
|
||||
const wantToDownload = await this.downloadService.confirmSize(size, 'volume');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.download$ = this.downloadService.downloadVolume(volume).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
}),
|
||||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
}));
|
||||
});
|
||||
} else if (this.utilityService.isChapter(this.entity)) {
|
||||
const chapter = this.utilityService.asChapter(this.entity);
|
||||
this.downloadService.downloadChapterSize(chapter.id).pipe(take(1)).subscribe(async (size) => {
|
||||
const wantToDownload = await this.downloadService.confirmSize(size, 'chapter');
|
||||
if (!wantToDownload) { return; }
|
||||
this.downloadInProgress = true;
|
||||
this.download$ = this.downloadService.downloadChapter(chapter).pipe(
|
||||
takeWhile(val => {
|
||||
return val.state != 'DONE';
|
||||
}),
|
||||
finalize(() => {
|
||||
this.download$ = null;
|
||||
this.downloadInProgress = false;
|
||||
}));
|
||||
});
|
||||
}
|
||||
return; // Don't propagate the download from a card
|
||||
}
|
||||
|
||||
if (typeof action.callback === 'function') {
|
||||
action.callback(action.action, this.entity);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -72,7 +72,8 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
|
|||
ngOnChanges(changes: any) {
|
||||
if (this.data) {
|
||||
this.actions = this.actionFactoryService.getSeriesActions((action: Action, series: Series) => this.handleSeriesActionCallback(action, series));
|
||||
this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.data.id));
|
||||
//this.imageUrl = this.imageService.randomize(this.imageService.getSeriesCoverImage(this.data.id));
|
||||
this.imageUrl = this.imageService.getSeriesCoverImage(this.data.id); // TODO: Do I need to do this since image now handles updates?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
<div class="row g-0 mb-4 mt-3">
|
||||
|
||||
<ng-container *ngIf="seriesMetadata">
|
||||
<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)="handleGoTo(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="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)="handleGoTo(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>
|
||||
|
||||
<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)="handleGoTo(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 *ngIf="series">
|
||||
<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)="handleGoTo(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="hasReadingProgress && showReadingTimeLeft && readingTimeLeft && 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>
|
|
@ -0,0 +1,55 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FilterQueryParam } from 'src/app/shared/_services/filter-utilities.service';
|
||||
import { UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { HourEstimateRange } from 'src/app/_models/hour-estimate-range';
|
||||
import { MangaFormat } from 'src/app/_models/manga-format';
|
||||
import { Series } from 'src/app/_models/series';
|
||||
import { SeriesMetadata } from 'src/app/_models/series-metadata';
|
||||
import { MetadataService } from 'src/app/_services/metadata.service';
|
||||
import { ReaderService } from 'src/app/_services/reader.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-series-info-cards',
|
||||
templateUrl: './series-info-cards.component.html',
|
||||
styleUrls: ['./series-info-cards.component.scss']
|
||||
})
|
||||
export class SeriesInfoCardsComponent implements OnInit {
|
||||
|
||||
@Input() series!: Series;
|
||||
@Input() seriesMetadata!: SeriesMetadata;
|
||||
@Input() hasReadingProgress: boolean = false;
|
||||
@Input() readingTimeLeft: HourEstimateRange | undefined;
|
||||
/**
|
||||
* If this should make an API call to request readingTimeLeft
|
||||
*/
|
||||
@Input() showReadingTimeLeft: boolean = true;
|
||||
@Output() goTo: EventEmitter<{queryParamName: FilterQueryParam, filter: any}> = new EventEmitter();
|
||||
|
||||
readingTime: HourEstimateRange = {avgHours: 0, maxHours: 0, minHours: 0};
|
||||
|
||||
get MangaFormat() {
|
||||
return MangaFormat;
|
||||
}
|
||||
|
||||
get FilterQueryParam() {
|
||||
return FilterQueryParam;
|
||||
}
|
||||
|
||||
constructor(public utilityService: UtilityService, public metadataService: MetadataService, private readerService: ReaderService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.series !== null) {
|
||||
if (this.showReadingTimeLeft) this.readerService.getTimeLeft(this.series.id).subscribe((timeLeft) => this.readingTimeLeft = timeLeft);
|
||||
this.readingTime.minHours = this.series.minHoursToRead;
|
||||
this.readingTime.maxHours = this.series.maxHoursToRead;
|
||||
this.readingTime.avgHours = this.series.avgHoursToRead;
|
||||
}
|
||||
}
|
||||
|
||||
handleGoTo(queryParamName: FilterQueryParam, filter: any) {
|
||||
this.goTo.emit({queryParamName, filter});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -23,13 +23,17 @@
|
|||
[items]="series"
|
||||
[pagination]="pagination"
|
||||
[filterSettings]="filterSettings"
|
||||
[trackByIdentity]="trackByIdentity"
|
||||
[filterOpen]="filterOpen"
|
||||
[jumpBarKeys]="jumpKeys"
|
||||
(applyFilter)="updateFilter($event)"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(pageChangeWithDirection)="handlePaginationChange($event)"
|
||||
>
|
||||
<ng-template #cardItem let-item let-position="idx">
|
||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()" (selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)" [selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
<app-series-card [data]="item" [libraryId]="libraryId" [suppressLibraryLink]="true" (reload)="loadPage()"
|
||||
(selection)="bulkSelectionService.handleCardSelection('series', position, series.length, $event)"
|
||||
[selected]="bulkSelectionService.isCardSelected('series', position)" [allowSelection]="true"></app-series-card>
|
||||
</ng-template>
|
||||
</app-card-detail-layout>
|
||||
</ng-container>
|
||||
|
|
|
@ -105,12 +105,13 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
|
||||
this.libraryService.getJumpBar(this.libraryId).subscribe(barDetails => {
|
||||
console.log('JumpBar: ', barDetails);
|
||||
//console.log('JumpBar: ', barDetails);
|
||||
this.jumpKeys = barDetails;
|
||||
});
|
||||
this.actions = this.actionFactoryService.getLibraryActions(this.handleAction.bind(this));
|
||||
|
||||
this.pagination = this.filterUtilityService.pagination(this.route.snapshot);
|
||||
this.pagination.itemsPerPage = 0; // TODO: Validate what pagination setting is ideal
|
||||
[this.filterSettings.presets, this.filterSettings.openByDefault] = this.filterUtilityService.filterPresetsFromUrl(this.route.snapshot);
|
||||
if (this.filterSettings.presets) this.filterSettings.presets.libraries = [this.libraryId];
|
||||
// Setup filterActiveCheck to check filter against
|
||||
|
@ -178,7 +179,12 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
this.loadPage();
|
||||
}
|
||||
|
||||
loadPage() {
|
||||
handlePaginationChange(direction: 0 | 1) {
|
||||
this.filterUtilityService.updateUrlFromFilter(this.pagination, undefined);
|
||||
this.loadPage(direction);
|
||||
}
|
||||
|
||||
loadPage(direction: 0 | 1 = 1) {
|
||||
// The filter is out of sync with the presets from typeaheads on first load but syncs afterwards
|
||||
if (this.filter == undefined) {
|
||||
this.filter = this.seriesService.createSeriesFilter();
|
||||
|
@ -188,7 +194,18 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
this.loadingSeries = true;
|
||||
this.filterActive = !this.utilityService.deepEqual(this.filter, this.filterActiveCheck);
|
||||
this.seriesService.getSeriesForLibrary(0, this.pagination?.currentPage, this.pagination?.itemsPerPage, this.filter).pipe(take(1)).subscribe(series => {
|
||||
this.series = series.result;
|
||||
//this.series = series.result; // Non-infinite scroll version
|
||||
if (this.series.length === 0) {
|
||||
this.series = series.result;
|
||||
} else {
|
||||
if (direction === 1) {
|
||||
//this.series = [...this.series, ...series.result];
|
||||
this.series.concat(series.result);
|
||||
} else {
|
||||
this.series = [...series.result, ...this.series];
|
||||
}
|
||||
}
|
||||
|
||||
this.pagination = series.pagination;
|
||||
this.loadingSeries = false;
|
||||
window.scrollTo(0, 0);
|
||||
|
@ -204,5 +221,5 @@ export class LibraryDetailComponent implements OnInit, OnDestroy {
|
|||
this.router.navigate(['library', this.libraryId, 'series', series.id]);
|
||||
}
|
||||
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.originalName}_${item.localizedName}_${item.pagesRead}`;
|
||||
trackByIdentity = (index: number, item: Series) => `${item.name}_${item.localizedName}_${item.pagesRead}`;
|
||||
}
|
||||
|
|
3
UI/Web/src/app/pdf-reader/package-lock.json
generated
Normal file
3
UI/Web/src/app/pdf-reader/package-lock.json
generated
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
22
UI/Web/src/app/pipe/age-rating.pipe.ts
Normal file
22
UI/Web/src/app/pipe/age-rating.pipe.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { AgeRating } from '../_models/metadata/age-rating';
|
||||
import { AgeRatingDto } from '../_models/metadata/age-rating-dto';
|
||||
import { MetadataService } from '../_services/metadata.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'ageRating'
|
||||
})
|
||||
export class AgeRatingPipe implements PipeTransform {
|
||||
|
||||
constructor(private metadataService: MetadataService) {}
|
||||
|
||||
transform(value: AgeRating | AgeRatingDto): Observable<string> {
|
||||
if (value.hasOwnProperty('title')) {
|
||||
return of((value as AgeRatingDto).title);
|
||||
}
|
||||
|
||||
return this.metadataService.getAgeRating((value as AgeRating));
|
||||
}
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@ import { RelationshipPipe } from './relationship.pipe';
|
|||
import { DefaultValuePipe } from './default-value.pipe';
|
||||
import { CompactNumberPipe } from './compact-number.pipe';
|
||||
import { LanguageNamePipe } from './language-name.pipe';
|
||||
import { AgeRatingPipe } from './age-rating.pipe';
|
||||
|
||||
|
||||
|
||||
|
@ -22,7 +23,8 @@ import { LanguageNamePipe } from './language-name.pipe';
|
|||
RelationshipPipe,
|
||||
DefaultValuePipe,
|
||||
CompactNumberPipe,
|
||||
LanguageNamePipe
|
||||
LanguageNamePipe,
|
||||
AgeRatingPipe
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -36,7 +38,8 @@ import { LanguageNamePipe } from './language-name.pipe';
|
|||
RelationshipPipe,
|
||||
DefaultValuePipe,
|
||||
CompactNumberPipe,
|
||||
LanguageNamePipe
|
||||
LanguageNamePipe,
|
||||
AgeRatingPipe
|
||||
]
|
||||
})
|
||||
export class PipeModule { }
|
||||
|
|
|
@ -45,14 +45,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="items.length === 0">
|
||||
<div class="mx-auto" *ngIf="items.length === 0" style="width: 200px;">
|
||||
Nothing added
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<app-dragable-ordered-list [items]="items" (orderUpdated)="orderUpdated($event)" (itemRemove)="itemRemoved($event)" [accessibilityMode]="accessibilityMode">
|
||||
<ng-template #draggableItem let-item let-position="idx">
|
||||
<div class="d-flex" style="width: 100%;">
|
||||
<div class="d-flex" style="width: 100%;">
|
||||
<app-image width="74px" class="img-top me-3" [imageUrl]="imageService.getChapterCoverImage(item.chapterId)"></app-image>
|
||||
<div class="flex-grow-1">
|
||||
<h5 class="mt-0 mb-1" id="item.id--{{position}}">{{formatTitle(item)}}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -71,10 +71,11 @@ export class FilterUtilitiesService {
|
|||
/**
|
||||
* Will fetch current page from route if present
|
||||
* @param ActivatedRouteSnapshot to fetch page from. Must be from component else may get stale data
|
||||
* @param itemsPerPage If you want pagination, pass non-zero number
|
||||
* @returns A default pagination object
|
||||
*/
|
||||
pagination(snapshot: ActivatedRouteSnapshot): Pagination {
|
||||
return {currentPage: parseInt(snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage: 30, totalItems: 0, totalPages: 1};
|
||||
pagination(snapshot: ActivatedRouteSnapshot, itemsPerPage: number = 0): Pagination {
|
||||
return {currentPage: parseInt(snapshot.queryParamMap.get('page') || '1', 10), itemsPerPage, totalItems: 0, totalPages: 1};
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,18 +1,3 @@
|
|||
<!-- <div class="circular">
|
||||
<div class="inner"></div>
|
||||
<div class="number">
|
||||
<span class="visually-hidden">{{currentValue}}%</span>
|
||||
<i class="fa fa-angle-double-down" style="font-size: 36px;" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="circle">
|
||||
<div class="bar left" #left>
|
||||
<div class="progress"></div>
|
||||
</div>
|
||||
<div class="bar right" #right>
|
||||
<div class="progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<ng-container *ngIf="currentValue > 0">
|
||||
|
||||
<div class="number">
|
||||
|
@ -28,11 +13,11 @@
|
|||
[backgroundPadding]="0"
|
||||
outerStrokeLinecap="butt"
|
||||
[outerStrokeColor]="'#4ac694'"
|
||||
[innerStrokeColor]="'transparent'"
|
||||
[innerStrokeColor]="innerStrokeColor"
|
||||
titleFontSize= "24"
|
||||
unitsFontSize= "24"
|
||||
[showSubtitle] = "false"
|
||||
[animation]="true"
|
||||
[animation]="animation"
|
||||
[animationDuration]="300"
|
||||
[startFromZero]="false"
|
||||
[responsive]="true"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, ElementRef, Input, OnChanges, OnInit, Renderer2, ViewChild } from '@angular/core';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-circular-loader',
|
||||
|
@ -9,8 +9,10 @@ export class CircularLoaderComponent implements OnInit {
|
|||
|
||||
@Input() currentValue: number = 0;
|
||||
@Input() maxValue: number = 0;
|
||||
@Input() animation: boolean = true;
|
||||
@Input() innerStrokeColor: string = 'transparent';
|
||||
|
||||
constructor(private renderer: Renderer2) { }
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
<img #img class="lazyload" [src]="imageService.placeholderImage" [attr.data-src]="imageUrl"
|
||||
(error)="imageService.updateErroredImage($event)"
|
||||
aria-hidden="true">
|
||||
aria-hidden="true">
|
||||
<!-- TODO: Need to think about how to not lazyload but also handle broken images-->
|
||||
<!-- <img #img [src]="imageUrl" aria-hidden="true"> -->
|
|
@ -1,3 +1,4 @@
|
|||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild } from '@angular/core';
|
||||
import { Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, ViewChild } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { CoverUpdateEvent } from 'src/app/_models/events/cover-update-event';
|
||||
|
@ -39,6 +39,10 @@ export class ImageComponent implements OnChanges, OnDestroy {
|
|||
* Border Radius of the image. If not defined, will not be applied
|
||||
*/
|
||||
@Input() borderRadius: string = '';
|
||||
/**
|
||||
* If the image component should respond to cover updates
|
||||
*/
|
||||
@Input() processEvents: boolean = true;
|
||||
|
||||
@ViewChild('img', {static: true}) imgElem!: ElementRef<HTMLImageElement>;
|
||||
|
||||
|
@ -46,27 +50,28 @@ export class ImageComponent implements OnChanges, OnDestroy {
|
|||
|
||||
constructor(public imageService: ImageService, private renderer: Renderer2, private hubService: MessageHubService) {
|
||||
this.hubService.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
|
||||
if (res.event === EVENTS.CoverUpdate) {
|
||||
const updateEvent = res.payload as CoverUpdateEvent;
|
||||
if (this.imageUrl === undefined || this.imageUrl === null || this.imageUrl === '') return;
|
||||
const enityType = this.imageService.getEntityTypeFromUrl(this.imageUrl);
|
||||
if (enityType === updateEvent.entityType) {
|
||||
const tokens = this.imageUrl.split('?')[1].split('&');
|
||||
if (!this.processEvents) return;
|
||||
if (res.event === EVENTS.CoverUpdate) {
|
||||
const updateEvent = res.payload as CoverUpdateEvent;
|
||||
if (this.imageUrl === undefined || this.imageUrl === null || this.imageUrl === '') return;
|
||||
const enityType = this.imageService.getEntityTypeFromUrl(this.imageUrl);
|
||||
if (enityType === updateEvent.entityType) {
|
||||
const tokens = this.imageUrl.split('?')[1].split('&');
|
||||
|
||||
//...seriesId=123&random=
|
||||
let id = tokens[0].replace(enityType + 'Id=', '');
|
||||
if (id.includes('&')) {
|
||||
id = id.split('&')[0];
|
||||
}
|
||||
if (id === (updateEvent.id + '')) {
|
||||
this.imageUrl = this.imageService.randomize(this.imageUrl);
|
||||
}
|
||||
//...seriesId=123&random=
|
||||
let id = tokens[0].replace(enityType + 'Id=', '');
|
||||
if (id.includes('&')) {
|
||||
id = id.split('&')[0];
|
||||
}
|
||||
if (id === (updateEvent.id + '')) {
|
||||
this.imageUrl = this.imageService.randomize(this.imageUrl);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
ngOnChanges(): void {
|
||||
if (this.width != '') {
|
||||
this.renderer.setStyle(this.imgElem.nativeElement, 'width', this.width);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,10 @@
|
|||
<ng-content select="[main]"></ng-content>
|
||||
</div>
|
||||
<div>
|
||||
<!-- This can get out of sync with offcanvas component. We might need a service to handle communication and service resets to collapsed on route changes -->
|
||||
<button *ngIf="hasExtras" class="btn btn-secondary btn-small" (click)="openExtrasDrawer()" [attr.aria-expanded]="isExtrasOpen" placement="left" ngbTooltip="Page Settings">
|
||||
<i class="fa-solid fa-sliders" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Page Settings</span>
|
||||
</button>
|
||||
<button *ngIf="hasFilter" class="btn btn-{{filterActive ? 'primary' : 'secondary'}} btn-small" (click)="toggleService.toggle()" [attr.aria-expanded]="filterOpen" placement="left" ngbTooltip="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting" attr.aria-label="{{filterOpen ? 'Open' : 'Close'}} Filtering and Sorting">
|
||||
<i class="fa fa-filter" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">Sort / Filter</span>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, TemplateRef } from '@angular/core';
|
||||
import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { Breakpoint, UtilityService } from 'src/app/shared/_services/utility.service';
|
||||
import { NavService } from 'src/app/_services/nav.service';
|
||||
|
@ -18,6 +19,10 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
|
|||
* If the page should show a filter
|
||||
*/
|
||||
@Input() hasFilter: boolean = false;
|
||||
/**
|
||||
* If the page should show an extra section button
|
||||
*/
|
||||
@Input() hasExtras: boolean = false;
|
||||
|
||||
/**
|
||||
* Is the input open by default
|
||||
|
@ -29,18 +34,18 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
|
|||
*/
|
||||
@Input() filterActive: boolean = false;
|
||||
|
||||
/**
|
||||
* Should be passed through from Filter component.
|
||||
*/
|
||||
//@Input() filterDisabled: EventEmitter<boolean> = new EventEmitter();
|
||||
@Input() extraDrawer!: TemplateRef<any>;
|
||||
|
||||
|
||||
@Output() filterOpen: EventEmitter<boolean> = new EventEmitter();
|
||||
|
||||
isFilterOpen = false;
|
||||
isExtrasOpen = false;
|
||||
|
||||
private onDestroy: Subject<void> = new Subject();
|
||||
|
||||
constructor(private navService: NavService, private utilityService: UtilityService, public toggleService: ToggleService) {
|
||||
constructor(private navService: NavService, private utilityService: UtilityService, public toggleService: ToggleService,
|
||||
private offcanvasService: NgbOffcanvas) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -65,4 +70,13 @@ export class SideNavCompanionBarComponent implements OnInit, OnDestroy {
|
|||
this.filterOpen.emit(this.isFilterOpen);
|
||||
}
|
||||
|
||||
openExtrasDrawer() {
|
||||
if (this.extraDrawer === undefined) return;
|
||||
|
||||
this.isExtrasOpen = true;
|
||||
const drawerRef = this.offcanvasService.open(this.extraDrawer, {position: 'end', scroll: true});
|
||||
drawerRef.closed.subscribe(() => this.isExtrasOpen = false);
|
||||
drawerRef.dismissed.subscribe(() => this.isExtrasOpen = false);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,213 +16,240 @@
|
|||
<form [formGroup]="settingsForm" *ngIf="user !== undefined">
|
||||
|
||||
<ngb-accordion [closeOthers]="true" [activeIds]="AccordionPanelID.ImageReader" #acc="ngbAccordion">
|
||||
<ngb-panel [id]="AccordionPanelID.ImageReader" title="Image Reader">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
|
||||
Image Reader
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-scaling-option" class="form-label">Scaling Options</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskBackupTooltip>How to scale the image to your screen.</ng-template>
|
||||
<span class="visually-hidden" id="settings-scaling-option-help">How to scale the image to your screen.</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
||||
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-pagesplit-option" class="form-label">Page Splitting</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #pageSplitOptionTooltip>How to split a full width image (ie both left and right images are combined)</ng-template>
|
||||
<span class="visually-hidden" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-readingmode-option" class="form-label">Reading Mode</label>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
||||
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="false">
|
||||
<label for="settings-layoutmode-option" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #layoutModeTooltip>Render a single image to the screen to two side-by-side images</ng-template>
|
||||
<span class="visually-hidden" id="settings-layoutmode-option-help"><ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
|
||||
<option *ngFor="let opt of layoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-backgroundcolor-option" class="form-label">Background Color</label>
|
||||
<input [value]="user.preferences.backgroundColor"
|
||||
class="form-control"
|
||||
(colorPickerChange)="settingsForm.markAsTouched()"
|
||||
[style.background]="user.preferences.backgroundColor"
|
||||
[cpAlphaChannel]="'disabled'"
|
||||
[(colorPicker)]="user.preferences.backgroundColor"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="show-screen-hints">Show Screen Hints</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
|
||||
<ngb-panel [id]="AccordionPanelID.BookReader" title="Book Reader">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
|
||||
Book Reader
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<ngb-panel [id]="AccordionPanelID.GlobalSettings" title="Global Settings">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.GlobalSettings)" aria-controls="collapseOne">
|
||||
Global Settings
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label id="taptopaginate-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
|
||||
<label id="taptopaginate" class="form-check-label">Tap to Paginate</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #tapToPaginateOptionTooltip>Should the sides of the book reader screen allow tapping on it to move to prev/next page</ng-template>
|
||||
<span class="visually-hidden" id="settings-taptopaginate-option-help">Should the sides of the book reader screen allow tapping on it to move to prev/next page</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label id="immersivemode-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
|
||||
<label id="immersivemode" class="form-check-label">Immersive Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #immersivemodeOptionTooltip>This will hide the menu behind a click on the reader document and turn tap to paginate on</ng-template>
|
||||
<span class="visually-hidden" id="settings-immersivemode-option-help">This will hide the menu behind a click on the reader document and turn tap to paginate on</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-global-layoutmode" class="form-label">Page Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #layoutModeTooltip>Show items as cards or list view on Series Detail page</ng-template>
|
||||
<span class="visually-hidden" id="settings-global-layoutmode-help">Show items as cards or list view on Series Detail page</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="globalPageLayoutMode" id="settings-global-layoutmode">
|
||||
<option *ngFor="let opt of pageLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-fontfamily-option" class="form-label">Font Family</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #fontFamilyOptionTooltip>Font familty to load up. Default will load the book's default font</ng-template>
|
||||
<span class="visually-hidden" id="settings-fontfamily-option-help">Font familty to load up. Default will load the book's default font</span>
|
||||
<select class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-layout-mode" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLayoutModeTooltip>How content should be laid out. Default is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page</ng-template>
|
||||
<span class="visually-hidden" id="settings-book-layout-mode-help"><ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
|
||||
<option *ngFor="let opt of bookLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-color-theme-option" class="form-label">Color Theme</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookColorThemeTooltip>What color theme to apply to the book reader content and menuing</ng-template>
|
||||
<span class="visually-hidden" id="settings-color-theme-option-help"><ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
|
||||
<option *ngFor="let opt of bookColorThemes" [value]="opt.name">{{opt.name | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label for="fontsize" class="form-label range-label">Font Size</label>
|
||||
<input type="range" class="form-range" id="fontsize"
|
||||
min="50" max="300" step="10" formControlName="bookReaderFontSize">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<div class="range-label">
|
||||
<label class="form-label" for="linespacing">Line Height</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
|
||||
<span class="visually-hidden" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
||||
</div>
|
||||
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
|
||||
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<div class="range-label">
|
||||
<label class="form-label">Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
||||
<span class="visually-hidden" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>
|
||||
</div>
|
||||
|
||||
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
|
||||
<ngb-panel [id]="AccordionPanelID.ImageReader" title="Image Reader">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.ImageReader)" aria-controls="collapseOne">
|
||||
Image Reader
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="readingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #readingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="readingDirection" id="settings-reading-direction">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-scaling-option" class="form-label">Scaling Options</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="taskBackupTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #taskBackupTooltip>How to scale the image to your screen.</ng-template>
|
||||
<span class="visually-hidden" id="settings-scaling-option-help">How to scale the image to your screen.</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="scalingOption" id="settings-scaling-option">
|
||||
<option *ngFor="let opt of scalingOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-pagesplit-option" class="form-label">Page Splitting</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="pageSplitOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #pageSplitOptionTooltip>How to split a full width image (ie both left and right images are combined)</ng-template>
|
||||
<span class="visually-hidden" id="settings-pagesplit-option-help">How to split a full width image (ie both left and right images are combined)</span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="pageSplitOption" id="settings-pagesplit-option">
|
||||
<option *ngFor="let opt of pageSplitOptions" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-readingmode-option" class="form-label">Reading Mode</label>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="readerMode" id="settings-readingmode-option">
|
||||
<option *ngFor="let opt of readingModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2" *ngIf="false">
|
||||
<label for="settings-layoutmode-option" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="layoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #layoutModeTooltip>Render a single image to the screen to two side-by-side images</ng-template>
|
||||
<span class="visually-hidden" id="settings-layoutmode-option-help"><ng-container [ngTemplateOutlet]="layoutModeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="manga-header" formControlName="layoutMode" id="settings-layoutmode-option">
|
||||
<option *ngFor="let opt of layoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<label for="settings-backgroundcolor-option" class="form-label">Background Color</label>
|
||||
<input [value]="user.preferences.backgroundColor"
|
||||
class="form-control"
|
||||
(colorPickerChange)="settingsForm.markAsTouched()"
|
||||
[style.background]="user.preferences.backgroundColor"
|
||||
[cpAlphaChannel]="'disabled'"
|
||||
[(colorPicker)]="user.preferences.backgroundColor"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="auto-close" role="switch" formControlName="autoCloseMenu" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="auto-close">Auto Close Menu</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-2">
|
||||
<div class="mb-3 mt-1">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" id="show-screen-hints" role="switch" formControlName="showScreenHints" class="form-check-input" [value]="true" aria-labelledby="auto-close-label">
|
||||
<label class="form-check-label" for="show-screen-hints">Show Screen Hints</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
|
||||
<ngb-panel [id]="AccordionPanelID.BookReader" title="Book Reader">
|
||||
<ng-template ngbPanelHeader>
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" ngbPanelToggle type="button" [attr.aria-expanded]="acc.isExpanded(AccordionPanelID.BookReader)" aria-controls="collapseOne">
|
||||
Book Reader
|
||||
</button>
|
||||
</h2>
|
||||
</ng-template>
|
||||
<ng-template ngbPanelContent>
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label id="taptopaginate-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" role="switch" id="taptopaginate" formControlName="bookReaderTapToPaginate" class="form-check-input" [value]="true" aria-labelledby="taptopaginate-label">
|
||||
<label id="taptopaginate" class="form-check-label">Tap to Paginate</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tapToPaginateOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #tapToPaginateOptionTooltip>Should the sides of the book reader screen allow tapping on it to move to prev/next page</ng-template>
|
||||
<span class="visually-hidden" id="settings-taptopaginate-option-help">Should the sides of the book reader screen allow tapping on it to move to prev/next page</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label id="immersivemode-label" class="form-label"></label>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" role="switch" id="immersivemode" formControlName="bookReaderImmersiveMode" class="form-check-input" [value]="true" aria-labelledby="immersivemode-label">
|
||||
<label id="immersivemode" class="form-check-label">Immersive Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="immersivemodeOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #immersivemodeOptionTooltip>This will hide the menu behind a click on the reader document and turn tap to paginate on</ng-template>
|
||||
<span class="visually-hidden" id="settings-immersivemode-option-help">This will hide the menu behind a click on the reader document and turn tap to paginate on</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-reading-direction" class="form-label">Reading Direction</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReadingDirectionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReadingDirectionTooltip>Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</ng-template>
|
||||
<span class="visually-hidden" id="settings-reading-direction-help">Direction to click to move to next page. Right to Left means you click on left side of screen to move to next page.</span>
|
||||
<select class="form-select" aria-describedby="settings-reading-direction-help" formControlName="bookReaderReadingDirection">
|
||||
<option *ngFor="let opt of readingDirections" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-fontfamily-option" class="form-label">Font Family</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="fontFamilyOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #fontFamilyOptionTooltip>Font familty to load up. Default will load the book's default font</ng-template>
|
||||
<span class="visually-hidden" id="settings-fontfamily-option-help">Font familty to load up. Default will load the book's default font</span>
|
||||
<select class="form-select" aria-describedby="settings-fontfamily-option-help" formControlName="bookReaderFontFamily">
|
||||
<option *ngFor="let opt of fontFamilies" [value]="opt">{{opt | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-book-layout-mode" class="form-label">Layout Mode</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLayoutModeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLayoutModeTooltip>How content should be laid out. Default is as the book packs it. 1 or 2 Column fits to the height of the device and fits 1 or 2 columns of text per page</ng-template>
|
||||
<span class="visually-hidden" id="settings-book-layout-mode-help"><ng-container [ngTemplateOutlet]="bookLayoutModeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-book-layout-mode-help" formControlName="bookReaderLayoutMode" id="settings-book-layout-mode">
|
||||
<option *ngFor="let opt of bookLayoutModes" [value]="opt.value">{{opt.text | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6 col-sm-12 pe-2 mb-3">
|
||||
<label for="settings-color-theme-option" class="form-label">Color Theme</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookColorThemeTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookColorThemeTooltip>What color theme to apply to the book reader content and menuing</ng-template>
|
||||
<span class="visually-hidden" id="settings-color-theme-option-help"><ng-container [ngTemplateOutlet]="bookColorThemeTooltip"></ng-container></span>
|
||||
<select class="form-select" aria-describedby="settings-color-theme-option-help" formControlName="bookReaderThemeName" id="settings-color-theme-option">
|
||||
<option *ngFor="let opt of bookColorThemes" [value]="opt.name">{{opt.name | titlecase}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<label for="fontsize" class="form-label range-label">Font Size</label>
|
||||
<input type="range" class="form-range" id="fontsize"
|
||||
min="50" max="300" step="10" formControlName="bookReaderFontSize">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderFontSize')?.value + '%'}}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<div class="range-label">
|
||||
<label class="form-label" for="linespacing">Line Height</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookLineHeightOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookLineHeightOptionTooltip>How much spacing between the lines of the book</ng-template>
|
||||
<span class="visually-hidden" id="settings-booklineheight-option-help">How much spacing between the lines of the book</span>
|
||||
</div>
|
||||
<input type="range" class="form-range" id="linespacing" min="100" max="200" step="10"
|
||||
formControlName="bookReaderLineSpacing" aria-describedby="settings-booklineheight-option-help">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderLineSpacing')?.value + '%'}}</span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-sm-12 pe-2 mb-3">
|
||||
<div class="range-label">
|
||||
<label class="form-label">Margin</label> <i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="bookReaderMarginOptionTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #bookReaderMarginOptionTooltip>How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</ng-template>
|
||||
<span class="visually-hidden" id="settings-bookmargin-option-help">How much spacing on each side of the screen. This will override to 0 on mobile devices regardless of this setting.</span>
|
||||
</div>
|
||||
|
||||
<input type="range" class="form-range" id="margin" min="0" max="30" step="5" formControlName="bookReaderMargin" aria-describedby="bookmargin">
|
||||
<span class="range-text">{{settingsForm.get('bookReaderMargin')?.value + '%'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto d-flex d-md-block justify-content-sm-center text-md-end mb-3">
|
||||
<button type="button" class="flex-fill btn btn-secondary me-2" (click)="resetForm()" aria-describedby="reading-panel">Reset</button>
|
||||
<button type="submit" class="flex-fill btn btn-primary" (click)="save()" aria-describedby="reading-panel" [disabled]="!settingsForm.touched && !settingsForm.dirty">Save</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-panel>
|
||||
</ngb-accordion>
|
||||
</form>
|
||||
</ng-container>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ToastrService } from 'ngx-toastr';
|
|||
import { map, shareReplay, take, takeUntil } from 'rxjs/operators';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { BookService } from 'src/app/book-reader/book.service';
|
||||
import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, bookLayoutModes, layoutModes } from 'src/app/_models/preferences/preferences';
|
||||
import { readingDirections, scalingOptions, pageSplitOptions, readingModes, Preferences, bookLayoutModes, layoutModes, pageLayoutModes } from 'src/app/_models/preferences/preferences';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
@ -15,7 +15,8 @@ import { forkJoin, Observable, of, Subject } from 'rxjs';
|
|||
|
||||
enum AccordionPanelID {
|
||||
ImageReader = 'image-reader',
|
||||
BookReader = 'book-reader'
|
||||
BookReader = 'book-reader',
|
||||
GlobalSettings = 'global-settings'
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
@ -32,6 +33,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
layoutModes = layoutModes;
|
||||
bookLayoutModes = bookLayoutModes;
|
||||
bookColorThemes = bookColorThemes;
|
||||
pageLayoutModes = pageLayoutModes;
|
||||
|
||||
settingsForm: FormGroup = new FormGroup({});
|
||||
passwordChangeForm: FormGroup = new FormGroup({});
|
||||
|
@ -120,6 +122,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
this.settingsForm.addControl('bookReaderImmersiveMode', new FormControl(this.user?.preferences.bookReaderImmersiveMode, []));
|
||||
|
||||
this.settingsForm.addControl('theme', new FormControl(this.user.preferences.theme, []));
|
||||
this.settingsForm.addControl('globalPageLayoutMode', new FormControl(this.user.preferences.globalPageLayoutMode, []));
|
||||
});
|
||||
|
||||
this.passwordChangeForm.addControl('password', new FormControl('', [Validators.required]));
|
||||
|
@ -165,6 +168,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
this.settingsForm.get('bookReaderThemeName')?.setValue(this.user.preferences.bookReaderThemeName);
|
||||
this.settingsForm.get('theme')?.setValue(this.user.preferences.theme);
|
||||
this.settingsForm.get('bookReaderImmersiveMode')?.setValue(this.user.preferences.bookReaderImmersiveMode);
|
||||
this.settingsForm.get('globalPageLayoutMode')?.setValue(this.user.preferences.globalPageLayoutMode);
|
||||
}
|
||||
|
||||
resetPasswordForm() {
|
||||
|
@ -184,7 +188,7 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
readerMode: parseInt(modelSettings.readerMode, 10),
|
||||
layoutMode: parseInt(modelSettings.layoutMode, 10),
|
||||
showScreenHints: modelSettings.showScreenHints,
|
||||
backgroundColor: modelSettings.backgroundColor, // this.user.preferences.backgroundColor,
|
||||
backgroundColor: modelSettings.backgroundColor,
|
||||
bookReaderFontFamily: modelSettings.bookReaderFontFamily,
|
||||
bookReaderLineSpacing: modelSettings.bookReaderLineSpacing,
|
||||
bookReaderFontSize: modelSettings.bookReaderFontSize,
|
||||
|
@ -194,7 +198,8 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||
bookReaderLayoutMode: parseInt(modelSettings.bookReaderLayoutMode, 10),
|
||||
bookReaderThemeName: modelSettings.bookReaderThemeName,
|
||||
theme: modelSettings.theme,
|
||||
bookReaderImmersiveMode: modelSettings.bookReaderImmersiveMode
|
||||
bookReaderImmersiveMode: modelSettings.bookReaderImmersiveMode,
|
||||
globalPageLayoutMode: parseInt(modelSettings.globalPageLayoutMode, 10),
|
||||
};
|
||||
|
||||
this.obserableHandles.push(this.accountService.updatePreferences(data).subscribe((updatedPrefs) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue