diff --git a/README.md b/README.md index 2e0bd4cc..f28f2c51 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ pip install -r requirements.txt You'll need an SteamGridDB API Key in order to fetch the game icons on installation. If you want to have onlinefix as a repacker you'll need to add your credentials to the .env -Once you have it, you can copy or rename the `.env.example` file to `.env`and put it on`STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`. +Once you have it, you can copy or rename the `.env.example` file to `.env` and put it on`STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`. ## Running diff --git a/package.json b/package.json index 0ed0305a..48741503 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@reduxjs/toolkit": "^2.2.3", "@vanilla-extract/css": "^1.14.2", "@vanilla-extract/recipes": "^0.5.2", + "iso-639-1": "3.1.2", "aria2": "^4.1.2", "auto-launch": "^5.0.6", "axios": "^1.6.8", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index c7db2e86..7d36cba2 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -149,6 +149,7 @@ "general": "General", "behavior": "Behavior", "download_sources": "Download sources", + "language": "Language", "real_debrid_api_token": "API Token", "enable_real_debrid": "Enable Real-Debrid", "real_debrid_description": "Real-Debrid is an unrestricted downloader that allows you to download files instantly and at the best of your Internet speed.", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 501fa1e2..b7c86c54 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -29,12 +29,13 @@ "downloads": "Descargas", "search_results": "Resultados de búsqueda", "settings": "Ajustes", - "version_available": "Version {{version}} disponible. Haga clic aquí para reiniciar e instalar." + "version_available": "Version {{version}} disponible. Haz clic aquí para reiniciar e instalar." }, "bottom_panel": { "no_downloads_in_progress": "Sin descargas en progreso", "downloading_metadata": "Descargando metadatos de {{title}}…", - "downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}" + "downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}", + "calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Calculando tiempo restante…" }, "catalogue": { "next_page": "Siguiente página", @@ -54,6 +55,7 @@ "remove_from_list": "Quitar", "space_left_on_disk": "{{space}} restantes en el disco", "eta": "Tiempo restante: {{eta}}", + "calculating_eta": "Calculando tiempo restante…", "downloading_metadata": "Descargando metadatos…", "filter": "Buscar repacks", "requirements": "Requisitos del Sistema", @@ -61,6 +63,7 @@ "recommended": "Recomendados", "no_minimum_requirements": "Sin requisitos mínimos para {{title}}", "no_recommended_requirements": "{{title}} no tiene requisitos recomendados", + "paused": "Pausado", "release_date": "Fecha de lanzamiento: {{date}}", "publisher": "Publicado por: {{publisher}}", "copy_link_to_clipboard": "Copiar enlace", @@ -99,7 +102,9 @@ "previous_screenshot": "Anterior captura", "next_screenshot": "Siguiente captura", "screenshot": "Captura {{number}}", - "open_screenshot": "Abrir captura {{number}}" + "open_screenshot": "Abrir captura {{number}}", + "download_settings": "Ajustes de descarga", + "downloader": "Descargador" }, "activation": { "title": "Activar Hydra", @@ -117,6 +122,7 @@ "verifying": "Verificando…", "completed_at": "Completado el {{date}}", "completed": "Completado", + "removed": "No descargado", "download_again": "Descargar de nuevo", "cancel": "Cancelar", "filter": "Buscar juegos descargados", @@ -143,9 +149,16 @@ "launch_with_system": "Iniciar Hydra al inicio del sistema", "general": "General", "behavior": "Otros", + "language": "Idioma", + "real_debrid_api_token": "Token API", "enable_real_debrid": "Activar Real-Debrid", + "real_debrid_description": "Real-Debrid es un descargador sin restricciones que te permite descargar archivos instantáneamente con la máxima velocidad de tu internet.", + "real_debrid_invalid_token": "Token de API inválido", "real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí", - "save_changes": "Guardar cambios" + "real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratuita. Por favor, suscríbete a Real-Debrid", + "real_debrid_linked_message": "Cuenta \"{{username}}\" vinculada", + "save_changes": "Guardar cambios", + "changes_saved": "Ajustes guardados exitosamente" }, "notifications": { "download_complete": "Descarga completada", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index f3e7f2cf..352128f7 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -109,7 +109,8 @@ "enable_download_notifications": "Quand un téléchargement est terminé", "enable_repack_list_notifications": "Quand un nouveau repack est ajouté", "telemetry": "Télémétrie", - "telemetry_description": "Activer les statistiques d'utilisation anonymes" + "telemetry_description": "Activer les statistiques d'utilisation anonymes", + "language": "Langue" }, "notifications": { "download_complete": "Téléchargement terminé", diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json index 4dcb8cbd..b2ec4e4b 100644 --- a/src/locales/pl/translation.json +++ b/src/locales/pl/translation.json @@ -142,6 +142,7 @@ "launch_with_system": "Uruchom Hydra przy starcie systemu", "general": "Ogólne", "behavior": "Zachowania", + "language": "Język", "enable_real_debrid": "Włącz Real-Debrid", "real_debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj", "save_changes": "Zapisz zmiany" diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json index 90d126be..65887813 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -146,6 +146,7 @@ "general": "Geral", "behavior": "Comportamento", "download_sources": "Bibliotecas de download", + "language": "Idioma", "real_debrid_api_token": "Token de API", "enable_real_debrid": "Habilitar Real-Debrid", "real_debrid_api_token_hint": "Você pode obter seu token de API <0>aqui", diff --git a/src/main/events/library/open-game-installer.ts b/src/main/events/library/open-game-installer.ts index 9d3f6a7a..5a3295eb 100644 --- a/src/main/events/library/open-game-installer.ts +++ b/src/main/events/library/open-game-installer.ts @@ -1,13 +1,28 @@ -import { gameRepository } from "@main/repository"; -import { generateYML } from "../helpers/generate-lutris-yaml"; +import { shell } from "electron"; import path from "node:path"; import fs from "node:fs"; import { writeFile } from "node:fs/promises"; import { spawnSync, exec } from "node:child_process"; -import { registerEvent } from "../register-event"; -import { shell } from "electron"; +import { gameRepository } from "@main/repository"; + +import { generateYML } from "../helpers/generate-lutris-yaml"; import { getDownloadsPath } from "../helpers/get-downloads-path"; +import { registerEvent } from "../register-event"; + +const executeGameInstaller = (filePath: string) => { + if (process.platform === "win32") { + shell.openPath(filePath); + return true; + } + + if (spawnSync("which", ["wine"]).status === 0) { + exec(`wine "${filePath}"`); + return true; + } + + return false; +}; const openGameInstaller = async ( _event: Electron.IpcMainInvokeEvent, @@ -17,7 +32,7 @@ const openGameInstaller = async ( where: { id: gameId, isDeleted: false }, }); - if (!game) return true; + if (!game || !game.folderName) return true; const gamePath = path.join( game.downloadPath ?? (await getDownloadsPath()), @@ -29,15 +44,24 @@ const openGameInstaller = async ( return true; } - const setupPath = path.join(gamePath, "setup.exe"); - if (!fs.existsSync(setupPath)) { - shell.openPath(gamePath); - return true; + if (fs.lstatSync(gamePath).isFile()) { + return executeGameInstaller(gamePath); } - if (process.platform === "win32") { - shell.openPath(setupPath); - return true; + const setupPath = path.join(gamePath, "setup.exe"); + if (fs.existsSync(setupPath)) { + return executeGameInstaller(setupPath); + } + + const gamePathFileNames = fs.readdirSync(gamePath); + const gamePathExecutableFiles = gamePathFileNames.filter( + (fileName: string) => path.extname(fileName).toLowerCase() === ".exe" + ); + + if (gamePathExecutableFiles.length === 1) { + return executeGameInstaller( + path.join(gamePath, gamePathExecutableFiles[0]) + ); } if (spawnSync("which", ["lutris"]).status === 0) { @@ -47,12 +71,8 @@ const openGameInstaller = async ( return true; } - if (spawnSync("which", ["wine"]).status === 0) { - exec(`wine "${setupPath}"`); - return true; - } - - return false; + shell.openPath(gamePath); + return true; }; registerEvent("openGameInstaller", openGameInstaller); diff --git a/src/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index da97bc5f..3885b300 100644 --- a/src/renderer/src/components/index.ts +++ b/src/renderer/src/components/index.ts @@ -8,4 +8,5 @@ export * from "./sidebar/sidebar"; export * from "./text-field/text-field"; export * from "./checkbox-field/checkbox-field"; export * from "./link/link"; +export * from "./select-field/select-field"; export * from "./toast/toast"; diff --git a/src/renderer/src/components/select-field/select-field.css.ts b/src/renderer/src/components/select-field/select-field.css.ts new file mode 100644 index 00000000..83a21c37 --- /dev/null +++ b/src/renderer/src/components/select-field/select-field.css.ts @@ -0,0 +1,58 @@ +import { SPACING_UNIT, vars } from "../../theme.css"; +import { style } from "@vanilla-extract/css"; +import { recipe } from "@vanilla-extract/recipes"; + +export const select = recipe({ + base: { + display: "inline-flex", + transition: "all ease 0.2s", + width: "fit-content", + alignItems: "center", + borderRadius: "8px", + border: `1px solid ${vars.color.border}`, + height: "40px", + minHeight: "40px", + }, + variants: { + focused: { + true: { + borderColor: "#DADBE1", + }, + false: { + ":hover": { + borderColor: "rgba(255, 255, 255, 0.5)", + }, + }, + }, + theme: { + primary: { + backgroundColor: vars.color.darkBackground, + }, + dark: { + backgroundColor: vars.color.background, + }, + }, + }, +}); + +export const option = style({ + backgroundColor: vars.color.darkBackground, + borderRight: "4px solid", + borderColor: "transparent", + borderRadius: "8px", + width: "fit-content", + height: "100%", + outline: "none", + color: "#DADBE1", + cursor: "default", + fontFamily: "inherit", + fontSize: vars.size.body, + textOverflow: "ellipsis", + padding: `${SPACING_UNIT}px`, +}); + +export const label = style({ + marginBottom: `${SPACING_UNIT}px`, + display: "block", + color: vars.color.body, +}); diff --git a/src/renderer/src/components/select-field/select-field.tsx b/src/renderer/src/components/select-field/select-field.tsx new file mode 100644 index 00000000..fb5038f6 --- /dev/null +++ b/src/renderer/src/components/select-field/select-field.tsx @@ -0,0 +1,51 @@ +import { useId, useState } from "react"; +import type { RecipeVariants } from "@vanilla-extract/recipes"; +import * as styles from "./select-field.css"; + +export interface SelectProps + extends React.DetailedHTMLProps< + React.SelectHTMLAttributes, + HTMLSelectElement + > { + theme?: NonNullable>["theme"]; + label?: string; + options?: { key: string; value: string; label: string }[]; +} + +export function SelectField({ + value, + label, + options = [{ key: "-", value: value?.toString() || "-", label: "-" }], + theme = "primary", + onChange, +}: SelectProps) { + const [isFocused, setIsFocused] = useState(false); + const id = useId(); + + return ( +
+ {label && ( + + )} + +
+ +
+
+ ); +} diff --git a/src/renderer/src/pages/settings/settings-general.tsx b/src/renderer/src/pages/settings/settings-general.tsx index 8e2ee6c5..e2f1f1d7 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -1,12 +1,26 @@ import { useEffect, useState } from "react"; +import ISO6391 from "iso-639-1"; -import { TextField, Button, CheckboxField } from "@renderer/components"; +import { + TextField, + Button, + CheckboxField, + SelectField, +} from "@renderer/components"; import { useTranslation } from "react-i18next"; - import * as styles from "./settings-general.css"; import type { UserPreferences } from "@types"; import { useAppSelector } from "@renderer/hooks"; +import { changeLanguage } from "i18next"; +import * as languageResources from "@locales"; +import { orderBy } from "lodash-es"; + +interface LanguageOption { + option: string; + nativeName: string; +} + export interface SettingsGeneralProps { updateUserPreferences: (values: Partial) => void; } @@ -14,6 +28,8 @@ export interface SettingsGeneralProps { export function SettingsGeneral({ updateUserPreferences, }: SettingsGeneralProps) { + const { t } = useTranslation("settings"); + const userPreferences = useAppSelector( (state) => state.userPreferences.value ); @@ -22,28 +38,45 @@ export function SettingsGeneral({ downloadsPath: "", downloadNotificationsEnabled: false, repackUpdatesNotificationsEnabled: false, + language: "", }); + const [languageOptions, setLanguageOptions] = useState([]); + + const [defaultDownloadsPath, setDefaultDownloadsPath] = useState(""); + useEffect(() => { - if (userPreferences) { - const { - downloadsPath, - downloadNotificationsEnabled, - repackUpdatesNotificationsEnabled, - } = userPreferences; - - window.electron.getDefaultDownloadsPath().then((defaultDownloadsPath) => { - setForm((prev) => ({ - ...prev, - downloadsPath: downloadsPath ?? defaultDownloadsPath, - downloadNotificationsEnabled, - repackUpdatesNotificationsEnabled, - })); - }); + async function fetchdefaultDownloadsPath() { + setDefaultDownloadsPath(await window.electron.getDefaultDownloadsPath()); } - }, [userPreferences]); - const { t } = useTranslation("settings"); + fetchdefaultDownloadsPath(); + + setLanguageOptions( + orderBy( + Object.keys(languageResources).map((language) => { + return { + nativeName: ISO6391.getNativeName(language), + option: language, + }; + }), + ["nativeName"], + "asc" + ) + ); + }, []); + + useEffect(updateFormWithUserPreferences, [ + userPreferences, + defaultDownloadsPath, + ]); + + const handleLanguageChange = (event) => { + const value = event.target.value; + + handleChange({ language: value }); + changeLanguage(value); + }; const handleChange = (values: Partial) => { setForm((prev) => ({ ...prev, ...values })); @@ -59,10 +92,25 @@ export function SettingsGeneral({ if (filePaths && filePaths.length > 0) { const path = filePaths[0]; handleChange({ downloadsPath: path }); - updateUserPreferences({ downloadsPath: path }); } }; + function updateFormWithUserPreferences() { + if (userPreferences) { + const parsedLanguage = userPreferences.language.split("-")[0]; + + setForm((prev) => ({ + ...prev, + downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath, + downloadNotificationsEnabled: + userPreferences.downloadNotificationsEnabled, + repackUpdatesNotificationsEnabled: + userPreferences.repackUpdatesNotificationsEnabled, + language: parsedLanguage, + })); + } + } + return ( <>
@@ -82,28 +130,40 @@ export function SettingsGeneral({
+ ({ + key: language.option, + value: language.option, + label: language.nativeName, + }))} + /> +

{t("notifications")}

+ <> + + handleChange({ + downloadNotificationsEnabled: !form.downloadNotificationsEnabled, + }) + } + /> - - handleChange({ - downloadNotificationsEnabled: !form.downloadNotificationsEnabled, - }) - } - /> - - - handleChange({ - repackUpdatesNotificationsEnabled: - !form.repackUpdatesNotificationsEnabled, - }) - } - /> + + handleChange({ + repackUpdatesNotificationsEnabled: + !form.repackUpdatesNotificationsEnabled, + }) + } + /> + ); } diff --git a/yarn.lock b/yarn.lock index bfdb4c24..9114d4f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4042,6 +4042,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +iso-639-1@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-3.1.2.tgz#86a53dcce056e0d856b17c022eb059eb16a35426" + integrity sha512-Le7BRl3Jt9URvaiEHJCDEdvPZCfhiQoXnFgLAWNRhzFMwRFdWO7/5tLRQbiPzE394I9xd7KdRCM7S6qdOhwG5A== + iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -5513,16 +5518,7 @@ stat-mode@^1.0.0: resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -5593,14 +5589,7 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6169,16 +6158,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==