This commit is contained in:
Manchik
2026-05-08 22:46:47 +03:00
parent 9b7001407f
commit 13dc22f74f
30 changed files with 4107 additions and 843 deletions
@@ -5,16 +5,31 @@ 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 }) {
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)} />
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
/>
<span className="toggle-track" />
</label>
);
}
function SettingRow({ label, desc, checked, onChange }: {
function SettingRow({
label,
desc,
checked,
onChange,
}: {
label: string;
desc?: string;
checked: boolean;
@@ -31,7 +46,13 @@ function SettingRow({ label, desc, checked, onChange }: {
);
}
function ThreadStepper({ value, onChange }: { value: number; onChange: (v: number) => void }) {
function ThreadStepper({
value,
onChange,
}: {
value: number;
onChange: (v: number) => void;
}) {
const [editing, setEditing] = useState(false);
const [input, setInput] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
@@ -42,14 +63,32 @@ function ThreadStepper({ value, onChange }: { value: number; onChange: (v: numbe
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";
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="Двойной клик на числе для ручного ввода">Max threads</span>
<span
className="text-sm text-gray-400 flex-1"
title="Двойной клик на числе для ручного ввода"
>
Max threads
</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>
<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}
@@ -57,18 +96,38 @@ function ThreadStepper({ value, onChange }: { value: number; onChange: (v: numbe
value={input}
onChange={(e) => setInput(e.target.value)}
onBlur={commit}
onKeyDown={(e) => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }}
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); }}
onDoubleClick={() => {
setInput(String(value));
setEditing(true);
}}
title="Двойной клик для ручного ввода"
>{value}</span>
>
{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>
<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>
);
@@ -81,21 +140,40 @@ export function ValidationSettings() {
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(() => {});
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) }));
addToast(
"error",
t("misc.error", { error: e instanceof Error ? e.message : String(e) }),
);
}
};
@@ -124,10 +202,75 @@ export function ValidationSettings() {
<SettingRow
label={t("tools.autoRevalidateBrowser")}
checked={settings.auto_revalidate_browser}
onChange={(v) => setSettings({ ...settings, auto_revalidate_browser: v })}
onChange={(v) =>
setSettings({ ...settings, auto_revalidate_browser: v })
}
/>
</div>
<ThreadStepper value={settings.max_threads} onChange={(v) => setSettings((s) => ({ ...s, max_threads: v }))} />
<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">
@@ -142,7 +285,9 @@ export function ValidationSettings() {
</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>
<button onClick={save} className="btn-primary">
{t("btn.save")}
</button>
</div>
</div>
);