diff --git a/package.json b/package.json index 608c91fd..99962918 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 31d1a5a0..643445e0 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -149,6 +149,7 @@ "launch_with_system": "Launch Hydra on system start-up", "general": "General", "behavior": "Behavior", + "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 67e59007..b7c86c54 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -149,6 +149,7 @@ "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.", 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 fadd1e49..df56c108 100644 --- a/src/locales/pt/translation.json +++ b/src/locales/pt/translation.json @@ -145,6 +145,7 @@ "launch_with_system": "Iniciar o Hydra junto com o sistema", "general": "Geral", "behavior": "Comportamento", + "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/renderer/src/components/index.ts b/src/renderer/src/components/index.ts index da97bc5f..5c95b649 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/select"; export * from "./toast/toast"; diff --git a/src/renderer/src/components/select/select.css.ts b/src/renderer/src/components/select/select.css.ts new file mode 100644 index 00000000..24c1e5f6 --- /dev/null +++ b/src/renderer/src/components/select/select.css.ts @@ -0,0 +1,61 @@ +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`, + ":focus": { + cursor: "text", + }, +}); + +export const label = style({ + marginBottom: `${SPACING_UNIT}px`, + display: "block", + color: vars.color.body, +}); diff --git a/src/renderer/src/components/select/select.tsx b/src/renderer/src/components/select/select.tsx new file mode 100644 index 00000000..af9dce0f --- /dev/null +++ b/src/renderer/src/components/select/select.tsx @@ -0,0 +1,51 @@ +import { useId, useState } from "react"; +import type { RecipeVariants } from "@vanilla-extract/recipes"; +import * as styles from "./select.css"; + +export interface SelectProps + extends React.DetailedHTMLProps< + React.SelectHTMLAttributes, + HTMLSelectElement + > { + theme?: NonNullable>["theme"]; + label?: string; + options?: { key: string; value: string; label: string }[]; +} + +export function Select({ + 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..eabdde80 100644 --- a/src/renderer/src/pages/settings/settings-general.tsx +++ b/src/renderer/src/pages/settings/settings-general.tsx @@ -1,12 +1,21 @@ import { useEffect, useState } from "react"; +import ISO6391 from "iso-639-1"; -import { TextField, Button, CheckboxField } from "@renderer/components"; +import { TextField, Button, CheckboxField, Select } 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 +23,8 @@ export interface SettingsGeneralProps { export function SettingsGeneral({ updateUserPreferences, }: SettingsGeneralProps) { + const { t } = useTranslation("settings"); + const userPreferences = useAppSelector( (state) => state.userPreferences.value ); @@ -22,28 +33,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 +87,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 +125,42 @@ export function SettingsGeneral({
+

{t("language")}

+ <> +