Files
NovaSonic/mobile/src/screens/ArtistDetail.js
T
HoneyandClaude Sonnet 4.6 1b96706a1c feat: add Expo mobile app (mobile/)
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>
2026-06-09 23:19:06 +02:00

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 },
});