Library Recomendations (#1236)

* Updated cover regex for finding cover images in archives to ignore back_cover or back-cover

* Fixed an issue where Tags wouldn't save due to not pulling them from the DB.

* Refactored All series to it's own lazy loaded module

* Modularized Dashboard and library detail. Had to change main dashboard page to be libraries. Subject to change.

* Refactored login component into registration module

* Series Detail module created

* Refactored nav stuff into it's own module, not lazy loaded, but self contained.

* Refactored theme component into a dev only module so we don't incur load for temp testing modules

* Finished off modularization code. Only missing thing is to re-introduce some dashboard functionality for library view.

* Implemented a basic recommendation page for library detail
This commit is contained in:
Joseph Milazzo 2022-04-29 17:27:01 -05:00 committed by GitHub
parent 743a3ba935
commit f237aa7ab7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 1077 additions and 501 deletions

View file

@ -0,0 +1,32 @@
<div *ngIf="series !== undefined">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">
{{series.name}} Review</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="close()">
</button>
</div>
<div class="modal-body">
<form [formGroup]="reviewGroup">
<div class="row g-0">
<label for="rating" class="form-label">Rating</label>
<div>
<ngb-rating style="margin-top: 2px; font-size: 1.5rem;" formControlName="rating"></ngb-rating>
<button class="btn btn-icon ms-2" (click)="clearRating()" title="clear"><i aria-hidden="true" class="fa fa-ban"></i></button>
</div>
</div>
<div class="row g-0">
<label for="review" class="form-label">Review</label>
<textarea id="review" class="form-control" formControlName="review" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" (click)="close()">Close</button>
<button type="submit" class="btn btn-primary" (click)="save()">Save</button>
</div>
</div>

View file

@ -0,0 +1,41 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Series } from 'src/app/_models/series';
import { SeriesService } from 'src/app/_services/series.service';
@Component({
selector: 'app-review-series-modal',
templateUrl: './review-series-modal.component.html',
styleUrls: ['./review-series-modal.component.scss']
})
export class ReviewSeriesModalComponent implements OnInit {
@Input() series!: Series;
reviewGroup!: FormGroup;
constructor(public modal: NgbActiveModal, private seriesService: SeriesService) {}
ngOnInit(): void {
this.reviewGroup = new FormGroup({
review: new FormControl(this.series.userReview, []),
rating: new FormControl(this.series.userRating, [])
});
}
close() {
this.modal.close({success: false, review: null});
}
clearRating() {
this.reviewGroup.get('rating')?.setValue(0);
}
save() {
const model = this.reviewGroup.value;
this.seriesService.updateRating(this.series?.id, model.rating, model.review).subscribe(() => {
this.modal.close({success: true, review: model.review, rating: model.rating});
});
}
}

View file

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { SeriesDetailComponent } from './series-detail.component';
const routes: Routes = [
{
path: '',
component: SeriesDetailComponent
}
];
@NgModule({
imports: [RouterModule.forChild(routes), ],
exports: [RouterModule]
})
export class SeriesDetailRoutingModule { }

View file

