Compare commits

..

23 Commits

Author SHA1 Message Date
Chubby Granny Chaser
497f5e7742 Merge pull request #1491 from Lianela/main
feat: update es translation.json
2025-03-09 22:10:59 +00:00
Chubby Granny Chaser
199b0d5b19 Merge branch 'main' into main 2025-03-09 22:08:09 +00:00
Chubby Granny Chaser
2554dc4c69 feat: updating steam-games.json 2025-03-09 21:47:27 +00:00
Chubby Granny Chaser
c4c401e054 Merge pull request #1495 from hydralauncher/feat/automatic-cloud-sync
feat: adding automatic cloud sync
2025-03-09 21:38:13 +00:00
Chubby Granny Chaser
864ff0070f chore: bump version 2025-03-09 21:33:09 +00:00
Chubby Granny Chaser
da8c40d5dc Merge branch 'feat/automatic-cloud-sync' of github.com:hydralauncher/hydra into feat/automatic-cloud-sync 2025-03-09 19:36:14 +00:00
Chubby Granny Chaser
a32fdf3385 style: using fs promises 2025-03-09 19:28:47 +00:00
Chubby Granny Chaser
c9c1750afb Merge branch 'main' into feat/automatic-cloud-sync 2025-03-09 19:18:22 +00:00
Chubby Granny Chaser
857063d2c7 feat: adding automatic cloud sync 2025-03-09 19:17:59 +00:00
Chubby Granny Chaser
ed699a8dee feat: adding automatic cloud sync 2025-03-09 19:14:24 +00:00
Lianela
778e921594 fix: translation.json
spacing
2025-03-05 22:53:34 -06:00
Lianela
206886c091 feat: update es translation.json 2025-03-05 20:26:58 -06:00
Zamitto
af2896efc3 chore: bump version 2025-03-04 17:47:25 -03:00
Chubby Granny Chaser
c7735362e0 Merge pull request #1486 from hydralauncher/feat/dynamic-badges
feat: adding dynamic badges
2025-03-04 20:45:36 +00:00
Chubby Granny Chaser
886e176b08 fix: using value-encoding for level value 2025-03-04 20:36:40 +00:00
Chubby Granny Chaser
f522a7c9ef feat: moving props spread 2025-03-04 20:10:04 +00:00
Chubby Granny Chaser
69f4ce821f feat: moving props spread 2025-03-04 20:00:51 +00:00
Chubby Granny Chaser
8bfd6e5547 Update src/renderer/src/components/avatar/avatar.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-04 19:58:27 +00:00
Chubby Granny Chaser
1cbde684e7 fix: fixing duplicate import for level 2025-03-04 19:48:03 +00:00
Chubby Granny Chaser
e5f7e9addc feat: adding dynamic badges 2025-03-04 19:44:35 +00:00
Zamitto
8513f83169 Merge pull request #1467 from hydrasources/patch-13
Update translation.json
2025-02-22 20:17:07 -03:00
hydrasources
b116e29dc0 Update translation.json 2025-02-21 22:24:00 +03:00
hydrasources
52b291fb24 Update translation.json 2025-02-21 07:41:39 +03:00
40 changed files with 449 additions and 230 deletions

View File

@@ -10,7 +10,7 @@ jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
@@ -23,14 +23,14 @@ jobs:
with:
node-version: 20.18.0
- name: Install dependencies
run: yarn
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install dependencies
run: yarn
- name: Install dependencies
run: pip install -r requirements.txt
@@ -47,19 +47,7 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- name: Build macOS
if: matrix.os == 'macos-latest'
run: yarn build:mac
env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -73,13 +61,14 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
- name: Upload build
- name: Test Upload build
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}

View File

@@ -12,7 +12,7 @@ jobs:
build:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
@@ -25,14 +25,14 @@ jobs:
with:
node-version: 20.18.0
- name: Install dependencies
run: yarn
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install dependencies
run: yarn
- name: Install dependencies
run: pip install -r requirements.txt
@@ -49,6 +49,8 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -62,6 +64,8 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.2.2",
"version": "3.3.0",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -71,6 +71,7 @@
"react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
"react-tooltip": "^5.28.0",
"sound-play": "^1.1.0",
"sudo-prompt": "^9.2.1",
"tar": "^7.4.3",

