mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 16:53:57 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
497f5e7742 | ||
|
|
199b0d5b19 | ||
|
|
2554dc4c69 | ||
|
|
c4c401e054 | ||
|
|
864ff0070f | ||
|
|
da8c40d5dc | ||
|
|
a32fdf3385 | ||
|
|
c9c1750afb | ||
|
|
857063d2c7 | ||
|
|
ed699a8dee | ||
|
|
778e921594 | ||
|
|
206886c091 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "3.2.3",
|
"version": "3.3.0",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -178,6 +178,8 @@
|
|||||||
"manage_files_description": "Manage which files will be backed up and restored",
|
"manage_files_description": "Manage which files will be backed up and restored",
|
||||||
"select_folder": "Select folder",
|
"select_folder": "Select folder",
|
||||||
"backup_from": "Backup from {{date}}",
|
"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",
|
"custom_backup_location_set": "Custom backup location set",
|
||||||
"no_directory_selected": "No directory selected",
|
"no_directory_selected": "No directory selected",
|
||||||
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
|
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
|
||||||
|
|||||||
@@ -174,6 +174,8 @@
|
|||||||
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
|
||||||
"select_folder": "Seleccionar carpeta",
|
"select_folder": "Seleccionar carpeta",
|
||||||
"backup_from": "Copia de seguridad de {{date}}",
|
"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",
|
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
|
||||||
"clear": "Limpiar",
|
"clear": "Limpiar",
|
||||||
"no_directory_selected": "No se seleccionó un directorio",
|
"no_directory_selected": "No se seleccionó un directorio",
|
||||||
@@ -185,7 +187,13 @@
|
|||||||
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
|
||||||
"reset_achievements_title": "¿Estás seguro?",
|
"reset_achievements_title": "¿Estás seguro?",
|
||||||
"reset_achievements_success": "Logros reiniciados exitosamente",
|
"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": {
|
"activation": {
|
||||||
"title": "Activar Hydra",
|
"title": "Activar Hydra",
|
||||||
@@ -297,7 +305,37 @@
|
|||||||
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
"subscription_renew_cancelled": "Está desactivada la renovación automática",
|
||||||
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
|
||||||
"update_email": "Actualizar correo",
|
"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": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
|
|||||||
@@ -165,6 +165,8 @@
|
|||||||
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
|
"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",
|
"achievements_not_sync": "Veja como exibir suas conquistas no perfil",
|
||||||
"backup_from": "Backup de {{date}}",
|
"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",
|
"custom_backup_location_set": "Localização customizada selecionada",
|
||||||
"select_folder": "Selecione a pasta",
|
"select_folder": "Selecione a pasta",
|
||||||
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
|
||||||
|
|||||||
@@ -178,6 +178,8 @@
|
|||||||
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
|
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
|
||||||
"select_folder": "Выбрать папку",
|
"select_folder": "Выбрать папку",
|
||||||
"backup_from": "Резервная копия от {{date}}",
|
"backup_from": "Резервная копия от {{date}}",
|
||||||
|
"automatic_backup_from": "Автоматическая резервная копия от {{date}}",
|
||||||
|
"enable_automatic_cloud_sync": "Включить автоматическую синхронизацию в облаке",
|
||||||
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
|
||||||
"no_directory_selected": "Не выбран каталог",
|
"no_directory_selected": "Не выбран каталог",
|
||||||
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
|
||||||
|
|||||||
@@ -1,44 +1,8 @@
|
|||||||
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
|
import { CloudSync } from "@main/services";
|
||||||
import { registerEvent } from "../register-event";
|
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 type { GameShop } from "@types";
|
||||||
import axios from "axios";
|
import { t } from "i18next";
|
||||||
import os from "node:os";
|
import { format } from "date-fns";
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const uploadSaveGame = async (
|
const uploadSaveGame = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@@ -46,61 +10,15 @@ const uploadSaveGame = async (
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
downloadOptionTitle: string | null
|
downloadOptionTitle: string | null
|
||||||
) => {
|
) => {
|
||||||
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
|
return CloudSync.uploadSaveGame(
|
||||||
|
|
||||||
const bundleLocation = await bundleBackup(
|
|
||||||
shop,
|
|
||||||
objectId,
|
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);
|
registerEvent("uploadSaveGame", uploadSaveGame);
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ import "./library/remove-game";
|
|||||||
import "./library/remove-game-from-library";
|
import "./library/remove-game-from-library";
|
||||||
import "./library/select-game-wine-prefix";
|
import "./library/select-game-wine-prefix";
|
||||||
import "./library/reset-game-achievements";
|
import "./library/reset-game-achievements";
|
||||||
|
import "./library/toggle-automatic-cloud-sync";
|
||||||
import "./misc/open-checkout";
|
import "./misc/open-checkout";
|
||||||
import "./misc/open-external";
|
import "./misc/open-external";
|
||||||
import "./misc/show-open-dialog";
|
import "./misc/show-open-dialog";
|
||||||
import "./misc/get-features";
|
import "./misc/get-features";
|
||||||
import "./misc/show-item-in-folder";
|
import "./misc/show-item-in-folder";
|
||||||
|
import "./misc/get-badges";
|
||||||
import "./torrenting/cancel-game-download";
|
import "./torrenting/cancel-game-download";
|
||||||
import "./torrenting/pause-game-download";
|
import "./torrenting/pause-game-download";
|
||||||
import "./torrenting/resume-game-download";
|
import "./torrenting/resume-game-download";
|
||||||
@@ -58,6 +60,7 @@ import "./user/get-blocked-users";
|
|||||||
import "./user/block-user";
|
import "./user/block-user";
|
||||||
import "./user/unblock-user";
|
import "./user/unblock-user";
|
||||||
import "./user/get-user-friends";
|
import "./user/get-user-friends";
|
||||||
|
import "./user/get-auth";
|
||||||
import "./user/get-user-stats";
|
import "./user/get-user-stats";
|
||||||
import "./user/report-user";
|
import "./user/report-user";
|
||||||
import "./user/get-unlocked-achievements";
|
import "./user/get-unlocked-achievements";
|
||||||
@@ -87,7 +90,6 @@ import "./themes/get-custom-theme-by-id";
|
|||||||
import "./themes/get-active-custom-theme";
|
import "./themes/get-active-custom-theme";
|
||||||
import "./themes/close-editor-window";
|
import "./themes/close-editor-window";
|
||||||
import "./themes/toggle-custom-theme";
|
import "./themes/toggle-custom-theme";
|
||||||
import "./misc/get-badges";
|
|
||||||
import { isPortableVersion } from "@main/helpers";
|
import { isPortableVersion } from "@main/helpers";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
|
|||||||
23
src/main/events/library/toggle-automatic-cloud-sync.ts
Normal file
23
src/main/events/library/toggle-automatic-cloud-sync.ts
Normal 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);
|
||||||
11
src/main/events/user/get-auth.ts
Normal file
11
src/main/events/user/get-auth.ts
Normal 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);
|
||||||
@@ -62,8 +62,6 @@ export const loadState = async () => {
|
|||||||
game.uri !== null
|
game.uri !== null
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("downloadsToSeed", downloadsToSeed);
|
|
||||||
|
|
||||||
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
|
||||||
|
|
||||||
startMainLoop();
|
startMainLoop();
|
||||||
|
|||||||
112
src/main/services/cloud-sync.ts
Normal file
112
src/main/services/cloud-sync.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
TorBoxAddTorrentRequest,
|
TorBoxAddTorrentRequest,
|
||||||
TorBoxRequestLinkRequest,
|
TorBoxRequestLinkRequest,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
import { appVersion } from "@main/constants";
|
||||||
|
|
||||||
export class TorBoxClient {
|
export class TorBoxClient {
|
||||||
private static instance: AxiosInstance;
|
private static instance: AxiosInstance;
|
||||||
@@ -18,6 +19,7 @@ export class TorBoxClient {
|
|||||||
baseURL: this.baseURL,
|
baseURL: this.baseURL,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${apiToken}`,
|
Authorization: `Bearer ${apiToken}`,
|
||||||
|
"User-Agent": `Hydra/${appVersion}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export * from "./process-watcher";
|
|||||||
export * from "./main-loop";
|
export * from "./main-loop";
|
||||||
export * from "./hydra-api";
|
export * from "./hydra-api";
|
||||||
export * from "./ludusavi";
|
export * from "./ludusavi";
|
||||||
|
export * from "./cloud-sync";
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import axios from "axios";
|
|||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { ProcessPayload } from "./download/types";
|
import { ProcessPayload } from "./download/types";
|
||||||
import { gamesSublevel, levelKeys } from "@main/level";
|
import { gamesSublevel, levelKeys } from "@main/level";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { CloudSync } from "./cloud-sync";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
const commands = {
|
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 ""}'`,
|
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) {
|
if (game.remoteId) {
|
||||||
updateGamePlaytime(game, 0, new Date()).catch(() => {});
|
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 {
|
} else {
|
||||||
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
|
||||||
}
|
}
|
||||||
@@ -287,6 +302,18 @@ const onCloseGame = (game: Game) => {
|
|||||||
performance.now() - gamePlaytime.lastSyncTick,
|
performance.now() - gamePlaytime.lastSyncTick,
|
||||||
game.lastTimePlayed!
|
game.lastTimePlayed!
|
||||||
).catch(() => {});
|
).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 {
|
} else {
|
||||||
createGame(game).catch(() => {});
|
createGame(game).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,17 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("putDownloadSource", objectIds),
|
ipcRenderer.invoke("putDownloadSource", objectIds),
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
|
toggleAutomaticCloudSync: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
automaticCloudSync: boolean
|
||||||
|
) =>
|
||||||
|
ipcRenderer.invoke(
|
||||||
|
"toggleAutomaticCloudSync",
|
||||||
|
shop,
|
||||||
|
objectId,
|
||||||
|
automaticCloudSync
|
||||||
|
),
|
||||||
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
|
||||||
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
|
||||||
createGameShortcut: (shop: GameShop, objectId: string) =>
|
createGameShortcut: (shop: GameShop, objectId: string) =>
|
||||||
@@ -326,6 +337,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
|
getAuth: () => ipcRenderer.invoke("getAuth"),
|
||||||
signOut: () => ipcRenderer.invoke("signOut"),
|
signOut: () => ipcRenderer.invoke("signOut"),
|
||||||
openAuthWindow: (page: AuthPage) =>
|
openAuthWindow: (page: AuthPage) =>
|
||||||
ipcRenderer.invoke("openAuthWindow", page),
|
ipcRenderer.invoke("openAuthWindow", page),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface CheckboxFieldProps
|
|||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
label: string;
|
label: string | React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
|
||||||
|
|||||||
7
src/renderer/src/declaration.d.ts
vendored
7
src/renderer/src/declaration.d.ts
vendored
@@ -31,6 +31,7 @@ import type {
|
|||||||
TorBoxUser,
|
TorBoxUser,
|
||||||
Theme,
|
Theme,
|
||||||
Badge,
|
Badge,
|
||||||
|
Auth,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { AxiosProgressEvent } from "axios";
|
import type { AxiosProgressEvent } from "axios";
|
||||||
import type disk from "diskusage";
|
import type disk from "diskusage";
|
||||||
@@ -87,6 +88,11 @@ declare global {
|
|||||||
getDevelopers: () => Promise<string[]>;
|
getDevelopers: () => Promise<string[]>;
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
|
toggleAutomaticCloudSync: (
|
||||||
|
shop: GameShop,
|
||||||
|
objectId: string,
|
||||||
|
automaticCloudSync: boolean
|
||||||
|
) => Promise<void>;
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
objectId: string,
|
objectId: string,
|
||||||
@@ -229,6 +235,7 @@ declare global {
|
|||||||
restartAndInstallUpdate: () => Promise<void>;
|
restartAndInstallUpdate: () => Promise<void>;
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
|
getAuth: () => Promise<Auth | null>;
|
||||||
signOut: () => Promise<void>;
|
signOut: () => Promise<void>;
|
||||||
openAuthWindow: (page: AuthPage) => Promise<void>;
|
openAuthWindow: (page: AuthPage) => Promise<void>;
|
||||||
getSessionHash: () => Promise<string | null>;
|
getSessionHash: () => Promise<string | null>;
|
||||||
|
|||||||
@@ -203,9 +203,10 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
|
|||||||
<div className="cloud-sync-modal__artifact-info">
|
<div className="cloud-sync-modal__artifact-info">
|
||||||
<div className="cloud-sync-modal__artifact-header">
|
<div className="cloud-sync-modal__artifact-header">
|
||||||
<h3>
|
<h3>
|
||||||
{t("backup_from", {
|
{artifact.label ??
|
||||||
date: format(artifact.createdAt, "dd/MM/yyyy"),
|
t("backup_from", {
|
||||||
})}
|
date: format(artifact.createdAt, "dd/MM/yyyy"),
|
||||||
|
})}
|
||||||
</h3>
|
</h3>
|
||||||
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
|
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 {
|
&__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: globals.$spacing-unit;
|
gap: globals.$spacing-unit;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useContext, useRef, useState } from "react";
|
import { useContext, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 type { LibraryGame } from "@types";
|
||||||
import { gameDetailsContext } from "@renderer/context";
|
import { gameDetailsContext } from "@renderer/context";
|
||||||
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
|
||||||
@@ -34,12 +34,17 @@ export function GameOptionsModal({
|
|||||||
achievements,
|
achievements,
|
||||||
} = useContext(gameDetailsContext);
|
} = useContext(gameDetailsContext);
|
||||||
|
|
||||||
|
const { hasActiveSubscription } = useUserDetails();
|
||||||
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
|
||||||
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
|
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
|
||||||
const [showResetAchievementsModal, setShowResetAchievementsModal] =
|
const [showResetAchievementsModal, setShowResetAchievementsModal] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [isDeletingAchievements, setIsDeletingAchievements] = useState(false);
|
const [isDeletingAchievements, setIsDeletingAchievements] = useState(false);
|
||||||
|
const [automaticCloudSync, setAutomaticCloudSync] = useState(
|
||||||
|
game.automaticCloudSync ?? false
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
removeGameInstaller,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteGameModal
|
<DeleteGameModal
|
||||||
@@ -266,6 +285,20 @@ export function GameOptionsModal({
|
|||||||
</div>
|
</div>
|
||||||
</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 && (
|
{shouldShowWinePrefixConfiguration && (
|
||||||
<div className="game-options-modal__wine-prefix">
|
<div className="game-options-modal__wine-prefix">
|
||||||
<div className="game-options-modal__header">
|
<div className="game-options-modal__header">
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ export interface GameArtifact {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
downloadCount: number;
|
downloadCount: number;
|
||||||
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComparedAchievements {
|
export interface ComparedAchievements {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface Game {
|
|||||||
executablePath?: string | null;
|
executablePath?: string | null;
|
||||||
launchOptions?: string | null;
|
launchOptions?: string | null;
|
||||||
favorite?: boolean;
|
favorite?: boolean;
|
||||||
|
automaticCloudSync?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Download {
|
export interface Download {
|
||||||
|
|||||||
Reference in New Issue
Block a user