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
+111
@@ -0,0 +1,111 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, StatusBar } from 'react-native';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import TrackPlayer, { Event, State } from 'react-native-track-player';
|
||||
import { PlaybackService } from './src/audio/service';
|
||||
import { setupPlayer } from './src/audio/player';
|
||||
import { useStore } from './src/store';
|
||||
|
||||
import HomeScreen from './src/screens/Home';
|
||||
import { ArtistsScreen, AlbumsScreen } from './src/screens/Library';
|
||||
import ArtistDetailScreen from './src/screens/ArtistDetail';
|
||||
import AlbumDetailScreen from './src/screens/AlbumDetail';
|
||||
import SearchScreen from './src/screens/Search';
|
||||
import LoginScreen from './src/screens/Login';
|
||||
import NowPlayingScreen from './src/screens/NowPlaying';
|
||||
import PlayerBar from './src/components/PlayerBar';
|
||||
|
||||
TrackPlayer.registerPlaybackService(() => PlaybackService);
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
const Stack = createStackNavigator();
|
||||
|
||||
function HomeTabs() {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
headerShown: false,
|
||||
tabBarStyle: { backgroundColor: '#1e1e1e', borderTopColor: '#2a2a2a' },
|
||||
tabBarActiveTintColor: '#1db954',
|
||||
tabBarInactiveTintColor: '#888',
|
||||
tabBarIcon: ({ color, size }) => {
|
||||
const icons = { Home: 'home', Artists: 'person', Albums: 'disc', Search: 'search', Settings: 'settings-outline' };
|
||||
return <Ionicons name={icons[route.name] || 'ellipse'} size={size} color={color} />;
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Tab.Screen name="Home" component={HomeScreen} options={{ title: 'Start' }} />
|
||||
<Tab.Screen name="Artists" component={ArtistsScreen} options={{ title: 'Künstler' }} />
|
||||
<Tab.Screen name="Albums" component={AlbumsScreen} options={{ title: 'Alben' }} />
|
||||
<Tab.Screen name="Search" component={SearchScreen} options={{ title: 'Suche' }} />
|
||||
<Tab.Screen name="Settings" component={LoginScreen} options={{ title: 'Einstellungen' }} />
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
function RootNavigator() {
|
||||
const currentTrack = useStore(s => s.currentTrack);
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="Tabs" component={HomeTabs} />
|
||||
<Stack.Screen name="NowPlaying" component={NowPlayingScreen} options={{ presentation: 'modal' }} />
|
||||
<Stack.Screen name="AlbumDetail" component={AlbumDetailScreen} />
|
||||
<Stack.Screen name="ArtistDetail" component={ArtistDetailScreen} />
|
||||
</Stack.Navigator>
|
||||
{currentTrack && <PlayerBar />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { loadConfig } = useStore();
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await loadConfig();
|
||||
await setupPlayer();
|
||||
setReady(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const sub1 = TrackPlayer.addEventListener(Event.PlaybackActiveTrackChanged, e => {
|
||||
if (e.track) useStore.getState().setCurrentTrack(e.track._song || e.track);
|
||||
});
|
||||
const sub2 = TrackPlayer.addEventListener(Event.PlaybackState, e => {
|
||||
useStore.getState().setIsPlaying(e.state === State.Playing);
|
||||
});
|
||||
return () => { sub1.remove(); sub2.remove(); };
|
||||
}, []);
|
||||
|
||||
if (!ready) return null;
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<SafeAreaProvider>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#121212" />
|
||||
<NavigationContainer theme={{
|
||||
dark: true,
|
||||
colors: {
|
||||
primary: '#1db954',
|
||||
background: '#121212',
|
||||
card: '#1e1e1e',
|
||||
text: '#ffffff',
|
||||
border: '#2a2a2a',
|
||||
notification: '#1db954',
|
||||
},
|
||||
}}>
|
||||
<RootNavigator />
|
||||
</NavigationContainer>
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user