feat(mobile): sign-in, tabs, search, collection and background player (M5)
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
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>
This commit is contained in:
1 parent
eaedd1fc62
commit
9fd616da57
28 files changed
+2590
-197
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import { unwrap } from '@pipu/core';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FlatList, Pressable, Text, View } from 'react-native';
|
||||
|
||||
import { useSession } from '../../lib/session';
|
||||
import { player } from '../../player/controller';
|
||||
import { usePlayerState } from '../../player/hooks';
|
||||
import { colors } from '../../theme';
|
||||
import { Button, Cover, Icon, Screen, TrackRow, formatDuration, useErrorText } from '../../ui/kit';
|
||||
|
||||
export default function AlbumScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
const current = usePlayerState().track;
|
||||
const errorText = useErrorText();
|
||||
const album = useQuery({
|
||||
queryKey: ['album', id],
|
||||
queryFn: async () => unwrap(await session.client.GET('/api/v1/albums/{id}', { params: { path: { id } } })),
|
||||
});
|
||||
const a = album.data;
|
||||
const ctx = { type: 'album' as const, id };
|
||||
|
||||
return (
|
||||
<Screen>
|
||||
<FlatList
|
||||
data={a?.tracks ?? []}
|
||||
keyExtractor={(tr) => tr.id}
|
||||
ListHeaderComponent={
|
||||
<View style={{ padding: 16, gap: 12, alignItems: 'center' }}>
|
||||
<Pressable onPress={() => router.back()} style={{ alignSelf: 'flex-start' }} accessibilityRole="button" accessibilityLabel={t('form.close')}>
|
||||
<Icon name="arrow-back" color={colors.text} />
|
||||
</Pressable>
|
||||
{album.error ? <Text style={{ color: colors.danger }}>{errorText(album.error)}</Text> : null}
|
||||
{a && (
|
||||
<>
|
||||
<Cover album={a} size={220} />
|
||||
<Text style={{ color: colors.text, fontSize: 22, fontWeight: '700', textAlign: 'center' }}>{a.title}</Text>
|
||||
<Pressable onPress={() => router.push(`/artist/${a.artist.id}`)}>
|
||||
<Text style={{ color: colors.text2 }}>
|
||||
{a.artist.name}
|
||||
{a.year ? ` · ${a.year}` : ''} · {formatDuration(a.duration_ms)}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Button title={t('player.play')} icon="play-arrow" onPress={() => void player.playList(a.tracks, 0, ctx)} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
renderItem={({ item, index }) => (
|
||||
<TrackRow
|
||||
track={item}
|
||||
numbered={item.track_no ?? index + 1}
|
||||
current={current?.id === item.id}
|
||||
onPress={() => void player.playList(a?.tracks ?? [], index, ctx)}
|
||||
onLike={() => void player.react(item, item.reaction === 'like' ? null : 'like').then(() => album.refetch())}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user