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
- 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>
111 lines
4.1 KiB
TypeScript
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>
|
|
);
|
|
}
|