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,21 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from '../_guards/auth.guard';
import { DashboardComponent } from './dashboard.component';
const routes: Routes = [
{
path: '',
runGuardsAndResolvers: 'always',
canActivate: [AuthGuard],
component: DashboardComponent,
}
];
@NgModule({
imports: [RouterModule.forChild(routes), ],
exports: [RouterModule]
})
export class DashboardRoutingModule { }

View file

@ -1,3 +1,31 @@
<app-side-nav-companion-bar>
</app-side-nav-companion-bar>
<app-library></app-library>
<ng-container *ngIf="libraries.length === 0 && !isLoading">
<div class="mt-3">
<div *ngIf="isAdmin" class="d-flex justify-content-center">
<p>There are no libraries setup yet. Configure some in <a routerLink="/admin/dashboard" fragment="libraries">Server settings</a>.</p>
</div>
<div *ngIf="!isAdmin" class="d-flex justify-content-center">
<p>You haven't been granted access to any libraries.</p>
</div>
</div>
</ng-container>
<app-carousel-reel [items]="inProgress" title="On Deck" (sectionClick)="handleSectionClick($event)">
<ng-template #carouselItem let-item let-position="idx">
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (reload)="reloadInProgress($event)" (dataChanged)="reloadInProgress($event)"></app-series-card>
</ng-template>
</app-carousel-reel>
<app-carousel-reel [items]="recentlyUpdatedSeries" title="Recently Updated Series" (sectionClick)="handleSectionClick($event)">
<ng-template #carouselItem let-item let-position="idx">
<app-card-item [entity]="item" [title]="item.seriesName" [suppressLibraryLink]="libraryId !== 0" [imageUrl]="imageService.getSeriesCoverImage(item.seriesId)"
[supressArchiveWarning]="true" (clicked)="handleRecentlyAddedChapterClick(item)" [count]="item.count"></app-card-item>
</ng-template>
</app-carousel-reel>
<app-carousel-reel [items]="recentlyAddedSeries" title="Newly Added Series" (sectionClick)="handleSectionClick($event)">
<ng-template #carouselItem let-item let-position="idx">
<app-series-card [data]="item" [libraryId]="item.libraryId" [suppressLibraryLink]="libraryId !== 0" (dataChanged)="loadRecentlyAddedSeries()"></app-series-card>
</ng-template>
</app-carousel-reel>

View file

