fix: simplify F95 cookie setup and constrain auth dialog layout
This commit is contained in:
1 parent
219005d16a
commit
ad622389bd
6 files changed
+172
-44
No files matched your search
@@ -708,6 +708,18 @@ input[type="file"] { padding: 0.4rem; }
|
||||
animation: pop 0.15s ease-out;
|
||||
}
|
||||
.modal-actions { display: flex; justify-content: flex-end; gap: 0.6rem; margin-top: var(--space-2); }
|
||||
.f95-modal {
|
||||
width: min(520px, 100%);
|
||||
min-width: 0;
|
||||
max-height: calc(100dvh - 2 * var(--space-4));
|
||||
overflow-y: auto;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.f95-modal > * { flex-shrink: 0; min-width: 0; }
|
||||
.f95-modal .modal-actions { flex-wrap: wrap; }
|
||||
.f95-modal .modal-actions .btn { flex: 1 1 180px; min-width: 0; white-space: normal; }
|
||||
.f95-modal details .field { margin-top: var(--space-3); }
|
||||
.f95-modal ol { padding-left: 1.25rem; margin-block: var(--space-2); }
|
||||
|
||||
.toasts {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { api } from "../api";
|
||||
|
||||
@@ -13,6 +13,9 @@ export interface F95Meta {
|
||||
metadata_available: boolean;
|
||||
}
|
||||
|
||||
interface CookieValues { xf_user: string; xf_session: string; cf_clearance: string }
|
||||
const EMPTY_COOKIES: CookieValues = { xf_user: "", xf_session: "", cf_clearance: "" };
|
||||
|
||||
/** Imports editable metadata and a locally cached cover from an F95zone thread. */
|
||||
export function F95Fetch({ onLoaded }: { onLoaded: (m: F95Meta) => void }) {
|
||||
const [url, setUrl] = useState("");
|
||||
@@ -20,12 +23,12 @@ export function F95Fetch({ onLoaded }: { onLoaded: (m: F95Meta) => void }) {
|
||||
const [error, setError] = useState("");
|
||||
const [warning, setWarning] = useState("");
|
||||
const [needsAuth, setNeedsAuth] = useState(false);
|
||||
const [cookie, setCookie] = useState("");
|
||||
const [cookies, setCookies] = useState<CookieValues>(EMPTY_COOKIES);
|
||||
const [agent, setAgent] = useState("");
|
||||
|
||||
const decline = () => {
|
||||
setNeedsAuth(false);
|
||||
setCookie("");
|
||||
setCookies(EMPTY_COOKIES);
|
||||
setAgent("");
|
||||
};
|
||||
|
||||
@@ -49,8 +52,8 @@ export function F95Fetch({ onLoaded }: { onLoaded: (m: F95Meta) => void }) {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await api.put("/api/metadata/f95/session", { cookie, user_agent: agent });
|
||||
setCookie("");
|
||||
await api.put("/api/metadata/f95/session", { ...cookies, user_agent: agent });
|
||||
setCookies(EMPTY_COOKIES);
|
||||
if (url.trim()) await load();
|
||||
else decline();
|
||||
} catch (e) {
|
||||
@@ -92,38 +95,43 @@ export function F95Fetch({ onLoaded }: { onLoaded: (m: F95Meta) => void }) {
|
||||
<button type="button" className="btn btn-ghost" disabled={busy} onClick={() => setNeedsAuth(true)}>Cookie F95zone</button>
|
||||
<button type="button" className="btn btn-ghost" disabled={busy} onClick={() => void forget()}>Забыть cookie F95zone</button>
|
||||
</div>
|
||||
{needsAuth && createPortal(<F95SessionDialog busy={busy} cookie={cookie} agent={agent}
|
||||
setCookie={setCookie} setAgent={setAgent} error={error} warning={warning}
|
||||
{needsAuth && createPortal(<F95SessionDialog busy={busy} cookies={cookies} agent={agent}
|
||||
setCookies={setCookies} setAgent={setAgent} error={error} warning={warning}
|
||||
onDecline={decline} onSave={() => void saveSession()} />, document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function F95SessionDialog({ busy, cookie, agent, setCookie, setAgent, error, warning, onDecline, onSave }: {
|
||||
busy: boolean; cookie: string; agent: string; error: string; warning: string;
|
||||
setCookie: (value: string) => void; setAgent: (value: string) => void;
|
||||
function F95SessionDialog({ busy, cookies, agent, setCookies, setAgent, error, warning, onDecline, onSave }: {
|
||||
busy: boolean; cookies: CookieValues; agent: string; error: string; warning: string;
|
||||
setCookies: (value: CookieValues) => void; setAgent: (value: string) => void;
|
||||
onDecline: () => void; onSave: () => void;
|
||||
}) {
|
||||
const dialog = useRef<HTMLDivElement>(null);
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
const userLabel = useId();
|
||||
const canSave = !!(cookies.xf_user.trim() || cookies.xf_session.trim());
|
||||
useEffect(() => {
|
||||
const previous = document.activeElement as HTMLElement | null;
|
||||
const overflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
input.current?.focus();
|
||||
return () => previous?.focus();
|
||||
return () => { document.body.style.overflow = overflow; previous?.focus(); };
|
||||
}, []);
|
||||
|
||||
return <div className="modal-backdrop" onMouseDown={(e) => {
|
||||
if (!busy && e.target === e.currentTarget) onDecline();
|
||||
}}>
|
||||
<div ref={dialog} className="modal" role="dialog" aria-modal="true" aria-label="Авторизация F95zone"
|
||||
<div ref={dialog} className="modal f95-modal" role="dialog" aria-modal="true" aria-label="Авторизация F95zone"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape" && !busy) { e.stopPropagation(); onDecline(); }
|
||||
if (e.key === "Enter" && e.target instanceof HTMLInputElement) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
if (!busy && cookie.trim()) onSave();
|
||||
if (!busy && canSave) onSave();
|
||||
}
|
||||
if (e.key === "Tab") {
|
||||
const controls = dialog.current?.querySelectorAll<HTMLElement>("input:not(:disabled), button:not(:disabled), summary");
|
||||
const controls = Array.from(dialog.current?.querySelectorAll<HTMLElement>("input:not(:disabled), button:not(:disabled), summary") ?? [])
|
||||
.filter((element) => element.getClientRects().length > 0);
|
||||
if (!controls?.length) return;
|
||||
const first = controls[0], last = controls[controls.length - 1];
|
||||
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
||||
@@ -131,28 +139,50 @@ function F95SessionDialog({ busy, cookie, agent, setCookie, setAgent, error, war
|
||||
}
|
||||
}}>
|
||||
<h2>Авторизация F95zone</h2>
|
||||
<p className="muted">Для превью может потребоваться вход. Если cookie отсутствуют или истекли, вставьте новые.
|
||||
Можно отказаться и заполнить данные игры вручную; уже загруженный текст останется в форме.</p>
|
||||
<p className="muted small">Добавьте cookie своего аккаунта для загрузки превью.
|
||||
Можно пропустить: уже загруженные данные останутся в форме.</p>
|
||||
<details>
|
||||
<summary>Где взять cookie</summary>
|
||||
<p className="small">Войдите на f95zone.to, откройте тему игры и инструменты разработчика → Network.
|
||||
Выберите запрос страницы темы → Request Headers и скопируйте значение Cookie целиком
|
||||
(включая xf_user, xf_session и cf_clearance, если они есть).
|
||||
User-Agent можно скопировать из того же запроса.</p>
|
||||
<ol className="small">
|
||||
<li>Войдите на f95zone.to. Нажмите F12 → «Приложение» (Application).</li>
|
||||
<li>Слева откройте «Хранилище → Файлы cookie → https://f95zone.to».</li>
|
||||
<li>Выберите строку <b>xf_user</b> и скопируйте только её «Значение» в поле ниже.
|
||||
При необходимости так же скопируйте <b>xf_session</b>.</li>
|
||||
</ol>
|
||||
<p className="hint">Если копируете из нижней панели Cookie Value, отключите «Показать декодированные параметры URL».
|
||||
TSVB_UID и xf_csrf для импорта не нужны.</p>
|
||||
</details>
|
||||
<label className="field"><span>Cookie</span>
|
||||
<input ref={input} type="password" autoComplete="off" spellCheck={false} maxLength={16384}
|
||||
value={cookie} onChange={(e) => setCookie(e.target.value)} disabled={busy} placeholder="xf_user=…; xf_session=…; cf_clearance=…" />
|
||||
<label className="field"><span id={userLabel}>xf_user</span>
|
||||
<input ref={input} type="password" autoComplete="off" spellCheck={false} maxLength={8192}
|
||||
aria-labelledby={userLabel} aria-describedby={`${userLabel}-hint`}
|
||||
value={cookies.xf_user} onChange={(e) => setCookies({ ...cookies, xf_user: e.target.value })}
|
||||
disabled={busy} placeholder="Только значение xf_user" />
|
||||
<span id={`${userLabel}-hint`} className="hint">Начните с этой cookie. Если её нет, используйте xf_session.</span>
|
||||
</label>
|
||||
<label className="field"><span>User-Agent (необязательно)</span>
|
||||
<input value={agent} onChange={(e) => setAgent(e.target.value)} disabled={busy} maxLength={2048} autoComplete="off" />
|
||||
<label className="field"><span>xf_session (необязательно)</span>
|
||||
<input type="password" autoComplete="off" spellCheck={false} maxLength={8192}
|
||||
value={cookies.xf_session} onChange={(e) => setCookies({ ...cookies, xf_session: e.target.value })}
|
||||
disabled={busy} placeholder="Только значение xf_session" />
|
||||
</label>
|
||||
<details>
|
||||
<summary>Если F95zone блокирует запрос</summary>
|
||||
<p className="hint">При проверке Cloudflare можно добавить cf_clearance из той же таблицы.
|
||||
Cookie браузера может не подойти серверу; повторный отказ не всегда означает истёкшую сессию.</p>
|
||||
<label className="field"><span>cf_clearance (необязательно)</span>
|
||||
<input type="password" value={cookies.cf_clearance} disabled={busy} maxLength={8192} autoComplete="off" spellCheck={false}
|
||||
onChange={(e) => setCookies({ ...cookies, cf_clearance: e.target.value })} placeholder="Только значение cf_clearance" />
|
||||
</label>
|
||||
<label className="field"><span>User-Agent (необязательно)</span>
|
||||
<input value={agent} onChange={(e) => setAgent(e.target.value)} disabled={busy} maxLength={2048} autoComplete="off" />
|
||||
<span className="hint">Его можно скопировать в Network → запрос страницы F95zone → Request Headers → User-Agent.</span>
|
||||
</label>
|
||||
</details>
|
||||
<p className="hint">Cookie сохраняются зашифрованными только в вашем профиле и используются сервером для запросов F95zone.</p>
|
||||
{warning && <p className="hint" role="status">{warning}</p>}
|
||||
{error && <p className="hint" role="alert">{error}</p>}
|
||||
<div className="modal-actions">
|
||||
<button type="button" className="btn" disabled={busy} onClick={onDecline}>Продолжить без авторизации</button>
|
||||
<button type="button" className="btn btn-primary" disabled={busy || !cookie.trim()} onClick={onSave}>
|
||||
<button type="button" className="btn" disabled={busy} onClick={onDecline}>Пропустить</button>
|
||||
<button type="button" className="btn btn-primary" disabled={busy || !canSave} onClick={onSave}>
|
||||
{busy ? "Проверка…" : "Сохранить и повторить"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user