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>
@if (mainImageQuery.isLoading()) {
@if (loadingMainImage()) {
<app-spinner></app-spinner>
} @else if (mainImageQuery.isError()) {
} @else if (loadMainImageError()) {
<p>There was an error loading the main image.</p>
} @else if (mainImageQuery.isEnabled()) {
} @else if (mainImage()) {
<img
id="main-image"
[src]="mainImageQuery.data()!.blobUrl"
[src]="mainImage()"
[alt]="recipe.mainImage!.alt"
[height]="recipe.mainImage!.height"
[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 { CreateQueryOptions, injectMutation, injectQuery } from '@tanstack/angular-query-experimental';
import { injectMutation } from '@tanstack/angular-query-experimental';
import { ImageService } from '../../../shared/services/ImageService';
import { faEllipsis, faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
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 { MatButton } from '@angular/material/button';
import { Spinner } from '../../../shared/components/spinner/spinner';
import { ImageViewWithBlobUrl } from '../../../shared/client-models/ImageViewWithBlobUrl';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { Router } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
@ -23,7 +22,7 @@ import { ToastrService } from 'ngx-toastr';
templateUrl: './recipe-page-content.html',
styleUrl: './recipe-page-content.css',
})
export class RecipePageContent {
export class RecipePageContent implements OnInit {
public recipeView = input.required<FullRecipeViewWrapper>();
private readonly imageService = inject(ImageService);
@ -34,23 +33,27 @@ export class RecipePageContent {
protected readonly isLoggedIn = computed(() => !!this.authService.accessToken());
protected readonly isOwner = computed(() => !!this.recipeView().isOwner);
protected readonly mainImageQuery = injectQuery(() => {
let options: Partial<
CreateQueryOptions<ImageViewWithBlobUrl, Error, ImageViewWithBlobUrl, [string, string, string]>
> = {};
const mainImageView = this.recipeView().recipe.mainImage;
if (mainImageView) {
options = this.imageService.getImage(mainImageView);
} else {
options.enabled = false;
protected readonly loadingMainImage = signal(false);
protected readonly loadMainImageError = signal<Error | null>(null);
protected readonly mainImage = signal<string | null>(null);
public ngOnInit(): void {
const recipe = this.recipeView().recipe;
if (recipe.mainImage) {
this.loadingMainImage.set(true);
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(() => ({
mutationFn: () => this.recipeService.toggleStar(this.recipeView()),

View File

@ -1,12 +1,12 @@
@let recipe = this.recipe();
<article>
<a [routerLink]="recipePageLink()">
@if (mainImage.isLoading()) {
@if (loadingMainImage()) {
<app-spinner></app-spinner>
} @else if (mainImage.isError()) {
<p>There was an error loading the image.</p>
} @else if (mainImage.isEnabled()) {
<img [src]="mainImage.data()!.blobUrl" id="recipe-card-image" [alt]="recipe.mainImage!.alt" />
} @else if (loadMainImageError()) {
<p>Error</p>
} @else if (mainImage()) {
<img [src]="mainImage()" id="recipe-card-image" [alt]="recipe.mainImage!.alt" />
} @else {
<div class="recipe-card-image-placeholder">
<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 { RouterLink } from '@angular/router';
import { CreateQueryOptions, injectQuery } from '@tanstack/angular-query-experimental';
import { ImageService } from '../../../services/ImageService';
import { faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { Logo } from '../../logo/logo';
import { ImageViewWithBlobUrl } from '../../../client-models/ImageViewWithBlobUrl';
import { Spinner } from '../../spinner/spinner';
@Component({
@ -15,7 +13,7 @@ import { Spinner } from '../../spinner/spinner';
templateUrl: './recipe-card.html',
styleUrl: './recipe-card.css',
})
export class RecipeCard {
export class RecipeCard implements OnInit {
public recipe = input.required<RecipeInfoView>();
protected readonly recipePageLink = computed(() => {
@ -30,21 +28,25 @@ export class RecipeCard {
private readonly imageService = inject(ImageService);
protected readonly mainImage = injectQuery(() => {
const mainImageView = this.recipe().mainImage;
let options: Partial<
CreateQueryOptions<ImageViewWithBlobUrl, Error, ImageViewWithBlobUrl, [string, string, string]>
> = {};
if (mainImageView) {
options = this.imageService.getImage(mainImageView);
} else {
options.enabled = false;
protected readonly loadingMainImage = signal(false);
protected readonly loadMainImageError = signal<Error | null>(null);
protected readonly mainImage = signal<string | null>(null);
public ngOnInit(): void {
const recipe = this.recipe();
if (recipe.mainImage) {
this.loadingMainImage.set(true);
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 { 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 { ImageView } from '../models/ImageView.model';
import { SliceView } from '../models/SliceView.model';
import { QueryParams } from '../models/Query.model';
import { QueryClient, QueryOptions, queryOptions } from '@tanstack/angular-query-experimental';
import { ImageViewWithBlobUrl } from '../client-models/ImageViewWithBlobUrl';
import { WithStringDates } from '../util';
import { ImageUpdateBody } from '../bodies/ImageUpdateBody';
@ -31,7 +30,12 @@ export class ImageService {
private readonly httpClient = inject(HttpClient);
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 {
return {
@ -90,32 +94,23 @@ export class ImageService {
.pipe(map((res) => res.count));
}
public getImageViewWithBlobUrl(imageView: ImageView): Promise<ImageViewWithBlobUrl> {
return firstValueFrom(
this.httpClient
.get(this.endpointService.getUrl('images', [imageView.owner.username, imageView.filename]), {
responseType: 'blob',
})
.pipe(
map((blob) => URL.createObjectURL(blob)),
map(
(blobUrl) =>
({
...imageView,
blobUrl,
}) satisfies ImageViewWithBlobUrl,
),
),
);
private fetchImageViewWithBlobUrl(username: string, filename: string): Observable<string> {
return this.httpClient
.get(this.endpointService.getUrl('images', [username, filename]), {
responseType: 'blob',
})
.pipe(map((blob) => URL.createObjectURL(blob)));
}
public getImage(
imageView: ImageView,
): QueryOptions<ImageViewWithBlobUrl, Error, ImageViewWithBlobUrl, [string, string, string]> {
return queryOptions({
queryKey: ['image-views-with-blob-urls', imageView.owner.username, imageView.filename],
queryFn: () => this.getImageViewWithBlobUrl(imageView),
});
public getImageBlobUrl(username: string, filename: string): Observable<string> {
const key = this.getUsernameFilenameKey(username, filename);
if (!this.imageBlobUrls.has(key)) {
const blobUrl$ = this.fetchImageViewWithBlobUrl(username, filename).pipe(
shareReplay({ bufferSize: 1, refCount: false }),
);
this.imageBlobUrls.set(key, blobUrl$);
}
return this.imageBlobUrls.get(key)!;
}
public getImageView2(username: string, filename: string): Observable<ImageView> {
@ -144,15 +139,7 @@ export class ImageService {
formData.append('isPublic', isPublic.toString());
}
return firstValueFrom(
this.httpClient.post<ImageView>(this.endpointService.getUrl('images'), formData).pipe(
tap(async () => {
await this.queryClient.invalidateQueries({
queryKey: ['image-views'],
});
}),
),
);
return firstValueFrom(this.httpClient.post<ImageView>(this.endpointService.getUrl('images'), formData));
}
public imageExists(username: string, filename: string): Promise<boolean> {