@ -1,20 +1,180 @@
import { Component, OnInit } from '@angular/core';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { Router } from '@angular/router';
import { ReplaySubject, Subject } from 'rxjs';
import { debounceTime, take, takeUntil } from 'rxjs/operators';
import { FilterQueryParam } from '../shared/_services/filter-utilities.service';
import { SeriesAddedEvent } from '../_models/events/series-added-event';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import { Library } from '../_models/library';
import { RecentlyAddedItem } from '../_models/recently-added-item';
import { Series } from '../_models/series';
import { SortField } from '../_models/series-filter';
import { SeriesGroup } from '../_models/series-group';
import { User } from '../_models/user';
import { AccountService } from '../_services/account.service';
import { ImageService } from '../_services/image.service';
import { LibraryService } from '../_services/library.service';
import { MessageHubService, EVENTS } from '../_services/message-hub.service';
import { SeriesService } from '../_services/series.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit {
export class DashboardComponent implements OnInit, OnDestroy {
/**
* By default, 0, but if non-zero, will limit all API calls to library id
*/
@Input() libraryId: number = 0;
constructor(public route: ActivatedRoute, private titleService: Title) {
this.titleService.setTitle('Kavita - Dashboard');
user: User | undefined;
libraries: Library[] = [];
isLoading = false;
isAdmin = false;
recentlyUpdatedSeries: SeriesGroup[] = [];
inProgress: Series[] = [];
recentlyAddedSeries: Series[] = [];
private readonly onDestroy = new Subject<void>();
/**
* We use this Replay subject to slow the amount of times we reload the UI
*/
private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>();
constructor(public accountService: AccountService, private libraryService: LibraryService,
private seriesService: SeriesService, private router: Router,
private titleService: Title, public imageService: ImageService,
private messageHub: MessageHubService) {
this.messageHub.messages$.pipe(takeUntil(this.onDestroy)).subscribe(res => {
if (res.event === EVENTS.SeriesAdded) {
const seriesAddedEvent = res.payload as SeriesAddedEvent;
this.seriesService.getSeries(seriesAddedEvent.seriesId).subscribe(series => {
this.recentlyAddedSeries.unshift(series);
});
} else if (res.event === EVENTS.SeriesRemoved) {
const seriesRemovedEvent = res.payload as SeriesRemovedEvent;
this.inProgress = this.inProgress.filter(item => item.id != seriesRemovedEvent.seriesId);
this.recentlyAddedSeries = this.recentlyAddedSeries.filter(item => item.id != seriesRemovedEvent.seriesId);
this.recentlyUpdatedSeries = this.recentlyUpdatedSeries.filter(item => item.seriesId != seriesRemovedEvent.seriesId);
} else if (res.event === EVENTS.ScanSeries) {
// We don't have events for when series are updated, but we do get events when a scan update occurs. Refresh recentlyAdded at that time.
this.loadRecentlyAdded$.next();
}
});
this.loadRecentlyAdded$.pipe(debounceTime(1000), takeUntil(this.onDestroy)).subscribe(() => this.loadRecentlyAdded());
}
ngOnInit(): void {
this.titleService.setTitle('Kavita - Dashboard');
this.isLoading = true;
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
this.user = user;
if (this.user) {
this.isAdmin = this.accountService.hasAdminRole(this.user);
this.libraryService.getLibrariesForMember().pipe(take(1)).subscribe(libraries => {
this.libraries = libraries;
this.isLoading = false;
});
}
});
this.reloadSeries();
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
reloadSeries() {
this.loadOnDeck();
this.loadRecentlyAdded();
this.loadRecentlyAddedSeries();
}
reloadInProgress(series: Series | boolean) {
if (series === true || series === false) {
if (!series) {return;}
}
// If the update to Series doesn't affect the requirement to be in this stream, then ignore update request
const seriesObj = (series as Series);
if (seriesObj.pagesRead !== seriesObj.pages && seriesObj.pagesRead !== 0) {
return;
}
this.loadOnDeck();
}
loadOnDeck() {
let api = this.seriesService.getOnDeck();
if (this.libraryId > 0) {
api = this.seriesService.getOnDeck(this.libraryId);
}
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
this.inProgress = updatedSeries.result;
});
}
loadRecentlyAddedSeries() {
let api = this.seriesService.getRecentlyAdded();
if (this.libraryId > 0) {
api = this.seriesService.getRecentlyAdded(this.libraryId);
}
api.pipe(takeUntil(this.onDestroy)).subscribe((updatedSeries) => {
this.recentlyAddedSeries = updatedSeries.result;
});
}
loadRecentlyAdded() {
let api = this.seriesService.getRecentlyUpdatedSeries();
if (this.libraryId > 0) {
api = this.seriesService.getRecentlyUpdatedSeries();
}
api.pipe(takeUntil(this.onDestroy)).subscribe(updatedSeries => {
this.recentlyUpdatedSeries = updatedSeries.filter(group => {
if (this.libraryId === 0) return true;
return group.libraryId === this.libraryId;
});
});
}
handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
this.router.navigate(['library', item.libraryId, 'series', item.seriesId]);
}
handleSectionClick(sectionTitle: string) {
if (sectionTitle.toLowerCase() === 'recently updated series') {
const params: any = {};
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
} else if (sectionTitle.toLowerCase() === 'on deck') {
const params: any = {};
params[FilterQueryParam.ReadStatus] = 'true,false,false';
params[FilterQueryParam.SortBy] = SortField.LastChapterAdded + ',false'; // sort by last chapter added, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
}else if (sectionTitle.toLowerCase() === 'newly added series') {
const params: any = {};
params[FilterQueryParam.SortBy] = SortField.Created + ',false'; // sort by created, desc
params[FilterQueryParam.Page] = 1;
this.router.navigate(['all-series'], {queryParams: params});
}
}
removeFromArray(arr: Array<any>, element: any) {
const index = arr.indexOf(element);
if (index >= 0) {
arr.splice(index);
}
}
}

View file

@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashboardRoutingModule } from './dashboard-routing.module';
import { CarouselModule } from '../carousel/carousel.module';
import { DashboardComponent } from './dashboard.component';
import { SharedSideNavCardsModule } from '../shared-side-nav-cards/shared-side-nav-cards.module';
@NgModule({
declarations: [DashboardComponent],
imports: [
CommonModule,
CarouselModule,
SharedSideNavCardsModule,
DashboardRoutingModule
]
})
export class DashboardModule { }