Compare commits

...

9 Commits

Author SHA1 Message Date
DecDuck c001a8c808 Bump version 2025-11-30 23:18:05 +11:00
DecDuck 505df10be3 Fix accessibility with API token modal 2025-11-30 23:17:42 +11:00
DecDuck cbc8cb4ea9 Translated using Weblate (German) (#290)
Currently translated at 100.0% (522 of 522 strings)

Translated using Weblate (German)

Currently translated at 100.0% (522 of 522 strings)

Translated using Weblate (German)

Currently translated at 100.0% (522 of 522 strings)

Translated using Weblate (German)

Currently translated at 96.3% (503 of 522 strings)

Translated using Weblate (German)

Currently translated at 96.3% (503 of 522 strings)

Translated using Weblate (French)

Currently translated at 100.0% (522 of 522 strings)

Translated using Weblate (French)

Currently translated at 100.0% (522 of 522 strings)






Translate-URL: http://translate.droposs.org/projects/drop/drop/de/
Translate-URL: http://translate.droposs.org/projects/drop/drop/fr/
Translation: Drop/Drop

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Hicks <hicksgaming99+weblate@gmail.com>
Co-authored-by: Ribemont Francois <ribemont.francois+weblate@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate Translation Memory <noreply-mt-weblate-translation-memory@weblate.org>
2025-11-30 23:07:11 +11:00
DecDuck c03152f299 Internal server error fixes, 7z fixes, OIDC fixes (#289)
* fix: add no-prisma-delete lint

* fix: typescript for lint

* fix: bump droplet

* fix: oidc scopes override

* fix: type errors

* feat: delete all notifications

* fix: lint

* fix: light mode style fixes
2025-11-30 23:01:52 +11:00
Weblate c9ead88015 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-11-22 10:34:45 +11:00
Hicks-99 246c97ccc9 Add additional content screenshots for Steam provider (#284) 2025-11-21 22:27:36 +11:00
DecDuck f1fccd9bff Remove .gitlab-ci.yml 2025-11-20 16:09:16 +11:00
DecDuck 2ae7f41be0 Fix 7z archives with spaces (#288)
* fix: ignore imported versions

* fix: bump droplet for 7z fixes
2025-11-20 14:02:56 +11:00
DecDuck beb824c8d9 Add metadata timeout (#287)
* Add metadata timeout

* Fix lint
2025-11-20 11:17:58 +11:00
46 changed files with 3167 additions and 1013 deletions
-54
View File
@@ -1,54 +0,0 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
stages:
- build
services:
- docker:24.0.5-dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
build:
stage: build
image: docker:latest
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest
script:
- docker build -t $IMAGE_NAME .
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
- docker push $IMAGE_NAME
- docker push $LATEST_IMAGE_NAME
- |
if [ $CI_COMMIT_TAG ]; then
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
docker push $PUBLISH_IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
fi
build-arm64:
stage: build
image: arm64v8/docker:latest
tags:
- aarch64
variables:
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHORT_SHA-arm64
LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_NAME:latest-arm64
PUBLISH_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG-arm64
PUBLISH_LATEST_IMAGE_NAME: $CI_REGISTRY_IMAGE:latest-arm64
script:
- docker build -t $IMAGE_NAME . --platform=linux/arm64
- docker image tag $IMAGE_NAME $LATEST_IMAGE_NAME
- docker push $IMAGE_NAME
- docker push $LATEST_IMAGE_NAME
- |
if [ $CI_COMMIT_TAG ]; then
docker image tag $IMAGE_NAME $PUBLISH_IMAGE_NAME
docker image tag $IMAGE_NAME $PUBLISH_LATEST_IMAGE_NAME
docker push $PUBLISH_IMAGE_NAME
docker push $PUBLISH_LATEST_IMAGE_NAME
fi
+3 -3
View File
@@ -119,10 +119,10 @@
<div class="flex h-6 shrink-0 items-center">
<div class="group grid size-4 grid-cols-1">
<input
id="acl"
:id="`acl-${acl}`"
v-model="currentACLs[acl]"
aria-describedby="acl-description"
name="acl"
:name="`acl-${acl}`"
type="checkbox"
class="col-start-1 row-start-1 appearance-none rounded-sm border checked:border-blue-600 checked:bg-blue-600 indeterminate:border-blue-600 indeterminate:bg-blue-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 border-white/10 bg-white/5 dark:checked:border-blue-500 dark:checked:bg-blue-500 dark:indeterminate:border-blue-500 dark:indeterminate:bg-blue-500 dark:focus-visible:outline-blue-500 dark:disabled:border-white/5 dark:disabled:bg-white/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto"
/>
@@ -150,7 +150,7 @@
</div>
<div class="text-sm/6">
<label
for="acl"
:for="`acl-${acl}`"
class="font-display font-medium text-white"
>{{ acl }}</label
>
+4 -1
View File
@@ -44,9 +44,12 @@
<script setup lang="ts">
import { XMarkIcon } from "@heroicons/vue/24/solid";
import type { SerializeObject } from "nitropack";
import type { NotificationModel } from "~/prisma/client/models";
const props = defineProps<{ notification: NotificationModel }>();
const props = defineProps<{
notification: SerializeObject<NotificationModel>;
}>();
async function deleteMe() {
await $dropFetch(`/api/v1/notifications/:id`, {
@@ -46,7 +46,10 @@
</template>
<script setup lang="ts">
import type { SerializeObject } from "nitropack";
import type { NotificationModel } from "~/prisma/client/models";
const props = defineProps<{ notifications: Array<NotificationModel> }>();
const props = defineProps<{
notifications: Array<SerializeObject<NotificationModel>>;
}>();
</script>
+1 -2
View File
@@ -1,4 +1,3 @@
import type { RouteLocationNormalized } from "vue-router";
import type { NavigationItem } from "./types";
export const useCurrentNavigationIndex = (
@@ -9,7 +8,7 @@ export const useCurrentNavigationIndex = (
const currentNavigation = ref(-1);
function calculateCurrentNavIndex(to: RouteLocationNormalized) {
function calculateCurrentNavIndex(to: typeof route) {
const validOptions = navigation
.map((e, i) => ({ ...e, index: i }))
.filter((e) => to.fullPath.startsWith(e.prefix));
+6 -2
View File
@@ -1,12 +1,16 @@
import type { SerializeObject } from "nitropack";
import type { NotificationModel } from "~/prisma/client/models";
const ws = new WebSocketHandler("/api/v1/notifications/ws");
export const useNotifications = () =>
useState<Array<NotificationModel>>("notifications", () => []);
useState<Array<SerializeObject<NotificationModel>>>(
"notifications",
() => [],
);
ws.listen((e) => {
const notification = JSON.parse(e) as NotificationModel;
const notification = JSON.parse(e) as SerializeObject<NotificationModel>;
const notifications = useNotifications();
notifications.value.push(notification);
});
+5
View File
@@ -2,6 +2,7 @@
import withNuxt from "./.nuxt/eslint.config.mjs";
import eslintConfigPrettier from "eslint-config-prettier/flat";
import vueI18n from "@intlify/eslint-plugin-vue-i18n";
import noPrismaDelete from "./rules/no-prisma-delete.mts";
export default withNuxt([
eslintConfigPrettier,
@@ -19,6 +20,7 @@ export default withNuxt([
},
],
"@intlify/vue-i18n/no-missing-keys": "error",
"drop/no-prisma-delete": "error",
},
settings: {
"vue-i18n": {
@@ -29,5 +31,8 @@ export default withNuxt([
messageSyntaxVersion: "^11.0.0",
},
},
plugins: {
drop: { rules: { "no-prisma-delete": noPrismaDelete } },
},
},
]);
+29 -2
View File
@@ -106,7 +106,9 @@
"friends": "Freunde",
"groups": "Gruppen",
"insert": "Einfügen",
"labelValueColon": "{label}: {value}",
"name": "Name",
"noData": "Keine Daten",
"noResults": "Keine Ergebnisse",
"noSelected": "Keine Elemente ausgewählt.",
"remove": "Entfernen",
@@ -249,7 +251,7 @@
"documentation": "Dokumentation",
"findGame": "Finde ein Spiel",
"footer": "Fußzeile",
"games": "Spiels",
"games": "Spiele",
"social": {
"discord": "Discord",
"github": "GitHub"
@@ -260,6 +262,8 @@
"header": {
"admin": {
"admin": "Admin",
"home": "Startseite",
"library": "Bibliothek",
"metadata": "Metadaten",
"settings": {
"store": "Store",
@@ -274,7 +278,24 @@
},
"helpUsTranslate": "Hilf uns Drop zu übersetzen {arrow}",
"highest": "Höchste",
"home": "Startseite",
"home": {
"admin": {
"activeInactiveUsers": "Aktive/inaktive Benutzer",
"activeUsers": "Aktive Benutzer",
"allVersionsCombined": "Alle Versionen zusammen",
"biggestGamesOnServer": "Größte Spiele auf dem Server",
"biggestGamesToDownload": "Die größten Spiele zum Herunterladen",
"games": "Spiele",
"goToUsers": "Zu den Benutzern",
"inactiveUsers": "Inaktive Benutzer",
"latestVersionOnly": "Nur die neueste Version",
"librarySources": "Bibliotheksquellen",
"subheader": "Übersicht",
"title": "Startseite",
"users": "Benutzer",
"version": "Version"
}
},
"library": {
"addGames": "Alle Spiele",
"addToLib": "Zur Bibliothek hinzufügen",
@@ -290,6 +311,7 @@
"deleteImage": "Bild löschen",
"editGameDescription": "Spielbeschreibung",
"editGameName": "Spielname",
"editReleaseDate": "Veröffentlichungsdatum",
"imageCarousel": "Bilderkarussell",
"imageCarouselDescription": "Anpassen, welche Bilder und in welcher Reihenfolge sie auf der Shop-Seite angezeigt werden.",
"imageCarouselEmpty": "Es wurden noch keine Bilder zum Karussell hinzugefügt.",
@@ -409,6 +431,7 @@
"desc": "Konfiguriere deine Bibliotheksquellen, wo Drop nach neuen Spielen und Versionen zum Import suchen wird.",
"documentationLink": "Dokumentation {arrow}",
"edit": "Quelle bearbeiten",
"freeSpace": "Freier Speicher",
"fsDesc": "Importiert Spiele von einem Pfad auf der Festplatte. Benötigt eine versionsbasierte Ordnerstruktur und unterstützt archivierte Spiele.",
"fsFlatDesc": "Importiert Spiele von einem Pfad auf der Festplatte, jedoch ohne separate Unterordner für Versionen. Nützlich beim Migrieren einer bestehenden Bibliothek zu Drop.",
"fsFlatTitle": "Kompatibilität",
@@ -419,8 +442,11 @@
"link": "Quellen {arrow}",
"nameDesc": "Der Name deiner Quelle, als Referenz.",
"namePlaceholder": "Meine neue Quelle",
"percentage": "{number}%",
"sources": "Bibliotheksquellen",
"totalSpace": "Gesamtspeicherplatz",
"typeDesc": "Der Typ deiner Quelle. Ändert die erforderlichen Optionen.",
"utilizationPercentage": "Nutzungsgrad",
"working": "Funktioniert es?"
},
"subheader": "Wenn du Ordner zu deinen Bibliotheksquellen hinzufügst, erkennt Drop diese und fordert dich auf, sie zu importieren. Jedes Spiel muss importiert werden, bevor du eine Version importieren kannst.",
@@ -559,6 +585,7 @@
"recentlyUpdated": "Kürzlich aktualisiert",
"released": "Veröffentlicht",
"reviews": "({0} Bewertungen)",
"size": "Größe",
"tags": "Tags",
"title": "Store",
"view": {
+1
View File
@@ -13,6 +13,7 @@
"all": "View all {arrow}",
"desc": "View and manage your notifications.",
"markAllAsRead": "Mark all as read",
"clear": "Clear notifications",
"markAsRead": "Mark as read",
"none": "No notifications",
"notifications": "Notifications",
+69 -8
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",
@@ -105,7 +106,9 @@
"friends": "Amis",
"groups": "Groupes",
"insert": "Insérer",
"labelValueColon": "{label} : {value}",
"name": "Nom",
"noData": "Pas de donnée",
"noResults": "Pas de résultat",
"noSelected": "Pas d'élément sélectionné.",
"remove": "Retirer",
@@ -147,6 +150,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 +159,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 +223,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}",
@@ -253,8 +262,14 @@
"header": {
"admin": {
"admin": "Administration",
"home": "Accueil",
"library": "Bibliothèque",
"metadata": "Méta",
"settings": "Paramètres",
"settings": {
"store": "Store",
"title": "Paramètres",
"tokens": "API tokens"
},
"tasks": "Tâches",
"users": "Utilisateurs"
},
@@ -263,7 +278,24 @@
},
"helpUsTranslate": "Aidez nous à traduire Drop {arrow}",
"highest": "le plus haut",
"home": "Accueil",
"home": {
"admin": {
"activeInactiveUsers": "Utilisateurs actifs/inactifs",
"activeUsers": "Utilisateurs actifs",
"allVersionsCombined": "Toutes les versions combinées",
"biggestGamesOnServer": "Les plus gros jeux sur le serveur",
"biggestGamesToDownload": "Les plus gros jeux à télécharger",
"games": "Jeux",
"goToUsers": "Aller aux utilisateurs",
"inactiveUsers": "Utilisateurs inactifs",
"latestVersionOnly": "Dernière version seulement",
"librarySources": "Sources de bibliothèques",
"subheader": "Résumé de l'instance",
"title": "Accueil",
"users": "Utilisateurs",
"version": "Version"
}
},
"library": {
"addGames": "Tous les jeux",
"addToLib": "Ajouter à la bibliothèque",
@@ -279,6 +311,7 @@
"deleteImage": "Supprimer l'image",
"editGameDescription": "Description du jeu",
"editGameName": "Nom du jeu",
"editReleaseDate": "Date de sortie",
"imageCarousel": "Carrousel d'images",
"imageCarouselDescription": "Personnaliser quelles images et dans quel ordre elles sont affichées sur la page du Store.",
"imageCarouselEmpty": "Aucune image n'a encore été ajoutée au carousel.",
@@ -327,6 +360,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 +375,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,17 +429,24 @@
"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",
"freeSpace": "Espace disponible",
"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",
"percentage": "{number}%",
"sources": "Sources de Bibliothèques",
"totalSpace": "Espace total",
"typeDesc": "Le type de source. Affecte les options requises.",
"utilizationPercentage": "Pourcentage d'utilisation",
"working": "Marche ?"
},
"subheader": "Lorsque que vous rajoutez des dossiers à vos sources de bibliothèques, Drop le détectera et vous demandera de les importer. Chaque jeu a besoin d’être importé avant que vous puissiez importer une version.",
@@ -447,6 +499,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 +562,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",
@@ -528,6 +585,7 @@
"recentlyUpdated": "Récemment Mis à Jour",
"released": "Publié",
"reviews": "({0} Avis)",
"size": "Taille",
"tags": "Tags",
"title": "Store",
"view": {
@@ -544,7 +602,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 +648,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 +666,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",
+2 -2
View File
@@ -1,10 +1,10 @@
<template>
<div v-if="!noWrapper" class="flex flex-col w-full min-h-screen bg-zinc-900">
<UserHeader class="z-50" hydrate-on-idle />
<LazyUserHeader class="z-50" hydrate-on-idle />
<div class="grow flex">
<NuxtPage />
</div>
<UserFooter class="z-50" hydrate-on-interaction />
<LazyUserFooter class="z-50" hydrate-on-interaction />
</div>
<div v-else class="flex w-full min-h-screen bg-zinc-900">
<NuxtPage />
+3
View File
@@ -175,6 +175,9 @@ export default defineNuxtConfig({
},
i18n: {
bundle: {
optimizeTranslationDirective: false,
},
defaultLocale: "en-us",
strategy: "no_prefix",
experimental: {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "drop",
"version": "0.3.4",
"version": "0.3.5",
"private": true,
"type": "module",
"license": "AGPL-3.0-or-later",
@@ -21,7 +21,7 @@
},
"dependencies": {
"@discordapp/twemoji": "^16.0.1",
"@drop-oss/droplet": "3.2.0",
"@drop-oss/droplet": "3.5.0",
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@lobomfz/prismark": "0.0.3",
@@ -37,6 +37,7 @@
"bcryptjs": "^3.0.2",
"cheerio": "^1.0.0",
"cookie-es": "^2.0.0",
"dotenv": "^17.2.3",
"fast-fuzzy": "^1.12.0",
"file-type-mime": "^0.4.3",
"jdenticon": "^3.3.0",
@@ -65,7 +66,6 @@
"@nuxt/eslint": "^1.3.0",
"@tailwindcss/forms": "^0.5.9",
"@tailwindcss/typography": "^0.5.15",
"@types/bcryptjs": "^3.0.0",
"@types/luxon": "^3.6.2",
"@types/node": "^22.13.16",
"@types/semver": "^7.7.0",
+42 -29
View File
@@ -1,20 +1,32 @@
<template>
<div>
<div class="border-b border-zinc-800 pb-4 w-full">
<div class="flex items-center justify-between w-full">
<div
class="gap-2 flex flex-col lg:flex-row lg:items-center justify-between w-full"
>
<h2
class="text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
>
{{ $t("account.notifications.notifications") }}
</h2>
<button
:disabled="notifications.length === 0"
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800 disabled:hover:scale-100 disabled:hover:shadow-none"
@click="markAllAsRead"
>
<CheckIcon class="size-4" />
{{ $t("account.notifications.markAllAsRead") }}
</button>
<div class="inline-flex gap-x-2">
<button
:disabled="notifications.length === 0"
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm transition-all duration-200 hover:bg-zinc-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800 disabled:hover:scale-100 disabled:hover:shadow-none"
@click="markAllAsRead"
>
<CheckIcon class="size-4" />
{{ $t("account.notifications.markAllAsRead") }}
</button>
<button
:disabled="notifications.length === 0"
class="inline-flex items-center justify-center gap-x-2 rounded-md bg-red-800 px-3 py-2 text-sm font-semibold text-red-100 shadow-sm transition-all duration-200 hover:bg-red-700 hover:scale-[1.02] hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-red-800 disabled:hover:scale-100 disabled:hover:shadow-none"
@click="clearNotifications"
>
<TrashIcon class="size-4" />
{{ $t("account.notifications.clear") }}
</button>
</div>
</div>
<p
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
@@ -31,7 +43,7 @@
:class="{ 'opacity-75': notification.read }"
>
<div class="p-6">
<div class="flex items-start justify-between">
<div class="flex flex-col lg:flex-row items-start justify-between">
<div class="flex-1">
<h3 class="text-base font-semibold text-zinc-100">
{{ notification.title }}
@@ -52,7 +64,9 @@
</NuxtLink>
</div>
</div>
<div class="ml-4 flex flex-shrink-0 items-center gap-x-2">
<div
class="mt-4 lg:mt-0 lg:ml-4 flex flex-shrink-0 items-center gap-x-2"
>
<span class="text-xs text-zinc-500">
<RelativeTime :date="notification.created" />
</span>
@@ -106,22 +120,12 @@ useHead({
});
// Fetch notifications
const notifications = ref<SerializeObject<NotificationModel>[]>([]);
async function fetchNotifications() {
const { data } = await useFetch("/api/v1/notifications");
notifications.value = data.value || [];
}
// Initial fetch
await fetchNotifications();
const notifications = useNotifications();
// Mark a notification as read
async function markAsRead(id: string) {
await $dropFetch(`/api/v1/notifications/${id}/read`, { method: "POST" });
const notification = notifications.value.find(
(n: SerializeObject<NotificationModel>) => n.id === id,
);
const notification = notifications.value.find((n) => n.id === id);
if (notification) {
notification.read = true;
}
@@ -129,12 +133,21 @@ async function markAsRead(id: string) {
// Mark all notifications as read
async function markAllAsRead() {
await $dropFetch("/api/v1/notifications/readall", { method: "POST" });
notifications.value.forEach(
(notification: SerializeObject<NotificationModel>) => {
notification.read = true;
},
);
await $dropFetch("/api/v1/notifications/readall", {
method: "POST",
failTitle: "Failed to read all notifications",
});
notifications.value.forEach((notification) => {
notification.read = true;
});
}
async function clearNotifications() {
await $dropFetch("/api/v1/notifications/clear", {
method: "POST",
failTitle: "Failed to clear notifications",
});
notifications.value = [];
}
// Delete a notification
+5 -5
View File
@@ -2,7 +2,7 @@
<div class="flex flex-col">
<!-- tabs-->
<div>
<div class="border-b border-gray-200 dark:border-white/10">
<div class="border-b border-white/10">
<nav class="-mb-px flex gap-x-2" aria-label="Tabs">
<NuxtLink
v-for="(tab, tabIdx) in navigation"
@@ -10,8 +10,8 @@
:href="tab.route"
:class="[
currentNavigationIndex == tabIdx
? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-white/20 dark:hover:text-gray-300',
? 'border-blue-400 text-blue-400'
: 'border-transparent text-gray-400 hover:border-white/20 hover:text-gray-300',
'group inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium',
]"
:aria-current="tab ? 'page' : undefined"
@@ -20,8 +20,8 @@
:is="tab.icon"
:class="[
currentNavigationIndex == tabIdx
? 'text-blue-500 dark:text-blue-400'
: 'text-gray-400 group-hover:text-gray-500 dark:text-gray-500 dark:group-hover:text-gray-400',
? 'text-blue-400'
: 'text-gray-500 group-hover:text-gray-400',
'mr-2 -ml-0.5 size-5',
]"
aria-hidden="true"
+2812 -807
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,7 +1,7 @@
onlyBuiltDependencies:
- '@prisma/client'
- '@prisma/engines'
- '@tailwindcss/oxide'
- "@prisma/client"
- "@prisma/engines"
- "@tailwindcss/oxide"
- esbuild
- prisma
+3
View File
@@ -1,6 +1,9 @@
import { config } from "dotenv";
import type { PrismaConfig } from "prisma";
import path from "node:path";
config();
export default {
schema: path.join("prisma"),
earlyAccess: true,
+34
View File
@@ -0,0 +1,34 @@
import type { TSESLint } from "@typescript-eslint/utils";
export default {
meta: {
type: "problem",
docs: {
description: "Don't use Prisma error-prone .delete function",
},
messages: {
noPrismaDelete:
"Prisma .delete(...) function is used. Use .deleteMany(..) and check count instead.",
},
schema: [],
},
create(context) {
return {
CallExpression: function (node) {
// @ts-expect-error It ain't typing properly
const funcId = node.callee.property;
if (!funcId || funcId.name !== "delete") return;
// @ts-expect-error It ain't typing properly
const tableExpr = node.callee.object;
if (!tableExpr) return;
const prismaExpr = tableExpr.object;
if (!prismaExpr || prismaExpr.name !== "prisma") return;
context.report({
node,
messageId: "noPrismaDelete",
});
},
};
},
defaultOptions: [],
} satisfies TSESLint.RuleModule<"noPrismaDelete">;
@@ -17,6 +17,10 @@ export default defineEventHandler<{
const body = await readDropValidatedBody(h3, DeleteInvite);
await prisma.invitation.delete({ where: { id: body.id } });
const { count } = await prisma.invitation.deleteMany({
where: { id: body.id },
});
if (count == 0)
throw createError({ statusCode: 404, message: "Invitation not found." });
return {};
});
@@ -7,7 +7,7 @@ export default defineEventHandler(async (h3) => {
const gameId = getRouterParam(h3, "id")!;
libraryManager.deleteGame(gameId);
await libraryManager.deleteGame(gameId);
return {};
});
@@ -18,11 +18,13 @@ export default defineEventHandler<{ body: typeof DeleteLibrarySource.infer }>(
const body = await readDropValidatedBody(h3, DeleteLibrarySource);
await prisma.library.delete({
const { count } = await prisma.library.deleteMany({
where: {
id: body.id,
},
});
if (count == 0)
throw createError({ statusCode: 404, message: "Library not found." });
libraryManager.removeLibrary(body.id);
},
@@ -13,10 +13,10 @@ export default defineEventHandler(async (h3) => {
statusMessage: "No id in router params",
});
const deleted = await prisma.aPIToken.delete({
const { count } = await prisma.aPIToken.deleteMany({
where: { id: id, mode: APITokenMode.System },
})!;
if (!deleted)
if (count == 0)
throw createError({ statusCode: 404, statusMessage: "Token not found" });
return;
@@ -27,6 +27,7 @@ export default defineEventHandler(async (h3) => {
if (!user)
throw createError({ statusCode: 404, statusMessage: "User not found." });
// eslint-disable-next-line drop/no-prisma-delete
await prisma.user.delete({ where: { id: userId } });
await userStatsManager.deleteUser();
return { success: true };
+1 -1
View File
@@ -84,7 +84,7 @@ export default defineEventHandler<{
user: true,
},
}),
prisma.invitation.delete({ where: { id: user.invitation } }),
prisma.invitation.deleteMany({ where: { id: user.invitation } }),
]);
await userStatsManager.addUser();
+1 -1
View File
@@ -42,7 +42,7 @@ export default defineEventHandler(async (h3) => {
)
throw createError({
statusCode: 400,
statusMessage: "Invalid capabilities.",
message: "Invalid capabilities.",
});
if (
+4 -15
View File
@@ -17,21 +17,10 @@ export default defineClientEventHandler(async (h3) => {
orderBy: {
versionIndex: "desc", // Latest one first
},
omit: {
dropletManifest: true,
},
});
const mappedVersions = versions
.map((version) => {
if (!version.dropletManifest) return undefined;
const newVersion = { ...version, dropletManifest: undefined };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore idk why we delete an undefined object
delete newVersion.dropletManifest;
return {
...newVersion,
};
})
.filter((e) => e);
return mappedVersions;
return versions;
});
@@ -38,16 +38,14 @@ export default defineClientEventHandler(
if (!game)
throw createError({ statusCode: 400, statusMessage: "Invalid game ID" });
const save = await prisma.saveSlot.delete({
const { count } = await prisma.saveSlot.deleteMany({
where: {
id: {
userId: user.id,
gameId: gameId,
index: slotIndex,
},
userId: user.id,
gameId: gameId,
index: slotIndex,
},
});
if (!save)
if (count == 0)
throw createError({ statusCode: 404, statusMessage: "Save not found" });
},
);
@@ -20,14 +20,14 @@ export default defineEventHandler(async (h3) => {
userIds.push("system");
}
const notification = await prisma.notification.delete({
const { count } = await prisma.notification.deleteMany({
where: {
id: notificationId,
userId: { in: userIds },
},
});
if (!notification)
if (count == 0)
throw createError({
statusCode: 400,
statusMessage: "Invalid notification ID",
+25
View File
@@ -0,0 +1,25 @@
import aclManager from "~/server/internal/acls";
import prisma from "~/server/internal/db/database";
export default defineEventHandler(async (h3) => {
const userId = await aclManager.getUserIdACL(h3, ["notifications:mark"]);
if (!userId) throw createError({ statusCode: 403 });
const acls = await aclManager.fetchAllACLs(h3);
if (!acls)
throw createError({
statusCode: 500,
statusMessage: "Got userId but no ACLs - what?",
});
await prisma.notification.deleteMany({
where: {
userId,
acls: {
hasSome: acls,
},
},
});
return;
});
@@ -13,10 +13,10 @@ export default defineEventHandler(async (h3) => {
statusMessage: "No id in router params",
});
const deleted = await prisma.aPIToken.delete({
const { count } = await prisma.aPIToken.deleteMany({
where: { id: id, userId: userId, mode: APITokenMode.User },
})!;
if (!deleted)
if (count == 0)
throw createError({ statusCode: 404, statusMessage: "Token not found" });
return;
+4 -1
View File
@@ -66,6 +66,7 @@ export class OIDCManager {
async create() {
const wellKnownUrl = process.env.OIDC_WELLKNOWN as string | undefined;
const scopes = process.env.OIDC_SCOPES as string | undefined;
let configuration: OIDCWellKnown;
if (wellKnownUrl) {
const response: OIDCWellKnown = await $fetch<OIDCWellKnown>(wellKnownUrl);
@@ -77,6 +78,9 @@ export class OIDCManager {
) {
throw new Error("Well known response was invalid");
}
if (scopes) {
response.scopes_supported = scopes.split(",");
}
configuration = response;
} else {
@@ -85,7 +89,6 @@ export class OIDCManager {
| undefined;
const tokenEndpoint = process.env.OIDC_TOKEN as string | undefined;
const userinfoEndpoint = process.env.OIDC_USERINFO as string | undefined;
const scopes = process.env.OIDC_SCOPES as string | undefined;
if (
!authorizationEndpoint ||
+4
View File
@@ -185,15 +185,19 @@ export class ClientHandler {
}
async removeClient(id: string) {
const client = await prisma.client.findUnique({ where: { id } });
if (!client) return false;
const ca = useCertificateAuthority();
await ca.blacklistClient(id);
// eslint-disable-next-line drop/no-prisma-delete
await prisma.client.delete({
where: {
id,
},
});
await userStatsManager.cacheUserStats();
return true;
}
}
+6
View File
@@ -4,6 +4,8 @@ class SystemConfig {
private libraryFolder = process.env.LIBRARY ?? "./.data/library";
private dataFolder = process.env.DATA ?? "./.data/data";
private metadataTimeout = parseInt(process.env.METADATA_TIMEOUT ?? "5000");
private externalUrl = normalizeUrl(
process.env.EXTERNAL_URL ?? "http://localhost:3000",
{ stripWWW: false },
@@ -28,6 +30,10 @@ class SystemConfig {
return this.dataFolder;
}
getMetadataTimeout() {
return this.metadataTimeout;
}
getDropVersion() {
return this.dropVersion;
}
+9 -8
View File
@@ -105,7 +105,10 @@ class LibraryManager {
if (!game) return undefined;
try {
const versions = await provider.listVersions(libraryPath);
const versions = await provider.listVersions(
libraryPath,
game.versions.map((v) => v.versionName),
);
const unimportedVersions = versions.filter(
(e) =>
game.versions.findIndex((v) => v.versionName == e) == -1 &&
@@ -375,12 +378,10 @@ class LibraryManager {
}
async deleteGameVersion(gameId: string, version: string) {
await prisma.gameVersion.delete({
await prisma.gameVersion.deleteMany({
where: {
gameId_versionName: {
gameId: gameId,
versionName: version,
},
gameId: gameId,
versionName: version,
},
});
@@ -388,12 +389,12 @@ class LibraryManager {
}
async deleteGame(gameId: string) {
await prisma.game.delete({
await prisma.game.deleteMany({
where: {
id: gameId,
},
});
gameSizeManager.deleteGame(gameId);
await gameSizeManager.deleteGame(gameId);
}
async getGameVersionSize(
+4 -1
View File
@@ -24,7 +24,10 @@ export abstract class LibraryProvider<CFG> {
* @param game folder name of the game to list versions for
* @returns list of version folder names
*/
abstract listVersions(game: string): Promise<string[]>;
abstract listVersions(
game: string,
existingPaths?: string[],
): Promise<string[]>;
/**
* @param game folder name of the game
+11 -12
View File
@@ -54,11 +54,15 @@ export class FilesystemProvider
return folderDirs;
}
async listVersions(game: string): Promise<string[]> {
async listVersions(
game: string,
ignoredVersions?: string[],
): Promise<string[]> {
const gameDir = path.join(this.config.baseDir, game);
if (!fs.existsSync(gameDir)) throw new GameNotFoundError();
const versionDirs = fs.readdirSync(gameDir);
const validVersionDirs = versionDirs.filter((e) => {
if (ignoredVersions && ignoredVersions.includes(e)) return false;
const fullDir = path.join(this.config.baseDir, game, e);
return DROPLET_HANDLER.hasBackendForPath(fullDir);
});
@@ -109,17 +113,12 @@ export class FilesystemProvider
) {
const filepath = path.join(this.config.baseDir, game, version);
if (!fs.existsSync(filepath)) return undefined;
let stream;
while (!(stream instanceof ReadableStream)) {
const v = DROPLET_HANDLER.readFile(
filepath,
filename,
options?.start ? BigInt(options.start) : undefined,
options?.end ? BigInt(options.end) : undefined,
);
if (!v) return undefined;
stream = v.getStream() as ReadableStream<unknown>;
}
const stream = DROPLET_HANDLER.readFile(
filepath,
filename,
options?.start ? BigInt(options.start) : undefined,
options?.end ? BigInt(options.end) : undefined,
);
return stream;
}
+1 -1
View File
@@ -112,7 +112,7 @@ export class FlatFilesystemProvider
);
if (!stream) return undefined;
return stream.getStream();
return stream;
}
fsStats() {
+4
View File
@@ -82,6 +82,10 @@ export class MetadataHandler {
// TODO: fix eslint error
// eslint-disable-next-line no-async-promise-executor
>(async (resolve, reject) => {
setTimeout(
() => reject(new Error("Timeout while fetching results")),
systemConfig.getMetadataTimeout(),
);
try {
const results = await provider.search(query);
const mappedResults: InternalGameMetadataResult[] = results.map(
+12 -4
View File
@@ -117,6 +117,10 @@ interface SteamAppDetailsLarge extends SteamAppDetailsSmall {
filename: string;
ordinal: number;
}[];
mature_content_screenshots: {
filename: string;
ordinal: number;
}[];
};
full_description: string;
}
@@ -689,16 +693,20 @@ export class SteamProvider implements MetadataProvider {
context?.progress(40);
const images = [cover, banner];
const screenshotCount = game.screenshots?.all_ages_screenshots?.length || 0;
context?.logger.info(`Processing ${screenshotCount} screenshots...`);
for (const image of game.screenshots?.all_ages_screenshots || []) {
const screenshots = game.screenshots?.all_ages_screenshots || [];
screenshots.push(...(game.screenshots?.mature_content_screenshots || []));
screenshots.sort((a, b) => a.ordinal - b.ordinal);
context?.logger.info(`Processing ${screenshots.length} screenshots...`);
for (const image of screenshots) {
const imageUrl = this._getImageUrl(image.filename);
images.push(createObject(imageUrl));
}
context?.logger.info(
`Image processing complete: icon, cover, banner and ${screenshotCount} screenshots`,
`Image processing complete: icon, cover, banner and ${screenshots.length} screenshots`,
);
context?.progress(50);
+4 -1
View File
@@ -124,7 +124,10 @@ class NewsManager {
}
async delete(id: string) {
const article = await prisma.article.delete({
const article = await prisma.article.findUnique({ where: { id } });
if (!article) return false;
// eslint-disable-next-line drop/no-prisma-delete
await prisma.article.delete({
where: { id },
});
if (article.imageObjectId) {
+5 -11
View File
@@ -259,16 +259,10 @@ class FsHashStore {
*/
async delete(id: ObjectReference) {
await this.cache.remove(id);
try {
// need to catch in case the object doesn't exist
await prisma.objectHash.delete({
where: {
id,
},
});
} catch {
/* empty */
}
await prisma.objectHash.deleteMany({
where: {
id,
},
});
}
}
+6 -2
View File
@@ -53,12 +53,16 @@ class ScreenshotManager {
* @param id
*/
async delete(id: string) {
const deletedScreenshot = await prisma.screenshot.delete({
const screenshot = await prisma.screenshot.findUnique({ where: { id } });
if (!screenshot) return false;
// eslint-disable-next-line drop/no-prisma-delete
await prisma.screenshot.delete({
where: {
id,
},
});
await objectHandler.deleteAsSystem(deletedScreenshot.objectId);
await objectHandler.deleteAsSystem(screenshot.objectId);
return true;
}
/**
+2 -2
View File
@@ -43,12 +43,12 @@ export default function createDBSessionHandler(): SessionProvider {
},
async removeSession(token) {
await cache.remove(token);
await prisma.session.delete({
const { count } = await prisma.session.deleteMany({
where: {
token,
},
});
return true;
return count > 0;
},
async cleanupSessions() {
const now = new Date();
+12 -14
View File
@@ -101,19 +101,16 @@ class UserLibraryManager {
async collectionRemove(gameId: string, collectionId: string, userId: string) {
// Delete if exists
return (
(
await prisma.collectionEntry.deleteMany({
where: {
collectionId,
gameId,
collection: {
userId,
},
},
})
).count > 0
);
const { count } = await prisma.collectionEntry.deleteMany({
where: {
collectionId,
gameId,
collection: {
userId,
},
},
});
return count > 0;
}
async collectionCreate(name: string, userId: string) {
@@ -133,12 +130,13 @@ class UserLibraryManager {
}
async deleteCollection(collectionId: string) {
await prisma.collection.delete({
const { count } = await prisma.collection.deleteMany({
where: {
id: collectionId,
isDefault: false,
},
});
return count > 0;
}
}
+2 -1
View File
@@ -2,6 +2,7 @@
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"exactOptionalPropertyTypes": false
"exactOptionalPropertyTypes": false,
"allowImportingTsExtensions": true
}
}