Files
pipu-music/apps/mobile/src/app/login.tsx
T
mrleo1nidandClaude Opus 5.5 9fd616da57
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m39s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / ts / ts (packages + web) (push) Successful in 1m5s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 39s
feat(mobile): sign-in, tabs, search, collection and background player (M5)
- expo-router app in src/app: connect (server check, recent servers),
  login, mandatory credential / password change, tabs Home / Search /
  Collection / Profile, album and artist pages, full-screen player
- AuthSession per server with the refresh token in expo-secure-store
  (Android Keystore); language from the profile
- player on react-native-track-player: ExoPlayer queue with a
  notification and lock-screen controls via a playback service, 500 MB
  stream cache, quality by network (Wi-Fi high, cellular medium),
  play events with played time, reactions, playback state restore and
  the endless My Wave with feedback
- home: pulsing wave circle driven by BPM / energy in the cover
  palette, activity and mood chips that switch the wave on the fly
- New Architecture is off until react-native-track-player supports it;
  the release build runs codegen only when it is on; query-string is a
  direct dependency because expo-router 5.1 imports it undeclared

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:48:55 +03:00

52 lines
1.9 KiB
TypeScript

import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView } from 'react-native';
import { useSessionCtx } from '../lib/session';
import { Button, ErrorBox, Field, H1, Muted, Screen, useErrorText } from '../ui/kit';
export default function LoginScreen() {
const { t } = useTranslation(['auth', 'common']);
const { server, session, disconnect } = useSessionCtx();
const errorText = useErrorText();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<unknown>(null);
const submit = async () => {
if (!session) return;
setBusy(true);
setError(null);
try {
await session.login(username.trim(), password); // переход сделает Gate
} catch (e) {
setError(e);
setPassword('');
} finally {
setBusy(false);
}
};
return (
<Screen>
<ScrollView contentContainerStyle={{ padding: 20, gap: 14, flexGrow: 1, justifyContent: 'center' }} keyboardShouldPersistTaps="handled">
<H1>{t('login.title')}</H1>
<Muted>{server?.replace(/^https?:\/\//, '')}</Muted>
<Field label={t('login.username')} value={username} onChangeText={setUsername} autoComplete="username" />
<Field
label={t('login.password')}
value={password}
onChangeText={setPassword}
secure
autoComplete="password"
onSubmitEditing={() => void submit()}
/>
<ErrorBox>{error ? errorText(error) : null}</ErrorBox>
<Button title={t('login.submit')} onPress={() => void submit()} busy={busy} disabled={!username.trim() || !password} />
<Button title={t('common:mobile.change_server')} variant="ghost" onPress={() => void disconnect()} />
</ScrollView>
</Screen>
);
}