Merge branch 'main' into main

This commit is contained in:
Hydra 2024-05-07 05:55:35 +01:00 committed by GitHub
commit 97ef496703
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 858 additions and 219 deletions

View file

@ -138,7 +138,8 @@
"telemetry": "Telemetry",
"telemetry_description": "Enable anonymous usage statistics",
"behavior": "Behavior",
"quit_app_instead_hiding": "Close app instead of minimizing to tray"
"quit_app_instead_hiding": "Close app instead of minimizing to tray",
"launch_with_system": "Launch app on system start-up"
},
"notifications": {
"download_complete": "Download complete",

View file

@ -15,9 +15,12 @@
"checking_files": "{{title}} ({{percentage}} - Analizando archivos…)",
"paused": "{{title}} (Pausado)",
"downloading": "{{title}} ({{percentage}} - Descargando…)",
"filter": "Filtrar biblioteca",
"filter": "Buscar en la biblioteca",
"follow_us": "Síguenos",
"home": "Inicio",
"follow_us": "Síguenos"
"discord": "Únete a nuestro Discord",
"x": "Síguenos en X",
"github": "Contribuye en GitHub"
},
"header": {
"search": "Buscar",
@ -45,7 +48,7 @@
"remove": "Eliminar",
"remove_from_list": "Quitar",
"space_left_on_disk": "{{space}} restantes en el disco",
"eta": "Finalizando {{eta}}",
"eta": "Finalizando en {{eta}}",
"downloading_metadata": "Descargando metadatos…",
"checking_files": "Analizando archivos…",
"filter": "Filtrar repacks",
@ -55,14 +58,12 @@
"no_minimum_requirements": "Sin requisitos mínimos para {{title}}",
"no_recommended_requirements": "{{title}} no tiene requisitos recomendados",
"paused_progress": "{{progress}} (Pausado)",
"release_date": "Fecha de lanzamiento {{date}}",
"publisher": "Publicado por {{publisher}}",
"release_date": "Fecha de lanzamiento: {{date}}",
"publisher": "Publicado por: {{publisher}}",
"copy_link_to_clipboard": "Copiar enlace",
"copied_link_to_clipboard": "Enlace copiado",
"hours": "horas",
"minutes": "minutos",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"accuracy": "{{accuracy}}% precisión",
"add_to_library": "Agregar a la biblioteca",
"remove_from_library": "Eliminar de la biblioteca",
@ -75,7 +76,21 @@
"close": "Cerrar",
"deleting": "Eliminando instalador…",
"playing_now": "Jugando ahora",
"last_time_played": "Jugado por última vez {{period}}"
"last_time_played": "Jugado por última vez {{period}}",
"got_it": "Entendido",
"change": "Cambiar",
"repacks_modal_description": "Selecciona el repack que quieres descargar",
"downloads_path": "Ruta de descarga",
"select_folder_hint": "Para cambiar la carpeta predeterminada, accede a",
"settings": "Ajustes",
"download_now": "Descargar ahora",
"installation_instructions": "Instrucciones de instalación",
"installation_instructions_description": "Se requieren de pasos adicionales para instalar este juego",
"online_fix_instruction": "Los juegos de OnlineFix requieren una contraseña para ser extraídos. Cuando se requiera, usa la siguiente contraseña:",
"dodi_installation_instruction": "Cuando abras el instalador de DODI, presiona la tecla hacia arriba del teclado <0 /> para iniciar el proceso de instalación:",
"dont_show_it_again": "No mostrar de nuevo",
"copy_to_clipboard": "Copiar",
"copied_to_clipboard": "Copiado"
},
"activation": {
"title": "Activar Hydra",
@ -88,7 +103,7 @@
"downloads": {
"resume": "Resumir",
"pause": "Pausa",
"eta": "Finalizando {{eta}}",
"eta": "Finalizando en {{eta}}",
"paused": "En Pausa",
"verifying": "Verificando…",
"completed_at": "Completado el {{date}}",
@ -103,8 +118,8 @@
"starting_download": "Iniciando descarga…",
"remove_from_list": "Eliminar",
"delete": "Eliminar instalador",
"delete_modal_description": "Esto eliminará todos los archivos de instalación de su computadora.",
"delete_modal_title": "¿Está seguro?",
"delete_modal_description": "Esto eliminará todos los archivos de instalación de tu computadora.",
"delete_modal_title": "¿Estás seguro?",
"deleting": "Eliminando instalador…",
"install": "Instalar"
},
@ -136,6 +151,9 @@
"description": "Los ejecutables de Wine o Lutris no se encontraron en su sistema",
"instructions": "Comprueba como instalar de forma correcta uno de los dos en tu distro de Linux para ejecutar el juego con normalidad"
},
"modal": {
"close": "Botón de cierre"
},
"catalogue": {
"next_page": "Siguiente página",
"previous_page": "Pagina anterior"

View file

@ -4,4 +4,5 @@ export { default as es } from "./es/translation.json";
export { default as fr } from "./fr/translation.json";
export { default as hu } from "./hu/translation.json";
export { default as it } from "./it/translation.json";
export { default as pl } from "./pl/translation.json";
export { default as ru } from "./ru/translation.json";

View file

@ -0,0 +1,147 @@
{
"home": {
"featured": "Wyróżnione",
"recently_added": "Ostatnio dodane",
"trending": "Trendujące",
"surprise_me": "Zaskocz mnie",
"no_results": "Nie znaleziono wyników"
},
"sidebar": {
"catalogue": "Katalog",
"downloads": "Pobrane",
"settings": "Ustawienia",
"my_library": "Moja biblioteka",
"downloading_metadata": "{{title}} (Pobieranie metadata…)",
"checking_files": "{{title}} ({{percentage}} - Sprawdzanie plików…)",
"paused": "{{title}} (Zatrzymano)",
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
"filter": "Filtruj biblioteke",
"follow_us": "Śledź nas",
"home": "Główna"
},
"header": {
"search": "Szukaj",
"home": "Główna",
"catalogue": "Katalog",
"downloads": "Pobrane",
"search_results": "Wyniki wyszukiwania",
"settings": "Ustawienia"
},
"bottom_panel": {
"no_downloads_in_progress": "Brak pobierań w toku",
"downloading_metadata": "Pobieranie {{title}} metadata…",
"checking_files": "Sprawdzanie {{title}} plików… (ukończone w {{percentage}})",
"downloading": "Pobieranie {{title}}… (ukończone w {{percentage}}) - Podsumowanie {{eta}} - {{speed}}"
},
"catalogue": {
"next_page": "Następna strona",
"previous_page": "Poprzednia strona"
},
"game_details": {
"open_download_options": "Otwórz opcje pobierania",
"download_options_zero": "Brak opcji pobierania",
"download_options_one": "{{count}} opcja pobierania",
"download_options_other": "{{count}} opcji pobierania",
"updated_at": "Zaktualizowano {{updated_at}}",
"install": "Instaluj",
"resume": "Wznów",
"pause": "Zatrzymaj",
"cancel": "Anuluj",
"remove": "Usuń",
"remove_from_list": "Usuń",
"space_left_on_disk": "{{space}} wolnego na dysku",
"eta": "Podsumowanie {{eta}}",
"downloading_metadata": "Pobieranie metadata…",
"checking_files": "Sprawdzanie plików…",
"filter": "Filtruj repacki",
"requirements": "Wymagania systemowe",
"minimum": "Minimalne",
"recommended": "Zalecane",
"no_minimum_requirements": "{{title}} nie zawiera informacji o minimalnych wymaganiach",
"no_recommended_requirements": "{{title}} nie zawiera informacji o zalecanych wymaganiach",
"paused_progress": "{{progress}} (Zatrzymano)",
"release_date": "Wydano w {{date}}",
"publisher": "Opublikowany przez {{publisher}}",
"copy_link_to_clipboard": "Kopiuj łącze",
"copied_link_to_clipboard": "Skopiowano łącze",
"hours": "godzin",
"minutes": "minut",
"accuracy": "{{accuracy}}% dokładność",
"add_to_library": "Dodaj do biblioteki",
"remove_from_library": "Usuń z biblioteki",
"no_downloads": "Brak dostępnych plików do pobrania",
"play_time": "Grano przez {{amount}}",
"last_time_played": "Ostatnio grano {{period}}",
"not_played_yet": "Nie grano {{title}}",
"next_suggestion": "Następna sugestia",
"play": "Graj",
"deleting": "Usuwanie instalatora…",
"close": "Zamknij",
"playing_now": "Granie teraz",
"change": "Zmień",
"repacks_modal_description": "Wybierz repack, który chcesz pobrać",
"downloads_path": "Ścieżka pobierania",
"select_folder_hint": "Aby zmienić domyślny folder, przejdź do",
"settings": "Ustawienia Hydra",
"download_now": "Pobierz teraz"
},
"activation": {
"title": "Aktywuj Hydra",
"installation_id": "Identyfikator instalacji:",
"enter_activation_code": "Enter your activation code",
"message": "Jeśli nie wiesz, gdzie o to poprosić, to nie powinieneś/aś tego mieć.",
"activate": "Aktywuj",
"loading": "Ładowanie…"
},
"downloads": {
"resume": "Wznów",
"pause": "Zatrzymaj",
"eta": "Podsumowanie {{eta}}",
"paused": "Zatrzymano",
"verifying": "Weryfikowanie…",
"completed_at": "Zakończono w {{date}}",
"completed": "Zakończono",
"cancelled": "Anulowano",
"download_again": "Pobierz ponownie",
"cancel": "Anuluj",
"filter": "Filtruj pobrane gry",
"remove": "Usuń",
"downloading_metadata": "Pobieranie metadata…",
"checking_files": "Sprawdzanie plików…",
"starting_download": "Rozpoczęto pobieranie…",
"deleting": "Usuwanie instalatora…",
"delete": "Usuń instalator",
"remove_from_list": "Usuń",
"delete_modal_title": "Czy na pewno?",
"delete_modal_description": "Spowoduje to usunięcie wszystkich plików instalacyjnych z komputera",
"install": "Instaluj"
},
"settings": {
"downloads_path": "Ścieżka pobierania",
"change": "Aktualizuj",
"notifications": "Powiadomienia",
"enable_download_notifications": "Gdy pobieranie zostanie zakończone",
"enable_repack_list_notifications": "Gdy dodawany jest nowy repack",
"telemetry": "Telemetria",
"telemetry_description": "Włącz anonimowe statystyki użycia"
},
"notifications": {
"download_complete": "Pobieranie zakończone",
"game_ready_to_install": "{{title}} jest gotowe do zainstalowania",
"repack_list_updated": "Lista repacków zaktualizowana",
"repack_count_one": "{{count}} repack dodany",
"repack_count_other": "{{count}} repacki dodane"
},
"system_tray": {
"open": "Otwórz Hydra",
"quit": "Wyjdź"
},
"game_card": {
"no_downloads": "Brak dostępnych plików do pobrania"
},
"binary_not_found_modal": {
"title": "Programy nie są zainstalowane",
"description": "Pliki wykonywalne Wine lub Lutris nie zostały znalezione na twoim systemie",
"instructions": "Sprawdź prawidłowy sposób instalacji dowolnego z nich w swojej dystrybucji Linuksa, aby gra działała normalnie"
}
}

View file

@ -134,7 +134,8 @@
"telemetry": "Telemetria",
"telemetry_description": "Habilitar estatísticas de uso anônimas",
"behavior": "Comportamento",
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo"
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo",
"launch_with_system": "Iniciar aplicativo na inicialização do sistema"
},
"notifications": {
"download_complete": "Download concluído",

View file

@ -17,7 +17,10 @@
"downloading": "{{title}} ({{percentage}} - Загрузка…)",
"filter": "Фильтровать библиотеку",
"follow_us": "Подписывайтесь на нас",
"home": "Главная"
"home": "Главная",
"discord": "Присоединяйся к Discord",
"x": "Подписывайтесь на X",
"github": "Внести свой вклад в GitHub"
},
"header": {
"search": "Поиск",
@ -66,6 +69,8 @@
"copied_link_to_clipboard": "Ссылка скопирована",
"hours": "часов",
"minutes": "минут",
"amount_hours": "{{amount}} часов",
"amount_minutes": "{{amount}} минут",
"accuracy": "{{accuracy}}% точность",
"add_to_library": "Добавить в библиотеку",
"remove_from_library": "Удалить из библиотеки",
@ -83,7 +88,15 @@
"downloads_path": "Путь загрузок",
"select_folder_hint": "Чтобы изменить папку по умолчанию, откройте",
"settings": "Настройки Hydra",
"download_now": "Загрузить сейчас"
"download_now": "Загрузить сейчас",
"installation_instructions": "Инструкция по установке",
"installation_instructions_description": "Для установки этой игры требуются дополнительные шаги",
"online_fix_instruction": "В играх с OnlineFix требуется ввести пароль для извлеченияя.При необходимости используйте следующий пароль:",
"dodi_installation_instruction": "При открытии установщика Dodi нажмите клавишу вверх клавиатуру <0 />, чтобы запустить процесс установки:",
"dont_show_it_again": "Не показывать это снова",
"copy_to_clipboard": "Копировать",
"copied_to_clipboard": "Скопировано",
"got_it": "Понятно"
},
"activation": {
"title": "Активировать Hydra",
@ -123,7 +136,10 @@
"enable_download_notifications": "По завершении загрузки",
"enable_repack_list_notifications": "При добавлении нового репака",
"telemetry": "Телеметрия",
"telemetry_description": "Включить анонимную статистику использования"
"telemetry_description": "Включить анонимную статистику использования",
"behavior": "Поведение",
"quit_app_instead_hiding": "Закрывать приложение вместо того, чтобы сворачивать его в трей"
"launch_with_system": "Запуск приложения при запуске системы"
},
"notifications": {
"download_complete": "Загрузка завершена",
@ -143,5 +159,8 @@
"title": "Программы не установлены",
"description": "Исполняемые файлы Wine или Lutris не найдены на вашей системе",
"instructions": "Узнайте правильный способ установить любой из них на вашем дистрибутиве Linux, чтобы игра могла нормально работать"
},
"modal": {
"close": "Кнопка закрытия"
}
}

