fix: keep album views in search tab when navigating from search results
This commit is contained in:
@@ -1,18 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { MusicStackParams } from './types';
|
|
||||||
import Albums from './stacks/Albums';
|
|
||||||
import Album from './stacks/Album';
|
|
||||||
import RecentAlbums from './stacks/RecentAlbums';
|
|
||||||
import { THEME_COLOR } from 'CONSTANTS';
|
import { THEME_COLOR } from 'CONSTANTS';
|
||||||
import { t } from '@localisation';
|
import { t } from '@localisation';
|
||||||
import useDefaultStyles from 'components/Colors';
|
import useDefaultStyles from 'components/Colors';
|
||||||
import Playlists from './stacks/Playlists';
|
import { StackParams } from 'screens/types';
|
||||||
import Playlist from './stacks/Playlist';
|
|
||||||
import NowPlaying from './overlays/NowPlaying';
|
import NowPlaying from './overlays/NowPlaying';
|
||||||
|
|
||||||
const Stack = createStackNavigator<MusicStackParams>();
|
import RecentAlbums from './stacks/RecentAlbums';
|
||||||
|
import Albums from './stacks/Albums';
|
||||||
|
import Album from './stacks/Album';
|
||||||
|
import Playlists from './stacks/Playlists';
|
||||||
|
import Playlist from './stacks/Playlist';
|
||||||
|
|
||||||
|
const Stack = createStackNavigator<StackParams>();
|
||||||
|
|
||||||
function MusicStack() {
|
function MusicStack() {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
|
|||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { calculateProgressTranslation } from 'components/Progresstrack';
|
import { calculateProgressTranslation } from 'components/Progresstrack';
|
||||||
import { THEME_COLOR } from 'CONSTANTS';
|
import { THEME_COLOR } from 'CONSTANTS';
|
||||||
import { MusicNavigationProp } from 'screens/Music/types';
|
import { NavigationProp } from 'screens/types';
|
||||||
import { ShadowWrapper } from 'components/Shadow';
|
import { ShadowWrapper } from 'components/Shadow';
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ function NowPlaying() {
|
|||||||
const previousBuffered = usePrevious(buffered);
|
const previousBuffered = usePrevious(buffered);
|
||||||
const previousPosition = usePrevious(position);
|
const previousPosition = usePrevious(position);
|
||||||
|
|
||||||
const navigation = useNavigation<MusicNavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
|
||||||
const bufferAnimation = useRef(new Animated.Value(0));
|
const bufferAnimation = useRef(new Animated.Value(0));
|
||||||
const progressAnimation = useRef(new Animated.Value(0));
|
const progressAnimation = useRef(new Animated.Value(0));
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react';
|
import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react';
|
||||||
import { useGetImage } from 'utility/JellyfinApi';
|
import { useGetImage } from 'utility/JellyfinApi';
|
||||||
import { MusicNavigationProp } from '../types';
|
|
||||||
import { SafeAreaView, SectionList, View } from 'react-native';
|
import { SafeAreaView, SectionList, View } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
@@ -17,6 +16,7 @@ import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
|
|||||||
import { Album } from 'store/music/types';
|
import { Album } from 'store/music/types';
|
||||||
import { Text } from 'components/Typography';
|
import { Text } from 'components/Typography';
|
||||||
import { ShadowWrapper } from 'components/Shadow';
|
import { ShadowWrapper } from 'components/Shadow';
|
||||||
|
import { NavigationProp } from 'screens/types';
|
||||||
|
|
||||||
const HeadingHeight = 50;
|
const HeadingHeight = 50;
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ const Albums: React.FC = () => {
|
|||||||
|
|
||||||
// Initialise helpers
|
// Initialise helpers
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigation = useNavigation<MusicNavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const getImage = useGetImage();
|
const getImage = useGetImage();
|
||||||
const listRef = useRef<SectionList<EntityId[]>>(null);
|
const listRef = useRef<SectionList<EntityId[]>>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useRef, ReactText } from 'react';
|
import React, { useCallback, useEffect, useRef, ReactText } from 'react';
|
||||||
import { useGetImage } from 'utility/JellyfinApi';
|
import { useGetImage } from 'utility/JellyfinApi';
|
||||||
import { MusicNavigationProp } from '../types';
|
|
||||||
import { Text, View, FlatList, ListRenderItem } from 'react-native';
|
import { Text, View, FlatList, ListRenderItem } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
@@ -11,6 +10,7 @@ import TouchableHandler from 'components/TouchableHandler';
|
|||||||
import AlbumImage, { AlbumItem } from './components/AlbumImage';
|
import AlbumImage, { AlbumItem } from './components/AlbumImage';
|
||||||
import { EntityId } from '@reduxjs/toolkit';
|
import { EntityId } from '@reduxjs/toolkit';
|
||||||
import useDefaultStyles from 'components/Colors';
|
import useDefaultStyles from 'components/Colors';
|
||||||
|
import { NavigationProp } from 'screens/types';
|
||||||
|
|
||||||
interface GeneratedAlbumItemProps {
|
interface GeneratedAlbumItemProps {
|
||||||
id: ReactText;
|
id: ReactText;
|
||||||
@@ -41,7 +41,7 @@ const Playlists: React.FC = () => {
|
|||||||
|
|
||||||
// Initialise helpers
|
// Initialise helpers
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigation = useNavigation<MusicNavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const getImage = useGetImage();
|
const getImage = useGetImage();
|
||||||
const listRef = useRef<FlatList<EntityId>>(null);
|
const listRef = useRef<FlatList<EntityId>>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useGetImage } from 'utility/JellyfinApi';
|
import { useGetImage } from 'utility/JellyfinApi';
|
||||||
import { MusicNavigationProp } from '../types';
|
|
||||||
import { Text, SafeAreaView, FlatList, StyleSheet } from 'react-native';
|
import { Text, SafeAreaView, FlatList, StyleSheet } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useAppDispatch, useTypedSelector } from 'store';
|
import { useAppDispatch, useTypedSelector } from 'store';
|
||||||
@@ -17,6 +16,7 @@ import { Album } from 'store/music/types';
|
|||||||
import Divider from 'components/Divider';
|
import Divider from 'components/Divider';
|
||||||
import styled from 'styled-components/native';
|
import styled from 'styled-components/native';
|
||||||
import { ShadowWrapper } from 'components/Shadow';
|
import { ShadowWrapper } from 'components/Shadow';
|
||||||
|
import { NavigationProp } from 'screens/types';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
columnWrapper: {
|
columnWrapper: {
|
||||||
@@ -31,7 +31,7 @@ const HeaderContainer = styled.View`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const NavigationHeader: React.FC = () => {
|
const NavigationHeader: React.FC = () => {
|
||||||
const navigation = useNavigation<MusicNavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]);
|
const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]);
|
||||||
const handlePlaylistsClick = useCallback(() => { navigation.navigate('Playlists'); }, [navigation]);
|
const handlePlaylistsClick = useCallback(() => { navigation.navigate('Playlists'); }, [navigation]);
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ const RecentAlbums: React.FC = () => {
|
|||||||
|
|
||||||
// Initialise helpers
|
// Initialise helpers
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const navigation = useNavigation<MusicNavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const getImage = useGetImage();
|
const getImage = useGetImage();
|
||||||
|
|
||||||
// Set callbacks
|
// Set callbacks
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import useDefaultStyles from 'components/Colors';
|
|||||||
import usePlayTracks from 'utility/usePlayTracks';
|
import usePlayTracks from 'utility/usePlayTracks';
|
||||||
import { EntityId } from '@reduxjs/toolkit';
|
import { EntityId } from '@reduxjs/toolkit';
|
||||||
import { WrappableButtonRow, WrappableButton } from 'components/WrappableButtonRow';
|
import { WrappableButtonRow, WrappableButton } from 'components/WrappableButtonRow';
|
||||||
import { MusicNavigationProp } from 'screens/Music/types';
|
import { NavigationProp } from 'screens/types';
|
||||||
import DownloadIcon from 'components/DownloadIcon';
|
import DownloadIcon from 'components/DownloadIcon';
|
||||||
import CloudDownArrow from 'assets/icons/cloud-down-arrow.svg';
|
import CloudDownArrow from 'assets/icons/cloud-down-arrow.svg';
|
||||||
import Trash from 'assets/icons/trash.svg';
|
import Trash from 'assets/icons/trash.svg';
|
||||||
@@ -89,7 +89,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
const getImage = useGetImage();
|
const getImage = useGetImage();
|
||||||
const playTracks = usePlayTracks();
|
const playTracks = usePlayTracks();
|
||||||
const { track: currentTrack } = useCurrentTrack();
|
const { track: currentTrack } = useCurrentTrack();
|
||||||
const navigation = useNavigation<MusicNavigationProp>();
|
const navigation = useNavigation<NavigationProp>();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// Setup callbacks
|
// Setup callbacks
|
||||||
@@ -101,7 +101,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
await TrackPlayer.play();
|
await TrackPlayer.play();
|
||||||
}, [playTracks, trackIds]);
|
}, [playTracks, trackIds]);
|
||||||
const longPressTrack = useCallback((index: number) => {
|
const longPressTrack = useCallback((index: number) => {
|
||||||
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index] });
|
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() });
|
||||||
}, [navigation, trackIds]);
|
}, [navigation, trackIds]);
|
||||||
const downloadAllTracks = useCallback(() => {
|
const downloadAllTracks = useCallback(() => {
|
||||||
trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId)));
|
trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId)));
|
||||||
|
|||||||
@@ -1,331 +1,27 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React from 'react';
|
||||||
import Input from 'components/Input';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
import { ActivityIndicator, Animated, SafeAreaView, View } from 'react-native';
|
import { THEME_COLOR } from 'CONSTANTS';
|
||||||
import styled from 'styled-components/native';
|
|
||||||
import { useAppDispatch, useTypedSelector } from 'store';
|
|
||||||
import Fuse from 'fuse.js';
|
|
||||||
import { Album, AlbumTrack } from 'store/music/types';
|
|
||||||
import { FlatList } from 'react-native-gesture-handler';
|
|
||||||
import TouchableHandler from 'components/TouchableHandler';
|
|
||||||
import { useNavigation } from '@react-navigation/native';
|
|
||||||
import { useGetImage } from 'utility/JellyfinApi';
|
|
||||||
import FastImage from 'react-native-fast-image';
|
|
||||||
import { t } from '@localisation';
|
import { t } from '@localisation';
|
||||||
import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
|
import useDefaultStyles from 'components/Colors';
|
||||||
import { searchAndFetchAlbums } from 'store/music/actions';
|
import { StackParams } from 'screens/types';
|
||||||
import { debounce } from 'lodash';
|
import Search from './stacks/Search';
|
||||||
import { Text } from 'components/Typography';
|
import Album from 'screens/Music/stacks/Album';
|
||||||
import { MusicNavigationProp } from 'screens/Music/types';
|
|
||||||
import DownloadIcon from 'components/DownloadIcon';
|
|
||||||
import ChevronRight from 'assets/icons/chevron-right.svg';
|
|
||||||
import SearchIcon from 'assets/icons/magnifying-glass.svg';
|
|
||||||
import { ShadowWrapper } from 'components/Shadow';
|
|
||||||
import { useKeyboardHeight } from 'utility/useKeyboardHeight';
|
|
||||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
|
||||||
// import MicrophoneIcon from 'assets/icons/microphone.svg';
|
|
||||||
// import AlbumIcon from 'assets/icons/collection.svg';
|
|
||||||
// import TrackIcon from 'assets/icons/note.svg';
|
|
||||||
// import PlaylistIcon from 'assets/icons/note-list.svg';
|
|
||||||
// import StreamIcon from 'assets/icons/cloud.svg';
|
|
||||||
// import LocalIcon from 'assets/icons/internal-drive.svg';
|
|
||||||
// import SelectableFilter from './components/SelectableFilter';
|
|
||||||
|
|
||||||
const Container = styled(View)`
|
const Stack = createStackNavigator<StackParams>();
|
||||||
padding: 4px 24px 0 24px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
padding-bottom: 0px;
|
|
||||||
border-top-width: 0.5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FullSizeContainer = styled.View`
|
function SearchStack() {
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Loading = styled.View`
|
|
||||||
position: absolute;
|
|
||||||
right: 12px;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AlbumImage = styled(FastImage)`
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
margin-right: 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HalfOpacity = styled.Text`
|
|
||||||
opacity: 0.5;
|
|
||||||
margin-top: 2px;
|
|
||||||
font-size: 12px;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SearchResult = styled.View`
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 32px;
|
|
||||||
height: 54px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const fuseOptions: Fuse.IFuseOptions<Album> = {
|
|
||||||
keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'],
|
|
||||||
threshold: 0.1,
|
|
||||||
includeScore: true,
|
|
||||||
fieldNormWeight: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
type AudioResult = {
|
|
||||||
type: 'Audio',
|
|
||||||
id: string;
|
|
||||||
album: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AlbumResult = {
|
|
||||||
type: 'AlbumArtist',
|
|
||||||
id: string;
|
|
||||||
album: undefined;
|
|
||||||
name: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
type CombinedResults = (AudioResult | AlbumResult)[];
|
|
||||||
|
|
||||||
export default function Search() {
|
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
const tabBarHeight = useBottomTabBarHeight();
|
|
||||||
|
|
||||||
// Prepare state for fuse and albums
|
|
||||||
const [fuseIsReady, setFuseReady] = useState(false);
|
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
|
||||||
const [isLoading, setLoading] = useState(false);
|
|
||||||
const [fuseResults, setFuseResults] = useState<CombinedResults>([]);
|
|
||||||
const [jellyfinResults, setJellyfinResults] = useState<CombinedResults>([]);
|
|
||||||
const albums = useTypedSelector(state => state.music.albums.entities);
|
|
||||||
const fuse = useRef<Fuse<Album>>();
|
|
||||||
|
|
||||||
// Prepare helpers
|
|
||||||
const navigation = useNavigation<MusicNavigationProp>();
|
|
||||||
const keyboardHeight = useKeyboardHeight();
|
|
||||||
const getImage = useGetImage();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Since it is impractical to have a global fuse variable, we need to
|
|
||||||
* instantiate it for thsi function. With this effect, we generate a new
|
|
||||||
* Fuse instance every time the albums change. This can of course be done
|
|
||||||
* more intelligently by removing and adding the changed albums, but this is
|
|
||||||
* an open todo.
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
fuse.current = new Fuse(Object.values(albums) as Album[], fuseOptions);
|
|
||||||
setFuseReady(true);
|
|
||||||
}, [albums, setFuseReady]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function retrieves search results from Jellyfin. It is a seperate
|
|
||||||
* callback, so that we can make sure it is properly debounced and doesn't
|
|
||||||
* cause execessive jank in the interface.
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
const fetchJellyfinResults = useCallback(debounce(async (searchTerm: string, currentResults: CombinedResults) => {
|
|
||||||
// First, query the Jellyfin API
|
|
||||||
const { payload } = await dispatch(searchAndFetchAlbums({ term: searchTerm }));
|
|
||||||
|
|
||||||
// Convert the current results to album ids
|
|
||||||
const albumIds = currentResults.map(item => item.id);
|
|
||||||
|
|
||||||
// Parse the result in correct typescript form
|
|
||||||
const results = (payload as { results: (Album | AlbumTrack)[] }).results;
|
|
||||||
|
|
||||||
// Filter any results that are already displayed
|
|
||||||
const items = results.filter(item => (
|
|
||||||
!(item.Type === 'MusicAlbum' && albumIds.includes(item.Id))
|
|
||||||
// Then convert the results to proper result form
|
|
||||||
)).map((item) => ({
|
|
||||||
type: item.Type,
|
|
||||||
id: item.Id,
|
|
||||||
album: item.Type === 'Audio'
|
|
||||||
? item.AlbumId
|
|
||||||
: undefined,
|
|
||||||
name: item.Type === 'Audio'
|
|
||||||
? item.Name
|
|
||||||
: undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Lastly, we'll merge the two and assign them to the state
|
|
||||||
setJellyfinResults([...items] as CombinedResults);
|
|
||||||
|
|
||||||
// Loading is now complete
|
|
||||||
setLoading(false);
|
|
||||||
}, 50), [dispatch, setJellyfinResults]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whenever the search term changes, we gather results from Fuse and assign
|
|
||||||
* them to state
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!searchTerm) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const retrieveResults = async () => {
|
|
||||||
// GUARD: In some extraordinary cases, Fuse might not be presented since
|
|
||||||
// it is assigned via refs. In this case, we can't handle any searching.
|
|
||||||
if (!fuse.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// First set the immediate results from fuse
|
|
||||||
const fuseResults = fuse.current.search(searchTerm);
|
|
||||||
const albums: AlbumResult[] = fuseResults
|
|
||||||
.map(({ item }) => ({
|
|
||||||
id: item.Id,
|
|
||||||
type: 'AlbumArtist',
|
|
||||||
album: undefined,
|
|
||||||
name: undefined,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Assign the preliminary results
|
|
||||||
setFuseResults(albums);
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
// Wrap the call in a try/catch block so that we catch any
|
|
||||||
// network issues in search and just use local search if the
|
|
||||||
// network is unavailable
|
|
||||||
fetchJellyfinResults(searchTerm, albums);
|
|
||||||
} catch {
|
|
||||||
// Reset the loading indicator if the network fails
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
retrieveResults();
|
|
||||||
}, [searchTerm, setFuseResults, setLoading, fuse, fetchJellyfinResults]);
|
|
||||||
|
|
||||||
// Handlers
|
|
||||||
const selectAlbum = useCallback((id: string) =>
|
|
||||||
navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]
|
|
||||||
);
|
|
||||||
|
|
||||||
const HeaderComponent = React.useMemo(() => (
|
|
||||||
<Animated.View style={[
|
|
||||||
{ position: 'absolute', bottom: tabBarHeight, right: 0, left: 0 },
|
|
||||||
{ transform: [{ translateY: keyboardHeight }] },
|
|
||||||
]}>
|
|
||||||
<ColoredBlurView>
|
|
||||||
<Container style={[ defaultStyles.border ]}>
|
|
||||||
<View>
|
|
||||||
<Input
|
|
||||||
value={searchTerm}
|
|
||||||
onChangeText={setSearchTerm}
|
|
||||||
style={[defaultStyles.view, { marginBottom: 12 }]}
|
|
||||||
placeholder={t('search') + '...'}
|
|
||||||
icon={<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />}
|
|
||||||
testID="search-input"
|
|
||||||
/>
|
|
||||||
{isLoading && <Loading style={{ marginTop: -4 }}><ActivityIndicator /></Loading>}
|
|
||||||
</View>
|
|
||||||
</Container>
|
|
||||||
{/* <ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
||||||
<View style={{ paddingHorizontal: 32, paddingBottom: 12, flex: 1, flexDirection: 'row' }}>
|
|
||||||
<SelectableFilter
|
|
||||||
text="Artists"
|
|
||||||
icon={MicrophoneIcon}
|
|
||||||
active
|
|
||||||
/>
|
|
||||||
<SelectableFilter
|
|
||||||
text="Albums"
|
|
||||||
icon={AlbumIcon}
|
|
||||||
active={false}
|
|
||||||
/>
|
|
||||||
<SelectableFilter
|
|
||||||
text="Tracks"
|
|
||||||
icon={TrackIcon}
|
|
||||||
active={false}
|
|
||||||
/>
|
|
||||||
<SelectableFilter
|
|
||||||
text="Playlist"
|
|
||||||
icon={PlaylistIcon}
|
|
||||||
active={false}
|
|
||||||
/>
|
|
||||||
<SelectableFilter
|
|
||||||
text="Streaming"
|
|
||||||
icon={StreamIcon}
|
|
||||||
active={false}
|
|
||||||
/>
|
|
||||||
<SelectableFilter
|
|
||||||
text="Local Playback"
|
|
||||||
icon={LocalIcon}
|
|
||||||
active={false}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</ScrollView> */}
|
|
||||||
</ColoredBlurView>
|
|
||||||
</Animated.View>
|
|
||||||
), [searchTerm, setSearchTerm, defaultStyles, isLoading, keyboardHeight, tabBarHeight]);
|
|
||||||
|
|
||||||
// GUARD: We cannot search for stuff unless Fuse is loaded with results.
|
|
||||||
// Therefore we delay rendering to when we are certain it's there.
|
|
||||||
if (!fuseIsReady) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1 }}>
|
<Stack.Navigator initialRouteName="Search" screenOptions={{
|
||||||
<FlatList
|
headerTintColor: THEME_COLOR,
|
||||||
style={{ flex: 2 }}
|
headerTitleStyle: defaultStyles.stackHeader,
|
||||||
data={[...jellyfinResults, ...fuseResults]}
|
cardStyle: defaultStyles.view,
|
||||||
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
|
}}>
|
||||||
const album = albums[trackAlbum || id];
|
<Stack.Screen name="Search" component={Search} options={{ headerTitle: t('search'), headerShown: false }} />
|
||||||
|
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
|
||||||
// GUARD: If the album cannot be found in the store, we
|
</Stack.Navigator>
|
||||||
// cannot display it.
|
|
||||||
if (!album) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableHandler<string> id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}>
|
|
||||||
<SearchResult>
|
|
||||||
<ShadowWrapper>
|
|
||||||
<AlbumImage source={{ uri: getImage(album.Id) }} style={defaultStyles.imageBackground} />
|
|
||||||
</ShadowWrapper>
|
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Text numberOfLines={1}>
|
|
||||||
{trackName || album.Name}
|
|
||||||
</Text>
|
|
||||||
{(album.AlbumArtist || album.Name) && (
|
|
||||||
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>
|
|
||||||
{type === 'AlbumArtist'
|
|
||||||
? `${t('album')} • ${album.AlbumArtist}`
|
|
||||||
: `${t('track')} • ${album.AlbumArtist} — ${album.Name}`
|
|
||||||
}
|
|
||||||
</HalfOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View style={{ marginLeft: 16 }}>
|
|
||||||
<DownloadIcon trackId={id} />
|
|
||||||
</View>
|
|
||||||
<View style={{ marginLeft: 16 }}>
|
|
||||||
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
|
|
||||||
</View>
|
|
||||||
</SearchResult>
|
|
||||||
</TouchableHandler>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
keyExtractor={(item) => item.id}
|
|
||||||
extraData={[searchTerm, albums]}
|
|
||||||
/>
|
|
||||||
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
|
|
||||||
<FullSizeContainer>
|
|
||||||
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
|
||||||
</FullSizeContainer>
|
|
||||||
) : null}
|
|
||||||
{HeaderComponent}
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default SearchStack;
|
||||||
331
src/screens/Search/stacks/Search/index.tsx
Normal file
331
src/screens/Search/stacks/Search/index.tsx
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import Input from 'components/Input';
|
||||||
|
import { ActivityIndicator, Animated, SafeAreaView, View } from 'react-native';
|
||||||
|
import styled from 'styled-components/native';
|
||||||
|
import { useAppDispatch, useTypedSelector } from 'store';
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
import { Album, AlbumTrack } from 'store/music/types';
|
||||||
|
import { FlatList } from 'react-native-gesture-handler';
|
||||||
|
import TouchableHandler from 'components/TouchableHandler';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { useGetImage } from 'utility/JellyfinApi';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
import { t } from '@localisation';
|
||||||
|
import useDefaultStyles, { ColoredBlurView } from 'components/Colors';
|
||||||
|
import { searchAndFetchAlbums } from 'store/music/actions';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { Text } from 'components/Typography';
|
||||||
|
import DownloadIcon from 'components/DownloadIcon';
|
||||||
|
import ChevronRight from 'assets/icons/chevron-right.svg';
|
||||||
|
import SearchIcon from 'assets/icons/magnifying-glass.svg';
|
||||||
|
import { ShadowWrapper } from 'components/Shadow';
|
||||||
|
import { useKeyboardHeight } from 'utility/useKeyboardHeight';
|
||||||
|
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||||
|
import { NavigationProp } from 'screens/types';
|
||||||
|
// import MicrophoneIcon from 'assets/icons/microphone.svg';
|
||||||
|
// import AlbumIcon from 'assets/icons/collection.svg';
|
||||||
|
// import TrackIcon from 'assets/icons/note.svg';
|
||||||
|
// import PlaylistIcon from 'assets/icons/note-list.svg';
|
||||||
|
// import StreamIcon from 'assets/icons/cloud.svg';
|
||||||
|
// import LocalIcon from 'assets/icons/internal-drive.svg';
|
||||||
|
// import SelectableFilter from './components/SelectableFilter';
|
||||||
|
|
||||||
|
const Container = styled(View)`
|
||||||
|
padding: 4px 24px 0 24px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
border-top-width: 0.5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const FullSizeContainer = styled.View`
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Loading = styled.View`
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AlbumImage = styled(FastImage)`
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HalfOpacity = styled.Text`
|
||||||
|
opacity: 0.5;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SearchResult = styled.View`
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 32px;
|
||||||
|
height: 54px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const fuseOptions: Fuse.IFuseOptions<Album> = {
|
||||||
|
keys: ['Name', 'AlbumArtist', 'AlbumArtists', 'Artists'],
|
||||||
|
threshold: 0.1,
|
||||||
|
includeScore: true,
|
||||||
|
fieldNormWeight: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
type AudioResult = {
|
||||||
|
type: 'Audio',
|
||||||
|
id: string;
|
||||||
|
album: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AlbumResult = {
|
||||||
|
type: 'AlbumArtist',
|
||||||
|
id: string;
|
||||||
|
album: undefined;
|
||||||
|
name: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CombinedResults = (AudioResult | AlbumResult)[];
|
||||||
|
|
||||||
|
export default function Search() {
|
||||||
|
const defaultStyles = useDefaultStyles();
|
||||||
|
const tabBarHeight = useBottomTabBarHeight();
|
||||||
|
|
||||||
|
// Prepare state for fuse and albums
|
||||||
|
const [fuseIsReady, setFuseReady] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [isLoading, setLoading] = useState(false);
|
||||||
|
const [fuseResults, setFuseResults] = useState<CombinedResults>([]);
|
||||||
|
const [jellyfinResults, setJellyfinResults] = useState<CombinedResults>([]);
|
||||||
|
const albums = useTypedSelector(state => state.music.albums.entities);
|
||||||
|
const fuse = useRef<Fuse<Album>>();
|
||||||
|
|
||||||
|
// Prepare helpers
|
||||||
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
const keyboardHeight = useKeyboardHeight();
|
||||||
|
const getImage = useGetImage();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since it is impractical to have a global fuse variable, we need to
|
||||||
|
* instantiate it for thsi function. With this effect, we generate a new
|
||||||
|
* Fuse instance every time the albums change. This can of course be done
|
||||||
|
* more intelligently by removing and adding the changed albums, but this is
|
||||||
|
* an open todo.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
fuse.current = new Fuse(Object.values(albums) as Album[], fuseOptions);
|
||||||
|
setFuseReady(true);
|
||||||
|
}, [albums, setFuseReady]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function retrieves search results from Jellyfin. It is a seperate
|
||||||
|
* callback, so that we can make sure it is properly debounced and doesn't
|
||||||
|
* cause execessive jank in the interface.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
const fetchJellyfinResults = useCallback(debounce(async (searchTerm: string, currentResults: CombinedResults) => {
|
||||||
|
// First, query the Jellyfin API
|
||||||
|
const { payload } = await dispatch(searchAndFetchAlbums({ term: searchTerm }));
|
||||||
|
|
||||||
|
// Convert the current results to album ids
|
||||||
|
const albumIds = currentResults.map(item => item.id);
|
||||||
|
|
||||||
|
// Parse the result in correct typescript form
|
||||||
|
const results = (payload as { results: (Album | AlbumTrack)[] }).results;
|
||||||
|
|
||||||
|
// Filter any results that are already displayed
|
||||||
|
const items = results.filter(item => (
|
||||||
|
!(item.Type === 'MusicAlbum' && albumIds.includes(item.Id))
|
||||||
|
// Then convert the results to proper result form
|
||||||
|
)).map((item) => ({
|
||||||
|
type: item.Type,
|
||||||
|
id: item.Id,
|
||||||
|
album: item.Type === 'Audio'
|
||||||
|
? item.AlbumId
|
||||||
|
: undefined,
|
||||||
|
name: item.Type === 'Audio'
|
||||||
|
? item.Name
|
||||||
|
: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Lastly, we'll merge the two and assign them to the state
|
||||||
|
setJellyfinResults([...items] as CombinedResults);
|
||||||
|
|
||||||
|
// Loading is now complete
|
||||||
|
setLoading(false);
|
||||||
|
}, 50), [dispatch, setJellyfinResults]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whenever the search term changes, we gather results from Fuse and assign
|
||||||
|
* them to state
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!searchTerm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const retrieveResults = async () => {
|
||||||
|
// GUARD: In some extraordinary cases, Fuse might not be presented since
|
||||||
|
// it is assigned via refs. In this case, we can't handle any searching.
|
||||||
|
if (!fuse.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First set the immediate results from fuse
|
||||||
|
const fuseResults = fuse.current.search(searchTerm);
|
||||||
|
const albums: AlbumResult[] = fuseResults
|
||||||
|
.map(({ item }) => ({
|
||||||
|
id: item.Id,
|
||||||
|
type: 'AlbumArtist',
|
||||||
|
album: undefined,
|
||||||
|
name: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Assign the preliminary results
|
||||||
|
setFuseResults(albums);
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// Wrap the call in a try/catch block so that we catch any
|
||||||
|
// network issues in search and just use local search if the
|
||||||
|
// network is unavailable
|
||||||
|
fetchJellyfinResults(searchTerm, albums);
|
||||||
|
} catch {
|
||||||
|
// Reset the loading indicator if the network fails
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
retrieveResults();
|
||||||
|
}, [searchTerm, setFuseResults, setLoading, fuse, fetchJellyfinResults]);
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const selectAlbum = useCallback((id: string) => {
|
||||||
|
navigation.navigate('Album', { id, album: albums[id] as Album });
|
||||||
|
}, [navigation, albums]);
|
||||||
|
|
||||||
|
const HeaderComponent = React.useMemo(() => (
|
||||||
|
<Animated.View style={[
|
||||||
|
{ position: 'absolute', bottom: tabBarHeight, right: 0, left: 0 },
|
||||||
|
{ transform: [{ translateY: keyboardHeight }] },
|
||||||
|
]}>
|
||||||
|
<ColoredBlurView>
|
||||||
|
<Container style={[ defaultStyles.border ]}>
|
||||||
|
<View>
|
||||||
|
<Input
|
||||||
|
value={searchTerm}
|
||||||
|
onChangeText={setSearchTerm}
|
||||||
|
style={[defaultStyles.view, { marginBottom: 12 }]}
|
||||||
|
placeholder={t('search') + '...'}
|
||||||
|
icon={<SearchIcon width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />}
|
||||||
|
testID="search-input"
|
||||||
|
/>
|
||||||
|
{isLoading && <Loading style={{ marginTop: -4 }}><ActivityIndicator /></Loading>}
|
||||||
|
</View>
|
||||||
|
</Container>
|
||||||
|
{/* <ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||||
|
<View style={{ paddingHorizontal: 32, paddingBottom: 12, flex: 1, flexDirection: 'row' }}>
|
||||||
|
<SelectableFilter
|
||||||
|
text="Artists"
|
||||||
|
icon={MicrophoneIcon}
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
<SelectableFilter
|
||||||
|
text="Albums"
|
||||||
|
icon={AlbumIcon}
|
||||||
|
active={false}
|
||||||
|
/>
|
||||||
|
<SelectableFilter
|
||||||
|
text="Tracks"
|
||||||
|
icon={TrackIcon}
|
||||||
|
active={false}
|
||||||
|
/>
|
||||||
|
<SelectableFilter
|
||||||
|
text="Playlist"
|
||||||
|
icon={PlaylistIcon}
|
||||||
|
active={false}
|
||||||
|
/>
|
||||||
|
<SelectableFilter
|
||||||
|
text="Streaming"
|
||||||
|
icon={StreamIcon}
|
||||||
|
active={false}
|
||||||
|
/>
|
||||||
|
<SelectableFilter
|
||||||
|
text="Local Playback"
|
||||||
|
icon={LocalIcon}
|
||||||
|
active={false}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView> */}
|
||||||
|
</ColoredBlurView>
|
||||||
|
</Animated.View>
|
||||||
|
), [searchTerm, setSearchTerm, defaultStyles, isLoading, keyboardHeight, tabBarHeight]);
|
||||||
|
|
||||||
|
// GUARD: We cannot search for stuff unless Fuse is loaded with results.
|
||||||
|
// Therefore we delay rendering to when we are certain it's there.
|
||||||
|
if (!fuseIsReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1 }}>
|
||||||
|
<FlatList
|
||||||
|
style={{ flex: 2 }}
|
||||||
|
data={[...jellyfinResults, ...fuseResults]}
|
||||||
|
renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
|
||||||
|
const album = albums[trackAlbum || id];
|
||||||
|
|
||||||
|
// GUARD: If the album cannot be found in the store, we
|
||||||
|
// cannot display it.
|
||||||
|
if (!album) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableHandler<string> id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}>
|
||||||
|
<SearchResult>
|
||||||
|
<ShadowWrapper>
|
||||||
|
<AlbumImage source={{ uri: getImage(album.Id) }} style={defaultStyles.imageBackground} />
|
||||||
|
</ShadowWrapper>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text numberOfLines={1}>
|
||||||
|
{trackName || album.Name}
|
||||||
|
</Text>
|
||||||
|
{(album.AlbumArtist || album.Name) && (
|
||||||
|
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>
|
||||||
|
{type === 'AlbumArtist'
|
||||||
|
? `${t('album')} • ${album.AlbumArtist}`
|
||||||
|
: `${t('track')} • ${album.AlbumArtist} — ${album.Name}`
|
||||||
|
}
|
||||||
|
</HalfOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={{ marginLeft: 16 }}>
|
||||||
|
<DownloadIcon trackId={id} />
|
||||||
|
</View>
|
||||||
|
<View style={{ marginLeft: 16 }}>
|
||||||
|
<ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color} />
|
||||||
|
</View>
|
||||||
|
</SearchResult>
|
||||||
|
</TouchableHandler>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
extraData={[searchTerm, albums]}
|
||||||
|
/>
|
||||||
|
{(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
|
||||||
|
<FullSizeContainer>
|
||||||
|
<Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text>
|
||||||
|
</FullSizeContainer>
|
||||||
|
) : null}
|
||||||
|
{HeaderComponent}
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|||||||
import { CompositeNavigationProp } from '@react-navigation/native';
|
import { CompositeNavigationProp } from '@react-navigation/native';
|
||||||
import { THEME_COLOR } from 'CONSTANTS';
|
import { THEME_COLOR } from 'CONSTANTS';
|
||||||
|
|
||||||
import Search from './Search';
|
import SearchStack from './Search';
|
||||||
import Music from './Music';
|
import Music from './Music';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
import Downloads from './Downloads';
|
import Downloads from './Downloads';
|
||||||
@@ -18,15 +18,15 @@ import NotesIcon from 'assets/icons/notes.svg';
|
|||||||
import GearIcon from 'assets/icons/gear.svg';
|
import GearIcon from 'assets/icons/gear.svg';
|
||||||
import DownloadsIcon from 'assets/icons/arrow-down-to-line.svg';
|
import DownloadsIcon from 'assets/icons/arrow-down-to-line.svg';
|
||||||
import { useTypedSelector } from 'store';
|
import { useTypedSelector } from 'store';
|
||||||
import { ModalStackParams } from './types';
|
|
||||||
import { t } from '@localisation';
|
import { t } from '@localisation';
|
||||||
import ErrorReportingAlert from 'utility/ErrorReportingAlert';
|
import ErrorReportingAlert from 'utility/ErrorReportingAlert';
|
||||||
import ErrorReportingPopup from './modals/ErrorReportingPopup';
|
import ErrorReportingPopup from './modals/ErrorReportingPopup';
|
||||||
import Player from './modals/Player';
|
import Player from './modals/Player';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import { ColoredBlurView } from 'components/Colors';
|
import { ColoredBlurView } from 'components/Colors';
|
||||||
|
import { StackParams } from './types';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<ModalStackParams>();
|
const Stack = createNativeStackNavigator<StackParams>();
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
type Screens = {
|
type Screens = {
|
||||||
@@ -50,9 +50,9 @@ function Screens() {
|
|||||||
screenOptions={({ route }) => ({
|
screenOptions={({ route }) => ({
|
||||||
tabBarIcon: function TabBarIcon({ color, size }) {
|
tabBarIcon: function TabBarIcon({ color, size }) {
|
||||||
switch (route.name) {
|
switch (route.name) {
|
||||||
case 'Search':
|
case 'SearchTab':
|
||||||
return <SearchIcon fill={color} height={size - 4} width={size - 4} />;
|
return <SearchIcon fill={color} height={size - 4} width={size - 4} />;
|
||||||
case 'Music':
|
case 'MusicTab':
|
||||||
return <NotesIcon fill={color} height={size} width={size} />;
|
return <NotesIcon fill={color} height={size} width={size} />;
|
||||||
case 'Settings':
|
case 'Settings':
|
||||||
return <GearIcon fill={color} height={size - 1} width={size - 1} />;
|
return <GearIcon fill={color} height={size - 1} width={size - 1} />;
|
||||||
@@ -72,8 +72,8 @@ function Screens() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Tab.Screen name="Music" component={Music} options={{ tabBarLabel: t('music'), tabBarTestID: 'music-tab' }} />
|
<Tab.Screen name="MusicTab" component={Music} options={{ tabBarLabel: t('music'), tabBarTestID: 'music-tab' }} />
|
||||||
<Tab.Screen name="Search" component={Search} options={{ tabBarLabel: t('search'), tabBarTestID: 'search-tab' }} />
|
<Tab.Screen name="SearchTab" component={SearchStack} options={{ tabBarLabel: t('search'), tabBarTestID: 'search-tab' }} />
|
||||||
<Tab.Screen name="Downloads" component={Downloads} options={{ tabBarLabel: t('downloads'), tabBarTestID: 'downloads-tab'}} />
|
<Tab.Screen name="Downloads" component={Downloads} options={{ tabBarLabel: t('downloads'), tabBarTestID: 'downloads-tab'}} />
|
||||||
<Tab.Screen name="Settings" component={Settings} options={{ tabBarLabel: t('settings'), tabBarTestID: 'settings-tab' }} />
|
<Tab.Screen name="Settings" component={Settings} options={{ tabBarLabel: t('settings'), tabBarTestID: 'settings-tab' }} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { StackNavigationProp } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { Album } from 'store/music/types';
|
||||||
|
|
||||||
export interface ModalStackParams {
|
export type StackParams = {
|
||||||
[key: string]: Record<string, unknown> | undefined;
|
[key: string]: Record<string, unknown> | undefined;
|
||||||
|
Albums: undefined;
|
||||||
|
Album: { id: string, album: Album };
|
||||||
|
Playlists: undefined;
|
||||||
|
Playlist: { id: string };
|
||||||
|
RecentAlbums: undefined;
|
||||||
|
Search: undefined;
|
||||||
SetJellyfinServer: undefined;
|
SetJellyfinServer: undefined;
|
||||||
TrackPopupMenu: { trackId: string };
|
TrackPopupMenu: { trackId: string };
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ModalNavigationProp = StackNavigationProp<ModalStackParams>;
|
export type NavigationProp = StackNavigationProp<StackParams>;
|
||||||
|
|||||||
Reference in New Issue
Block a user