Gallerie Photo Angular

Projet

L’idée est de créer une gallerie photo. Une seule contrainte : pas de backend.

Fonctionnellement on va avoir des dossiers, chacun contenant une série d’images. Il y aura donc une page listant les différents dossier, une page avec la liste des images et une lightbox pour afficher une image en détails.

Héberger les images

Le premier besoin à résoudre est le stocker des images, il faut un hébergement permettant de lister tous les éléments de manière publique depuis l’application Angular. J’ai regardé du côté de AWS S3 et de Scaleway Object Storage mais je ne suis pas parvenu à accéder aux fichers de manière anonyme.

Au final, le choix s’est porté sur Firebase Storage : il est facile de configurer l’accès anonyme et l’intégration au sein d’une application Angular se fait sans douleur avec AngularFire.

Je passe la création du projet sur la console Firebase.

Gallery Create App

Dans le stockage on va créer des dossiers qui seront les dossiers de la gallerie.

Gallery Storage Folder

Dans chacun des dossiers on place les fichiers images qui nous intéressent.

Gallery Storage Files

Enfin pour permettre l’accès anonyme il faut changer les règles d’accès :

1
2
3
4
5
6
7
8
9
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read;
      allow write: if request.auth != null;
    }
  }
}

L’application Angular

On passe au code Angular et on configure Angular Fire pour l’application.

Une fois le projet configuré, on ajoute le module pour firebase storage.

Normalement, le module App devrait ressemble à ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import {NgModule} from '@angular/core';
import {AngularFireModule} from '@angular/fire';
import {AngularFireStorageModule} from '@angular/fire/storage';
import {BrowserModule} from '@angular/platform-browser';

import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireStorageModule,
  ],
  providers: [],
  bootstrap: [
    AppComponent
  ]
})
export class AppModule {
}

Listing des photos

Pour lister les dossiers et images, voici les deux méthodes :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import {Injectable} from '@angular/core';
import {AngularFireStorage} from '@angular/fire/storage';
import {from, Observable} from 'rxjs';
import {map, mergeMap, toArray} from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class PhotoService {

  constructor(
    private storage: AngularFireStorage
  ) {
  }

  listFolders(): Observable<string[]> {
    return this.storage.ref('').listAll()
      .pipe(
        map(res => {
          return res.prefixes.map(prefix => prefix.name);
        }),
      );
  }

  listFiles(folder: string): Observable<string[]> {
    return this.storage.ref(folder).listAll()
      .pipe(
        mergeMap(res => {
          return from(res.items);
        }),
        mergeMap(item => {
          return from(item.getDownloadURL());
        }),
        map(uri => uri.toString()),
        toArray(),
      );
  }
}

Ensuite on crée un composant capable d’afficher les dossiers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {Component, OnInit} from '@angular/core';
import {PhotoService} from '../../services/photo.service';

@Component({
  selector: 'app-listing',
  templateUrl: './listing.component.html',
  styleUrls: ['./listing.component.scss']
})
export class ListingComponent implements OnInit {

  folders: string[] = [];

  constructor(
    private photoService: PhotoService
  ) {
  }

  ngOnInit(): void {
    this.photoService.listFolders()
      .subscribe(folders => {
        this.folders = folders;
      });
  }

}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<article class="container folder-list">
  <div fxLayout="row wrap" fxLayoutGap="12px">
    <a *ngFor="let folder of folders"
       fxFlex="auto"
       class="button">
        <span class="icon">
          <fa-icon [icon]="['far', 'folder']"></fa-icon>
        </span>
      <span>
          {{ folder }}
        </span>
    </a>
  </div>
</article>

Enfin on s’attaque à la gallerie d’images :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {filter, map, switchMap} from 'rxjs/operators';
import {PhotoService} from '../../services/photo.service';

@Component({
  selector: 'app-gallery',
  templateUrl: './gallery.component.html',
  styleUrls: ['./gallery.component.scss']
})
export class GalleryComponent implements OnInit {

  pictures: string[] = [];

  constructor(
    private activatedRoute: ActivatedRoute,
    private photoService: PhotoService,
  ) {
  }

  ngOnInit(): void {
    this.activatedRoute.paramMap
      .pipe(
        map(params => params.get('folder')),
        filter(folder => folder != null),
        switchMap(folder => this.photoService.listFiles(folder as string))
      )
      .subscribe(pictures => {
        this.pictures = pictures;
      });
  }

}
1
2
3
4
5
6
7
<article class="container gallery"
         fxLayout="row wrap" fxLayoutAlign="space-evenly center">
  <img *ngFor="let picture of pictures"
       fxFlex="nogrow"
       class="gallery__picture"
       [src]="picture">
</article>

Le design ici est volontairement simple mais permet d’avoir un résultat satisfaisant sans trop de lignes de code.

Générer des miniatures

On a décidé de stocker uniquement un fichier par image et de ne pas créer de backend. Le mieux est donc de stocker sur Firebase Storage la version en haute définition de nos images. Par contre cela entraîne plusieurs petis soucis : la consommation de donnée va être importante, à la fois pour le client qui va devoir charger de gros fichiers alors que des versions allégées seraient suffisantes mais également du côté de Google qui va donc nous facturer de manière importante.

On peut imaginer plusieurs solutions à ce problème, je propose ici celle qui me parait être la plus simple à mettre en place basée sur le service Images.weserv.nl. Ce service complètement gratuit permet de mettre en cache et de redimensionner les images à la demande.

On va donc pouvoir servir des images correctement dimensionnée dans notre galleries et elles seront en plus mise en cache pour éviter d’être trop facturé.

On peut donc modifier notre GalleryComponent pour utiliser des liens qui redimensionnent les images.

1
2
3
4
5
6
7
export class GalleryComponent implements OnInit {
    deviceRatio = window?.devicePixelRatio ?? 1;

    resizeLink(imageUrl: string, height: number): string {
        return `https://images.weserv.nl/?url=${encodeURIComponent(imageUrl)}&h=${height}&dpr=${this.deviceRatio}`;
    }
}
1
2
  <img *ngFor="let picture of pictures"
       [src]="resizeLink(picture, 300)">

La lightbox

Enfin pour la lightbox, après un rapide tour d’horizon des solutions existantes, j’ai choisi la solution de Crystal UI qui m’a semblé la plus simple d’utilisation pour un résultat très correct.

Il nous suffit de l’ajouter dans la gallerie :

1
2
3
4
5
export class GalleryComponent implements OnInit {

  deviceFullHeight = window?.screen?.availHeight ?? 1080;
  
}
1
2
3
4
5
6
<article lightbox-group>
  <img *ngFor="let picture of pictures"
       [src]="resizeLink(picture, 300)"
       lightbox
       [fullImage]="{path: resizeLink(picture, deviceFullHeight)}">
</article>

Projet complet

Le projet complet est disponible ici : https://gitlab.com/gautier-levert/angular-photo-gallery.

Une démo du project est accessible ici : https://gallerie-photo-68c69.web.app/

Load Comments?