View File

@@ -6,4 +6,3 @@ psutil
Pillow
flask
aria2p

View File

@@ -20,7 +20,7 @@ const s3 = new S3Client({
const dist = path.resolve(__dirname, "..", "dist");
const extensionsToUpload = [".deb", ".exe", ".pacman", ".dmg"];
const extensionsToUpload = [".deb", ".exe", ".pacman"];
fs.readdir(dist, async (err, files) => {
if (err) throw err;

File diff suppressed because one or more lines are too long

View File

@@ -178,6 +178,8 @@
"manage_files_description": "Manage which files will be backed up and restored",
"select_folder": "Select folder",
"backup_from": "Backup from {{date}}",
"automatic_backup_from": "Automatic backup from {{date}}",
"enable_automatic_cloud_sync": "Enable automatic cloud sync",
"custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected",
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
@@ -444,9 +446,6 @@
"show_achievements_on_profile": "Show your achievements on your profile",
"show_points_on_profile": "Show your earned points on your profile"
},
"badge": {
"badge_description_theme_creator": "Awarded to those who created a custom theme"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",
"user_achievements": "{{displayName}}'s Achievements",

View File

@@ -174,6 +174,8 @@
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
"select_folder": "Seleccionar carpeta",
"backup_from": "Copia de seguridad de {{date}}",
"automatic_backup_from": "Copia de seguridad automática de {{date}}",
"enable_automatic_cloud_sync": "Habilitar sincronización automática en la nube",
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
"clear": "Limpiar",
"no_directory_selected": "No se seleccionó un directorio",
@@ -185,7 +187,13 @@
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
"reset_achievements_title": "¿Estás seguro?",
"reset_achievements_success": "Logros reiniciados exitosamente",
"reset_achievements_error": "Se produjo un error al reiniciar los logros"
"reset_achievements_error": "Se produjo un error al reiniciar los logros",
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
"download_error_not_cached_in_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
"download_error_not_cached_in_torbox": "Esta descarga no está disponible en Torbox y el estado de descarga del sondeo aún no está disponible.",
"game_added_to_favorites": "Juego añadido a favoritos",
"game_removed_from_favorites": "Juego removido de favoritos"
},
"activation": {
"title": "Activar Hydra",
@@ -297,7 +305,37 @@
"subscription_renew_cancelled": "Está desactivada la renovación automática",
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
"update_email": "Actualizar correo",
"update_password": "Actualizar contraseña"
"update_password": "Actualizar contraseña",
"appearance": "Apariencia",
"become_subscriber": "Sé Hydra Cloud",
"cancel": "Cancelar",
"clear_themes": "Limpiar",
"create_theme": "Crear",
"create_theme_modal_description": "Crea un nuevo tema para personalizar la apariencia de Hydra",
"create_theme_modal_title": "Crear tema personalizado",
"delete_all_themes": "Eliminar todos los temas",
"delete_all_themes_description": "Esto eliminará todos tus temas personalizados",
"delete_theme": "Eliminar tema",
"delete_theme_description": "Esto eliminará el tema {{theme}}",
"edit_theme": "Editar tema",
"editor_tab_code": "Código",
"editor_tab_info": "Info",
"editor_tab_save": "Guardar",
"enable_torbox": "Habilitar Torbox",
"error_importing_theme": "Error al importar el tema",
"import_theme": "Importar tema",
"import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas",
"insert_theme_name": "Introducí el nombre del tema",
"name_min_length": "El tema tiene que tener 3 carácteres de largo mínimo",
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para crear tu primer tema.",
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
"set_theme": "Establecer tema",
"theme_imported": "Tema importado exitosamente",
"theme_name": "Nombre",
"torbox_account_linked": "Cuenta de TorBox vinculada",
"torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.",
"unset_theme": "Desactivar tema",
"web_store": "Tienda Web"
},
"notifications": {
"download_complete": "Descarga completada",

View File

@@ -165,6 +165,8 @@
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
"achievements_not_sync": "Veja como exibir suas conquistas no perfil",
"backup_from": "Backup de {{date}}",
"automatic_backup_from": "Backup automático de {{date}}",
"enable_automatic_cloud_sync": "Habilitar sincronização automática na nuvem",
"custom_backup_location_set": "Localização customizada selecionada",
"select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
@@ -440,9 +442,6 @@
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
},
"badge": {
"badge_description_theme_creator": "Concedido àqueles que criaram um tema customizado"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
"your_achievements": "Suas Conquistas",

View File

@@ -178,6 +178,8 @@
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
"select_folder": "Выбрать папку",
"backup_from": "Резервная копия от {{date}}",
"automatic_backup_from": "Автоматическая резервная копия от {{date}}",
"enable_automatic_cloud_sync": "Включить автоматическую синхронизацию в облаке",
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
"no_directory_selected": "Не выбран каталог",
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",

View File

@@ -1,44 +1,8 @@
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
import { CloudSync } from "@main/services";
import { registerEvent } from "../register-event";
import fs from "node:fs";
import path from "node:path";
import * as tar from "tar";
import crypto from "node:crypto";
import type { GameShop } from "@types";
import axios from "axios";
import os from "node:os";
import { backupsPath } from "@main/constants";
import { app } from "electron";
import { normalizePath } from "@main/helpers";
import { gamesSublevel, levelKeys } from "@main/level";
const bundleBackup = async (
shop: GameShop,
objectId: string,
winePrefix: string | null
) => {
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
// Remove existing backup
if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath, { recursive: true });
}
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
await tar.create(
{
gzip: false,
file: tarLocation,
cwd: backupPath,
},
["."]
);
return tarLocation;
};
import { t } from "i18next";
import { format } from "date-fns";
const uploadSaveGame = async (
_event: Electron.IpcMainInvokeEvent,
@@ -46,61 +10,15 @@ const uploadSaveGame = async (
shop: GameShop,
downloadOptionTitle: string | null
) => {
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
const bundleLocation = await bundleBackup(
shop,
return CloudSync.uploadSaveGame(
objectId,
game?.winePrefixPath ?? null
shop,
downloadOptionTitle,
t("backup_from", {
ns: "game_details",
date: format(new Date(), "dd/MM/yyyy"),
})
);
fs.stat(bundleLocation, async (err, stat) => {
if (err) {
logger.error("Failed to get zip file stats", err);
throw err;
}
const { uploadUrl } = await HydraApi.post<{
id: string;
uploadUrl: string;
}>("/profile/games/artifacts", {
artifactLengthInBytes: stat.size,
shop,
objectId,
hostname: os.hostname(),
homeDir: normalizePath(app.getPath("home")),
downloadOptionTitle,
platform: os.platform(),
});
fs.readFile(bundleLocation, async (err, fileBuffer) => {
if (err) {
logger.error("Failed to read zip file", err);
throw err;
}
await axios.put(uploadUrl, fileBuffer, {
headers: {
"Content-Type": "application/tar",
},
onUploadProgress: (progressEvent) => {
logger.log(progressEvent);
},
});
WindowManager.mainWindow?.webContents.send(
`on-upload-complete-${objectId}-${shop}`,
true
);
fs.rm(bundleLocation, (err) => {
if (err) {
logger.error("Failed to remove tar file", err);
throw err;
}
});
});
});
};
registerEvent("uploadSaveGame", uploadSaveGame);

View File

@@ -31,11 +31,13 @@ import "./library/remove-game";
import "./library/remove-game-from-library";
import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./library/toggle-automatic-cloud-sync";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";
import "./misc/get-features";
import "./misc/show-item-in-folder";
import "./misc/get-badges";
import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download";
@@ -58,6 +60,7 @@ import "./user/get-blocked-users";
import "./user/block-user";
import "./user/unblock-user";
import "./user/get-user-friends";
import "./user/get-auth";
import "./user/get-user-stats";
import "./user/report-user";
import "./user/get-unlocked-achievements";

View File

@@ -0,0 +1,23 @@
import { registerEvent } from "../register-event";
import { levelKeys, gamesSublevel } from "@main/level";
import type { GameShop } from "@types";
const toggleAutomaticCloudSync = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string,
automaticCloudSync: boolean
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
await gamesSublevel.put(gameKey, {
...game,
automaticCloudSync,
});
};
registerEvent("toggleAutomaticCloudSync", toggleAutomaticCloudSync);

View File

@@ -0,0 +1,22 @@
import { Badge } from "@types";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { db, levelKeys } from "@main/level";
const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
const language = await db
.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
})
.then((language) => language || "en");
const params = new URLSearchParams({
locale: language,
});
return HydraApi.get<Badge[]>(`/badges?${params.toString()}`, null, {
needsAuth: false,
});
};
registerEvent("getBadges", getBadges);

