import type { components } from '@pipu/api-client'; import { unwrap } from '@pipu/core'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { useState, type FormEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { useSession, useUser } from '../app/session'; import { useDesktop } from '../desktop'; import { useErrorText, useFieldError } from '../shared/lib/errors'; import { formatDateTime } from '../shared/lib/format'; import { Button, Field, FormError, Segmented } from '../shared/ui/ui'; import { LanguageSwitch } from '../widgets/LanguageSwitch'; import s from './page.module.css'; import { Onboarding } from './Onboarding'; import { DesktopOfflineSection, DesktopUpdateButton } from '../features/desktop/DesktopFeatures'; type Theme = NonNullable; type WaveVariant = NonNullable; export function SettingsPage() { const { t } = useTranslation(); return (

{t('settings.title')}

); } function ProfileSection() { const { t } = useTranslation(); const session = useSession(); const user = useUser(); const errorText = useErrorText(); const [name, setName] = useState(user.display_name ?? ''); const save = useMutation({ mutationFn: async () => unwrap(await session.client.PATCH('/api/v1/me', { body: { display_name: name.trim() } })), onSuccess: (u) => session.setUser(u), }); return (

{t('settings.account')}

{ e.preventDefault(); save.mutate(); }} >
{ setName(v); save.reset(); }} maxLength={64} hint={`@${user.username}`} />
{save.isSuccess && {t('settings.saved')}} {save.error ? errorText(save.error) : null}
); } function AppearanceSection() { const { t } = useTranslation(); const session = useSession(); const user = useUser(); const theme: Theme = user.ui_prefs.theme ?? 'dark'; const wave: WaveVariant = user.ui_prefs.wave_variant ?? 'orb'; // Оптимистично: настройка применяется сразу, сервер подтверждает. const setPrefs = async (prefs: components['schemas']['UiPrefs']) => { session.setUser({ ...user, ui_prefs: { ...user.ui_prefs, ...prefs } }); try { session.setUser(unwrap(await session.client.PATCH('/api/v1/me', { body: { ui_prefs: prefs } }))); } catch { session.setUser(user); } }; const setTheme = (value: Theme) => setPrefs({ theme: value }); return (

{t('settings.appearance')}

{t('settings.theme')} label={t('settings.theme')} value={theme} onChange={(v) => void setTheme(v)} options={[ { value: 'dark', label: t('settings.theme_dark') }, { value: 'light', label: t('settings.theme_light') }, { value: 'system', label: t('settings.theme_system') }, ]} />
{t('settings.wave_variant')} label={t('settings.wave_variant')} value={wave} onChange={(v) => void setPrefs({ wave_variant: v })} options={[ { value: 'orb', label: t('settings.wave_orb') }, { value: 'liquid', label: t('settings.wave_liquid') }, ]} />
{t('settings.player_view')} label={t('settings.player_view')} value={user.ui_prefs.web_player ?? 'bar'} onChange={(v) => void setPrefs({ web_player: v })} options={[ { value: 'bar', label: t('settings.player_bar') }, { value: 'island', label: t('settings.player_island') }, ]} />
{t('settings.search_position')} label={t('settings.search_position')} value={user.ui_prefs.search_position ?? 'center'} onChange={(v) => void setPrefs({ search_position: v })} options={[ { value: 'left', label: t('settings.search_left') }, { value: 'center', label: t('settings.search_center') }, { value: 'right', label: t('settings.search_right') }, ]} />
{t('settings.search_collapse')} label={t('settings.search_collapse')} value={user.ui_prefs.search_collapse === true ? 'on' : 'off'} onChange={(v) => void setPrefs({ search_collapse: v === 'on' })} options={[ { value: 'on', label: t('settings.on') }, { value: 'off', label: t('settings.off') }, ]} />
{t('settings.home_activities')} label={t('settings.home_activities')} value={user.ui_prefs.home_activities === false ? 'off' : 'on'} onChange={(v) => void setPrefs({ home_activities: v === 'on' })} options={[ { value: 'on', label: t('settings.on') }, { value: 'off', label: t('settings.off') }, ]} />
{t('settings.home_moods')} label={t('settings.home_moods')} value={user.ui_prefs.home_moods === false ? 'off' : 'on'} onChange={(v) => void setPrefs({ home_moods: v === 'on' })} options={[ { value: 'on', label: t('settings.on') }, { value: 'off', label: t('settings.off') }, ]} />
{t('settings.ambient')} label={t('settings.ambient')} value={user.ui_prefs.ambient === false ? 'off' : 'on'} onChange={(v) => void setPrefs({ ambient: v === 'on' })} options={[ { value: 'on', label: t('settings.on') }, { value: 'off', label: t('settings.off') }, ]} />
{t('settings.normalize')} label={t('settings.normalize')} value={user.ui_prefs.normalize === false ? 'off' : 'on'} onChange={(v) => void setPrefs({ normalize: v === 'on' })} options={[ { value: 'on', label: t('settings.on') }, { value: 'off', label: t('settings.off') }, ]} />
{t('settings.crossfade', { seconds: user.ui_prefs.crossfade_s ?? 0 })} void setPrefs({ crossfade_s: Number(e.target.value) })} />
{t('language.label')}
); } function PasswordSection() { const { t } = useTranslation(['common', 'auth']); const session = useSession(); const user = useUser(); const queryClient = useQueryClient(); const errorText = useErrorText(); const fieldError = useFieldError(); const [current, setCurrent] = useState(''); const [next, setNext] = useState(''); const [repeat, setRepeat] = useState(''); const mismatch = repeat !== '' && repeat !== next; const change = useMutation({ mutationFn: async () => unwrap( await session.client.POST('/api/v1/me/password', { body: { current_password: current, new_password: next } }), ), onSuccess: async (tokens) => { await session.apply(tokens); setCurrent(''); setNext(''); setRepeat(''); await queryClient.invalidateQueries({ queryKey: ['sessions'] }); }, }); const err = change.error; const currentError = fieldError(err, 'current_password'); const nextError = fieldError(err, 'new_password'); const submit = (e: FormEvent) => { e.preventDefault(); if (!mismatch) change.mutate(); }; return (

{t('settings.password')}

{change.isSuccess && {t('settings.password_changed')}} {err && !currentError && !nextError ? errorText(err) : null}
); } function SessionsSection() { const { t, i18n } = useTranslation(); const session = useSession(); const desktop = useDesktop(); const navigate = useNavigate(); const queryClient = useQueryClient(); const errorText = useErrorText(); const sessions = useQuery({ queryKey: ['sessions'], queryFn: async () => unwrap(await session.client.GET('/api/v1/me/sessions')).items, }); const revoke = useMutation({ mutationFn: async (id: string) => unwrap(await session.client.DELETE('/api/v1/me/sessions/{id}', { params: { path: { id } } })), onSuccess: () => queryClient.invalidateQueries({ queryKey: ['sessions'] }), }); return (

{t('settings.sessions')}

{sessions.data?.map((ss) => (
{ss.device_name ?? ss.client_type}
{ss.current ? t('settings.session_current') : t('settings.session_last_used', { date: formatDateTime(ss.last_used_at, i18n.language) })}
{!ss.current && ( )}
))}
{sessions.error || revoke.error ? errorText(sessions.error ?? revoke.error) : null}
{desktop && ( )}
); } function RecommendationSettings() { const { t } = useTranslation(); const session = useSession(); const queryClient = useQueryClient(); const [editing, setEditing] = useState(false); const errorText = useErrorText(); const options = useQuery({ queryKey: ['onboarding'], queryFn: async () => unwrap(await session.client.GET('/api/v1/onboarding')), staleTime: 0 }); return
{editing && options.data ? { setEditing(false); void queryClient.invalidateQueries({ queryKey: ['onboarding'] }); }} /> : } {options.error ? errorText(options.error) : null}
; }