import { FormEvent, useEffect, useState } from 'react' import toast from 'react-hot-toast' import { Check, Copy, EyeOff, KeyRound, Loader2, Save, Tv } from 'lucide-react' import { authAPI } from '../api/auth' import { profileAPI } from '../api/profile' import { requestPassword } from '../components/requestPassword' import { useAuthStore } from '../stores/auth' export function ProfilePage() { const user = useAuthStore((s) => s.user) const setUser = useAuthStore((s) => s.setUser) const [username, setUsername] = useState(user?.username ?? '') const [nickname, setNickname] = useState(user?.nickname ?? '') const [email, setEmail] = useState(user?.email ?? '') const [avatar, setAvatar] = useState(user?.avatar_url ?? '') const [hideAdult, setHideAdult] = useState(Boolean(user?.hide_adult)) const [oldPwd, setOldPwd] = useState('') const [newPwd, setNewPwd] = useState('') const [savingProfile, setSavingProfile] = useState(false) const [savingPassword, setSavingPassword] = useState(false) const [otpCode, setOtpCode] = useState('') const [otpExpiresIn, setOtpExpiresIn] = useState(0) const [generatingOtp, setGeneratingOtp] = useState(false) const [copiedOtp, setCopiedOtp] = useState(false) useEffect(() => { if (otpExpiresIn <= 0) return const timer = setInterval(() => { setOtpExpiresIn((prev) => { if (prev <= 1) { clearInterval(timer) return 0 } return prev - 1 }) }, 1000) return () => clearInterval(timer) }, [otpExpiresIn]) useEffect(() => { if (window.location.hash === '#tv-otp') { const el = document.getElementById('tv-otp') if (el) { el.scrollIntoView({ behavior: 'smooth' }) } } }, []) const onGenerateOTP = async () => { if (generatingOtp) return setGeneratingOtp(true) try { const res = await authAPI.createTemporaryPassword() setOtpCode(res.code) setOtpExpiresIn(res.expires_in) setCopiedOtp(false) toast.success('已生成 6 位临时登录码') } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '生成临时密码失败' toast.error(msg) } finally { setGeneratingOtp(false) } } const copyOTP = async () => { if (!otpCode) return try { await navigator.clipboard.writeText(otpCode) setCopiedOtp(true) toast.success('已复制到剪贴板') setTimeout(() => setCopiedOtp(false), 2000) } catch { toast.error('复制失败,请手动长按复制') } } const onProfile = async (e: FormEvent) => { e.preventDefault() if (savingProfile) return setSavingProfile(true) try { let password: string | undefined const hideAdultChanged = hideAdult !== Boolean(user?.hide_adult) const usernameChanged = username.trim() !== (user?.username ?? '') if (hideAdultChanged || usernameChanged) { const input = await requestPassword({ title: usernameChanged ? '修改用户名' : hideAdult ? '隐藏成人目录' : '取消隐藏成人目录', message: usernameChanged ? '修改用户名后需要使用新用户名登录,请输入当前账号密码确认。' : '此设置会同步影响 Web 与 Emby/Jellyfin/Infuse 等第三方客户端,请输入当前账号密码确认。', confirmText: '保存设置', }) if (!input) return password = input } const patch: Parameters[0] = { username, nickname, email, avatar_url: avatar, password, } if (hideAdultChanged) { patch.hide_adult = hideAdult } const u = await profileAPI.update(patch) setUser(u) toast.success('资料已更新') } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '保存失败' toast.error(msg) } finally { setSavingProfile(false) } } const onPwd = async (e: FormEvent) => { e.preventDefault() if (savingPassword) return setSavingPassword(true) try { await authAPI.changePassword(oldPwd, newPwd) toast.success('密码已更新') setOldPwd('') setNewPwd('') } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '密码更新失败' toast.error(msg) } finally { setSavingPassword(false) } } return (

个人资料

基本信息

setUsername(e.target.value)} autoComplete="username" /> setNickname(e.target.value)} /> setEmail(e.target.value)} /> setAvatar(e.target.value)} />

修改密码

setOldPwd(e.target.value)} autoComplete="current-password" /> setNewPwd(e.target.value)} autoComplete="new-password" />

Emby / 电视端临时登录码 (OTP)

适用于电视盒子、Apple TV、车机或朋友设备上的 Emby / Infuse / Jellyfin 客户端快速登录。无需使用遥控器输入长密码。

{otpCode && otpExpiresIn > 0 ? (
登录账号: {user?.username}
有效时间剩余: {Math.floor(otpExpiresIn / 60).toString().padStart(2, '0')}:{(otpExpiresIn % 60).toString().padStart(2, '0')}
{otpCode}

在客户端的 Emby / Jellyfin 登录界面输入用户名 {user?.username} 和上方 6 位数字临时密码即可完成登录。登录成功后临时密码立即作废(一次性使用),客户端将自动换取长期持久令牌。

) : null}
) } function Field({ label, children }: { label: string; children: React.ReactNode }) { return ( ) }