View file

@ -29,6 +29,9 @@ export class UserPreferences {
@Column("boolean", { default: false })
preferQuitInsteadOfHiding: boolean;
@Column("boolean", { default: false })
runAtStartup: boolean;
@CreateDateColumn()
createdAt: Date;

View file

@ -18,7 +18,6 @@ import "./library/open-game";
import "./library/open-game-installer";
import "./library/remove-game";
import "./library/remove-game-from-library";
import "./misc/get-or-cache-image";
import "./misc/open-external";
import "./misc/show-open-dialog";
import "./torrenting/cancel-game-download";
@ -27,6 +26,7 @@ import "./torrenting/resume-game-download";
import "./torrenting/start-game-download";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());

View file

@ -3,7 +3,7 @@ import { gameRepository } from "@main/repository";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { getImageBase64 } from "@main/helpers";
import { getFileBase64 } from "@main/helpers";
import { getSteamGameIconUrl } from "@main/services";
const addGameToLibrary = async (
@ -11,7 +11,7 @@ const addGameToLibrary = async (
objectID: string,
title: string,
gameShop: GameShop,
executablePath: string
executablePath: string | null
) => {
const game = await gameRepository.findOne({
where: {
@ -31,7 +31,7 @@ const addGameToLibrary = async (
}
);
} else {
const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID));
const iconUrl = await getFileBase64(await getSteamGameIconUrl(objectID));
return gameRepository.insert({
title,

View file

@ -1,40 +0,0 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { registerEvent } from "../register-event";
import { getFileBuffer } from "@main/helpers";
import { logger } from "@main/services";
import { imageCachePath } from "@main/constants";
const getOrCacheImage = async (
_event: Electron.IpcMainInvokeEvent,
url: string
) => {
if (!fs.existsSync(imageCachePath)) fs.mkdirSync(imageCachePath);
const extname = path.extname(url);
const checksum = crypto.createHash("sha256").update(url).digest("hex");
const cachePath = path.join(imageCachePath, `${checksum}${extname}`);
const cache = fs.existsSync(cachePath);
if (cache) return `hydra://${cachePath}`;
getFileBuffer(url).then((buffer) =>
fs.writeFile(cachePath, buffer, (err) => {
if (err) {
logger.error(`Failed to cache image`, err, {
method: "getOrCacheImage",
});
}
})
);
return url;
};
registerEvent(getOrCacheImage, {
name: "getOrCacheImage",
});

View file

@ -5,7 +5,11 @@ import { registerEvent } from "../register-event";
const showOpenDialog = async (
_event: Electron.IpcMainInvokeEvent,
options: Electron.OpenDialogOptions
) => dialog.showOpenDialog(WindowManager.mainWindow, options);
) => {
if (WindowManager.mainWindow) {
dialog.showOpenDialog(WindowManager.mainWindow, options);
}
};
registerEvent(showOpenDialog, {
name: "showOpenDialog",

View file

@ -5,7 +5,7 @@ import { GameStatus } from "@main/constants";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { getImageBase64 } from "@main/helpers";
import { getFileBase64 } from "@main/helpers";
import { In } from "typeorm";
const startGameDownload = async (
@ -72,7 +72,7 @@ const startGameDownload = async (
return game;
} else {
const iconUrl = await getImageBase64(await getSteamGameIconUrl(objectID));
const iconUrl = await getFileBase64(await getSteamGameIconUrl(objectID));
const createdGame = await gameRepository.save({
title,

View file

@ -0,0 +1,21 @@
import { registerEvent } from "../register-event";
import AutoLaunch from "auto-launch";
import { app } from "electron";
const autoLaunch = async (
_event: Electron.IpcMainInvokeEvent,
enabled: boolean
) => {
const appLauncher = new AutoLaunch({
name: app.getName(),
});
if (enabled) {
appLauncher.enable().catch();
} else {
appLauncher.disable().catch();
}
};
registerEvent(autoLaunch, {
name: "autoLaunch",
});

View file

@ -79,10 +79,24 @@ export const getFileBuffer = async (url: string) =>
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
);
export const getImageBase64 = async (url: string) =>
getFileBuffer(url).then((buffer) => {
return `data:image/jpeg;base64,${Buffer.from(buffer).toString("base64")}`;
});
export const getFileBase64 = async (url: string) =>
fetch(url, { method: "GET" }).then((response) =>
response.arrayBuffer().then((buffer) => {
const base64 = Buffer.from(buffer).toString("base64");
const contentType = response.headers.get("content-type");
return `data:${contentType};base64,${base64}`;
})
);
export const steamUrlBuilder = {
library: (objectID: string) =>
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`,
libraryHero: (objectID: string) =>
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/library_hero.jpg`,
logo: (objectID: string) =>
`https://cdn.cloudflare.steamstatic.com/steam/apps/${objectID}/logo.png`,
};
export * from "./formatters";
export * from "./ps";

View file

@ -85,7 +85,7 @@ app.on("second-instance", (_event, commandLine) => {
WindowManager.createMainWindow();
}
const [, path] = commandLine.pop().split("://");
const [, path] = commandLine.pop()?.split("://") ?? [];
if (path) WindowManager.redirect(path);
});

View file

@ -41,7 +41,8 @@ export const getSteam250List = async () => {
).flat();
const gamesMap: Map<string, Steam250Game> = gamesList.reduce((map, item) => {
map.set(item.objectID, item);
if (item) map.set(item.objectID, item);
return map;
}, new Map());

View file

@ -1,6 +1,5 @@
import path from "node:path";
import cp from "node:child_process";
import crypto from "node:crypto";
import fs from "node:fs";
import * as Sentry from "@sentry/electron/main";
import { Notification, app, dialog } from "electron";
@ -99,25 +98,6 @@ export class TorrentClient {
return game.progress;
}
private static createTempIcon(encodedIcon: string): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.randomBytes(16).toString("hex");
const iconPath = path.join(app.getPath("temp"), `${hash}.png`);
fs.writeFile(
iconPath,
Buffer.from(
encodedIcon.replace("data:image/jpeg;base64,", ""),
"base64"
),
(err) => {
if (err) reject(err);
resolve(iconPath);
}
);
});
}
public static async onSocketData(data: Buffer) {
const message = Buffer.from(data).toString("utf-8");
@ -159,10 +139,7 @@ export class TorrentClient {
});
if (userPreferences?.downloadNotificationsEnabled) {
const iconPath = await this.createTempIcon(game.iconUrl);
new Notification({
icon: iconPath,
title: t("download_complete", {
ns: "notifications",
lng: userPreferences.language,

View file

@ -26,7 +26,7 @@ export class WindowManager {
}
}
public static async createMainWindow() {
public static createMainWindow() {
// Create the browser window.
this.mainWindow = new BrowserWindow({
width: 1200,
@ -50,11 +50,15 @@ export class WindowManager {
this.loadURL();
this.mainWindow.removeMenu();
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
this.mainWindow.on("ready-to-show", () => {
if (!app.isPackaged) WindowManager.mainWindow?.webContents.openDevTools();
});
this.mainWindow.on("close", () => {
this.mainWindow.on("close", async () => {
const userPreferences = await userPreferencesRepository.findOne({
where: { id: 1 },
});
if (userPreferences?.preferQuitInsteadOfHiding) {
app.quit();
}

View file

@ -1,13 +1,16 @@
import { parentPort } from "worker_threads";
import parseTorrent from "parse-torrent";
import { getFileBuffer } from "@main/helpers";
const port = parentPort;
if (!port) throw new Error("IllegalState");
export const getFileBuffer = async (url: string) =>
fetch(url, { method: "GET" }).then((response) =>
response.arrayBuffer().then((buffer) => Buffer.from(buffer))
);
port.on("message", async (url: string) => {
const buffer = await getFileBuffer(url);
const torrent = await parseTorrent(buffer);
port.postMessage(torrent);
});

View file

@ -48,6 +48,7 @@ contextBridge.exposeInMainWorld("electron", {
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
updateUserPreferences: (preferences: UserPreferences) =>
ipcRenderer.invoke("updateUserPreferences", preferences),
autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled),
/* Library */
addGameToLibrary: (
@ -94,7 +95,6 @@ contextBridge.exposeInMainWorld("electron", {
getDiskFreeSpace: () => ipcRenderer.invoke("getDiskFreeSpace"),
/* Misc */
getOrCacheImage: (url: string) => ipcRenderer.invoke("getOrCacheImage", url),
ping: () => ipcRenderer.invoke("ping"),
getVersion: () => ipcRenderer.invoke("getVersion"),
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),

View file

@ -57,6 +57,7 @@ contextBridge.exposeInMainWorld("electron", {
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
updateUserPreferences: (preferences: UserPreferences) =>
ipcRenderer.invoke("updateUserPreferences", preferences),
autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled),
/* Library */
addGameToLibrary: (
@ -104,7 +105,6 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getDiskFreeSpace", path),
/* Misc */
getOrCacheImage: (url: string) => ipcRenderer.invoke("getOrCacheImage", url),
ping: () => ipcRenderer.invoke("ping"),
getVersion: () => ipcRenderer.invoke("getVersion"),
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),

View file

@ -6,7 +6,7 @@
<title>Hydra</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: hydra: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: hydra: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com;"
/>
</head>
<body style="background-color: #1c1c1c">

View file

@ -1,29 +0,0 @@
import { forwardRef, useEffect, useState } from "react";
export interface AsyncImageProps
extends React.DetailedHTMLProps<
React.ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
> {
onSettled?: (url: string) => void;
}
export const AsyncImage = forwardRef<HTMLImageElement, AsyncImageProps>(
({ onSettled, ...props }, ref) => {
const [source, setSource] = useState<string | null>(null);
useEffect(() => {
if (props.src && props.src.startsWith("http")) {
window.electron.getOrCacheImage(props.src).then((url) => {
setSource(url);
if (onSettled) onSettled(url);
});
}
}, [props.src, onSettled]);
return <img ref={ref} {...props} src={source ?? props.src} />;
}
);
AsyncImage.displayName = "AsyncImage";

View file

@ -4,8 +4,6 @@ import type { CatalogueEntry } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import EpicGamesLogo from "@renderer/assets/epic-games-logo.svg?react";
import { AsyncImage } from "../async-image/async-image";
import * as styles from "./game-card.css";
import { useAppSelector } from "@renderer/hooks";
import { useTranslation } from "react-i18next";
@ -43,11 +41,7 @@ export function GameCard({ game, disabled, ...props }: GameCardProps) {
disabled={disabled}
>
<div className={styles.backdrop}>
<AsyncImage
src={game.cover}
alt={game.title}
className={styles.cover}
/>
<img src={game.cover} alt={game.title} className={styles.cover} />
<div className={styles.content}>
<div className={styles.titleContainer}>

View file

@ -1,5 +1,4 @@
import { useNavigate } from "react-router-dom";
import { AsyncImage } from "@renderer/components";
import * as styles from "./hero.css";
import { useEffect, useState } from "react";
import { ShopDetails } from "@types";
@ -35,14 +34,14 @@ export function Hero() {
className={styles.hero}
>
<div className={styles.backdrop}>
<AsyncImage
<img
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
alt={featuredGameDetails?.name}
className={styles.heroMedia}
/>
<div className={styles.content}>
<AsyncImage
<img
src={steamUrlBuilder.logo(FEATURED_GAME_ID)}
width="250px"
alt={featuredGameDetails?.name}

View file

@ -5,6 +5,5 @@ export * from "./header/header";
export * from "./hero/hero";
export * from "./modal/modal";
export * from "./sidebar/sidebar";
export * from "./async-image/async-image";
export * from "./text-field/text-field";
export * from "./checkbox-field/checkbox-field";

View file

@ -74,7 +74,6 @@ export const menuItem = recipe({
active: {
true: {
backgroundColor: "rgba(255, 255, 255, 0.1)",
fontWeight: "bold",
},
},
muted: {
@ -97,11 +96,6 @@ export const menuItemButton = style({
overflow: "hidden",
width: "100%",
padding: `9px ${SPACING_UNIT}px`,
selectors: {
[`${menuItem({ active: true }).split(" ")[1]} &`]: {
fontWeight: "bold",
},
},
});
export const menuItemButtonLabel = style({

View file

@ -4,7 +4,7 @@ import { useLocation, useNavigate } from "react-router-dom";
import type { Game } from "@types";
import { AsyncImage, TextField } from "@renderer/components";
import { TextField } from "@renderer/components";
import { useDownload, useLibrary } from "@renderer/hooks";
import { routes } from "./routes";
@ -14,7 +14,6 @@ import DiscordLogo from "@renderer/assets/discord-icon.svg?react";
import XLogo from "@renderer/assets/x-icon.svg?react";
import * as styles from "./sidebar.css";
import { vars } from "@renderer/theme.css";
const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250;
@ -217,7 +216,11 @@ export function Sidebar() {
)
}
>
<AsyncImage className={styles.gameIcon} src={game.iconUrl} />
<img
className={styles.gameIcon}
src={game.iconUrl}
alt={game.title}
/>
<span className={styles.menuItemButtonLabel}>
{getGameTitle(game)}
</span>

View file

@ -56,7 +56,7 @@ declare global {
objectID: string,
title: string,
shop: GameShop,
executablePath: string
executablePath: string | null
) => Promise<void>;
getLibrary: () => Promise<Game[]>;
getRepackersFriendlyNames: () => Promise<Record<string, string>>;
@ -74,12 +74,12 @@ declare global {
updateUserPreferences: (
preferences: Partial<UserPreferences>
) => Promise<void>;
autoLaunch: (enabled: boolean) => Promise<void>;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskSpace>;
/* Misc */
getOrCacheImage: (url: string) => Promise<string>;
openExternal: (src: string) => Promise<void>;
getVersion: () => Promise<string>;
ping: () => string;

View file

@ -24,5 +24,7 @@ export const getSteamLanguage = (language: string) => {
if (language.startsWith("ru")) return "russian";
if (language.startsWith("it")) return "italian";
if (language.startsWith("hu")) return "hungarian";
if (language.startsWith("pl")) return "polish";
return "english";
};

View file

@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { AsyncImage, Button, TextField } from "@renderer/components";
import { Button, TextField } from "@renderer/components";
import { formatDownloadProgress, steamUrlBuilder } from "@renderer/helpers";
import { useDownload, useLibrary } from "@renderer/hooks";
import type { Game } from "@types";
@ -103,6 +103,7 @@ export function Downloads() {
</>
);
}
if (game?.status === "cancelled") return <p>{t("cancelled")}</p>;
if (game?.status === "downloading_metadata")
return <p>{t("starting_download")}</p>;
@ -115,6 +116,8 @@ export function Downloads() {
</>
);
}
return null;
};
const openDeleteModal = (gameId: number) => {
@ -210,6 +213,12 @@ export function Downloads() {
);
};
const handleDeleteGame = () => {
if (gameToBeDeleted.current) {
deleteGame(gameToBeDeleted.current).then(updateLibrary);
}
};
return (
<section className={styles.downloadsContainer}>
<BinaryNotFoundModal
@ -219,9 +228,7 @@ export function Downloads() {
<DeleteModal
visible={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
deleteGame={() =>
deleteGame(gameToBeDeleted.current).then(updateLibrary)
}
deleteGame={handleDeleteGame}
/>
<TextField placeholder={t("filter")} onChange={handleFilter} />
@ -235,7 +242,7 @@ export function Downloads() {
cancelled: game.status === "cancelled",
})}
>
<AsyncImage
<img
src={steamUrlBuilder.library(game.objectID)}
className={styles.downloadCover}
alt={game.title}

View file

@ -12,7 +12,7 @@ import type {
SteamAppDetails,
} from "@types";
import { AsyncImage, Button } from "@renderer/components";
import { Button } from "@renderer/components";
import { setHeaderTitle } from "@renderer/features";
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
import { useAppDispatch, useDownload } from "@renderer/hooks";
@ -70,14 +70,16 @@ export function GameDetails() {
const { game: gameDownloading, startDownload, isDownloading } = useDownload();
const handleImageSettled = useCallback((url: string) => {
average(url, { amount: 1, format: "hex" })
const heroImage = steamUrlBuilder.libraryHero(objectID!);
const handleHeroLoad = () => {
average(heroImage, { amount: 1, format: "hex" })
.then((color) => {
const darkColor = new Color(color).darken(0.6).toString() as string;
setColor({ light: color as string, dark: darkColor });
})
.catch(() => {});
}, []);
};
const getGame = useCallback(() => {
window.electron
@ -218,15 +220,15 @@ export function GameDetails() {
) : (
<section className={styles.container}>
<div className={styles.hero}>
<AsyncImage
src={steamUrlBuilder.libraryHero(objectID!)}
<img
src={heroImage}
className={styles.heroImage}
alt={game?.title}
onSettled={handleImageSettled}
onLoad={handleHeroLoad}
/>
<div className={styles.heroBackdrop}>
<div className={styles.heroContent}>
<AsyncImage
<img
src={steamUrlBuilder.logo(objectID!)}
style={{ width: 300, alignSelf: "flex-end" }}
/>

View file

@ -68,7 +68,7 @@ export function HeroPanelActions({
try {
if (game) {
await removeGameFromLibrary(game.id);
} else {
} else if (gameDetails) {
const gameExecutablePath = await selectGameExecutable();
await window.electron.addGameToLibrary(
@ -87,30 +87,37 @@ export function HeroPanelActions({
};
const openGameInstaller = () => {
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
if (!isBinaryInPath) openBinaryNotFoundModal();
updateLibrary();
});
if (game) {
window.electron.openGameInstaller(game.id).then((isBinaryInPath) => {
if (!isBinaryInPath) openBinaryNotFoundModal();
updateLibrary();
});
}
};
const openGame = async () => {
if (game.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
if (game) {
if (game.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
if (game?.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
if (game?.executablePath) {
window.electron.openGame(game.id, game.executablePath);
return;
}
const gameExecutablePath = await selectGameExecutable();
window.electron.openGame(game.id, gameExecutablePath);
const gameExecutablePath = await selectGameExecutable();
if (gameExecutablePath)
window.electron.openGame(game.id, gameExecutablePath);
}
};
const closeGame = () => window.electron.closeGame(game.id);
const closeGame = () => {
if (game) window.electron.closeGame(game.id);
};
const deleting = isGameDeleting(game?.id);
const deleting = game ? isGameDeleting(game?.id) : false;
const toggleGameOnLibraryButton = (
<Button
@ -124,7 +131,7 @@ export function HeroPanelActions({
</Button>
);
if (isGameDownloading) {
if (game && isGameDownloading) {
return (
<>
<Button

View file

@ -98,7 +98,7 @@ export function HeroPanel({
return <p>{t("deleting")}</p>;
}
if (isGameDownloading) {
if (isGameDownloading && gameDownloading?.status) {
return (
<>
<p className={styles.downloadDetailsRow}>
@ -106,14 +106,14 @@ export function HeroPanel({
{eta && <small>{t("eta", { eta })}</small>}
</p>
{gameDownloading?.status !== "downloading" ? (
{gameDownloading.status !== "downloading" ? (
<>
<p>{t(gameDownloading?.status)}</p>
<p>{t(gameDownloading.status)}</p>
{eta && <small>{t("eta", { eta })}</small>}
</>
) : (
<p className={styles.downloadDetailsRow}>
{formatBytes(gameDownloading?.bytesDownloaded)} /{" "}
{formatBytes(gameDownloading.bytesDownloaded)} /{" "}
{finalDownloadSize}
<small>
{numPeers} peers / {numSeeds} seeds
@ -148,7 +148,7 @@ export function HeroPanel({
<>
<p>
{t("play_time", {
amount: formatPlayTime(game.playTimeInMilliseconds),
amount: formatPlayTime(),
})}
</p>

View file

@ -89,7 +89,9 @@ export function RepacksModal({
<p style={{ color: "#DADBE1" }}>{repack.title}</p>
<p style={{ fontSize: "12px" }}>
{repack.fileSize} - {repackersFriendlyNames[repack.repacker]} -{" "}
{format(repack.uploadDate, "dd/MM/yyyy")}
{repack.uploadDate
? format(repack.uploadDate, "dd/MM/yyyy")
: ""}
</p>
</Button>
))}

View file

@ -12,6 +12,7 @@ export function Settings() {
repackUpdatesNotificationsEnabled: false,
telemetryEnabled: false,
preferQuitInsteadOfHiding: false,
runAtStartup: false,
});
const { t } = useTranslation("settings");
@ -30,6 +31,7 @@ export function Settings() {
telemetryEnabled: userPreferences?.telemetryEnabled ?? false,
preferQuitInsteadOfHiding:
userPreferences?.preferQuitInsteadOfHiding ?? false,
runAtStartup: userPreferences?.runAtStartup ?? false,
});
});
}, []);
@ -123,6 +125,15 @@ export function Settings() {
)
}
/>
<CheckboxField
label={t("launch_with_system")}
onChange={() => {
updateUserPreferences("runAtStartup", !form.runAtStartup);
window.electron.autoLaunch(!form.runAtStartup);
}}
checked={form.runAtStartup}
/>
</div>
</section>
);

View file

@ -43,7 +43,7 @@ export interface SteamAppDetails {
minimum: string;
recommended: string;
};
linux_requirmenets: {
linux_requirements: {
minimum: string;
recommended: string;
};
@ -121,6 +121,7 @@ export interface UserPreferences {
repackUpdatesNotificationsEnabled: boolean;
telemetryEnabled: boolean;
preferQuitInsteadOfHiding: boolean;
runAtStartup: boolean;
}
export interface HowLongToBeatCategory {