View File

@@ -0,0 +1,11 @@
import { db, levelKeys } from "@main/level";
import type { Auth } from "@types";
import { registerEvent } from "../register-event";
const getAuth = async (_event: Electron.IpcMainInvokeEvent) =>
db.get<string, Auth>(levelKeys.auth, {
valueEncoding: "json",
});
registerEvent("getAuth", getAuth);

View File

@@ -62,8 +62,6 @@ export const loadState = async () => {
game.uri !== null
);
console.log("downloadsToSeed", downloadsToSeed);
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
startMainLoop();
@@ -132,7 +130,9 @@ const migrateFromSqlite = async () => {
);
if (rest.language) {
await db.put(levelKeys.language, rest.language);
await db.put<string, string>(levelKeys.language, rest.language, {
valueEncoding: "utf-8",
});
}
}
})

View File

@@ -0,0 +1,112 @@
import { levelKeys, gamesSublevel, db } from "@main/level";
import { app } from "electron";
import path from "node:path";
import * as tar from "tar";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import type { GameShop, User } from "@types";
import { backupsPath } from "@main/constants";
import { HydraApi } from "./hydra-api";
import { normalizePath } from "@main/helpers";
import { logger } from "./logger";
import { WindowManager } from "./window-manager";
import axios from "axios";
import { Ludusavi } from "./ludusavi";
import { isFuture, isToday } from "date-fns";
import { SubscriptionRequiredError } from "@shared";
export class CloudSync {
private static async bundleBackup(
shop: GameShop,
objectId: string,
winePrefix: string | null
) {
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
// Remove existing backup
if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath, { recursive: true });
}
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
await tar.create(
{
gzip: false,
file: tarLocation,
cwd: backupPath,
},
["."]
);
return tarLocation;
}
public static async uploadSaveGame(
objectId: string,
shop: GameShop,
downloadOptionTitle: string | null,
label?: string
) {
const hasActiveSubscription = await db
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
.then((user) => {
const expiresAt = user?.subscription?.expiresAt;
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
});
if (!hasActiveSubscription) {
throw new SubscriptionRequiredError();
}
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
const bundleLocation = await this.bundleBackup(
shop,
objectId,
game?.winePrefixPath ?? null
);
const stat = await fs.promises.stat(bundleLocation);
const { uploadUrl } = await HydraApi.post<{
id: string;
uploadUrl: string;
}>("/profile/games/artifacts", {
artifactLengthInBytes: stat.size,
shop,
objectId,
hostname: os.hostname(),
homeDir: normalizePath(app.getPath("home")),
downloadOptionTitle,
platform: os.platform(),
label,
});
const fileBuffer = await fs.promises.readFile(bundleLocation);
await axios.put(uploadUrl, fileBuffer, {
headers: {
"Content-Type": "application/tar",
},
onUploadProgress: (progressEvent) => {
logger.log(progressEvent);
},
});
WindowManager.mainWindow?.webContents.send(
`on-upload-complete-${objectId}-${shop}`,
true
);
fs.rm(bundleLocation, (err) => {
if (err) {
logger.error("Failed to remove tar file", err);
throw err;
}
});
}
}

