diff --git a/desktop/angular/src/app/shared/pipes/common-pipes.module.ts b/desktop/angular/src/app/shared/pipes/common-pipes.module.ts index c64df8a1..3350d6bd 100644 --- a/desktop/angular/src/app/shared/pipes/common-pipes.module.ts +++ b/desktop/angular/src/app/shared/pipes/common-pipes.module.ts @@ -5,6 +5,7 @@ import { ToAppProfilePipe } from "./to-profile.pipe"; import { DurationPipe } from "./duration.pipe"; import { RoundPipe } from "./round.pipe"; import { ToSecondsPipe } from "./to-seconds.pipe"; +import { HttpImgSrcPipe } from "./http-img-src.pipe"; @NgModule({ declarations: [ @@ -13,7 +14,8 @@ import { ToSecondsPipe } from "./to-seconds.pipe"; ToAppProfilePipe, DurationPipe, RoundPipe, - ToSecondsPipe + ToSecondsPipe, + HttpImgSrcPipe ], exports: [ TimeAgoPipe, @@ -21,7 +23,8 @@ import { ToSecondsPipe } from "./to-seconds.pipe"; ToAppProfilePipe, DurationPipe, RoundPipe, - ToSecondsPipe + ToSecondsPipe, + HttpImgSrcPipe ] }) export class CommonPipesModule { } diff --git a/desktop/angular/src/app/shared/pipes/http-img-src.pipe.ts b/desktop/angular/src/app/shared/pipes/http-img-src.pipe.ts new file mode 100644 index 00000000..c3aae731 --- /dev/null +++ b/desktop/angular/src/app/shared/pipes/http-img-src.pipe.ts @@ -0,0 +1,80 @@ + +import { Pipe, PipeTransform } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; +import { Observable, of } from 'rxjs'; +import { map, catchError, tap } from 'rxjs/operators'; + +//** +// * This pipe fetches an image from a given URL and returns a SafeUrl for use in Angular templates. +// * It caches the image to avoid multiple requests for the same URL. +// * If the image fails to load, it returns null and logs an error message. +// * +// * The pipe uses Angular's HttpClient to make the HTTP request and DomSanitizer to sanitize the URL. +// * +// * This pipe is useful for forcing usage of the HTTP interceptor in Tauri instead of the WebView +// * (for example, to use the Tauri API for HTTP requests) +// * +// * @example +// * +// **/ + +@Pipe({ + name: 'httpImgSrc' +}) +export class HttpImgSrcPipe implements PipeTransform { + private static cache = new Map(); + + constructor(private http: HttpClient, private sanitizer: DomSanitizer) { } + + transform(url: string | SafeUrl | undefined): Observable { + if (!url) { + return of(null); + } + + if (this.isSafeUrl(url)) { + //console.log('[Pipe httpImgSrc] URL is already a SafeUrl:', url); + return of(url); + } + + if (typeof url !== 'string') { + //console.error('[Pipe httpImgSrc] Invalid URL:', url); + return of(null); + } + + if (HttpImgSrcPipe.cache.has(url)) { + //console.log('[Pipe httpImgSrc] Returning cached image:', url); + return of(HttpImgSrcPipe.cache.get(url) as SafeUrl | null); + } + + return this.http.get(url, { responseType: 'blob' }).pipe( + //tap(blob => console.log('[Pipe httpImgSrc] Successfully loaded image:', url, 'Size:', blob.size, 'Type:', blob.type)), + map(blob => URL.createObjectURL(blob)), + map(objectUrl => { + const safeUrl = this.sanitizer.bypassSecurityTrustUrl(objectUrl); + + if (HttpImgSrcPipe.cache.size > 1000) { + // Very simple cache eviction strategy: clear the cache if it exceeds 1000 items. + // Normally it should never exceed this size, but just in case. + // TODO: Implement a more sophisticated cache eviction strategy if needed. + console.warn('[Pipe httpImgSrc] Cache size exceeded 1000 items. Clearing images cache.'); + HttpImgSrcPipe.cache.clear(); + } + + HttpImgSrcPipe.cache.set(url, safeUrl); + return safeUrl; + }), + catchError(() => { + console.error('[Pipe httpImgSrc] Failed to load image:', url); + HttpImgSrcPipe.cache.set(url, null); + return of(null); + }) + ); + } + // Type guard for SafeUrl + private isSafeUrl(value: any): value is SafeUrl { + // SafeUrl is an object with an internal property changing per Angular versions + // But it’s always object-like, not a string, and created by DomSanitizer + return typeof value === 'object' && value !== null && value.changingThisBreaksApplicationSecurity !== undefined; + } +} diff --git a/desktop/angular/src/app/shared/pipes/index.ts b/desktop/angular/src/app/shared/pipes/index.ts index 6eddfdd2..52f68966 100644 --- a/desktop/angular/src/app/shared/pipes/index.ts +++ b/desktop/angular/src/app/shared/pipes/index.ts @@ -4,3 +4,4 @@ export * from './to-profile.pipe'; export * from './duration.pipe'; export * from './to-seconds.pipe'; export * from './round.pipe'; +export * from './http-img-src.pipe'