Files
NovaSonic/mobile/App.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

112 lines
4.2 KiB
JavaScript

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