View File

@@ -6,6 +6,7 @@ import type {
TorBoxAddTorrentRequest,
TorBoxRequestLinkRequest,
} from "@types";
import { appVersion } from "@main/constants";
export class TorBoxClient {
private static instance: AxiosInstance;
@@ -18,6 +19,7 @@ export class TorBoxClient {
baseURL: this.baseURL,
headers: {
Authorization: `Bearer ${apiToken}`,
"User-Agent": `Hydra/${appVersion}`,
},
});
}

View File

@@ -7,3 +7,4 @@ export * from "./process-watcher";
export * from "./main-loop";
export * from "./hydra-api";
export * from "./ludusavi";
export * from "./cloud-sync";

View File

@@ -6,6 +6,9 @@ import axios from "axios";
import { exec } from "child_process";
import { ProcessPayload } from "./download/types";
import { gamesSublevel, levelKeys } from "@main/level";
import { t } from "i18next";
import { CloudSync } from "./cloud-sync";
import { format } from "date-fns";
const commands = {
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
@@ -225,6 +228,18 @@ function onOpenGame(game: Game) {
if (game.remoteId) {
updateGamePlaytime(game, 0, new Date()).catch(() => {});
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
game.objectId,
game.shop,
null,
t("automatic_backup_from", {
ns: "game_details",
date: format(new Date(), "dd/MM/yyyy"),
})
);
}
} else {
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
}
@@ -287,6 +302,18 @@ const onCloseGame = (game: Game) => {
performance.now() - gamePlaytime.lastSyncTick,
game.lastTimePlayed!
).catch(() => {});
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
game.objectId,
game.shop,
null,
t("automatic_backup_from", {
ns: "game_details",
date: format(new Date(), "dd/MM/yyyy"),
})
);
}
} else {
createGame(game).catch(() => {});
}

