Files
pipu-music/apps/mobile/src/ui/settings.tsx
T
mrleo1nidandClaude Opus 5.5 ed063d3763
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m30s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 2m57s
ci / ts / ts (packages + web) (push) Successful in 1m16s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 33s
feat(mobile): Skia wave, wave player view, themes and playback settings (M5)
- My Wave is rendered with react-native-skia on the UI thread; physics
  and colour helpers moved to @pipu/core (wave-anim) and shared with web.
- Full-screen player honours ui_prefs.mobile_player (cover / wave).
- Theme switching (dark / light / system) via a theme context and
  useColors() across all screens.
- Profile: theme, wave variant, player view, sound quality sheet
  (Wi-Fi / mobile / downloads), recent-tracks cache clearing on next
  cold start, and a change-password sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:30:21 +03:00

111 lines
4.1 KiB
TypeScript

import { ApiError, unwrap } from '@pipu/core';
import { useState, useSyncExternalStore } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, View } from 'react-native';
import { QUALITIES, qualityPrefs, type Quality, type QualityPrefs } from '../lib/prefs';
import { useSession } from '../lib/session';
import { useColors } from '../theme';
import { Button, ErrorBox, Field, Segmented, Sheet, useErrorText } from './kit';
export function useQualityPrefs(): QualityPrefs {
return useSyncExternalStore(qualityPrefs.subscribe, qualityPrefs.get);
}
/** Лист «Качество звука»: Wi-Fi / сотовая / загрузки. */
export function QualitySheet({ visible, onClose }: { visible: boolean; onClose: () => void }) {
const { t } = useTranslation();
const colors = useColors();
const prefs = useQualityPrefs();
const row = (key: keyof QualityPrefs) => (
<View style={{ gap: 8 }}>
<Text style={{ color: colors.text2, fontSize: 13, fontWeight: '600' }}>{t(`mobile.quality_${key}`)}</Text>
<Segmented<Quality>
value={prefs[key]}
options={QUALITIES.map((q) => ({ value: q, label: t(`mobile.q_${q}`) }))}
onChange={(q) => void qualityPrefs.set({ [key]: q })}
/>
</View>
);
return (
<Sheet visible={visible} title={t('mobile.quality')} onClose={onClose}>
{row('wifi')}
{row('cellular')}
{row('download')}
<Text style={{ color: colors.text3, fontSize: 12 }}>{t('mobile.quality_hint')}</Text>
</Sheet>
);
}
/** Лист смены пароля: после смены сервер выдаёт новую пару токенов, остальные сессии завершаются. */
export function PasswordSheet({ visible, onClose }: { visible: boolean; onClose: () => void }) {
const { t } = useTranslation();
const colors = useColors();
const session = useSession();
const errorText = useErrorText();
const [current, setCurrent] = useState('');
const [next, setNext] = useState('');
const [repeat, setRepeat] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<unknown>(null);
const [done, setDone] = useState(false);
const mismatch = repeat !== '' && repeat !== next;
const fieldErr = (f: string) => (error instanceof ApiError && error.fieldCode(f) ? errorText(error) : undefined);
const close = () => {
setCurrent('');
setNext('');
setRepeat('');
setError(null);
setDone(false);
onClose();
};
const submit = async () => {
setBusy(true);
setError(null);
try {
const tokens = unwrap(await session.client.POST('/api/v1/me/password', { body: { current_password: current, new_password: next } }));
await session.apply(tokens);
setDone(true);
setCurrent('');
setNext('');
setRepeat('');
} catch (e) {
setError(e);
} finally {
setBusy(false);
}
};
return (
<Sheet visible={visible} title={t('settings.password')} onClose={close}>
{done ? (
<>
<Text style={{ color: colors.ok }}>{t('settings.password_changed')}</Text>
<Button title={t('form.close')} onPress={close} />
</>
) : (
<>
<Field label={t('settings.current_password')} value={current} onChangeText={setCurrent} secure autoComplete="current-password" error={fieldErr('current_password')} />
<Field label={t('settings.new_password')} value={next} onChangeText={setNext} secure autoComplete="new-password" error={fieldErr('new_password')} />
<Field
label={t('settings.repeat_password')}
value={repeat}
onChangeText={setRepeat}
secure
autoComplete="new-password"
error={mismatch ? t('form.passwords_mismatch') : undefined}
/>
<ErrorBox>{error && !fieldErr('current_password') && !fieldErr('new_password') ? errorText(error) : null}</ErrorBox>
<Button
title={t('settings.change_password')}
busy={busy}
disabled={!current || !next || mismatch || repeat === ''}
onPress={() => void submit()}
/>
</>
)}
</Sheet>
);
}