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>
59 lines
2.5 KiB
TypeScript
59 lines
2.5 KiB
TypeScript
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 { useColors } from '../../theme';
|
|
import { Cover, Icon, Screen } from '../../ui/kit';
|
|
|
|
export default function ArtistScreen() {
|
|
const colors = useColors();
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { t } = useTranslation();
|
|
const router = useRouter();
|
|
const session = useSession();
|
|
const artist = useQuery({
|
|
queryKey: ['artist', id],
|
|
queryFn: async () => unwrap(await session.client.GET('/api/v1/artists/{id}', { params: { path: { id } } })),
|
|
});
|
|
const albums = useQuery({
|
|
queryKey: ['artist-albums', id],
|
|
queryFn: async () => unwrap(await session.client.GET('/api/v1/artists/{id}/albums', { params: { path: { id } } })).items,
|
|
});
|
|
|
|
return (
|
|
<Screen>
|
|
<FlatList
|
|
data={albums.data ?? []}
|
|
keyExtractor={(a) => a.id}
|
|
numColumns={2}
|
|
columnWrapperStyle={{ gap: 12, paddingHorizontal: 16 }}
|
|
contentContainerStyle={{ gap: 16, paddingBottom: 24 }}
|
|
ListHeaderComponent={
|
|
<View style={{ padding: 16, gap: 10, alignItems: 'center' }}>
|
|
<Pressable onPress={() => router.back()} style={{ alignSelf: 'flex-start' }} accessibilityRole="button" accessibilityLabel={t('form.close')}>
|
|
<Icon name="arrow-back" color={colors.text} />
|
|
</Pressable>
|
|
<Cover album={{ id }} size={140} round />
|
|
<Text style={{ color: colors.text, fontSize: 24, fontWeight: '700' }}>{artist.data?.name ?? '…'}</Text>
|
|
{artist.data && (
|
|
<Text style={{ color: colors.text2 }}>
|
|
{t('catalog.album_count', { count: artist.data.album_count })} · {t('catalog.track_count', { count: artist.data.track_count })}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
}
|
|
renderItem={({ item }) => (
|
|
<Pressable onPress={() => router.push(`/album/${item.id}`)} style={{ flex: 1, gap: 6 }} accessibilityRole="button">
|
|
<Cover album={item} size={160} />
|
|
<Text numberOfLines={1} style={{ color: colors.text, fontWeight: '600' }}>{item.title}</Text>
|
|
<Text style={{ color: colors.text3, fontSize: 12 }}>{item.year ?? ''}</Text>
|
|
</Pressable>
|
|
)}
|
|
/>
|
|
</Screen>
|
|
);
|
|
}
|