@ -13,7 +13,7 @@ import { ConfirmService } from '../shared/confirm.service';
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
import { DownloadService } from '../shared/_services/download.service';
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component';
import { ReviewSeriesModalComponent } from './review-series-modal/review-series-modal.component';
import { Chapter } from '../_models/chapter';
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
@ -33,8 +33,8 @@ import { ReaderService } from '../_services/reader.service';
import { ReadingListService } from '../_services/reading-list.service';
import { SeriesService } from '../_services/series.service';
import { NavService } from '../_services/nav.service';
import { RelationKind } from '../_models/series-detail/relation-kind';
import { RelatedSeries } from '../_models/series-detail/related-series';
import { RelationKind } from '../_models/series-detail/relation-kind';
interface RelatedSeris {
series: Series;

View file

@ -0,0 +1,38 @@
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 { 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';
import { SharedModule } from '../shared/shared.module';
import { TypeaheadModule } from '../typeahead/typeahead.module';
import { PipeModule } from '../pipe/pipe.module';
import { ReactiveFormsModule } from '@angular/forms';
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
@NgModule({
declarations: [
SeriesDetailComponent,
ReviewSeriesModalComponent,
SeriesMetadataDetailComponent
],
imports: [
CommonModule,
ReactiveFormsModule, // Review Series Modal
NgbCollapseModule, // Series Metadata
NgbNavModule,
NgbRatingModule,
TypeaheadModule,
PipeModule,
SharedModule, // person badge, badge expander (these 2 can be their own module)
SharedSideNavCardsModule,
SeriesDetailRoutingModule
]
})
export class SeriesDetailModule { }

View file

@ -0,0 +1,216 @@
<div class="row g-0 mt-2 mb-2">
<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-2">
<app-tag-badge title="Age Rating" *ngIf="seriesMetadata.ageRating" a11y-click="13,32" class="clickable col-auto" (click)="goTo(FilterQueryParam.AgeRating, seriesMetadata.ageRating)" [selectionMode]="TagBadgeCursor.Clickable">{{metadataService.getAgeRating(this.seriesMetadata.ageRating) | async}}</app-tag-badge>
<ng-container *ngIf="series">
<app-tag-badge *ngIf="seriesMetadata.releaseYear > 0" title="Release date" class="col-auto">{{seriesMetadata.releaseYear}}</app-tag-badge>
<app-tag-badge *ngIf="seriesMetadata.language !== null && seriesMetadata.language !== ''" title="Language" a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Languages, seriesMetadata.language)" [selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.language}}</app-tag-badge>
<app-tag-badge title="Publication Status ({{seriesMetadata.maxCount}} / {{seriesMetadata.totalCount}})" [fillStyle]="seriesMetadata.maxCount != 0 && seriesMetadata.totalCount != 0 && seriesMetadata.maxCount >= seriesMetadata.totalCount ? 'filled' : 'outline'" a11y-click="13,32" class="col-auto"
(click)="goTo(FilterQueryParam.PublicationStatus, seriesMetadata.publicationStatus)"
[selectionMode]="TagBadgeCursor.Clickable">{{seriesMetadata.publicationStatus | publicationStatus}}</app-tag-badge>
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Format, series.format)" [selectionMode]="TagBadgeCursor.Clickable">
<app-series-format [format]="series.format">{{utilityService.mangaFormat(series.format)}}</app-series-format>
</app-tag-badge>
<app-tag-badge title="Last Read" class="col-auto" *ngIf="series.latestReadDate && series.latestReadDate !== '' && (series.latestReadDate | date: 'shortDate') !== '1/1/01'" [selectionMode]="TagBadgeCursor.Selectable">
Last Read: {{series.latestReadDate | date:'shortDate'}}
</app-tag-badge>
</ng-container>
</div>
<div class="row g-0" *ngIf="seriesMetadata.genres && seriesMetadata.genres.length > 0">
<div class="col-md-4">
<h5>Genres</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.genres">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Genres, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.collectionTags && seriesMetadata.collectionTags.length > 0">
<div class="col-md-4">
<h5>Collections</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.collectionTags">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" routerLink="/collections/{{item.id}}" [selectionMode]="TagBadgeCursor.Clickable">
{{item.title}}
</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="readingLists && readingLists.length > 0">
<div class="col-md-4">
<h5>Reading Lists</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="readingLists">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" routerLink="/lists/{{item.id}}" [selectionMode]="TagBadgeCursor.Clickable">
<!-- TODO: Build a promoted badge code -->
<span *ngIf="item.promoted">
<i class="fa fa-angle-double-up" aria-hidden="true"></i>&nbsp;
<span class="visually-hidden">(promoted)</span>
</span>
{{item.title}}
</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.writers && seriesMetadata.writers.length > 0">
<div class="col-md-4">
<h5>Writers/Authors</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.writers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Writers, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0">
<hr class="col mt-3" *ngIf="hasExtendedProperites" >
<a [class.hidden]="hasExtendedProperites" *ngIf="hasExtendedProperites" class="col col-md-auto align-self-end read-more-link" (click)="toggleView()">&nbsp;<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}}" aria-controls="extended-series-metadata"></i>&nbsp;See {{isCollapsed ? 'More' : 'Less'}}</a>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
<div class="row g-0 mt-1" *ngIf="seriesMetadata.coverArtists && seriesMetadata.coverArtists.length > 0">
<div class="col-md-4">
<h5>Cover Artists</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.coverArtists">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.CoverArtists, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.characters && seriesMetadata.characters.length > 0">
<div class="col-md-4">
<h5>Characters</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.characters">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Character, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.colorists && seriesMetadata.colorists.length > 0">
<div class="col-md-4">
<h5>Colorists</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.colorists">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Colorist, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.editors && seriesMetadata.editors.length > 0">
<div class="col-md-4">
<h5>Editors</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.editors">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Editor, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.inkers && seriesMetadata.inkers.length > 0">
<div class="col-md-4">
<h5>Inkers</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.inkers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Inker, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.letterers && seriesMetadata.letterers.length > 0">
<div class="col-md-4">
<h5>Letterers</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.letterers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Letterer, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
<div class="col-md-4">
<h5>Tags</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.tags">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-tag-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Tags, item.id)" [selectionMode]="TagBadgeCursor.Clickable">{{item.title}}</app-tag-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.translators && seriesMetadata.translators.length > 0">
<div class="col-md-4">
<h5>Translators</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.translators">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Translator, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.pencillers && seriesMetadata.pencillers.length > 0">
<div class="col-md-4">
<h5>Pencillers</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.pencillers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Penciller, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
<div class="row g-0 mt-1" *ngIf="seriesMetadata.publishers && seriesMetadata.publishers.length > 0">
<div class="col-md-4">
<h5>Publishers</h5>
</div>
<div class="col-md-8">
<app-badge-expander [items]="seriesMetadata.publishers">
<ng-template #badgeExpanderItem let-item let-position="idx">
<app-person-badge a11y-click="13,32" class="col-auto" (click)="goTo(FilterQueryParam.Publisher, item.id)" [person]="item"></app-person-badge>
</ng-template>
</app-badge-expander>
</div>
</div>
</div>

