mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
add language selection menu
This commit is contained in:
parent
29d9c43834
commit
72aa822655
9853
package-lock.json
generated
Normal file
9853
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,7 @@
|
|||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@vanilla-extract/css": "^1.14.2",
|
"@vanilla-extract/css": "^1.14.2",
|
||||||
"@vanilla-extract/recipes": "^0.5.2",
|
"@vanilla-extract/recipes": "^0.5.2",
|
||||||
|
"iso-639-1": "3.1.2",
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"better-sqlite3": "^9.5.0",
|
"better-sqlite3": "^9.5.0",
|
||||||
|
@ -153,7 +153,8 @@
|
|||||||
"enable_real_debrid": "Enable Real Debrid",
|
"enable_real_debrid": "Enable Real Debrid",
|
||||||
"real_debrid": "Real Debrid",
|
"real_debrid": "Real Debrid",
|
||||||
"real_debrid_api_token_hint": "You can get your API key <0>here</0>.",
|
"real_debrid_api_token_hint": "You can get your API key <0>here</0>.",
|
||||||
"save_changes": "Save changes"
|
"save_changes": "Save changes",
|
||||||
|
"language": "Language"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download complete",
|
"download_complete": "Download complete",
|
||||||
|
@ -153,7 +153,8 @@
|
|||||||
"enable_real_debrid": "Activar Real Debrid",
|
"enable_real_debrid": "Activar Real Debrid",
|
||||||
"real_debrid": "Real Debrid",
|
"real_debrid": "Real Debrid",
|
||||||
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>.",
|
"real_debrid_api_token_hint": "Puedes obtener tu clave de API <0>aquí</0>.",
|
||||||
"save_changes": "Guardar cambios"
|
"save_changes": "Guardar cambios",
|
||||||
|
"language": "Idioma"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Descarga completada",
|
"download_complete": "Descarga completada",
|
||||||
|
@ -115,7 +115,8 @@
|
|||||||
"enable_download_notifications": "Quand un téléchargement est terminé",
|
"enable_download_notifications": "Quand un téléchargement est terminé",
|
||||||
"enable_repack_list_notifications": "Quand un nouveau repack est ajouté",
|
"enable_repack_list_notifications": "Quand un nouveau repack est ajouté",
|
||||||
"telemetry": "Télémétrie",
|
"telemetry": "Télémétrie",
|
||||||
"telemetry_description": "Activer les statistiques d'utilisation anonymes"
|
"telemetry_description": "Activer les statistiques d'utilisation anonymes",
|
||||||
|
"language": "Langue"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Téléchargement terminé",
|
"download_complete": "Téléchargement terminé",
|
||||||
|
@ -149,7 +149,8 @@
|
|||||||
"enable_real_debrid": "Habilitar Real Debrid",
|
"enable_real_debrid": "Habilitar Real Debrid",
|
||||||
"real_debrid": "Real Debrid",
|
"real_debrid": "Real Debrid",
|
||||||
"real_debrid_api_token_hint": "Você pode obter sua chave de API <0>aqui</0>.",
|
"real_debrid_api_token_hint": "Você pode obter sua chave de API <0>aqui</0>.",
|
||||||
"save_changes": "Salvar mudanças"
|
"save_changes": "Salvar mudanças",
|
||||||
|
"language": "Idioma"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"download_complete": "Download concluído",
|
"download_complete": "Download concluído",
|
||||||
|
@ -8,3 +8,4 @@ export * from "./sidebar/sidebar";
|
|||||||
export * from "./text-field/text-field";
|
export * from "./text-field/text-field";
|
||||||
export * from "./checkbox-field/checkbox-field";
|
export * from "./checkbox-field/checkbox-field";
|
||||||
export * from "./link/link";
|
export * from "./link/link";
|
||||||
|
export * from "./select/select";
|
||||||
|
60
src/renderer/src/components/select/select.css.ts
Normal file
60
src/renderer/src/components/select/select.css.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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,
|
||||||
|
borderColor: "transparent",
|
||||||
|
borderRadius: "8px",
|
||||||
|
width: "fit-content",
|
||||||
|
height: "100%",
|
||||||
|
outline: "none",
|
||||||
|
color: "#DADBE1",
|
||||||
|
cursor: "default",
|
||||||
|
fontFamily: "inherit",
|
||||||
|
fontSize: vars.size.bodyFontSize,
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
padding: `${SPACING_UNIT}px`,
|
||||||
|
":focus": {
|
||||||
|
cursor: "text",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const label = style({
|
||||||
|
marginBottom: `${SPACING_UNIT}px`,
|
||||||
|
display: "block",
|
||||||
|
color: vars.color.bodyText,
|
||||||
|
});
|
46
src/renderer/src/components/select/select.tsx
Normal file
46
src/renderer/src/components/select/select.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
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>,
|
||||||
|
HTMLSelectElement
|
||||||
|
> {
|
||||||
|
theme?: NonNullable<RecipeVariants<typeof styles.select>>["theme"];
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Select({
|
||||||
|
value,
|
||||||
|
theme = "primary",
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
onChange,
|
||||||
|
}: SelectProps) {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const id = useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
{label && (
|
||||||
|
<label htmlFor={id} className={styles.label}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.select({ focused: isFocused, theme })}>
|
||||||
|
<select
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
className={styles.option}
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={() => setIsFocused(false)}
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,11 +1,14 @@
|
|||||||
import { useEffect, useState } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import * as styles from "./settings-general.css";
|
import * as styles from "./settings-general.css";
|
||||||
import type { UserPreferences } from "@types";
|
import type { UserPreferences } from "@types";
|
||||||
|
|
||||||
|
import { changeLanguage } from "i18next";
|
||||||
|
import * as languageResources from "@locales";
|
||||||
|
|
||||||
export interface SettingsGeneralProps {
|
export interface SettingsGeneralProps {
|
||||||
userPreferences: UserPreferences | null;
|
userPreferences: UserPreferences | null;
|
||||||
updateUserPreferences: (values: Partial<UserPreferences>) => void;
|
updateUserPreferences: (values: Partial<UserPreferences>) => void;
|
||||||
@ -15,34 +18,41 @@ export function SettingsGeneral({
|
|||||||
userPreferences,
|
userPreferences,
|
||||||
updateUserPreferences,
|
updateUserPreferences,
|
||||||
}: SettingsGeneralProps) {
|
}: SettingsGeneralProps) {
|
||||||
const [form, setForm] = useState({
|
|
||||||
downloadsPath: "",
|
|
||||||
downloadNotificationsEnabled: false,
|
|
||||||
repackUpdatesNotificationsEnabled: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userPreferences) {
|
|
||||||
const {
|
|
||||||
downloadsPath,
|
|
||||||
downloadNotificationsEnabled,
|
|
||||||
repackUpdatesNotificationsEnabled,
|
|
||||||
} = userPreferences;
|
|
||||||
|
|
||||||
window.electron.getDefaultDownloadsPath().then((defaultDownloadsPath) => {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
downloadsPath: downloadsPath ?? defaultDownloadsPath,
|
|
||||||
downloadNotificationsEnabled,
|
|
||||||
repackUpdatesNotificationsEnabled,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [userPreferences]);
|
|
||||||
|
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
|
|
||||||
|
const [form, setForm] = useState(
|
||||||
|
{
|
||||||
|
downloadsPath: '',
|
||||||
|
downloadNotificationsEnabled: false,
|
||||||
|
repackUpdatesNotificationsEnabled: false,
|
||||||
|
language: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [defaultDownloadsPath, setdefaultDownloadsPath] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchdefaultDownloadsPath() {
|
||||||
|
setdefaultDownloadsPath(await window.electron.getDefaultDownloadsPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchdefaultDownloadsPath();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(updateFormWithUserPreferences, [userPreferences, defaultDownloadsPath]);
|
||||||
|
|
||||||
|
const handleLanguageChange = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
|
||||||
|
handleChange({ language: value });
|
||||||
|
changeLanguage(value);
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (values: Partial<typeof form>) => {
|
const handleChange = (values: Partial<typeof form>) => {
|
||||||
|
// TODO: why is the setForm needed if updateUserPreferences already changes the
|
||||||
|
// UserPreferences and useEffect(updateFormWithUserPreferences)
|
||||||
|
// does the setForm((prev) in the callback function?
|
||||||
setForm((prev) => ({ ...prev, ...values }));
|
setForm((prev) => ({ ...prev, ...values }));
|
||||||
updateUserPreferences(values);
|
updateUserPreferences(values);
|
||||||
};
|
};
|
||||||
@ -56,10 +66,21 @@ export function SettingsGeneral({
|
|||||||
if (filePaths && filePaths.length > 0) {
|
if (filePaths && filePaths.length > 0) {
|
||||||
const path = filePaths[0];
|
const path = filePaths[0];
|
||||||
handleChange({ downloadsPath: path });
|
handleChange({ downloadsPath: path });
|
||||||
updateUserPreferences({ downloadsPath: path });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function updateFormWithUserPreferences(){
|
||||||
|
if (userPreferences) {
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
downloadsPath: userPreferences.downloadsPath ?? defaultDownloadsPath,
|
||||||
|
downloadNotificationsEnabled: userPreferences.downloadNotificationsEnabled,
|
||||||
|
repackUpdatesNotificationsEnabled: userPreferences.repackUpdatesNotificationsEnabled,
|
||||||
|
language: userPreferences.language
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.downloadsPathField}>
|
<div className={styles.downloadsPathField}>
|
||||||
@ -79,28 +100,38 @@ export function SettingsGeneral({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>{t("language")}</h3>
|
||||||
|
<>
|
||||||
|
<Select value={form.language} onChange={handleLanguageChange}>
|
||||||
|
{Object.keys(languageResources).map(language => (
|
||||||
|
<option key={language} value={language}>{ISO6391.getName(language)}</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
|
||||||
<h3>{t("notifications")}</h3>
|
<h3>{t("notifications")}</h3>
|
||||||
|
<>
|
||||||
|
<CheckboxField
|
||||||
|
label={t("enable_download_notifications")}
|
||||||
|
checked={form.downloadNotificationsEnabled}
|
||||||
|
onChange={() =>
|
||||||
|
handleChange({
|
||||||
|
downloadNotificationsEnabled: !form.downloadNotificationsEnabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<CheckboxField
|
<CheckboxField
|
||||||
label={t("enable_download_notifications")}
|
label={t("enable_repack_list_notifications")}
|
||||||
checked={form.downloadNotificationsEnabled}
|
checked={form.repackUpdatesNotificationsEnabled}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
handleChange({
|
handleChange({
|
||||||
downloadNotificationsEnabled: !form.downloadNotificationsEnabled,
|
repackUpdatesNotificationsEnabled:
|
||||||
})
|
!form.repackUpdatesNotificationsEnabled,
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
<CheckboxField
|
</>
|
||||||
label={t("enable_repack_list_notifications")}
|
|
||||||
checked={form.repackUpdatesNotificationsEnabled}
|
|
||||||
onChange={() =>
|
|
||||||
handleChange({
|
|
||||||
repackUpdatesNotificationsEnabled:
|
|
||||||
!form.repackUpdatesNotificationsEnabled,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -27,32 +27,31 @@ export function Settings() {
|
|||||||
window.electron.updateUserPreferences(values);
|
window.electron.updateUserPreferences(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCategory = () => {
|
function renderCategory() {
|
||||||
if (currentCategory === "general") {
|
switch (currentCategory) {
|
||||||
return (
|
case "general":
|
||||||
<SettingsGeneral
|
return (
|
||||||
userPreferences={userPreferences}
|
<SettingsGeneral
|
||||||
updateUserPreferences={handleUpdateUserPreferences}
|
userPreferences={userPreferences}
|
||||||
/>
|
updateUserPreferences={handleUpdateUserPreferences}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
case "real_debrid":
|
||||||
|
return (
|
||||||
|
<SettingsRealDebrid
|
||||||
|
userPreferences={userPreferences}
|
||||||
|
updateUserPreferences={handleUpdateUserPreferences}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<SettingsBehavior
|
||||||
|
userPreferences={userPreferences}
|
||||||
|
updateUserPreferences={handleUpdateUserPreferences}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (currentCategory === "real_debrid") {
|
|
||||||
return (
|
|
||||||
<SettingsRealDebrid
|
|
||||||
userPreferences={userPreferences}
|
|
||||||
updateUserPreferences={handleUpdateUserPreferences}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsBehavior
|
|
||||||
userPreferences={userPreferences}
|
|
||||||
updateUserPreferences={handleUpdateUserPreferences}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.container}>
|
<section className={styles.container}>
|
||||||
|
Loading…
Reference in New Issue
Block a user