3 Commits

Author SHA1 Message Date
ab929d803b Translated using Weblate (French)
Currently translated at 100.0% (499 of 499 strings)

Translated using Weblate (French)

Currently translated at 96.9% (484 of 499 strings)

Co-authored-by: Ribemont Francois <ribemont.francois+weblate@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.droposs.org/projects/drop/drop/fr/
Translation: Drop/Drop
2025-10-23 22:33:53 +00:00
2a23f4d14c Fix lints 2025-10-24 09:33:39 +11:00
b20d355527 Improve igdb metadata fetching (#257)
* improve igdb metadata fetching

    * Make sure to get images with reasonable resolution.
      By default the url igdb returns is in "t_thumb" size,
      an image of size 90x90, which is good only for the icon,
      but bad for pretty much else. This commit will make sure
      covers will be of size "t_cover_big", artworks of 1080p
      height (i.e. "t_1080p") and logos will have their original
      size ("t_original"). Maybe "t_logo_med" is more appropriate?

    * Fetch screenshots as well.

    * Use a separate image for icon and for cover.
      icon needs to be a square, and can be of low
      resolution, so the "t_thmb" size is more appropriate
      for him.

    * If there is a storyline for a game use it as a short
      description.

* IDGB -> IGDB

* use the longer text between storyline and description for description

---------

Co-authored-by: udifogiel <udifogiel@proton.me>
2025-10-24 09:25:54 +11:00
4 changed files with 109 additions and 39 deletions

View File

@ -1,3 +1,12 @@
<i18n>
{
"en": {
"↓": "↓",
"↑": "↑"
}
}
</i18n>
<template>
<div>
<div>
@ -180,7 +189,7 @@
>
{{ option.name }}
<span v-if="currentSort === option.param">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
{{ sortOrder === "asc" ? $t("↑") : $t("↓") }}
</span>
</button>
</MenuItem>
@ -500,11 +509,10 @@ await updateGames(filterQuery.value, true);
function handleSortClick(option: StoreSortOption, event: MouseEvent) {
event.stopPropagation();
if (currentSort.value === option.param) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
sortOrder.value = sortOrder.value === "asc" ? "desc" : "asc";
} else {
currentSort.value = option.param;
sortOrder.value = option.param === 'name' ? 'asc' : 'desc';
sortOrder.value = option.param === "name" ? "asc" : "desc";
}
}
</script>

View File

