mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-31 04:04:19 -04:00
254 lines
11 KiB
TypeScript
254 lines
11 KiB
TypeScript
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core';
|
|
import {Title} from '@angular/platform-browser';
|
|
import {Router, RouterLink} from '@angular/router';
|
|
import {Observable, of, ReplaySubject, Subject, switchMap} from 'rxjs';
|
|
import {debounceTime, map, shareReplay, take, tap, throttleTime} from 'rxjs/operators';
|
|
import {FilterUtilitiesService} from 'src/app/shared/_services/filter-utilities.service';
|
|
import {Library} from 'src/app/_models/library/library';
|
|
import {RecentlyAddedItem} from 'src/app/_models/recently-added-item';
|
|
import {SortField} from 'src/app/_models/metadata/series-filter';
|
|
import {AccountService} from 'src/app/_services/account.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 {SeriesService} from 'src/app/_services/series.service';
|
|
import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
|
|
import {CardItemComponent} from '../../cards/card-item/card-item.component';
|
|
import {SeriesCardComponent} from '../../cards/series-card/series-card.component';
|
|
import {CarouselReelComponent} from '../../carousel/_components/carousel-reel/carousel-reel.component';
|
|
import {AsyncPipe, NgForOf, NgTemplateOutlet} from '@angular/common';
|
|
import {
|
|
SideNavCompanionBarComponent
|
|
} from '../../sidenav/_components/side-nav-companion-bar/side-nav-companion-bar.component';
|
|
import {translate, TranslocoDirective} from "@ngneat/transloco";
|
|
import {FilterField} from "../../_models/metadata/v2/filter-field";
|
|
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
|
|
import {DashboardService} from "../../_services/dashboard.service";
|
|
import {MetadataService} from "../../_services/metadata.service";
|
|
import {RecommendationService} from "../../_services/recommendation.service";
|
|
import {Genre} from "../../_models/metadata/genre";
|
|
import {DashboardStream} from "../../_models/dashboard/dashboard-stream";
|
|
import {StreamType} from "../../_models/dashboard/stream-type.enum";
|
|
import {LoadingComponent} from "../../shared/loading/loading.component";
|
|
import {ScrobbleProvider, ScrobblingService} from "../../_services/scrobbling.service";
|
|
import {ToastrService} from "ngx-toastr";
|
|
|
|
|
|
enum StreamId {
|
|
OnDeck,
|
|
RecentlyUpdatedSeries,
|
|
NewlyAddedSeries,
|
|
MoreInGenre,
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-dashboard',
|
|
templateUrl: './dashboard.component.html',
|
|
styleUrls: ['./dashboard.component.scss'],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
standalone: true,
|
|
imports: [SideNavCompanionBarComponent, RouterLink, CarouselReelComponent, SeriesCardComponent,
|
|
CardItemComponent, AsyncPipe, TranslocoDirective, NgForOf, NgTemplateOutlet, LoadingComponent],
|
|
})
|
|
export class DashboardComponent implements OnInit {
|
|
|
|
private readonly destroyRef = inject(DestroyRef);
|
|
private readonly filterUtilityService = inject(FilterUtilitiesService);
|
|
private readonly metadataService = inject(MetadataService);
|
|
private readonly recommendationService = inject(RecommendationService);
|
|
public readonly accountService = inject(AccountService);
|
|
private readonly libraryService = inject(LibraryService);
|
|
private readonly seriesService = inject(SeriesService);
|
|
private readonly router = inject(Router);
|
|
private readonly titleService = inject(Title);
|
|
public readonly imageService = inject(ImageService);
|
|
private readonly messageHub = inject(MessageHubService);
|
|
private readonly cdRef = inject(ChangeDetectorRef);
|
|
private readonly dashboardService = inject(DashboardService);
|
|
private readonly scrobblingService = inject(ScrobblingService);
|
|
private readonly toastr = inject(ToastrService);
|
|
|
|
libraries$: Observable<Library[]> = this.libraryService.getLibraries().pipe(take(1), takeUntilDestroyed(this.destroyRef))
|
|
isLoadingDashboard = true;
|
|
isAdmin$: Observable<boolean> = of(false);
|
|
|
|
streams: Array<DashboardStream> = [];
|
|
genre: Genre | undefined;
|
|
refreshStreams$ = new Subject<void>();
|
|
refreshStreamsFromDashboardUpdate$ = new Subject<void>();
|
|
|
|
streamCount: number = 0;
|
|
streamsLoaded: number = 0;
|
|
|
|
/**
|
|
* We use this Replay subject to slow the amount of times we reload the UI
|
|
*/
|
|
private loadRecentlyAdded$: ReplaySubject<void> = new ReplaySubject<void>();
|
|
protected readonly StreamType = StreamType;
|
|
protected readonly StreamId = StreamId;
|
|
|
|
constructor() {
|
|
this.loadDashboard();
|
|
|
|
this.refreshStreamsFromDashboardUpdate$.pipe(takeUntilDestroyed(this.destroyRef), debounceTime(1000),
|
|
tap(() => {
|
|
this.loadDashboard();
|
|
}))
|
|
.subscribe();
|
|
|
|
this.refreshStreams$.pipe(takeUntilDestroyed(this.destroyRef), throttleTime(10_000),
|
|
tap(() => {
|
|
this.loadDashboard()
|
|
}))
|
|
.subscribe();
|
|
|
|
|
|
this.messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(res => {
|
|
// TODO: Make the event have a stream Id so I can refresh just that stream
|
|
if (res.event === EVENTS.DashboardUpdate) {
|
|
this.refreshStreamsFromDashboardUpdate$.next();
|
|
} else if (res.event === EVENTS.SeriesAdded) {
|
|
this.refreshStreams$.next();
|
|
} else if (res.event === EVENTS.SeriesRemoved) {
|
|
this.refreshStreams$.next();
|
|
} 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.refreshStreams$.next();
|
|
}
|
|
});
|
|
|
|
this.scrobblingService.hasTokenExpired(ScrobbleProvider.AniList).subscribe(hasExpired => {
|
|
if (hasExpired) {
|
|
this.toastr.error(translate('toasts.anilist-token-expired'));
|
|
}
|
|
this.cdRef.markForCheck();
|
|
});
|
|
|
|
|
|
this.isAdmin$ = this.accountService.currentUser$.pipe(
|
|
takeUntilDestroyed(this.destroyRef),
|
|
map(user => (user && this.accountService.hasAdminRole(user)) || false),
|
|
shareReplay({bufferSize: 1, refCount: true})
|
|
);
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
this.titleService.setTitle('Kavita');
|
|
}
|
|
|
|
|
|
loadDashboard() {
|
|
this.isLoadingDashboard = true;
|
|
this.streamsLoaded = 0;
|
|
this.streamCount = 0;
|
|
this.cdRef.markForCheck();
|
|
this.dashboardService.getDashboardStreams().subscribe(streams => {
|
|
this.streams = streams;
|
|
this.streamCount = streams.length;
|
|
this.streams.forEach(s => {
|
|
switch (s.streamType) {
|
|
case StreamType.OnDeck:
|
|
s.api = this.seriesService.getOnDeck(0, 1, 20)
|
|
.pipe(map(d => d.result), tap(() => this.increment()), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
|
|
break;
|
|
case StreamType.NewlyAdded:
|
|
s.api = this.seriesService.getRecentlyAdded(1, 20)
|
|
.pipe(map(d => d.result), tap(() => this.increment()), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
|
|
break;
|
|
case StreamType.RecentlyUpdated:
|
|
s.api = this.seriesService.getRecentlyUpdatedSeries().pipe(tap(() => this.increment()), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
|
|
break;
|
|
case StreamType.SmartFilter:
|
|
s.api = this.filterUtilityService.decodeFilter(s.smartFilterEncoded!).pipe(
|
|
switchMap(filter => {
|
|
return this.seriesService.getAllSeriesV2(0, 20, filter);
|
|
}))
|
|
.pipe(map(d => d.result),tap(() => this.increment()), takeUntilDestroyed(this.destroyRef), shareReplay({bufferSize: 1, refCount: true}));
|
|
break;
|
|
case StreamType.MoreInGenre:
|
|
s.api = this.metadataService.getAllGenres().pipe(
|
|
map(genres => {
|
|
this.genre = genres[Math.floor(Math.random() * genres.length)];
|
|
return this.genre;
|
|
}),
|
|
switchMap(genre => this.recommendationService.getMoreIn(0, genre.id, 0, 30)),
|
|
map(p => p.result),
|
|
tap(() => this.increment()),
|
|
takeUntilDestroyed(this.destroyRef),
|
|
shareReplay({bufferSize: 1, refCount: true})
|
|
);
|
|
break;
|
|
}
|
|
});
|
|
this.isLoadingDashboard = false;
|
|
this.cdRef.markForCheck();
|
|
});
|
|
}
|
|
|
|
increment() {
|
|
this.streamsLoaded++;
|
|
this.cdRef.markForCheck();
|
|
}
|
|
|
|
reloadStream(streamId: number) {
|
|
const index = this.streams.findIndex(s => s.id === streamId);
|
|
if (index < 0) return;
|
|
this.streams[index] = {...this.streams[index]};
|
|
console.log('swapped out stream: ', this.streams[index]);
|
|
this.cdRef.detectChanges();
|
|
}
|
|
|
|
async handleRecentlyAddedChapterClick(item: RecentlyAddedItem) {
|
|
await this.router.navigate(['library', item.libraryId, 'series', item.seriesId]);
|
|
}
|
|
|
|
async handleFilterSectionClick(stream: DashboardStream) {
|
|
await this.router.navigateByUrl('all-series?' + stream.smartFilterEncoded);
|
|
}
|
|
|
|
handleSectionClick(streamId: StreamId) {
|
|
if (streamId === StreamId.RecentlyUpdatedSeries) {
|
|
const params: any = {};
|
|
params['page'] = 1;
|
|
params['title'] = translate('dashboard.recently-updated-title');
|
|
const filter = this.filterUtilityService.createSeriesV2Filter();
|
|
if (filter.sortOptions) {
|
|
filter.sortOptions.sortField = SortField.LastChapterAdded;
|
|
filter.sortOptions.isAscending = false;
|
|
}
|
|
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
|
|
} else if (streamId === StreamId.OnDeck) {
|
|
const params: any = {};
|
|
params['page'] = 1;
|
|
params['title'] = translate('dashboard.on-deck-title');
|
|
|
|
const filter = this.filterUtilityService.createSeriesV2Filter();
|
|
filter.statements.push({field: FilterField.ReadProgress, comparison: FilterComparison.GreaterThan, value: '0'});
|
|
filter.statements.push({field: FilterField.ReadProgress, comparison: FilterComparison.LessThan, value: '100'});
|
|
if (filter.sortOptions) {
|
|
filter.sortOptions.sortField = SortField.LastChapterAdded;
|
|
filter.sortOptions.isAscending = false;
|
|
}
|
|
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
|
|
} else if (streamId === StreamId.NewlyAddedSeries) {
|
|
const params: any = {};
|
|
params['page'] = 1;
|
|
params['title'] = translate('dashboard.recently-added-title');
|
|
const filter = this.filterUtilityService.createSeriesV2Filter();
|
|
if (filter.sortOptions) {
|
|
filter.sortOptions.sortField = SortField.Created;
|
|
filter.sortOptions.isAscending = false;
|
|
}
|
|
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
|
|
} else if (streamId === StreamId.MoreInGenre) {
|
|
const params: any = {};
|
|
params['page'] = 1;
|
|
params['title'] = translate('dashboard.more-in-genre-title', {genre: this.genre?.title});
|
|
const filter = this.filterUtilityService.createSeriesV2Filter();
|
|
filter.statements.push({field: FilterField.Genres, value: this.genre?.id + '', comparison: FilterComparison.MustContains});
|
|
this.filterUtilityService.applyFilterWithParams(['all-series'], filter, params).subscribe();
|
|
}
|
|
}
|
|
}
|