diff --git a/src/Ombi/ClientApp/src/app/components/detailed-card/detailed-card.component.ts b/src/Ombi/ClientApp/src/app/components/detailed-card/detailed-card.component.ts index 49f7603a8..0892bc9b2 100644 --- a/src/Ombi/ClientApp/src/app/components/detailed-card/detailed-card.component.ts +++ b/src/Ombi/ClientApp/src/app/components/detailed-card/detailed-card.component.ts @@ -1,92 +1,154 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from "@angular/core"; import { IRecentlyRequested, RequestType } from "../../interfaces"; import { ImageService } from "app/services"; import { Subject, takeUntil } from "rxjs"; import { DomSanitizer, SafeStyle } from "@angular/platform-browser"; @Component({ - standalone: false, - selector: 'ombi-detailed-card', - templateUrl: './detailed-card.component.html', - styleUrls: ['./detailed-card.component.scss'] - }) - export class DetailedCardComponent implements OnInit, OnDestroy { - @Input() public request: IRecentlyRequested; - @Input() public isAdmin: boolean = false; - @Output() public onClick: EventEmitter = new EventEmitter(); - @Output() public onApprove: EventEmitter = new EventEmitter(); - - public RequestType = RequestType; - public loading: false; - - private $imageSub = new Subject(); - - public background: SafeStyle; - - constructor(private imageService: ImageService, private sanitizer: DomSanitizer) { } - - ngOnInit(): void { - if (!this.request.posterPath) { - this.loadImages(); - } else { - this.request.posterPath = `https://image.tmdb.org/t/p/w300${this.request.posterPath}`; - this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(https://image.tmdb.org/t/p/w300" + this.request.background + ")"); - } - } - - public getStatus(request: IRecentlyRequested) { - if (request.available) { - return "Common.Available"; - } - if (request.tvPartiallyAvailable) { - return "Common.PartiallyAvailable"; - } - if (request.approved) { - return "Common.Approved"; - } else { - return "Common.Pending"; - } - } - - public click() { - this.onClick.emit(); - } - - public approve() { - this.onApprove.emit(); - } - - public getClass(request: IRecentlyRequested) { - if (request.available || request.tvPartiallyAvailable) { - return "success"; - } - if (request.approved) { - return "primary"; - } else { - return "info"; - } - } - - public ngOnDestroy() { - this.$imageSub.next(); - this.$imageSub.complete(); + standalone: false, + selector: "ombi-detailed-card", + templateUrl: "./detailed-card.component.html", + styleUrls: ["./detailed-card.component.scss"], +}) +export class DetailedCardComponent implements OnInit, OnDestroy { + @Input() public request: IRecentlyRequested; + @Input() public isAdmin: boolean = false; + @Output() public onClick: EventEmitter = new EventEmitter(); + @Output() public onApprove: EventEmitter = new EventEmitter(); + + public RequestType = RequestType; + public loading: false; + + private $imageSub = new Subject(); + + public background: SafeStyle; + + constructor( + private imageService: ImageService, + private sanitizer: DomSanitizer + ) {} + + ngOnInit(): void { + this.loadPosterPath(); + this.loadBackgroundPath(); + } + + public getStatus(request: IRecentlyRequested) { + if (request.available) { + return "Common.Available"; + } + if (request.tvPartiallyAvailable) { + return "Common.PartiallyAvailable"; + } + if (request.approved) { + return "Common.Approved"; + } else { + return "Common.Pending"; + } + } + + public click() { + this.onClick.emit(); + } + + public approve() { + this.onApprove.emit(); + } + + public getClass(request: IRecentlyRequested) { + if (request.available || request.tvPartiallyAvailable) { + return "success"; } + if (request.approved) { + return "primary"; + } else { + return "info"; + } + } + + public ngOnDestroy() { + this.$imageSub.next(); + this.$imageSub.complete(); + } + + private loadPosterPath() { + if (this.request.posterPath) { + this.setPosterPath(this.request.posterPath); + return; + } + + switch (this.request.type) { + case RequestType.movie: + this.imageService + .getMoviePoster(this.request.mediaId) + .pipe(takeUntil(this.$imageSub)) + .subscribe((x) => this.setPosterPath(x)); + break; + case RequestType.tvShow: + this.imageService + .getTmdbTvPoster(Number(this.request.mediaId)) + .pipe(takeUntil(this.$imageSub)) + .subscribe((x) => this.setPosterPath(x)); + break; + } + } - private loadImages() { - switch (this.request.type) { - case RequestType.movie: - this.imageService.getMoviePoster(this.request.mediaId).pipe(takeUntil(this.$imageSub)).subscribe(x => this.request.posterPath = x); - this.imageService.getMovieBackground(this.request.mediaId).pipe(takeUntil(this.$imageSub)).subscribe(x => { - this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(" + x + ")"); - }); - break; - case RequestType.tvShow: - this.imageService.getTmdbTvPoster(Number(this.request.mediaId)).pipe(takeUntil(this.$imageSub)).subscribe(x => this.request.posterPath = `https://image.tmdb.org/t/p/w300${x}`); - this.imageService.getTmdbTvBackground(Number(this.request.mediaId)).pipe(takeUntil(this.$imageSub)).subscribe(x => { - this.background = this.sanitizer.bypassSecurityTrustStyle("linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(https://image.tmdb.org/t/p/w300" + x + ")"); - }); - break; - } + private setPosterPath(posterPath: string) { + if (!posterPath) { + this.request.posterPath = null; + } else { + this.request.posterPath = this.getImageUrl(posterPath); } + } - } \ No newline at end of file + private loadBackgroundPath() { + if (this.request.background) { + this.setBackgroundStyle(this.request.background); + return; + } + + // Set background style while image path is loading. + this.setBackgroundStyle(null); + switch (this.request.type) { + case RequestType.movie: + this.imageService + .getMovieBackground(this.request.mediaId) + .pipe(takeUntil(this.$imageSub)) + .subscribe((x) => this.setBackgroundStyle(x)); + break; + case RequestType.tvShow: + this.imageService + .getTmdbTvBackground(Number(this.request.mediaId)) + .pipe(takeUntil(this.$imageSub)) + .subscribe((x) => this.setBackgroundStyle(x)); + break; + } + } + + private setBackgroundStyle(backgroundPath: string) { + if (backgroundPath) { + this.background = this.sanitizer.bypassSecurityTrustStyle( + `linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5)), url(${this.getImageUrl( + backgroundPath + )})` + ); + } else { + this.background = "linear-gradient(rgba(0,0,0,.5), rgba(0,0,0,.5))"; + } + } + + private getImageUrl(path: string) { + if (new RegExp("^(http|https)://").test(path)) { + return path; + } else { + return `https://image.tmdb.org/t/p/w300${path}`; + } + } +} diff --git a/src/Ombi/ClientApp/src/app/components/image/image.component.ts b/src/Ombi/ClientApp/src/app/components/image/image.component.ts index 1257d150b..b712e9064 100644 --- a/src/Ombi/ClientApp/src/app/components/image/image.component.ts +++ b/src/Ombi/ClientApp/src/app/components/image/image.component.ts @@ -1,63 +1,77 @@ import { OmbiCommonModules } from "../modules"; -import { ChangeDetectionStrategy, Component, ElementRef, Inject, Input, ViewEncapsulation } from "@angular/core"; +import { + ChangeDetectionStrategy, + Component, + Inject, + Input, + ViewEncapsulation, +} from "@angular/core"; import { RequestType } from "../../interfaces"; import { APP_BASE_HREF } from "@angular/common"; @Component({ - standalone: true, - selector: 'ombi-image', - imports: [...OmbiCommonModules], - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './image.component.html', - }) - export class ImageComponent { - - @Input() public src: string; - @Input() public type: RequestType; - - // Attributes from the parent - @Input() public class: string; - @Input() public id: string; - @Input() public alt: string; - @Input() public style: string; - - public baseUrl: string = ""; - - public defaultTv = "/images/default_tv_poster.png"; - private defaultMovie = "/images/default_movie_poster.png"; - private defaultMusic = "i/mages/default-music-placeholder.png"; - - private alreadyErrored = false; - - constructor (@Inject(APP_BASE_HREF) public href: string) { - if (this.href.length > 1) { - this.baseUrl = this.href; - } + standalone: true, + selector: "ombi-image", + imports: [...OmbiCommonModules], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: "./image.component.html", +}) +export class ImageComponent { + @Input() public src: string; + @Input() public type: RequestType; + + // Attributes from the parent + @Input() public class: string; + @Input() public id: string; + @Input() public alt: string; + @Input() public style: string; + + private baseUrl: string = ""; + + private defaultTv = "/images/default_tv_poster.png"; + private defaultMovie = "/images/default_movie_poster.png"; + private defaultMusic = "/images/default-music-placeholder.png"; + + private maxRetries = 1; + private retriesPerformed = 0; + + constructor(@Inject(APP_BASE_HREF) private href: string) { + if (this.href.length > 1) { + this.baseUrl = this.href; } + } + + ngOnInit() { + if (!this.src) { + // Prevent unnecessary error handling when src is not specified. + this.src = this.getPlaceholderImage(); + } + } + + public onError(event: any) { + event.target.src = this.getPlaceholderImage(); + + if (!this.src || this.retriesPerformed === this.maxRetries) { + return; + } + + // Retry the original image. + this.retriesPerformed++; + const timeout = setTimeout(() => { + clearTimeout(timeout); + event.target.src = this.src; + }, Math.floor(Math.random() * (7000 - 1000 + 1)) + 1000); + } - public onError(event: any) { - if (this.alreadyErrored) { - return; - } - // set to a placeholder - switch(this.type) { - case RequestType.movie: - event.target.src = this.baseUrl + this.defaultMovie; - break; - case RequestType.tvShow: - event.target.src = this.baseUrl + this.defaultTv; - break; - case RequestType.album: - event.target.src = this.baseUrl + this.defaultMusic; - break; - } - - this.alreadyErrored = true; - // Retry the original image - const timeout = setTimeout(() => { - clearTimeout(timeout); - event.target.src = this.src; - }, Math.floor(Math.random() * (7000 - 1000 + 1)) + 1000); + private getPlaceholderImage() { + switch (this.type) { + case RequestType.movie: + return this.baseUrl + this.defaultMovie; + case RequestType.tvShow: + return this.baseUrl + this.defaultTv; + case RequestType.album: + return this.baseUrl + this.defaultMusic; } - } \ No newline at end of file + } +}