mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 06:03:20 +00:00
Remove fetchPaginated and migrate viewer.js to viewer-index.js in webpack
This commit is contained in:
parent
9199f91151
commit
46e58bb49e
@ -123,37 +123,3 @@ function updateQueryString(key, value, action = History.REPLACE, url = null) {
|
|||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO : If one day a test workflow is made for JS in this project
|
|
||||||
// please test this function. A all cost.
|
|
||||||
/**
|
|
||||||
* Given a paginated endpoint, fetch all the items of this endpoint,
|
|
||||||
* performing multiple API calls if necessary.
|
|
||||||
* @param {string} url The paginated endpoint to fetch
|
|
||||||
* @return {Promise<Object[]>}
|
|
||||||
*/
|
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: used in other scripts
|
|
||||||
async function fetchPaginated(url) {
|
|
||||||
const maxPerPage = 199;
|
|
||||||
const paginatedUrl = new URL(url, document.location.origin);
|
|
||||||
paginatedUrl.searchParams.set("page_size", maxPerPage.toString());
|
|
||||||
paginatedUrl.searchParams.set("page", "1");
|
|
||||||
|
|
||||||
const firstPage = await (await fetch(paginatedUrl)).json();
|
|
||||||
const results = firstPage.results;
|
|
||||||
|
|
||||||
const nbPictures = firstPage.count;
|
|
||||||
const nbPages = Math.ceil(nbPictures / maxPerPage);
|
|
||||||
|
|
||||||
if (nbPages > 1) {
|
|
||||||
const promises = [];
|
|
||||||
for (let i = 2; i <= nbPages; i++) {
|
|
||||||
paginatedUrl.searchParams.set("page", i.toString());
|
|
||||||
promises.push(
|
|
||||||
fetch(paginatedUrl).then((res) => res.json().then((json) => json.results)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
results.push(...(await Promise.all(promises)).flat());
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
@ -37,7 +37,7 @@ import { picturesFetchPictures } from "#openapi";
|
|||||||
* Load user picture page with a nice download bar
|
* Load user picture page with a nice download bar
|
||||||
* @param {PicturePageConfig} Configuration
|
* @param {PicturePageConfig} Configuration
|
||||||
**/
|
**/
|
||||||
window.window.loadPicturePage = (config) => {
|
window.loadPicturePage = (config) => {
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("user_pictures", () => ({
|
Alpine.data("user_pictures", () => ({
|
||||||
isDownloading: false,
|
isDownloading: false,
|
||||||
|
@ -19,6 +19,8 @@ type PaginatedEndpoint<T> = <ThrowOnError extends boolean = false>(
|
|||||||
options?: Options<PaginatedRequest, ThrowOnError>,
|
options?: Options<PaginatedRequest, ThrowOnError>,
|
||||||
) => RequestResult<PaginatedResponse<T>, unknown, ThrowOnError>;
|
) => RequestResult<PaginatedResponse<T>, unknown, ThrowOnError>;
|
||||||
|
|
||||||
|
// TODO : If one day a test workflow is made for JS in this project
|
||||||
|
// please test this function. A all cost.
|
||||||
export const paginated = async <T>(
|
export const paginated = async <T>(
|
||||||
endpoint: PaginatedEndpoint<T>,
|
endpoint: PaginatedEndpoint<T>,
|
||||||
options?: PaginatedRequest,
|
options?: PaginatedRequest,
|
||||||
|
@ -1,268 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef PictureIdentification
|
|
||||||
* @property {number} id The actual id of the identification
|
|
||||||
* @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;
|
|
||||||
imageLoading = false;
|
|
||||||
identificationsLoading = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Picture} picture
|
|
||||||
*/
|
|
||||||
constructor(picture) {
|
|
||||||
Object.assign(this, picture);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Picture} picture
|
|
||||||
*/
|
|
||||||
static fromPicture(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 loadIdentifications(options) {
|
|
||||||
if (this.identificationsLoading) {
|
|
||||||
return; // The users are already being fetched.
|
|
||||||
}
|
|
||||||
if (!!this.identifications && !options?.forceReload) {
|
|
||||||
// The users are already fetched
|
|
||||||
// and the user does not want to force the reload
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.identificationsLoading = true;
|
|
||||||
const url = `/api/sas/picture/${this.id}/identified`;
|
|
||||||
this.identifications = await (await fetch(url)).json();
|
|
||||||
this.identificationsLoading = 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.imageLoading = true;
|
|
||||||
img.addEventListener("load", () => {
|
|
||||||
this.imageLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await this.loadIdentifications();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
|
||||||
Alpine.data("picture_viewer", () => ({
|
|
||||||
/**
|
|
||||||
* All the pictures that can be displayed on this picture viewer
|
|
||||||
* @type PictureWithIdentifications[]
|
|
||||||
**/
|
|
||||||
pictures: [],
|
|
||||||
/**
|
|
||||||
* The currently displayed picture
|
|
||||||
* Default dummy data are pre-loaded to avoid javascript error
|
|
||||||
* when loading the page at the beginning
|
|
||||||
* @type PictureWithIdentifications
|
|
||||||
**/
|
|
||||||
currentPicture: {
|
|
||||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
|
||||||
is_moderated: true,
|
|
||||||
id: null,
|
|
||||||
name: "",
|
|
||||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
|
||||||
display_name: "",
|
|
||||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
|
||||||
compressed_url: "",
|
|
||||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
|
||||||
profile_url: "",
|
|
||||||
// biome-ignore lint/style/useNamingConvention: json is snake_case
|
|
||||||
full_size_url: "",
|
|
||||||
owner: "",
|
|
||||||
date: new Date(),
|
|
||||||
identifications: [],
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The picture which will be displayed next if the user press the "next" button
|
|
||||||
* @type ?PictureWithIdentifications
|
|
||||||
**/
|
|
||||||
nextPicture: null,
|
|
||||||
/**
|
|
||||||
* The picture which will be displayed next if the user press the "previous" button
|
|
||||||
* @type ?PictureWithIdentifications
|
|
||||||
**/
|
|
||||||
previousPicture: null,
|
|
||||||
/**
|
|
||||||
* The select2 component used to identify users
|
|
||||||
**/
|
|
||||||
selector: undefined,
|
|
||||||
/**
|
|
||||||
* true if the page is in a loading state, else false
|
|
||||||
**/
|
|
||||||
/**
|
|
||||||
* Error message when a moderation operation fails
|
|
||||||
* @type string
|
|
||||||
**/
|
|
||||||
moderationError: "",
|
|
||||||
/**
|
|
||||||
* Method of pushing new url to the browser history
|
|
||||||
* Used by popstate event and always reset to it's default value when used
|
|
||||||
* @type History
|
|
||||||
**/
|
|
||||||
pushstate: History.PUSH,
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
|
|
||||||
this.pictures = (await fetchPaginated(pictureEndpoint)).map(
|
|
||||||
PictureWithIdentifications.fromPicture,
|
|
||||||
);
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
|
|
||||||
this.selector = sithSelect2({
|
|
||||||
element: $(this.$refs.search),
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from script.js
|
|
||||||
dataSource: remoteDataSource("/api/user/search", {
|
|
||||||
excluded: () => [
|
|
||||||
...(this.currentPicture.identifications || []).map((i) => i.user.id),
|
|
||||||
],
|
|
||||||
resultConverter: (obj) => new Object({ ...obj, text: obj.display_name }),
|
|
||||||
}),
|
|
||||||
pictureGetter: (user) => user.profile_pict,
|
|
||||||
});
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from picture.jinja
|
|
||||||
this.currentPicture = this.pictures.find((i) => i.id === firstPictureId);
|
|
||||||
this.$watch("currentPicture", (current, previous) => {
|
|
||||||
if (current === previous) {
|
|
||||||
/* Avoid recursive updates */
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.updatePicture();
|
|
||||||
});
|
|
||||||
window.addEventListener("popstate", async (event) => {
|
|
||||||
if (!event.state || event.state.sasPictureId === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.pushstate = History.REPLACE;
|
|
||||||
this.currentPicture = this.pictures.find(
|
|
||||||
(i) => i.id === Number.parseInt(event.state.sasPictureId),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this.pushstate = History.REPLACE; /* Avoid first url push */
|
|
||||||
await this.updatePicture();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the page.
|
|
||||||
* Called when the `currentPicture` property changes.
|
|
||||||
*
|
|
||||||
* The url is modified without reloading the page,
|
|
||||||
* and the previous picture, the next picture and
|
|
||||||
* the list of identified users are updated.
|
|
||||||
*/
|
|
||||||
async updatePicture() {
|
|
||||||
const updateArgs = [
|
|
||||||
{ sasPictureId: this.currentPicture.id },
|
|
||||||
"",
|
|
||||||
`/sas/picture/${this.currentPicture.id}/`,
|
|
||||||
];
|
|
||||||
if (this.pushstate === History.REPLACE) {
|
|
||||||
window.history.replaceState(...updateArgs);
|
|
||||||
this.pushstate = History.PUSH;
|
|
||||||
} else {
|
|
||||||
window.history.pushState(...updateArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.moderationError = "";
|
|
||||||
const index = this.pictures.indexOf(this.currentPicture);
|
|
||||||
this.previousPicture = this.pictures[index - 1] || null;
|
|
||||||
this.nextPicture = this.pictures[index + 1] || null;
|
|
||||||
await this.currentPicture.loadIdentifications();
|
|
||||||
this.$refs.mainPicture?.addEventListener("load", () => {
|
|
||||||
// once the current picture is loaded,
|
|
||||||
// start preloading the next and previous pictures
|
|
||||||
this.nextPicture?.preload();
|
|
||||||
this.previousPicture?.preload();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async moderatePicture() {
|
|
||||||
const res = await fetch(`/api/sas/picture/${this.currentPicture.id}/moderate`, {
|
|
||||||
method: "PATCH",
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
this.moderationError = `${gettext("Couldn't moderate picture")} : ${res.statusText}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.currentPicture.is_moderated = true;
|
|
||||||
this.currentPicture.askedForRemoval = false;
|
|
||||||
},
|
|
||||||
|
|
||||||
async deletePicture() {
|
|
||||||
const res = await fetch(`/api/sas/picture/${this.currentPicture.id}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
this.moderationError = `${gettext("Couldn't delete picture")} : ${res.statusText}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1);
|
|
||||||
if (this.pictures.length === 0) {
|
|
||||||
// The deleted picture was the only one in the list.
|
|
||||||
// As the album is now empty, go back to the parent page
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja
|
|
||||||
document.location.href = albumUrl;
|
|
||||||
}
|
|
||||||
this.currentPicture = this.nextPicture || this.previousPicture;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the identification request and update the list of identified users.
|
|
||||||
*/
|
|
||||||
async submitIdentification() {
|
|
||||||
const url = `/api/sas/picture/${this.currentPicture.id}/identified`;
|
|
||||||
await fetch(url, {
|
|
||||||
method: "PUT",
|
|
||||||
body: JSON.stringify(this.selector.val().map((i) => Number.parseInt(i))),
|
|
||||||
});
|
|
||||||
// refresh the identified users list
|
|
||||||
await this.currentPicture.loadIdentifications({ forceReload: true });
|
|
||||||
this.selector.empty().trigger("change");
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an identification can be removed by the currently logged user
|
|
||||||
* @param {PictureIdentification} identification
|
|
||||||
* @return {boolean}
|
|
||||||
*/
|
|
||||||
canBeRemoved(identification) {
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: imported from picture.jinja
|
|
||||||
return userIsSasAdmin || identification.user.id === userId;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Untag a user from the current picture
|
|
||||||
* @param {PictureIdentification} identification
|
|
||||||
*/
|
|
||||||
async removeIdentification(identification) {
|
|
||||||
const res = await fetch(`/api/sas/relation/${identification.id}`, {
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
if (res.ok && Array.isArray(this.currentPicture.identifications)) {
|
|
||||||
this.currentPicture.identifications =
|
|
||||||
this.currentPicture.identifications.filter((i) => i.id !== identification.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
302
sas/static/webpack/sas/viewer-index.js
Normal file
302
sas/static/webpack/sas/viewer-index.js
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import { paginated } from "#core:utils/api";
|
||||||
|
import {
|
||||||
|
picturesDeletePicture,
|
||||||
|
picturesFetchIdentifications,
|
||||||
|
picturesFetchPictures,
|
||||||
|
picturesIdentifyUsers,
|
||||||
|
picturesModeratePicture,
|
||||||
|
usersidentifiedDeleteRelation,
|
||||||
|
} from "#openapi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef PictureIdentification
|
||||||
|
* @property {number} id The actual id of the identification
|
||||||
|
* @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;
|
||||||
|
imageLoading = false;
|
||||||
|
identificationsLoading = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Picture} picture
|
||||||
|
*/
|
||||||
|
constructor(picture) {
|
||||||
|
Object.assign(this, picture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Picture} picture
|
||||||
|
*/
|
||||||
|
static fromPicture(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 loadIdentifications(options) {
|
||||||
|
if (this.identificationsLoading) {
|
||||||
|
return; // The users are already being fetched.
|
||||||
|
}
|
||||||
|
if (!!this.identifications && !options?.forceReload) {
|
||||||
|
// The users are already fetched
|
||||||
|
// and the user does not want to force the reload
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.identificationsLoading = true;
|
||||||
|
this.identifications = (
|
||||||
|
await picturesFetchIdentifications({
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
path: { picture_id: this.id },
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
this.identificationsLoading = 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.imageLoading = true;
|
||||||
|
img.addEventListener("load", () => {
|
||||||
|
this.imageLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.loadIdentifications();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ViewerConfig
|
||||||
|
* @param {number} userId Id of the user to get the pictures from
|
||||||
|
* @param {number} albumId Id of the album to displlay
|
||||||
|
* @param {number} firstPictureId id of the first picture to load on the page
|
||||||
|
* @param {bool} userIsSasAdmin if the user is sas admin
|
||||||
|
**/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load user picture page with a nice download bar
|
||||||
|
* @param {ViewerConfig} Configuration
|
||||||
|
**/
|
||||||
|
window.loadViewer = (config) => {
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("picture_viewer", () => ({
|
||||||
|
/**
|
||||||
|
* All the pictures that can be displayed on this picture viewer
|
||||||
|
* @type PictureWithIdentifications[]
|
||||||
|
**/
|
||||||
|
pictures: [],
|
||||||
|
/**
|
||||||
|
* The currently displayed picture
|
||||||
|
* Default dummy data are pre-loaded to avoid javascript error
|
||||||
|
* when loading the page at the beginning
|
||||||
|
* @type PictureWithIdentifications
|
||||||
|
**/
|
||||||
|
currentPicture: {
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
is_moderated: true,
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
display_name: "",
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
compressed_url: "",
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
profile_url: "",
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
full_size_url: "",
|
||||||
|
owner: "",
|
||||||
|
date: new Date(),
|
||||||
|
identifications: [],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The picture which will be displayed next if the user press the "next" button
|
||||||
|
* @type ?PictureWithIdentifications
|
||||||
|
**/
|
||||||
|
nextPicture: null,
|
||||||
|
/**
|
||||||
|
* The picture which will be displayed next if the user press the "previous" button
|
||||||
|
* @type ?PictureWithIdentifications
|
||||||
|
**/
|
||||||
|
previousPicture: null,
|
||||||
|
/**
|
||||||
|
* The select2 component used to identify users
|
||||||
|
**/
|
||||||
|
selector: undefined,
|
||||||
|
/**
|
||||||
|
* true if the page is in a loading state, else false
|
||||||
|
**/
|
||||||
|
/**
|
||||||
|
* Error message when a moderation operation fails
|
||||||
|
* @type string
|
||||||
|
**/
|
||||||
|
moderationError: "",
|
||||||
|
/**
|
||||||
|
* Method of pushing new url to the browser history
|
||||||
|
* Used by popstate event and always reset to it's default value when used
|
||||||
|
* @type History
|
||||||
|
**/
|
||||||
|
pushstate: History.PUSH,
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.pictures = (
|
||||||
|
await paginated(picturesFetchPictures, {
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
query: { album_id: config.albumId },
|
||||||
|
})
|
||||||
|
).map(PictureWithIdentifications.fromPicture);
|
||||||
|
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from sith-select2.js
|
||||||
|
this.selector = sithSelect2({
|
||||||
|
element: $(this.$refs.search),
|
||||||
|
// biome-ignore lint/correctness/noUndeclaredVariables: Imported from sith-select2.js
|
||||||
|
dataSource: remoteDataSource("/api/user/search", {
|
||||||
|
excluded: () => [
|
||||||
|
...(this.currentPicture.identifications || []).map((i) => i.user.id),
|
||||||
|
],
|
||||||
|
resultConverter: (obj) => new Object({ ...obj, text: obj.display_name }),
|
||||||
|
}),
|
||||||
|
pictureGetter: (user) => user.profile_pict,
|
||||||
|
});
|
||||||
|
this.currentPicture = this.pictures.find((i) => i.id === config.firstPictureId);
|
||||||
|
this.$watch("currentPicture", (current, previous) => {
|
||||||
|
if (current === previous) {
|
||||||
|
/* Avoid recursive updates */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.updatePicture();
|
||||||
|
});
|
||||||
|
window.addEventListener("popstate", async (event) => {
|
||||||
|
if (!event.state || event.state.sasPictureId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pushstate = History.REPLACE;
|
||||||
|
this.currentPicture = this.pictures.find(
|
||||||
|
(i) => i.id === Number.parseInt(event.state.sasPictureId),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.pushstate = History.REPLACE; /* Avoid first url push */
|
||||||
|
await this.updatePicture();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the page.
|
||||||
|
* Called when the `currentPicture` property changes.
|
||||||
|
*
|
||||||
|
* The url is modified without reloading the page,
|
||||||
|
* and the previous picture, the next picture and
|
||||||
|
* the list of identified users are updated.
|
||||||
|
*/
|
||||||
|
async updatePicture() {
|
||||||
|
const updateArgs = [
|
||||||
|
{ sasPictureId: this.currentPicture.id },
|
||||||
|
"",
|
||||||
|
`/sas/picture/${this.currentPicture.id}/`,
|
||||||
|
];
|
||||||
|
if (this.pushstate === History.REPLACE) {
|
||||||
|
window.history.replaceState(...updateArgs);
|
||||||
|
this.pushstate = History.PUSH;
|
||||||
|
} else {
|
||||||
|
window.history.pushState(...updateArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.moderationError = "";
|
||||||
|
const index = this.pictures.indexOf(this.currentPicture);
|
||||||
|
this.previousPicture = this.pictures[index - 1] || null;
|
||||||
|
this.nextPicture = this.pictures[index + 1] || null;
|
||||||
|
await this.currentPicture.loadIdentifications();
|
||||||
|
this.$refs.mainPicture?.addEventListener("load", () => {
|
||||||
|
// once the current picture is loaded,
|
||||||
|
// start preloading the next and previous pictures
|
||||||
|
this.nextPicture?.preload();
|
||||||
|
this.previousPicture?.preload();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async moderatePicture() {
|
||||||
|
const res = await picturesModeratePicture({
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
path: { picture_id: this.currentPicture.id },
|
||||||
|
});
|
||||||
|
if (res.error) {
|
||||||
|
this.moderationError = `${gettext("Couldn't moderate picture")} : ${res.statusText}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentPicture.is_moderated = true;
|
||||||
|
this.currentPicture.askedForRemoval = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deletePicture() {
|
||||||
|
const res = await picturesDeletePicture({
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
path: { picture_id: this.currentPicture.id },
|
||||||
|
});
|
||||||
|
if (res.error) {
|
||||||
|
this.moderationError = `${gettext("Couldn't delete picture")} : ${res.statusText}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pictures.splice(this.pictures.indexOf(this.currentPicture), 1);
|
||||||
|
if (this.pictures.length === 0) {
|
||||||
|
// The deleted picture was the only one in the list.
|
||||||
|
// As the album is now empty, go back to the parent page
|
||||||
|
document.location.href = config.albumUrl;
|
||||||
|
}
|
||||||
|
this.currentPicture = this.nextPicture || this.previousPicture;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the identification request and update the list of identified users.
|
||||||
|
*/
|
||||||
|
async submitIdentification() {
|
||||||
|
await picturesIdentifyUsers({
|
||||||
|
path: {
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
picture_id: this.currentPicture.id,
|
||||||
|
},
|
||||||
|
body: this.selector.val().map((i) => Number.parseInt(i)),
|
||||||
|
});
|
||||||
|
// refresh the identified users list
|
||||||
|
await this.currentPicture.loadIdentifications({ forceReload: true });
|
||||||
|
this.selector.empty().trigger("change");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an identification can be removed by the currently logged user
|
||||||
|
* @param {PictureIdentification} identification
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
canBeRemoved(identification) {
|
||||||
|
return config.userIsSasAdmin || identification.user.id === config.userId;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Untag a user from the current picture
|
||||||
|
* @param {PictureIdentification} identification
|
||||||
|
*/
|
||||||
|
async removeIdentification(identification) {
|
||||||
|
const res = await usersidentifiedDeleteRelation({
|
||||||
|
// biome-ignore lint/style/useNamingConvention: api is in snake_case
|
||||||
|
path: { relation_id: identification.id },
|
||||||
|
});
|
||||||
|
if (!res.error && Array.isArray(this.currentPicture.identifications)) {
|
||||||
|
this.currentPicture.identifications =
|
||||||
|
this.currentPicture.identifications.filter(
|
||||||
|
(i) => i.id !== identification.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
};
|
@ -5,7 +5,7 @@
|
|||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{%- block additional_js -%}
|
{%- block additional_js -%}
|
||||||
<script defer src="{{ static("sas/js/viewer.js") }}"></script>
|
<script defer src="{{ static("webpack/sas/viewer-index.js") }}"></script>
|
||||||
{%- endblock -%}
|
{%- endblock -%}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
@ -171,10 +171,14 @@
|
|||||||
{% block script %}
|
{% block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
const pictureEndpoint = "{{ url("api:pictures") + "?album_id=" + album.id|string }}";
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
const albumUrl = "{{ album.get_absolute_url() }}";
|
loadViewer({
|
||||||
const firstPictureId = {{ picture.id }}; {# id of the first picture to show after page load #}
|
albumId: {{ album.id }} ,
|
||||||
const userId = {{ user.id }};
|
albumUrl: "{{ album.get_absolute_url() }}",
|
||||||
const userIsSasAdmin = {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
|
firstPictureId: {{ picture.id }}, {# id of the first picture to show after page load #}
|
||||||
|
userId: {{ user.id }},
|
||||||
|
userIsSasAdmin: {{ (user.is_root or user.is_in_group(pk = settings.SITH_GROUP_SAS_ADMIN_ID))|tojson }}
|
||||||
|
});
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
Reference in New Issue
Block a user