import React, { useState, useEffect, useRef, useCallback } from "react"; import { invoke } from "@tauri-apps/api/core"; import logoUrl from "../assets/logo.png"; import type { UserDto, AuthResponse } from "../types"; import { playTvBeep, playTvSelectBeep, playTvBackBeep, playTvErrorBeep } from "../utils/tvAudio"; import "./TvProfileSelector.css"; interface TvProfileSelectorProps { onSelectProfile: (user: UserDto) => void; onSwitchToDesktop?: () => void; } const STORAGE_KEY = "has_tv_saved_profiles"; export const TvProfileSelector: React.FC = ({ onSelectProfile, onSwitchToDesktop, }) => { const [profiles, setProfiles] = useState(() => { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); if (Array.isArray(parsed) && parsed.length > 0) return parsed; } // Fallback: check if standard remembered user exists const rem = localStorage.getItem("has_remembered_user"); if (rem) { const parsedUser = JSON.parse(rem); if (parsedUser?.email) { localStorage.setItem(STORAGE_KEY, JSON.stringify([parsedUser])); return [parsedUser]; } } } catch {} return []; }); // Focus state on arc: either a profile index or the central "+" button const [focusState, setFocusState] = useState<{ type: "profile"; index: number } | { type: "plus" }>( () => { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved && JSON.parse(saved).length > 0) { return { type: "profile", index: 0 }; } } catch {} return { type: "plus" }; } ); // 3-button TV Authorization Modal State const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); const [tvEmail, setTvEmail] = useState(""); const [tvPassword, setTvPassword] = useState(""); const [tvLoginFocus, setTvLoginFocus] = useState<0 | 1 | 2>(0); // 0: Email, 1: Password, 2: Submit const [tvLoading, setTvLoading] = useState(false); const [tvError, setTvError] = useState(null); const emailInputRef = useRef(null); const passwordInputRef = useRef(null); const submitButtonRef = useRef(null); // Synchronize focus when modal opens useEffect(() => { if (isLoginModalOpen) { setTvLoginFocus(0); setTvError(null); setTimeout(() => emailInputRef.current?.focus(), 60); } }, [isLoginModalOpen]); // Handle focus changes in login modal useEffect(() => { if (!isLoginModalOpen) return; if (tvLoginFocus === 0) { emailInputRef.current?.focus(); } else if (tvLoginFocus === 1) { passwordInputRef.current?.focus(); } else if (tvLoginFocus === 2) { submitButtonRef.current?.focus(); } }, [tvLoginFocus, isLoginModalOpen]); // Compute profile coordinates on the arc (matching the user's sketch) const getCoordinates = (index: number, total: number) => { const cx = 440; const cy = 360; const rx = 330; const ry = 210; if (total === 1) { return { x: cx, y: cy - ry }; } // Distribute angles along the semicircle arc const startAngle = (155 * Math.PI) / 180; const endAngle = (25 * Math.PI) / 180; const step = (startAngle - endAngle) / (total - 1); const angle = startAngle - index * step; const x = cx + rx * Math.cos(angle); const y = cy - ry * Math.sin(angle); return { x, y }; }; // Login handler for 3-button TV login const handleTvLogin = async (e?: React.FormEvent) => { if (e) e.preventDefault(); setTvError(null); const cleanEmail = tvEmail.trim(); if (!cleanEmail || !tvPassword) { setTvError("Заполните почту и пароль"); playTvBeep(320, 0.08); return; } setTvLoading(true); try { const res = await invoke("login", { email: cleanEmail, password: tvPassword, }); if (res.success && res.user) { playTvBeep(780, 0.06); // Save to TV profiles list const updated = [ res.user, ...profiles.filter((p) => p.email.toLowerCase() !== res.user!.email.toLowerCase()), ]; setProfiles(updated); localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); localStorage.setItem("has_remembered_user", JSON.stringify(res.user)); localStorage.setItem("has_remembered_email", cleanEmail); setIsLoginModalOpen(false); onSelectProfile(res.user); } else { setTvError(res.message || "Неверный логин или пароль"); playTvErrorBeep(); } } catch (err: any) { setTvError(`Ошибка: ${err?.message || err}`); playTvErrorBeep(); } finally { setTvLoading(false); } }; // Select profile directly from arc const handleSelect = (user: UserDto) => { playTvSelectBeep(); localStorage.setItem("has_remembered_user", JSON.stringify(user)); onSelectProfile(user); }; // Remove profile from TV list const handleDeleteProfile = (e: React.MouseEvent, email: string) => { e.stopPropagation(); const filtered = profiles.filter((p) => p.email.toLowerCase() !== email.toLowerCase()); setProfiles(filtered); localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); setFocusState({ type: "plus" }); playTvBackBeep(); }; // Global D-pad Remote Control Key Listener const handleKeyDown = useCallback( (e: KeyboardEvent) => { // ---------------------------------------------------------------------- // CASE 1: 3-Button TV Login Modal is OPEN (Strict Remote Focus Trap) // "больше пультом дистанционного управления никуда нельзя будет нажать только на эти три кнопки" // ---------------------------------------------------------------------- if (isLoginModalOpen) { // Back / Escape key returns to Profile Arc if (e.key === "Escape" || e.keyCode === 27 || e.keyCode === 4) { e.preventDefault(); setIsLoginModalOpen(false); setFocusState({ type: "plus" }); playTvBackBeep(); return; } // Down Arrow: Email (0) -> Password (1) -> Button (2) if (e.key === "ArrowDown" || e.keyCode === 20) { e.preventDefault(); setTvLoginFocus((prev) => ((prev + 1) % 3) as 0 | 1 | 2); playTvBeep(520, 0.025); return; } // Up Arrow: Button (2) -> Password (1) -> Email (0) if (e.key === "ArrowUp" || e.keyCode === 19) { e.preventDefault(); setTvLoginFocus((prev) => ((prev - 1 + 3) % 3) as 0 | 1 | 2); playTvBeep(520, 0.025); return; } // Tab / Shift+Tab cycling strictly between these 3 controls if (e.key === "Tab") { e.preventDefault(); if (e.shiftKey) { setTvLoginFocus((prev) => ((prev - 1 + 3) % 3) as 0 | 1 | 2); } else { setTvLoginFocus((prev) => ((prev + 1) % 3) as 0 | 1 | 2); } playTvBeep(520, 0.025); return; } // Enter / OK (KeyCode 13, 23, 66) if (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 23 || e.keyCode === 66) { if (tvLoginFocus === 0) { e.preventDefault(); setTvLoginFocus(1); playTvBeep(580, 0.03); } else if (tvLoginFocus === 1 || tvLoginFocus === 2) { e.preventDefault(); handleTvLogin(); } return; } // Left / Right keys stay inside the input for text editing return; } // ---------------------------------------------------------------------- // CASE 2: Arc Profile Selection Screen // ---------------------------------------------------------------------- const total = profiles.length; // Left Arrow if (e.key === "ArrowLeft" || e.keyCode === 21) { e.preventDefault(); if (focusState.type === "profile") { if (focusState.index > 0) { setFocusState({ type: "profile", index: focusState.index - 1 }); playTvBeep(540, 0.025); } } else { // From plus button: jump to left-middle profile if exists if (total > 0) { const targetIdx = Math.max(0, Math.floor((total - 1) / 2)); setFocusState({ type: "profile", index: targetIdx }); playTvBeep(540, 0.025); } } return; } // Right Arrow if (e.key === "ArrowRight" || e.keyCode === 22) { e.preventDefault(); if (focusState.type === "profile") { if (focusState.index < total - 1) { setFocusState({ type: "profile", index: focusState.index + 1 }); playTvBeep(540, 0.025); } } else { // From plus button: jump to right-middle profile if exists if (total > 0) { const targetIdx = Math.min(total - 1, Math.ceil((total - 1) / 2)); setFocusState({ type: "profile", index: targetIdx }); playTvBeep(540, 0.025); } } return; } // Down Arrow -> moves focus to the Plus button in center if (e.key === "ArrowDown" || e.keyCode === 20) { e.preventDefault(); if (focusState.type === "profile") { setFocusState({ type: "plus" }); playTvBeep(500, 0.025); } return; } // Up Arrow -> moves focus from Plus button up to nearest profile if (e.key === "ArrowUp" || e.keyCode === 19) { e.preventDefault(); if (focusState.type === "plus" && total > 0) { const targetIdx = Math.round((total - 1) / 2); setFocusState({ type: "profile", index: targetIdx }); playTvBeep(560, 0.025); } return; } // OK / Enter -> Select Profile or Open Plus Login if (e.key === "Enter" || e.key === " " || e.keyCode === 13 || e.keyCode === 23 || e.keyCode === 66) { e.preventDefault(); if (focusState.type === "profile" && profiles[focusState.index]) { handleSelect(profiles[focusState.index]); } else if (focusState.type === "plus") { setIsLoginModalOpen(true); playTvBeep(640, 0.03); } return; } // Delete / Backspace key removes focused profile if (e.key === "Delete" || e.keyCode === 46) { if (focusState.type === "profile" && profiles[focusState.index]) { e.preventDefault(); const p = profiles[focusState.index]; const filtered = profiles.filter((item) => item.email.toLowerCase() !== p.email.toLowerCase()); setProfiles(filtered); localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered)); setFocusState({ type: "plus" }); playTvBeep(360, 0.04); } } }, [isLoginModalOpen, tvLoginFocus, focusState, profiles, tvEmail, tvPassword] ); useEffect(() => { window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [handleKeyDown]); return (
{/* Header */}
HAS Logo

Кто сейчас смотрит?

Выберите профиль для автоматического входа

{/* Arc Stage with Semicircle & Profile Circles */}
{/* SVG Semicircle Arc (matching user's drawing) */} {/* Profile Avatars along the Arc */} {profiles.map((user, idx) => { const coords = getCoordinates(idx, profiles.length); const isFocused = focusState.type === "profile" && focusState.index === idx; return ( ); })} {/* Plus Button: Positioned in Center under the Arc */}
{/* Footer Navigation Hints */}
◄ ► Влево / Вправо • ▼ Кнопка + • OK Вход в аккаунт {onSwitchToDesktop && ( )}
{/* ================================================================== STRICT 3-BUTTON TV LOGIN MODAL "два поля почта и пароль с кнопкой входа больше пультом дистанционного управления никуда нельзя будет нажать только на эти три кнопки" ================================================================== */} {isLoginModalOpen && (
{ setIsLoginModalOpen(false); setFocusState({ type: "plus" }); }} >
e.stopPropagation()}>

Добавить профиль

Введите данные для авторизации нового аккаунта на ТВ

{/* Field 1: Email */} setTvEmail(e.target.value)} placeholder="Почта" className={`has-tv-login-input ${tvLoginFocus === 0 ? "focused" : ""}`} tabIndex={0} autoFocus /> {/* Field 2: Password */} setTvPassword(e.target.value)} placeholder="Пароль" className={`has-tv-login-input ${tvLoginFocus === 1 ? "focused" : ""}`} tabIndex={0} /> {tvError &&
{tvError}
} {/* Button 3: Submit Button */}
▲ ▼ Пульт: перемещение • OK: выбор • Назад (Back / Esc): отмена
)}
); };