MME-29 Remove tanstack query stuff from ImageService.

This commit is contained in:
Jesse Brault 2026-02-15 16:40:51 -06:00
parent e071b0ed8c
commit fb4e9f7de1
5 changed files with 77 additions and 85 deletions

View File

@ -41,14 +41,14 @@
} }
</div> </div>
</div> </div>
@if (mainImageQuery.isLoading()) { @if (loadingMainImage()) {
<app-spinner></app-spinner> <app-spinner></app-spinner>
} @else if (mainImageQuery.isError()) { } @else if (loadMainImageError()) {
<p>There was an error loading the main image.</p> <p>There was an error loading the main image.</p>
} @else if (mainImageQuery.isEnabled()) { } @else if (mainImage()) {
<img <img
id="main-image" id="main-image"
[src]="mainImageQuery.data()!.blobUrl" [src]="mainImage()"
[alt]="recipe.mainImage!.alt" [alt]="recipe.mainImage!.alt"
[height]="recipe.mainImage!.height" [height]="recipe.mainImage!.height"
[width]="recipe.mainImage!.width" [width]="recipe.mainImage!.width"

View File

@ -1,6 +1,6 @@
import { Component, computed, inject, input } from '@angular/core'; import { Component, computed, inject, input, OnInit, signal } from '@angular/core';
import { FullRecipeViewWrapper } from '../../../shared/models/Recipe.model'; import { FullRecipeViewWrapper } from '../../../shared/models/Recipe.model';
import { CreateQueryOptions, injectMutation, injectQuery } from '@tanstack/angular-query-experimental'; import { injectMutation } from '@tanstack/angular-query-experimental';
import { ImageService } from '../../../shared/services/ImageService'; import { ImageService } from '../../../shared/services/ImageService';
import { faEllipsis, faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons'; import { faEllipsis, faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@ -9,7 +9,6 @@ import { AuthService } from '../../../shared/services/AuthService';
import { RecipeCommentsList } from '../../../shared/components/recipe-comments-list/recipe-comments-list'; import { RecipeCommentsList } from '../../../shared/components/recipe-comments-list/recipe-comments-list';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { Spinner } from '../../../shared/components/spinner/spinner'; import { Spinner } from '../../../shared/components/spinner/spinner';
import { ImageViewWithBlobUrl } from '../../../shared/client-models/ImageViewWithBlobUrl';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@ -23,7 +22,7 @@ import { ToastrService } from 'ngx-toastr';
templateUrl: './recipe-page-content.html', templateUrl: './recipe-page-content.html',
styleUrl: './recipe-page-content.css', styleUrl: './recipe-page-content.css',
}) })
export class RecipePageContent { export class RecipePageContent implements OnInit {
public recipeView = input.required<FullRecipeViewWrapper>(); public recipeView = input.required<FullRecipeViewWrapper>();
private readonly imageService = inject(ImageService); private readonly imageService = inject(ImageService);
@ -34,23 +33,27 @@ export class RecipePageContent {
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken()); protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
protected readonly isOwner = computed(() => !!this.recipeView().isOwner); protected readonly isOwner = computed(() => !!this.recipeView().isOwner);
protected readonly mainImageQuery = injectQuery(() => { protected readonly loadingMainImage = signal(false);
let options: Partial< protected readonly loadMainImageError = signal<Error | null>(null);
CreateQueryOptions<ImageViewWithBlobUrl, Error, ImageViewWithBlobUrl, [string, string, string]> protected readonly mainImage = signal<string | null>(null);
> = {};
const mainImageView = this.recipeView().recipe.mainImage; public ngOnInit(): void {
if (mainImageView) { const recipe = this.recipeView().recipe;
options = this.imageService.getImage(mainImageView); if (recipe.mainImage) {
} else { this.loadingMainImage.set(true);
options.enabled = false; this.imageService.getImageBlobUrl(recipe.mainImage.owner.username, recipe.mainImage.filename).subscribe({
next: (blobUrl) => {
this.loadingMainImage.set(false);
this.mainImage.set(blobUrl);
},
error: (e) => {
this.loadingMainImage.set(false);
this.loadMainImageError.set(e);
console.error(e);
},
});
} }
return options as CreateQueryOptions< }
ImageViewWithBlobUrl,
Error,
ImageViewWithBlobUrl,
[string, string, string]
>;
});
protected readonly starMutation = injectMutation(() => ({ protected readonly starMutation = injectMutation(() => ({
mutationFn: () => this.recipeService.toggleStar(this.recipeView()), mutationFn: () => this.recipeService.toggleStar(this.recipeView()),

View File

@ -1,12 +1,12 @@
@let recipe = this.recipe(); @let recipe = this.recipe();
<article> <article>
<a [routerLink]="recipePageLink()"> <a [routerLink]="recipePageLink()">
@if (mainImage.isLoading()) { @if (loadingMainImage()) {
<app-spinner></app-spinner> <app-spinner></app-spinner>
} @else if (mainImage.isError()) { } @else if (loadMainImageError()) {
<p>There was an error loading the image.</p> <p>Error</p>
} @else if (mainImage.isEnabled()) { } @else if (mainImage()) {
<img [src]="mainImage.data()!.blobUrl" id="recipe-card-image" [alt]="recipe.mainImage!.alt" /> <img [src]="mainImage()" id="recipe-card-image" [alt]="recipe.mainImage!.alt" />
} @else { } @else {
<div class="recipe-card-image-placeholder"> <div class="recipe-card-image-placeholder">
<app-logo></app-logo> <app-logo></app-logo>

View File

@ -1,12 +1,10 @@
import { Component, computed, inject, input } from '@angular/core'; import { Component, computed, inject, input, OnInit, signal } from '@angular/core';
import { RecipeInfoView } from '../../../models/Recipe.model'; import { RecipeInfoView } from '../../../models/Recipe.model';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { CreateQueryOptions, injectQuery } from '@tanstack/angular-query-experimental';
import { ImageService } from '../../../services/ImageService'; import { ImageService } from '../../../services/ImageService';
import { faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons'; import { faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { Logo } from '../../logo/logo'; import { Logo } from '../../logo/logo';
import { ImageViewWithBlobUrl } from '../../../client-models/ImageViewWithBlobUrl';
import { Spinner } from '../../spinner/spinner'; import { Spinner } from '../../spinner/spinner';
@Component({ @Component({
@ -15,7 +13,7 @@ import { Spinner } from '../../spinner/spinner';
templateUrl: './recipe-card.html', templateUrl: './recipe-card.html',
styleUrl: './recipe-card.css', styleUrl: './recipe-card.css',
}) })
export class RecipeCard { export class RecipeCard implements OnInit {
public recipe = input.required<RecipeInfoView>(); public recipe = input.required<RecipeInfoView>();
protected readonly recipePageLink = computed(() => { protected readonly recipePageLink = computed(() => {
@ -30,21 +28,25 @@ export class RecipeCard {
private readonly imageService = inject(ImageService); private readonly imageService = inject(ImageService);
protected readonly mainImage = injectQuery(() => { protected readonly loadingMainImage = signal(false);
const mainImageView = this.recipe().mainImage; protected readonly loadMainImageError = signal<Error | null>(null);
let options: Partial< protected readonly mainImage = signal<string | null>(null);
CreateQueryOptions<ImageViewWithBlobUrl, Error, ImageViewWithBlobUrl, [string, string, string]>
> = {}; public ngOnInit(): void {
if (mainImageView) { const recipe = this.recipe();
options = this.imageService.getImage(mainImageView); if (recipe.mainImage) {
} else { this.loadingMainImage.set(true);
options.enabled = false; this.imageService.getImageBlobUrl(recipe.mainImage.owner.username, recipe.mainImage.filename).subscribe({
next: (blobUrl) => {
this.loadingMainImage.set(false);
this.mainImage.set(blobUrl);
},
error: (e) => {
this.loadingMainImage.set(false);
this.loadMainImageError.set(e);
console.error(e);
},
});
} }
return options as CreateQueryOptions< }
ImageViewWithBlobUrl,
Error,
ImageViewWithBlobUrl,
[string, string, string]
>;
});
} }

View File

@ -1,11 +1,10 @@
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { firstValueFrom, from, map, mergeMap, Observable, tap, toArray } from 'rxjs'; import { firstValueFrom, from, map, mergeMap, Observable, shareReplay, toArray } from 'rxjs';
import { EndpointService } from './EndpointService'; import { EndpointService } from './EndpointService';
import { ImageView } from '../models/ImageView.model'; import { ImageView } from '../models/ImageView.model';
import { SliceView } from '../models/SliceView.model'; import { SliceView } from '../models/SliceView.model';
import { QueryParams } from '../models/Query.model'; import { QueryParams } from '../models/Query.model';
import { QueryClient, QueryOptions, queryOptions } from '@tanstack/angular-query-experimental';
import { ImageViewWithBlobUrl } from '../client-models/ImageViewWithBlobUrl'; import { ImageViewWithBlobUrl } from '../client-models/ImageViewWithBlobUrl';
import { WithStringDates } from '../util'; import { WithStringDates } from '../util';
import { ImageUpdateBody } from '../bodies/ImageUpdateBody'; import { ImageUpdateBody } from '../bodies/ImageUpdateBody';
@ -31,7 +30,12 @@ export class ImageService {
private readonly httpClient = inject(HttpClient); private readonly httpClient = inject(HttpClient);
private readonly endpointService = inject(EndpointService); private readonly endpointService = inject(EndpointService);
private readonly queryClient = inject(QueryClient);
private readonly imageBlobUrls: Map<string, Observable<string>> = new Map();
private getUsernameFilenameKey(username: string, filename: string): string {
return username + '/' + filename;
}
public hydrateImageView(rawImageView: WithStringDates<ImageView>): ImageView { public hydrateImageView(rawImageView: WithStringDates<ImageView>): ImageView {
return { return {
@ -90,32 +94,23 @@ export class ImageService {
.pipe(map((res) => res.count)); .pipe(map((res) => res.count));
} }
public getImageViewWithBlobUrl(imageView: ImageView): Promise<ImageViewWithBlobUrl> { private fetchImageViewWithBlobUrl(username: string, filename: string): Observable<string> {
return firstValueFrom( return this.httpClient
this.httpClient .get(this.endpointService.getUrl('images', [username, filename]), {
.get(this.endpointService.getUrl('images', [imageView.owner.username, imageView.filename]), { responseType: 'blob',
responseType: 'blob', })
}) .pipe(map((blob) => URL.createObjectURL(blob)));
.pipe(
map((blob) => URL.createObjectURL(blob)),
map(
(blobUrl) =>
({
...imageView,
blobUrl,
}) satisfies ImageViewWithBlobUrl,
),
),
);
} }
public getImage( public getImageBlobUrl(username: string, filename: string): Observable<string> {
imageView: ImageView, const key = this.getUsernameFilenameKey(username, filename);
): QueryOptions<ImageViewWithBlobUrl, Error, ImageViewWithBlobUrl, [string, string, string]> { if (!this.imageBlobUrls.has(key)) {
return queryOptions({ const blobUrl$ = this.fetchImageViewWithBlobUrl(username, filename).pipe(
queryKey: ['image-views-with-blob-urls', imageView.owner.username, imageView.filename], shareReplay({ bufferSize: 1, refCount: false }),
queryFn: () => this.getImageViewWithBlobUrl(imageView), );
}); this.imageBlobUrls.set(key, blobUrl$);
}
return this.imageBlobUrls.get(key)!;
} }
public getImageView2(username: string, filename: string): Observable<ImageView> { public getImageView2(username: string, filename: string): Observable<ImageView> {
@ -144,15 +139,7 @@ export class ImageService {
formData.append('isPublic', isPublic.toString()); formData.append('isPublic', isPublic.toString());
} }
return firstValueFrom( return firstValueFrom(this.httpClient.post<ImageView>(this.endpointService.getUrl('images'), formData));
this.httpClient.post<ImageView>(this.endpointService.getUrl('images'), formData).pipe(
tap(async () => {
await this.queryClient.invalidateQueries({
queryKey: ['image-views'],
});
}),
),
);
} }
public imageExists(username: string, filename: string): Promise<boolean> { public imageExists(username: string, filename: string): Promise<boolean> {