@ -3,16 +3,16 @@
"devices": {
"capabilities": "Capacités",
"lastConnected": "Dernière Connexion",
"noDevices": "Aucun appareil n'est connecté à vôtre compte.",
"noDevices": "Aucun appareil connecté à vôtre compte.",
"platform": "Plateforme",
"revoke": "Révoquer",
"subheader": "Gérer les appareils authorisés à accéder à votre compte Drop.",
"title": "Appareils"
},
"notifications": {
"all": "Tout voir {arrow}",
"all": "Voir tout {arrow}",
"desc": "Voir et gérer vos notifications.",
"markAllAsRead": "Tout marqué comme lu",
"markAllAsRead": "Marquer tout comme lu",
"markAsRead": "Marquer comme lu",
"none": "Pas de notification",
"notifications": "Notifications",
@ -62,6 +62,7 @@
"description": "Utiliser un code pour vous connecter à votre client Drop si vous ne pouvez pas ouvrir un navigateur web sur votre appareil.",
"title": "Connecter votre client Drop"
},
"confirmPassword": "Confirmez @:auth.password",
"displayName": "Nom d'Affichage",
"email": "Email",
"password": "Mot de passe",
@ -147,6 +148,7 @@
"auth": {
"disabled": "Compte invalide or désactivé. Merci de contacter l'administrateur du serveur.",
"invalidInvite": "Invitation invalide ou expirée",
"invalidPassState": "Le mot de passe enregistré est invalide. Merci de contacter l'administrateur du serveur.",
"invalidUserOrPass": "Nom d'utilisateur ou password invalide.",
"inviteIdRequired": "id est requis pour récupérer l'invitation",
"method": {
@ -155,6 +157,10 @@
"usernameTaken": "Nom d'utilisateur déjà pris."
},
"backHome": "{arrow} Retour a l'accueil",
"externalUrl": {
"subtitle": "Ce message n'est visible qu'aux administrateurs.",
"title": "Accès via une EXTERNAL_URL différente. Veuillez consulter la documentation."
},
"game": {
"banner": {
"description": "Drop a échoué a mettre à jour l'image de la bannière : {0}",
@ -215,6 +221,7 @@
"revokeClient": "Échec de la révocation du client",
"revokeClientFull": "Échec de la revocation du client {0}",
"signIn": "Se connecter {arrow}",
"support": "Assistance Discord",
"unknown": "Une erreur inconnue est survenue",
"upload": {
"description": "Drop n'a pas pu uploader le fichier : {0}",
@ -254,7 +261,11 @@
"admin": {
"admin": "Administration",
"metadata": "Méta",
"settings": "Paramètres",
"settings": {
"store": "Store",
"title": "Paramètres",
"tokens": "API tokens"
},
"tasks": "Tâches",
"users": "Utilisateurs"
},
@ -327,6 +338,8 @@
},
"withoutMetadata": "Importer sans les données méta"
},
"libraryHint": "Pas de bibliothèque configurée.",
"libraryHintDocsLink": "Qu'est-ce que cela veut dire ? {arrow}",
"metadata": {
"companies": {
"action": "Gérer {arrow}",
@ -340,15 +353,25 @@
"description": "Les sociétés organisent les jeux par qui les a développer ou éditer.",
"editor": {
"action": "Ajouter un jeu {plus}",
"descriptionPlaceholder": "{'<'}description{'>'}",
"developed": "Développé",
"libraryDescription": "Ajouter, supprimer ou personnaliser ce que cette société a développé et/ou publié.",
"libraryTitle": "Bibliothèque de jeux",
"noDescription": "(pas de description)",
"published": "Publié",
"uploadBanner": "Uploader bannière",
"uploadIcon": "Uplader icône"
"uploadIcon": "Uplader icône",
"websitePlaceholder": "{'<'}site web{'>'}"
},
"modals": {
"createDescription": "Créez une société pour mieux organizer vos jeux.",
"createFieldDescription": "Description de la Société",
"createFieldDescriptionPlaceholder": "Un petit studio indépendant qui...",
"createFieldName": "Nom de la société",
"createFieldNamePlaceholder": "Ma nouvelle société...",
"createFieldWebsite": "Site web de la société",
"createFieldWebsitePlaceholder": "https://exemple com/",
"createTitle": "Créer une société",
"nameDescription": "Éditer le nom de la société. Ce nom est utilisé pour trouver les jeux nouvellement importés.",
"nameTitle": "Éditer le nom de la société",
"shortDeckDescription": "Éditer la description de la company. Cela n'affecte pas la description longue (markdown).",
@ -384,12 +407,15 @@
"create": "Créer une source",
"createDesc": "Drop va utiliser cette source pour accéder à votre bibliothèque de jeux, et les rendre disponible.",
"desc": "Configurer vos sources de bibliothèques où Drop va regarder pour les nouveaux jeux et versions à importer.",
"documentationLink": "Documentation {arrow}",
"edit": "Éditer la source",
"fsDesc": "Importe les jeux à partir d'un chemin d'accès sur le disque. Cela requière une structure des dossiers basées sur la version, et qui supporte les jeux archivés.",
"fsFlatDesc": "Importe les jeux à partir d'un chemin daccès sur le disque, mais sans le sous-dossier version séparé. Utile pour migrer une bibliothèque vers Drop.",
"fsFlatTitle": "Compatibilité",
"fsPath": "Chemin daccès",
"fsPathDesc": "Un chemin daccès absolu à votre bibliothèque de jeux.",
"fsPathPlaceholder": "/mnt/jeux",
"fsTitle": "Drop-style",
"link": "Sources {arrow}",
"nameDesc": "Le nom de votre source, pour référence.",
"namePlaceholder": "Mes Nouvelle Source",
@ -447,6 +473,7 @@
"checkLater": "Vérifier plus tard pour les mises à jour.",
"delete": "Supprimer l'Article",
"filter": {
"all": "Tous les temps",
"month": "Ce mois",
"week": "Cette semaine",
"year": "Cette année"
@ -509,15 +536,19 @@
"store": {
"about": "À propos",
"commingSoon": "prochainement",
"developers": "Développeurs | Développeur | Développeurs",
"exploreMore": "Explorer plus {arrow}",
"featured": "Mis en avant",
"images": "Images de Jeux",
"lookAt": "Découvrez le maintenant",
"noDevelopers": "Pas de développeur",
"noGame": "pas de jeu",
"noFeatured": "PAS DE JEU MIS EN AVANT",
"noGame": "PAS DE JEU",
"noImages": "Pas d'image",
"noPublishers": "Pas d'éditeur.",
"noTags": "Pas de tag",
"openAdminDashboard": "Ouvrir dans le Tableau de Bord d'Administration",
"openFeatured": "Mettez des étoiles aux jeux dans l'administration de la bibliothèque {arrow}",
"platform": "Plateforme | Plateforme | Plateformes",
"publishers": "Éditeurs | Éditeur | Éditeurs",
"rating": "Note",
@ -544,7 +575,9 @@
"back": "{arrow} Retour aux Tâches",
"completedTasksTitle": "Tâches complétées",
"dailyScheduledTitle": "Tâches quotidiennes planifiées",
"execute": "{arrow} Exécuter",
"noTasksRunning": "Pas de tâche en cours",
"progress": "{0]%",
"runningTasksTitle": "Tâches en cours d'exécution",
"scheduled": {
"checkUpdateDescription": "Vérifier si Drop a une mise à jour.",
@ -588,6 +621,7 @@
"description": "Drop supporte une variété de \"mécanismes d'authentification\". Lorsque vous les activez ou les désactivez, ils sont affichés sur la page de connection pour que les utilisateurs puissent les sélectionner. Cliquer sur le menu à points pour configurer le mécanisme d'authentification.",
"disabled": "Désactivé",
"enabled": "Activé",
"enabledKey": "Activée ?",
"oidc": "OpenID Connect",
"simple": "Simple (nom d'utilisateur/mot de passe)",
"srOpenOptions": "Ouvrir les options",
@ -605,7 +639,7 @@
"createInvitation": "Créer invitation",
"description": "L'authentification simple utilise un système d'invitations pour créer les utilisateurs. Tu peux créer une invitation et optionnellement spécifier le nom d'utilisateur ou email de cet utilisateur, et un lien magique sera généré un lien magique qui peut être utilisé pour créer le compte.",
"expires": "Expire : {expiry}",
"invitationTitle": "invitations",
"invitationTitle": "Invitations",
"invite3Days": "3 jours",
"invite6Months": "6 mois",
"inviteAdminSwitchDescription": "Créer cet utilisateur en tant qu'adminstrateur",

View File

@ -72,7 +72,7 @@ interface IGDBCompanyWebsite extends IGDBItem {
}
interface IGDBCover extends IGDBItem {
url: string;
image_id: string;
}
interface IGDBSearchStub extends IGDBItem {
@ -179,7 +179,7 @@ export class IGDBProvider implements MetadataProvider {
if (response.status !== 200)
throw new Error(
`Error in IDGB \nStatus Code: ${response.status}\n${response.data}`,
`Error in IGDB \nStatus Code: ${response.status}\n${response.data}`,
);
this.accessToken = response.data.access_token;
@ -187,7 +187,7 @@ export class IGDBProvider implements MetadataProvider {
seconds: response.data.expires_in,
});
logger.info("IDGB done authorizing with twitch");
logger.info("IGDB done authorizing with twitch");
}
private async refreshCredentials() {
@ -246,39 +246,47 @@ export class IGDBProvider implements MetadataProvider {
return <T[]>response.data;
}
private async _getMediaInternal(mediaID: IGDBID, type: string) {
private async _getMediaInternal(
mediaID: IGDBID,
type: string,
size: string = "t_thumb",
) {
if (mediaID === undefined)
throw new Error(
`IGDB mediaID when getting item of type ${type} was undefined`,
);
const body = `where id = ${mediaID}; fields url;`;
const body = `where id = ${mediaID}; fields image_id;`;
const response = await this.request<IGDBCover>(type, body);
let result = "";
response.forEach((cover) => {
if (cover.url.startsWith("https:")) {
result = cover.url;
} else {
// twitch *sometimes* provides it in the format "//images.igdb.com"
result = `https:${cover.url}`;
if (!response.length || !response[0].image_id) {
throw new Error(`No image_id found for ${type} with id ${mediaID}`);
}
});
const imageId = response[0].image_id;
const result = `https://images.igdb.com/igdb/image/upload/${size}/${imageId}.jpg`;
return result;
}
private async getCoverURL(id: IGDBID) {
return await this._getMediaInternal(id, "covers");
return await this._getMediaInternal(id, "covers", "t_cover_big");
}
private async getArtworkURL(id: IGDBID) {
return await this._getMediaInternal(id, "artworks");
return await this._getMediaInternal(id, "artworks", "t_1080p");
}
private async getScreenshotURL(id: IGDBID) {
return await this._getMediaInternal(id, "screenshots", "t_1080p");
}
private async getIconURL(id: IGDBID) {
return await this._getMediaInternal(id, "covers", "t_thumb");
}
private async getCompanyLogoURl(id: IGDBID) {
return await this._getMediaInternal(id, "company_logos");
return await this._getMediaInternal(id, "company_logos", "t_original");
}
private trimMessage(msg: string, len: number) {
@ -327,7 +335,7 @@ export class IGDBProvider implements MetadataProvider {
let icon = "";
const cover = response[i].cover;
if (cover !== undefined) {
icon = await this.getCoverURL(cover);
icon = await this.getIconURL(cover);
} else {
icon = "";
}
@ -355,23 +363,26 @@ export class IGDBProvider implements MetadataProvider {
const currentGame = (await this.request<IGDBGameFull>("games", body)).at(0);
if (!currentGame) throw new Error("No game found on IGDB with that id");
context?.logger.info("Using IDGB provider.");
context?.logger.info("Using IGDB provider.");
let iconRaw;
let iconRaw, coverRaw;
const cover = currentGame.cover;
if (cover !== undefined) {
context?.logger.info("Found cover URL, using...");
iconRaw = await this.getCoverURL(cover);
iconRaw = await this.getIconURL(cover);
coverRaw = await this.getCoverURL(cover);
} else {
context?.logger.info("Missing cover URL, using fallback...");
iconRaw = jdenticon.toPng(id, 512);
coverRaw = iconRaw;
}
const icon = createObject(iconRaw);
const coverID = createObject(coverRaw);
let banner;
const images = [icon];
const images = [coverID];
for (const art of currentGame.artworks ?? []) {
const objectId = createObject(await this.getArtworkURL(art));
if (!banner) {
@ -384,6 +395,11 @@ export class IGDBProvider implements MetadataProvider {
banner = createObject(jdenticon.toPng(id, 512));
}
for (const screenshot of currentGame.screenshots ?? []) {
const objectId = createObject(await this.getScreenshotURL(screenshot));
images.push(objectId);
}
context?.progress(20);
const publishers: CompanyModel[] = [];
@ -452,13 +468,25 @@ export class IGDBProvider implements MetadataProvider {
const genres = await this.getGenres(currentGame.genres);
const deck = this.trimMessage(currentGame.summary, 280);
let description = "";
let shortDescription = "";
if (currentGame.summary.length > (currentGame.storyline?.length ?? 0)) {
description = currentGame.summary;
shortDescription = this.trimMessage(
currentGame.storyline ?? currentGame.summary,
280,
);
} else {
description = currentGame.storyline ?? currentGame.summary;
shortDescription = this.trimMessage(currentGame.summary, 280);
}
const metadata = {
id: currentGame.id.toString(),
name: currentGame.name,
shortDescription: deck,
description: currentGame.summary,
shortDescription,
description,
released,
genres,
@ -471,7 +499,7 @@ export class IGDBProvider implements MetadataProvider {
icon,
bannerId: banner,
coverId: icon,
coverId: coverID,
images,
};