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>
This commit is contained in:
1 parent
59adee60d5
commit
1b96706a1c
31 files changed
+8640
No files matched your search
@@ -0,0 +1,97 @@
|
||||
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 },
|
||||
});
|
||||
Reference in new issue
Block a user