From 813bbbb94aa8675c11d6fd212311df1380dbf906 Mon Sep 17 00:00:00 2001 From: thomas girod Date: Mon, 16 Sep 2024 18:14:37 +0200 Subject: [PATCH] preload images and identifications --- sas/static/sas/js/viewer.js | 116 ++++++++++++++++++++++++-------- sas/templates/sas/picture.jinja | 16 +++-- 2 files changed, 99 insertions(+), 33 deletions(-) diff --git a/sas/static/sas/js/viewer.js b/sas/static/sas/js/viewer.js index 017aee1a..9120f982 100644 --- a/sas/static/sas/js/viewer.js +++ b/sas/static/sas/js/viewer.js @@ -4,23 +4,78 @@ * @property {UserProfile} user The identified user */ +/** + * A container for a picture with the users identified on it + * able to prefetch its data. + */ +class PictureWithIdentifications { + identifications = null; + image_loading = false; + identifications_loading = false; + + /** + * @param {Picture} picture + */ + constructor(picture) { + Object.assign(this, picture); + } + /** + * @param {Picture} picture + */ + static from_picture(picture) { + return new PictureWithIdentifications(picture); + } + + /** + * If not already done, fetch the users identified on this picture and + * populate the identifications field + * @param {?Object=} options + * @return {Promise} + */ + async load_identifications(options) { + if (this.identifications_loading) { + return; // The users are already being fetched. + } + if (!!this.identifications && !options?.force_reload) { + // The users are already fetched + // and the user does not want to force the reload + return; + } + this.identifications_loading = true; + const url = `/api/sas/picture/${this.id}/identified`; + this.identifications = await (await fetch(url)).json(); + this.identifications_loading = false; + } + + /** + * Preload the photo and the identifications + * @return {Promise} + */ + async preload() { + const img = new Image(); + img.src = this.compressed_url; + if (!img.complete) { + this.image_loading = true; + img.addEventListener("load", () => { + this.image_loading = false; + }); + } + await this.load_identifications(); + } +} + document.addEventListener("alpine:init", () => { Alpine.data("picture_viewer", () => ({ /** * All the pictures that can be displayed on this picture viewer - * @type Picture[] + * @type PictureWithIdentifications[] **/ pictures: [], - /** - * The users identified on the currently displayed picture - * @type PictureIdentification[] - **/ - identifications: [], /** * The currently displayed picture * Default dummy data are pre-loaded to avoid javascript error * when loading the page at the beginning - * @type Picture + * @type PictureWithIdentifications **/ current_picture: { is_moderated: true, @@ -32,15 +87,16 @@ document.addEventListener("alpine:init", () => { full_size_url: "", owner: "", date: new Date(), + identifications: [], }, /** * The picture which will be displayed next if the user press the "next" button - * @type ?Picture + * @type ?PictureWithIdentifications **/ next_picture: null, /** * The picture which will be displayed next if the user press the "previous" button - * @type ?Picture + * @type ?PictureWithIdentifications **/ previous_picture: null, /** @@ -50,7 +106,6 @@ document.addEventListener("alpine:init", () => { /** * true if the page is in a loading state, else false **/ - loading: true, /** * Error message when a moderation operation fails * @type string @@ -64,11 +119,17 @@ document.addEventListener("alpine:init", () => { pushstate: History.PUSH, async init() { - this.pictures = await fetch_paginated(picture_endpoint); + this.pictures = (await fetch_paginated(picture_endpoint)).map( + PictureWithIdentifications.from_picture, + ); this.selector = sithSelect2({ element: $(this.$refs.search), data_source: remote_data_source("/api/user/search", { - excluded: () => [...this.identifications.map((i) => i.user.id)], + excluded: () => [ + ...(this.current_picture.identifications || []).map( + (i) => i.user.id, + ), + ], result_converter: (obj) => Object({ ...obj, text: obj.display_name }), }), picture_getter: (user) => user.profile_pict, @@ -99,8 +160,6 @@ document.addEventListener("alpine:init", () => { * the list of identified users are updated. */ async update_picture() { - this.loading = true; - const update_args = [ { sas_picture_id: this.current_picture.id }, "", @@ -117,10 +176,13 @@ document.addEventListener("alpine:init", () => { const index = this.pictures.indexOf(this.current_picture); this.previous_picture = this.pictures[index - 1] || null; this.next_picture = this.pictures[index + 1] || null; - this.identifications = await ( - await fetch(`/api/sas/picture/${this.current_picture.id}/identified`) - ).json(); - this.loading = false; + await this.current_picture.load_identifications(); + this.$refs.main_picture?.addEventListener("load", () => { + // once the current picture is loaded, + // start preloading the next and previous pictures + this.next_picture?.preload(); + this.previous_picture?.preload(); + }); }, async moderate_picture() { @@ -131,8 +193,7 @@ document.addEventListener("alpine:init", () => { }, ); if (!res.ok) { - this.moderation_error = - gettext("Couldn't moderate picture") + " : " + res.statusText; + this.moderation_error = `${gettext("Couldn't moderate picture")} : ${res.statusText}`; return; } this.current_picture.is_moderated = true; @@ -161,16 +222,14 @@ document.addEventListener("alpine:init", () => { * Send the identification request and update the list of identified users. */ async submit_identification() { - this.loading = true; const url = `/api/sas/picture/${this.current_picture.id}/identified`; await fetch(url, { method: "PUT", body: JSON.stringify(this.selector.val().map((i) => parseInt(i))), }); // refresh the identified users list - this.identifications = await (await fetch(url)).json(); + await this.current_picture.load_identifications({ force_reload: true }); this.selector.empty().trigger("change"); - this.loading = false; }, /** @@ -187,16 +246,15 @@ document.addEventListener("alpine:init", () => { * @param {PictureIdentification} identification */ async remove_identification(identification) { - this.loading = true; const res = await fetch(`/api/sas/relation/${identification.id}`, { method: "DELETE", }); - if (res.ok) { - this.identifications = this.identifications.filter( - (i) => i.id !== identification.id, - ); + if (res.ok && Array.isArray(this.current_picture.identifications)) { + this.current_picture.identifications = + this.current_picture.identifications.filter( + (i) => i.id !== identification.id, + ); } - this.loading = false; }, })); }); diff --git a/sas/templates/sas/picture.jinja b/sas/templates/sas/picture.jinja index 62d069d8..36214379 100644 --- a/sas/templates/sas/picture.jinja +++ b/sas/templates/sas/picture.jinja @@ -58,8 +58,13 @@
-
- +
+
@@ -137,7 +142,10 @@ {% endif %}
    - -