preload images and identifications

This commit is contained in:
thomas girod 2024-09-16 18:14:37 +02:00
parent a2a858262a
commit 813bbbb94a
2 changed files with 99 additions and 33 deletions

View File

@ -4,23 +4,78 @@
* @property {UserProfile} user The identified user * @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<void>}
*/
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<void>}
*/
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", () => { document.addEventListener("alpine:init", () => {
Alpine.data("picture_viewer", () => ({ Alpine.data("picture_viewer", () => ({
/** /**
* All the pictures that can be displayed on this picture viewer * All the pictures that can be displayed on this picture viewer
* @type Picture[] * @type PictureWithIdentifications[]
**/ **/
pictures: [], pictures: [],
/**
* The users identified on the currently displayed picture
* @type PictureIdentification[]
**/
identifications: [],
/** /**
* The currently displayed picture * The currently displayed picture
* Default dummy data are pre-loaded to avoid javascript error * Default dummy data are pre-loaded to avoid javascript error
* when loading the page at the beginning * when loading the page at the beginning
* @type Picture * @type PictureWithIdentifications
**/ **/
current_picture: { current_picture: {
is_moderated: true, is_moderated: true,
@ -32,15 +87,16 @@ document.addEventListener("alpine:init", () => {
full_size_url: "", full_size_url: "",
owner: "", owner: "",
date: new Date(), date: new Date(),
identifications: [],
}, },
/** /**
* The picture which will be displayed next if the user press the "next" button * The picture which will be displayed next if the user press the "next" button
* @type ?Picture * @type ?PictureWithIdentifications
**/ **/
next_picture: null, next_picture: null,
/** /**
* The picture which will be displayed next if the user press the "previous" button * The picture which will be displayed next if the user press the "previous" button
* @type ?Picture * @type ?PictureWithIdentifications
**/ **/
previous_picture: null, previous_picture: null,
/** /**
@ -50,7 +106,6 @@ document.addEventListener("alpine:init", () => {
/** /**
* true if the page is in a loading state, else false * true if the page is in a loading state, else false
**/ **/
loading: true,
/** /**
* Error message when a moderation operation fails * Error message when a moderation operation fails
* @type string * @type string
@ -64,11 +119,17 @@ document.addEventListener("alpine:init", () => {
pushstate: History.PUSH, pushstate: History.PUSH,
async init() { async init() {
this.pictures = await fetch_paginated(picture_endpoint); this.pictures = (await fetch_paginated(picture_endpoint)).map(
PictureWithIdentifications.from_picture,
);
this.selector = sithSelect2({ this.selector = sithSelect2({
element: $(this.$refs.search), element: $(this.$refs.search),
data_source: remote_data_source("/api/user/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 }), result_converter: (obj) => Object({ ...obj, text: obj.display_name }),
}), }),
picture_getter: (user) => user.profile_pict, picture_getter: (user) => user.profile_pict,
@ -99,8 +160,6 @@ document.addEventListener("alpine:init", () => {
* the list of identified users are updated. * the list of identified users are updated.
*/ */
async update_picture() { async update_picture() {
this.loading = true;
const update_args = [ const update_args = [
{ sas_picture_id: this.current_picture.id }, { sas_picture_id: this.current_picture.id },
"", "",
@ -117,10 +176,13 @@ document.addEventListener("alpine:init", () => {
const index = this.pictures.indexOf(this.current_picture); const index = this.pictures.indexOf(this.current_picture);
this.previous_picture = this.pictures[index - 1] || null; this.previous_picture = this.pictures[index - 1] || null;
this.next_picture = this.pictures[index + 1] || null; this.next_picture = this.pictures[index + 1] || null;
this.identifications = await ( await this.current_picture.load_identifications();
await fetch(`/api/sas/picture/${this.current_picture.id}/identified`) this.$refs.main_picture?.addEventListener("load", () => {
).json(); // once the current picture is loaded,
this.loading = false; // start preloading the next and previous pictures
this.next_picture?.preload();
this.previous_picture?.preload();
});
}, },
async moderate_picture() { async moderate_picture() {
@ -131,8 +193,7 @@ document.addEventListener("alpine:init", () => {
}, },
); );
if (!res.ok) { if (!res.ok) {
this.moderation_error = this.moderation_error = `${gettext("Couldn't moderate picture")} : ${res.statusText}`;
gettext("Couldn't moderate picture") + " : " + res.statusText;
return; return;
} }
this.current_picture.is_moderated = true; 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. * Send the identification request and update the list of identified users.
*/ */
async submit_identification() { async submit_identification() {
this.loading = true;
const url = `/api/sas/picture/${this.current_picture.id}/identified`; const url = `/api/sas/picture/${this.current_picture.id}/identified`;
await fetch(url, { await fetch(url, {
method: "PUT", method: "PUT",
body: JSON.stringify(this.selector.val().map((i) => parseInt(i))), body: JSON.stringify(this.selector.val().map((i) => parseInt(i))),
}); });
// refresh the identified users list // 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.selector.empty().trigger("change");
this.loading = false;
}, },
/** /**
@ -187,16 +246,15 @@ document.addEventListener("alpine:init", () => {
* @param {PictureIdentification} identification * @param {PictureIdentification} identification
*/ */
async remove_identification(identification) { async remove_identification(identification) {
this.loading = true;
const res = await fetch(`/api/sas/relation/${identification.id}`, { const res = await fetch(`/api/sas/relation/${identification.id}`, {
method: "DELETE", method: "DELETE",
}); });
if (res.ok) { if (res.ok && Array.isArray(this.current_picture.identifications)) {
this.identifications = this.identifications.filter( this.current_picture.identifications =
this.current_picture.identifications.filter(
(i) => i.id !== identification.id, (i) => i.id !== identification.id,
); );
} }
this.loading = false;
}, },
})); }));
}); });

View File

@ -58,8 +58,13 @@
<div class="container" id="pict"> <div class="container" id="pict">
<div class="main"> <div class="main">
<div class="photo" :aria-busy="loading"> <div class="photo" :aria-busy="current_picture.image_loading">
<img :src="current_picture.compressed_url" :alt="current_picture.name"/> <img
:src="current_picture.compressed_url"
:alt="current_picture.name"
id="main-picture"
x-ref="main_picture"
/>
</div> </div>
<div class="general"> <div class="general">
@ -137,7 +142,10 @@
</form> </form>
{% endif %} {% endif %}
<ul> <ul>
<template x-for="identification in identifications" :key="identification.id"> <template
x-for="identification in (current_picture.identifications || [])"
:key="identification.id"
>
<li> <li>
<a class="user" :href="identification.user.profile_url"> <a class="user" :href="identification.user.profile_url">
<img class="profile-pic" :src="identification.user.profile_pict" alt="image de profil"/> <img class="profile-pic" :src="identification.user.profile_pict" alt="image de profil"/>
@ -148,7 +156,7 @@
</template> </template>
</li> </li>
</template> </template>
<template x-if="loading"> <template x-if="current_picture.identifications_loading">
{# shadow element that exists only to put the loading wheel below {# shadow element that exists only to put the loading wheel below
the list of identified people #} the list of identified people #}
<li class="loader" aria-busy="true"></li> <li class="loader" aria-busy="true"></li>