Full React Native app using react-native-track-player for native audio — fixes background freeze, autoplay stopping, stuck play button, and enables Android Auto in future. Includes: - Navidrome API layer (ported from src/api.js) - Zustand global store with SecureStore config persistence - react-native-track-player with background playback service - Bottom tab navigation: Home, Artists, Albums, Search, Settings - Screens: Home, Library (Artists/Albums), ArtistDetail, AlbumDetail, Search, NowPlaying, Login/Settings - Persistent PlayerBar with progress indicator - Romaji/sort-tag display support carried over from web app Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
3.3 KiB
JavaScript
98 lines
3.3 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
View, Text, FlatList, Image, TouchableOpacity,
|
|
StyleSheet, ActivityIndicator, ScrollView,
|
|
} from 'react-native';
|
|
import { useNavigation, useRoute } from '@react-navigation/native';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { api, apiCached, dispArtistById, dispAlbumName, coverUrl, getArtistImage, cacheArtistSortNames } from '../api';
|
|
import AlbumCard from '../components/AlbumCard';
|
|
|
|
export default function ArtistDetailScreen() {
|
|
const navigation = useNavigation();
|
|
const { artist } = useRoute().params;
|
|
const [albums, setAlbums] = useState([]);
|
|
const [img, setImg] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
try {
|
|
const r = await apiCached('getArtist', `&id=${artist.id}`);
|
|
const ndArtist = r.artist;
|
|
if (ndArtist) cacheArtistSortNames([ndArtist]);
|
|
setAlbums(ndArtist?.album || []);
|
|
} catch {}
|
|
setLoading(false);
|
|
getArtistImage(artist.id).then(setImg).catch(() => {});
|
|
})();
|
|
}, [artist.id]);
|
|
|
|
const name = dispArtistById(artist.id, artist.name);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{/* Hero */}
|
|
<View style={styles.hero}>
|
|
{img ? (
|
|
<Image source={{ uri: img }} style={styles.heroImg} />
|
|
) : (
|
|
<View style={[styles.heroImg, styles.heroFallback]}>
|
|
<Text style={{ fontSize: 64 }}>🎤</Text>
|
|
</View>
|
|
)}
|
|
<TouchableOpacity style={styles.backBtn} onPress={() => navigation.goBack()}>
|
|
<Ionicons name="chevron-back" size={24} color="#fff" />
|
|
</TouchableOpacity>
|
|
<View style={styles.heroOverlay}>
|
|
<Text style={styles.heroName}>{name}</Text>
|
|
<Text style={styles.heroSub}>{albums.length} Alben</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{loading ? (
|
|
<View style={styles.center}><ActivityIndicator color="#1db954" /></View>
|
|
) : (
|
|
<FlatList
|
|
data={albums}
|
|
keyExtractor={a => a.id}
|
|
numColumns={2}
|
|
renderItem={({ item }) => (
|
|
<View style={{ flex: 1, padding: 8 }}>
|
|
<AlbumCard
|
|
album={item}
|
|
size={(require('react-native').Dimensions.get('window').width / 2) - 20}
|
|
onPress={() => navigation.navigate('AlbumDetail', { album: item })}
|
|
/>
|
|
</View>
|
|
)}
|
|
contentContainerStyle={{ padding: 4 }}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#121212' },
|
|
hero: { width: '100%', height: 220, position: 'relative' },
|
|
heroImg: { width: '100%', height: 220 },
|
|
heroFallback: {
|
|
backgroundColor: '#1e1e1e',
|
|
alignItems: 'center', justifyContent: 'center',
|
|
},
|
|
heroOverlay: {
|
|
position: 'absolute', bottom: 0, left: 0, right: 0,
|
|
padding: 16, paddingBottom: 20,
|
|
background: 'transparent',
|
|
},
|
|
heroName: { color: '#fff', fontSize: 24, fontWeight: '800', textShadowColor: '#000', textShadowRadius: 8 },
|
|
heroSub: { color: '#ddd', fontSize: 13, marginTop: 2 },
|
|
backBtn: {
|
|
position: 'absolute', top: 44, left: 12,
|
|
backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 20,
|
|
padding: 6,
|
|
},
|
|
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingTop: 40 },
|
|
});
|