forked from FOSS/Steam-Panel
296 lines
9.0 KiB
TypeScript
296 lines
9.0 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
|
import type { ValidationSettings as VS } from "@/api/types";
|
|
import { api } from "@/api/client";
|
|
import { useUiStore } from "@/stores/uiStore";
|
|
import { useT } from "@/lib/i18n";
|
|
import { IconSettings } from "@/components/shared/Icons";
|
|
|
|
function Toggle({
|
|
checked,
|
|
onChange,
|
|
}: {
|
|
checked: boolean;
|
|
onChange: (v: boolean) => void;
|
|
}) {
|
|
return (
|
|
<label className="toggle-switch">
|
|
<input
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={(e) => onChange(e.target.checked)}
|
|
/>
|
|
<span className="toggle-track" />
|
|
</label>
|
|
);
|
|
}
|
|
|
|
function SettingRow({
|
|
label,
|
|
desc,
|
|
checked,
|
|
onChange,
|
|
}: {
|
|
label: string;
|
|
desc?: string;
|
|
checked: boolean;
|
|
onChange: (v: boolean) => void;
|
|
}) {
|
|
return (
|
|
<div className="flex items-center justify-between gap-4 py-2.5">
|
|
<div className="min-w-0">
|
|
<p className="text-sm text-gray-200 leading-tight">{label}</p>
|
|
{desc && <p className="text-xs text-gray-500 mt-0.5">{desc}</p>}
|
|
</div>
|
|
<Toggle checked={checked} onChange={onChange} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ThreadStepper({
|
|
value,
|
|
onChange,
|
|
}: {
|
|
value: number;
|
|
onChange: (v: number) => void;
|
|
}) {
|
|
const t = useT();
|
|
const [editing, setEditing] = useState(false);
|
|
const [input, setInput] = useState("");
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const clamp = (n: number) => Math.max(1, n);
|
|
const commit = () => {
|
|
onChange(clamp(parseInt(input) || value));
|
|
setEditing(false);
|
|
};
|
|
|
|
const btnCls =
|
|
"h-6 px-1.5 rounded bg-dark-700 border border-dark-600 text-gray-300 hover:bg-dark-600 text-xs flex items-center justify-center select-none leading-none";
|
|
|
|
return (
|
|
<div className="flex items-center gap-3 pt-3 mt-0.5">
|
|
<span
|
|
className="text-sm text-gray-400 flex-1"
|
|
title="Двойной клик на числе для ручного ввода"
|
|
>
|
|
{t("tools.maxThreads")}
|
|
</span>
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
className={btnCls}
|
|
onClick={() => onChange(clamp(value - 10))}
|
|
>
|
|
-10
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={btnCls}
|
|
onClick={() => onChange(clamp(value - 1))}
|
|
>
|
|
-1
|
|
</button>
|
|
{editing ? (
|
|
<input
|
|
ref={inputRef}
|
|
autoFocus
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onBlur={commit}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") commit();
|
|
if (e.key === "Escape") setEditing(false);
|
|
}}
|
|
className="w-10 text-center text-sm font-mono bg-dark-900 border border-accent rounded outline-none text-gray-100 py-0.5"
|
|
/>
|
|
) : (
|
|
<span
|
|
className="w-10 text-center text-sm font-mono text-gray-100 cursor-pointer select-none"
|
|
onDoubleClick={() => {
|
|
setInput(String(value));
|
|
setEditing(true);
|
|
}}
|
|
title="Двойной клик для ручного ввода"
|
|
>
|
|
{value}
|
|
</span>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className={btnCls}
|
|
onClick={() => onChange(clamp(value + 1))}
|
|
>
|
|
+1
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={btnCls}
|
|
onClick={() => onChange(clamp(value + 10))}
|
|
>
|
|
+10
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ValidationSettings() {
|
|
const t = useT();
|
|
const [settings, setSettings] = useState<VS>({
|
|
fetch_profile: true,
|
|
check_ban: true,
|
|
max_threads: 5,
|
|
auto_revalidate_browser: true,
|
|
auto_proxy_on_import: false,
|
|
auto_validate_on_import: false,
|
|
auto_proxy_on_import_mafile: true,
|
|
auto_proxy_on_import_logpass: true,
|
|
auto_proxy_on_import_token: true,
|
|
auto_validate_on_import_mafile: true,
|
|
auto_validate_on_import_logpass: true,
|
|
auto_validate_on_import_token: true,
|
|
});
|
|
const [importTab, setImportTab] = useState<"mafile" | "logpass" | "token">(
|
|
"mafile",
|
|
);
|
|
const hidePasswords = useUiStore((s) => s.hidePasswords);
|
|
const setHidePasswords = useUiStore((s) => s.setHidePasswords);
|
|
const loadImportSettings = useUiStore((s) => s.loadImportSettings);
|
|
const addToast = useUiStore((s) => s.addToast);
|
|
|
|
useEffect(() => {
|
|
api
|
|
.getValidationSettings()
|
|
.then(setSettings)
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const save = async () => {
|
|
try {
|
|
await api.updateValidationSettings(settings);
|
|
await loadImportSettings(); // sync stale uiStore values
|
|
addToast("success", t("toast.settingsSaved"));
|
|
} catch (e: unknown) {
|
|
addToast(
|
|
"error",
|
|
t("misc.error", { error: e instanceof Error ? e.message : String(e) }),
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="bg-dark-800 border border-dark-600 rounded-xl overflow-hidden flex flex-col">
|
|
<div className="px-5 py-3.5 border-b border-dark-600 flex items-center gap-2.5">
|
|
<IconSettings size={15} className="text-accent shrink-0" />
|
|
<span className="text-sm font-semibold text-gray-100">Settings</span>
|
|
</div>
|
|
|
|
<div className="px-5 pt-4 pb-3 border-b border-dark-600">
|
|
<p className="text-[10px] font-semibold text-gray-500 uppercase tracking-widest mb-0.5">
|
|
{t("tools.validationSettings")}
|
|
</p>
|
|
<div className="divide-y divide-dark-600/60">
|
|
<SettingRow
|
|
label={t("tools.loadProfile")}
|
|
checked={settings.fetch_profile}
|
|
onChange={(v) => setSettings({ ...settings, fetch_profile: v })}
|
|
/>
|
|
<SettingRow
|
|
label={t("tools.checkBans")}
|
|
checked={settings.check_ban}
|
|
onChange={(v) => setSettings({ ...settings, check_ban: v })}
|
|
/>
|
|
<SettingRow
|
|
label={t("tools.autoRevalidateBrowser")}
|
|
checked={settings.auto_revalidate_browser}
|
|
onChange={(v) =>
|
|
setSettings({ ...settings, auto_revalidate_browser: v })
|
|
}
|
|
/>
|
|
</div>
|
|
<ThreadStepper
|
|
value={settings.max_threads}
|
|
onChange={(v) => setSettings((s) => ({ ...s, max_threads: v }))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="px-5 pt-4 pb-3 border-b border-dark-600">
|
|
<p className="text-[10px] font-semibold text-gray-500 uppercase tracking-widest mb-1.5">
|
|
{t("tools.importSettings")}
|
|
</p>
|
|
{/* Tab switcher */}
|
|
<div className="flex gap-1 mb-3">
|
|
{(["mafile", "logpass", "token"] as const).map((tab) => (
|
|
<button
|
|
key={tab}
|
|
type="button"
|
|
onClick={() => setImportTab(tab)}
|
|
className={`px-2.5 py-1 rounded text-xs font-medium transition-colors ${
|
|
importTab === tab
|
|
? "bg-accent/20 text-accent border border-accent/30"
|
|
: "text-gray-500 hover:text-gray-300 border border-transparent"
|
|
}`}
|
|
>
|
|
{t(
|
|
`tools.importTab${tab.charAt(0).toUpperCase() + tab.slice(1)}` as never,
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{/* Per-tab toggles */}
|
|
{(["mafile", "logpass", "token"] as const).map((tab) =>
|
|
importTab === tab ? (
|
|
<div key={tab} className="divide-y divide-dark-600/60">
|
|
<SettingRow
|
|
label={t("tools.autoProxyOnImport")}
|
|
desc={t("tools.autoProxyOnImportDesc")}
|
|
checked={
|
|
settings[`auto_proxy_on_import_${tab}` as keyof VS] as boolean
|
|
}
|
|
onChange={(v) =>
|
|
setSettings({
|
|
...settings,
|
|
[`auto_proxy_on_import_${tab}`]: v,
|
|
})
|
|
}
|
|
/>
|
|
<SettingRow
|
|
label={t("tools.autoValidateOnImport")}
|
|
desc={t("tools.autoValidateOnImportDesc")}
|
|
checked={
|
|
settings[
|
|
`auto_validate_on_import_${tab}` as keyof VS
|
|
] as boolean
|
|
}
|
|
onChange={(v) =>
|
|
setSettings({
|
|
...settings,
|
|
[`auto_validate_on_import_${tab}`]: v,
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
) : null,
|
|
)}
|
|
</div>
|
|
|
|
<div className="px-5 pt-4 pb-3">
|
|
<p className="text-[10px] font-semibold text-gray-500 uppercase tracking-widest mb-0.5">
|
|
{t("settings.display")}
|
|
</p>
|
|
<SettingRow
|
|
label={t("settings.hidePasswords")}
|
|
checked={hidePasswords}
|
|
onChange={setHidePasswords}
|
|
/>
|
|
</div>
|
|
|
|
<div className="px-5 py-3 border-t border-dark-600 flex justify-end">
|
|
<button onClick={save} className="btn-primary">
|
|
{t("btn.save")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|