View file

@ -0,0 +1,79 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { TagBadgeCursor } from '../../shared/tag-badge/tag-badge.component';
import { FilterQueryParam } from '../../shared/_services/filter-utilities.service';
import { UtilityService } from '../../shared/_services/utility.service';
import { MangaFormat } from '../../_models/manga-format';
import { ReadingList } from '../../_models/reading-list';
import { Series } from '../../_models/series';
import { SeriesMetadata } from '../../_models/series-metadata';
import { MetadataService } from '../../_services/metadata.service';
@Component({
selector: 'app-series-metadata-detail',
templateUrl: './series-metadata-detail.component.html',
styleUrls: ['./series-metadata-detail.component.scss']
})
export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
@Input() seriesMetadata!: SeriesMetadata;
/**
* Reading lists with a connection to the Series
*/
@Input() readingLists: Array<ReadingList> = [];
@Input() series!: Series;
isCollapsed: boolean = true;
hasExtendedProperites: boolean = false;
/**
* Html representation of Series Summary
*/
seriesSummary: string = '';
get MangaFormat(): typeof MangaFormat {
return MangaFormat;
}
get TagBadgeCursor(): typeof TagBadgeCursor {
return TagBadgeCursor;
}
get FilterQueryParam() {
return FilterQueryParam;
}
constructor(public utilityService: UtilityService, public metadataService: MetadataService, private router: Router) { }
ngOnChanges(changes: SimpleChanges): void {
this.hasExtendedProperites = this.seriesMetadata.colorists.length > 0 ||
this.seriesMetadata.editors.length > 0 ||
this.seriesMetadata.coverArtists.length > 0 ||
this.seriesMetadata.inkers.length > 0 ||
this.seriesMetadata.letterers.length > 0 ||
this.seriesMetadata.pencillers.length > 0 ||
this.seriesMetadata.publishers.length > 0 ||
this.seriesMetadata.translators.length > 0 ||
this.seriesMetadata.tags.length > 0;
if (this.seriesMetadata !== null) {
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
}
}
ngOnInit(): void {
}
toggleView() {
this.isCollapsed = !this.isCollapsed;
}
goTo(queryParamName: FilterQueryParam, filter: any) {
let params: any = {};
params[queryParamName] = filter;
params[FilterQueryParam.Page] = 1;
this.router.navigate(['library', this.series.libraryId], {queryParams: params});
}
}