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
- 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>
52 lines
1.9 KiB
TypeScript
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>
|
|
);
|
|
}
|