View File

@@ -221,7 +221,7 @@ export class WindowManager {
minHeight: 540,
backgroundColor: "#1c1c1c",
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
icon,
...(process.platform === "linux" ? { icon } : {}),
trafficLightPosition: { x: 16, y: 16 },
titleBarOverlay: {
symbolColor: "#DADBE1",

View File

@@ -101,6 +101,17 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("putDownloadSource", objectIds),
/* Library */
toggleAutomaticCloudSync: (
shop: GameShop,
objectId: string,
automaticCloudSync: boolean
) =>
ipcRenderer.invoke(
"toggleAutomaticCloudSync",
shop,
objectId,
automaticCloudSync
),
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
createGameShortcut: (shop: GameShop, objectId: string) =>
@@ -266,6 +277,7 @@ contextBridge.exposeInMainWorld("electron", {
showItemInFolder: (path: string) =>
ipcRenderer.invoke("showItemInFolder", path),
getFeatures: () => ipcRenderer.invoke("getFeatures"),
getBadges: () => ipcRenderer.invoke("getBadges"),
platform: process.platform,
/* Auto update */
@@ -325,6 +337,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
/* Auth */
getAuth: () => ipcRenderer.invoke("getAuth"),
signOut: () => ipcRenderer.invoke("signOut"),
openAuthWindow: (page: AuthPage) =>
ipcRenderer.invoke("openAuthWindow", page),

View File

@@ -1,29 +0,0 @@
<svg width="240" height="246" viewBox="0 0 240 246" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint0_linear_1378_2496)"/>
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint1_linear_1378_2496)"/>
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" stroke="url(#paint2_linear_1378_2496)"/>
<g opacity="0.9">
<g style="mix-blend-mode:overlay">
<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" fill="black"/>
<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" stroke="url(#paint3_linear_1378_2496)" stroke-width="2"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_1378_2496" x1="5.63736e-07" y1="12.92" x2="246" y2="233.08" gradientUnits="userSpaceOnUse">
<stop stop-color="#0CF1CA"/>
<stop offset="1" stop-color="#1DCCEB"/>
</linearGradient>
<linearGradient id="paint1_linear_1378_2496" x1="19.8951" y1="-3.50306e-06" x2="226.105" y2="246" gradientUnits="userSpaceOnUse">
<stop stop-color="#0DDEBB"/>
<stop offset="1" stop-color="#052520"/>
</linearGradient>
<linearGradient id="paint2_linear_1378_2496" x1="-1.9947e-06" y1="18.0561" x2="246" y2="227.944" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.7"/>
<stop offset="1" stop-color="white" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint3_linear_1378_2496" x1="61.9253" y1="71.6411" x2="164.664" y2="169.814" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.2"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -10,6 +10,7 @@
cursor: pointer;
color: globals.$muted-color;
position: relative;
overflow: hidden;
&__image {
height: 100%;

View File

@@ -1,4 +1,5 @@
import { PersonIcon } from "@primer/octicons-react";
import cn from "classnames";
import "./avatar.scss";
@@ -14,11 +15,18 @@ export interface AvatarProps
src?: string | null;
}
export function Avatar({ size, alt, src, ...props }: AvatarProps) {
export function Avatar({ size, alt, src, className, ...props }: AvatarProps) {
return (
<div className="profile-avatar" style={{ width: size, height: size }}>
{src ? (
<img className="profile-avatar__image" alt={alt} src={src} {...props} />
<img
className={cn("profile-avatar__image", className)}
alt={alt}
src={src}
width={size}
height={size}
{...props}
/>
) : (
<PersonIcon size={size * 0.7} />
)}

View File

@@ -7,7 +7,7 @@ export interface CheckboxFieldProps
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
label: string;
label: string | React.ReactNode;
}
export function CheckboxField({ label, ...props }: CheckboxFieldProps) {

View File

@@ -1,6 +1,6 @@
import { darkenColor } from "@renderer/helpers";
import { useAppSelector, useToast } from "@renderer/hooks";
import type { UserProfile, UserStats } from "@types";
import type { Badge, UserProfile, UserStats } from "@types";
import { average } from "color.js";
import { createContext, useCallback, useEffect, useState } from "react";
@@ -16,6 +16,7 @@ export interface UserProfileContext {
getUserProfile: () => Promise<void>;
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
backgroundImage: string;
badges: Badge[];
}
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
@@ -28,6 +29,7 @@ export const userProfileContext = createContext<UserProfileContext>({
getUserProfile: async () => {},
setSelectedBackgroundImage: () => {},
backgroundImage: "",
badges: [],
});
const { Provider } = userProfileContext;
@@ -47,6 +49,7 @@ export function UserProfileContextProvider({
const [userStats, setUserStats] = useState<UserStats | null>(null);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const [badges, setBadges] = useState<Badge[]>([]);
const [heroBackground, setHeroBackground] = useState(
DEFAULT_USER_PROFILE_BACKGROUND
);
@@ -101,12 +104,18 @@ export function UserProfileContextProvider({
});
}, [navigate, getUserStats, showErrorToast, userId, t]);
const getBadges = useCallback(async () => {
const badges = await window.electron.getBadges();
setBadges(badges);
}, []);
useEffect(() => {
setUserProfile(null);
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
getUserProfile();
}, [getUserProfile]);
getBadges();
}, [getUserProfile, getBadges]);
return (
<Provider
@@ -118,6 +127,7 @@ export function UserProfileContextProvider({
setSelectedBackgroundImage,
backgroundImage: getBackgroundImageUrl(),
userStats,
badges,
}}
>
{children}

View File

@@ -30,6 +30,8 @@ import type {
GameRunning,
TorBoxUser,
Theme,
Badge,
Auth,
} from "@types";
import type { AxiosProgressEvent } from "axios";
import type disk from "diskusage";
@@ -86,6 +88,11 @@ declare global {
getDevelopers: () => Promise<string[]>;
/* Library */
toggleAutomaticCloudSync: (
shop: GameShop,
objectId: string,
automaticCloudSync: boolean
) => Promise<void>;
addGameToLibrary: (
shop: GameShop,
objectId: string,
@@ -217,6 +224,7 @@ declare global {
) => Promise<Electron.OpenDialogReturnValue>;
showItemInFolder: (path: string) => Promise<void>;
getFeatures: () => Promise<string[]>;
getBadges: () => Promise<Badge[]>;
platform: NodeJS.Platform;
/* Auto update */
@@ -227,6 +235,7 @@ declare global {
restartAndInstallUpdate: () => Promise<void>;
/* Auth */
getAuth: () => Promise<Auth | null>;
signOut: () => Promise<void>;
openAuthWindow: (page: AuthPage) => Promise<void>;
getSessionHash: () => Promise<string | null>;

View File

@@ -11,6 +11,7 @@ import "@fontsource/noto-sans/500.css";
import "@fontsource/noto-sans/700.css";
import "react-loading-skeleton/dist/skeleton.css";
import "react-tooltip/dist/react-tooltip.css";
import { App } from "./app";

View File

@@ -31,7 +31,7 @@ $logo-max-width: 200px;
display: flex;
justify-content: center;
width: 100%;
gap: globals.$spacing-unit / 2;
gap: calc(globals.$spacing-unit / 2);
color: globals.$body-color;
cursor: pointer;

View File

@@ -203,9 +203,10 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
<div className="cloud-sync-modal__artifact-info">
<div className="cloud-sync-modal__artifact-header">
<h3>
{t("backup_from", {
date: format(artifact.createdAt, "dd/MM/yyyy"),
})}
{artifact.label ??
t("backup_from", {
date: format(artifact.createdAt, "dd/MM/yyyy"),
})}
</h3>
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
</div>

View File

@@ -23,6 +23,20 @@
}
}
&__cloud-sync-label {
display: flex;
gap: globals.$spacing-unit;
align-items: center;
}
&__cloud-sync-hydra-cloud {
background: linear-gradient(270deg, #16b195 50%, #3e62c0 100%);
color: #fff;
padding: 0 globals.$spacing-unit;
border-radius: 4px;
font-size: globals.$small-font-size;
}
&__row {
display: flex;
gap: globals.$spacing-unit;

View File

@@ -1,6 +1,6 @@
import { useContext, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
import { Button, CheckboxField, Modal, TextField } from "@renderer/components";
import type { LibraryGame } from "@types";
import { gameDetailsContext } from "@renderer/context";
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
@@ -34,12 +34,17 @@ export function GameOptionsModal({
achievements,
} = useContext(gameDetailsContext);
const { hasActiveSubscription } = useUserDetails();
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
const [showResetAchievementsModal, setShowResetAchievementsModal] =
useState(false);
const [isDeletingAchievements, setIsDeletingAchievements] = useState(false);
const [automaticCloudSync, setAutomaticCloudSync] = useState(
game.automaticCloudSync ?? false
);
const {
removeGameInstaller,
@@ -183,6 +188,20 @@ export function GameOptionsModal({
}
};
const handleToggleAutomaticCloudSync = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
setAutomaticCloudSync(event.target.checked);
await window.electron.toggleAutomaticCloudSync(
game.shop,
game.objectId,
event.target.checked
);
updateGame();
};
return (
<>
<DeleteGameModal
@@ -266,6 +285,20 @@ export function GameOptionsModal({
</div>
</div>
<CheckboxField
label={
<div className="game-options-modal__cloud-sync-label">
{t("enable_automatic_cloud_sync")}
<span className="game-options-modal__cloud-sync-hydra-cloud">
Hydra Cloud
</span>
</div>
}
checked={automaticCloudSync}
disabled={!hasActiveSubscription || !game.executablePath}
onChange={handleToggleAutomaticCloudSync}
/>
{shouldShowWinePrefixConfiguration && (
<div className="game-options-modal__wine-prefix">
<div className="game-options-modal__header">

View File

@@ -27,6 +27,11 @@
}
}
&__badges {
display: flex;
gap: calc(globals.$spacing-unit / 2);
}
&__user-information {
display: flex;
padding: calc(globals.$spacing-unit * 7) calc(globals.$spacing-unit * 3);
@@ -82,12 +87,6 @@
text-shadow: 0 0 5px rgb(0 0 0 / 40%);
}
&__display-name-badges-container {
display: flex;
gap: globals.$spacing-unit;
align-items: center;
}
&__current-game {
&-wrapper {
display: flex;

View File

@@ -24,8 +24,8 @@ import type { FriendRequestAction } from "@types";
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
import Skeleton from "react-loading-skeleton";
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
import { Tooltip } from "react-tooltip";
import "./profile-hero.scss";
import { UserBadges } from "./user-badges";
type FriendAction =
| FriendRequestAction
@@ -35,8 +35,14 @@ export function ProfileHero() {
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const [isPerformingAction, setIsPerformingAction] = useState(false);
const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } =
useContext(userProfileContext);
const {
isMe,
badges,
getUserProfile,
userProfile,
heroBackground,
backgroundImage,
} = useContext(userProfileContext);
const {
signOut,
updateFriendRequestState,
@@ -261,14 +267,6 @@ export function ProfileHero() {
return (
<>
{/* <ConfirmationModal
visible
title={t("sign_out_modal_title")}
descriptionText={t("sign_out_modal_text")}
confirmButtonLabel={t("sign_out")}
cancelButtonLabel={t("cancel")}
/> */}
<EditProfileModal
visible={showEditProfileModal}
onClose={() => setShowEditProfileModal(false)}
@@ -312,7 +310,29 @@ export function ProfileHero() {
<h2 className="profile-hero__display-name">
{userProfile?.displayName}
</h2>
<UserBadges />
<div className="profile-hero__badges">
{userProfile.badges.map((badgeName) => {
const badge = badges.find((b) => b.name === badgeName);
if (!badge) return null;
return (
<img
key={badge.name}
src={badge.badge.url}
alt={badge.name}
width={24}
height={24}
data-tooltip-place="top"
data-tooltip-content={badge.description}
data-tooltip-id="badge-name"
/>
);
})}
<Tooltip id="badge-name" />
</div>
</div>
) : (
<Skeleton width={150} height={28} />

View File

@@ -1,40 +0,0 @@
import BadgeThemeCreator from "@renderer/assets/icons/badge-theme-creator.svg?react";
import "./profile-hero.scss";
import { useContext } from "react";
import { userProfileContext } from "@renderer/context";
import { UserBadge } from "@types";
import { useTranslation } from "react-i18next";
export function UserBadges() {
const { t } = useTranslation("badge");
const { userProfile } = useContext(userProfileContext);
if (!userProfile?.badges?.length) return null;
const getBadgeIcon = (badge: UserBadge) => {
if (badge === "THEME_CREATOR") {
return <BadgeThemeCreator width={24} height={24} />;
}
return null;
};
return (
<div className="profile-hero__display-name-badges-container">
{userProfile.badges.map((badge) => {
const badgeIcon = getBadgeIcon(badge);
if (!badgeIcon) return null;
return (
<div
className={`badge__${badge.toLowerCase()}`}
key={badge}
title={t(`badge_description_${badge.toLowerCase()}`)}
>
{badgeIcon}
</div>
);
})}
</div>
);
}

View File

@@ -130,7 +130,13 @@ export interface UserProfileCurrentGame extends Omit<GameRunning, "objectId"> {
export type ProfileVisibility = "PUBLIC" | "PRIVATE" | "FRIENDS";
export type UserBadge = "THEME_CREATOR";
export interface Badge {
name: string;
description: string;
badge: {
url: string;
};
}
export interface UserDetails {
id: string;
@@ -166,7 +172,7 @@ export interface UserProfile {
quirks: {
backupsPerGameLimit: number;
};
badges: UserBadge[];
badges: string[];
}
export interface UpdateProfileRequest {
@@ -247,6 +253,7 @@ export interface GameArtifact {
updatedAt: string;
hostname: string;
downloadCount: number;
label?: string;
}
export interface ComparedAchievements {

View File

@@ -43,6 +43,7 @@ export interface Game {
executablePath?: string | null;
launchOptions?: string | null;
favorite?: boolean;
automaticCloudSync?: boolean;
}
export interface Download {

View File

@@ -1663,6 +1663,14 @@
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.8"
"@floating-ui/dom@^1.6.1":
version "1.6.13"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.13.tgz#a8a938532aea27a95121ec16e667a7cbe8c59e34"
integrity sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==
dependencies:
"@floating-ui/core" "^1.6.0"
"@floating-ui/utils" "^0.2.9"
"@floating-ui/react-dom@^2.0.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz#a1349bbf6a0e5cb5ded55d023766f20a4d439a31"
@@ -1675,6 +1683,11 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.8.tgz#21a907684723bbbaa5f0974cf7730bd797eb8e62"
integrity sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==
"@floating-ui/utils@^0.2.9":
version "0.2.9"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429"
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
"@fontsource/noto-sans@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@fontsource/noto-sans/-/noto-sans-5.1.0.tgz#54a5edd1b2b8c8e17bec6a85d4ee3a53b4b89c1f"
@@ -4279,7 +4292,7 @@ classic-level@^2.0.0:
napi-macros "^2.2.2"
node-gyp-build "^4.3.0"
classnames@^2.2.1, classnames@^2.2.6, classnames@^2.5.1:
classnames@^2.2.1, classnames@^2.2.6, classnames@^2.3.0, classnames@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
@@ -8093,6 +8106,14 @@ react-style-singleton@^2.2.1:
invariant "^2.2.4"
tslib "^2.0.0"
react-tooltip@^5.28.0:
version "5.28.0"
resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.28.0.tgz#c7b5343ab2d740a428494a3d8315515af1f26f46"
integrity sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==
dependencies:
"@floating-ui/dom" "^1.6.1"
classnames "^2.3.0"
react@^18.2.0:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"