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.
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.

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

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

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;
}
}
}
|
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 {
}
|
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.
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)">
|
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>
|
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/