better tabs and more layout
70
App.tsx
@ -1,78 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|
||||||
|
|
||||||
import { Image } from 'react-native';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
import RootNavigator from './src/components/RootNavigator';
|
||||||
import SettingsView from './src/components/Settings';
|
|
||||||
import ArtistsList from './src/components/ArtistsList';
|
|
||||||
import NowPlayingLayout from './src/components/NowPlayingLayout';
|
|
||||||
|
|
||||||
|
|
||||||
const SettingsIcon: React.FC<{ focused: boolean }> = ({ focused }) => (
|
|
||||||
<Image
|
|
||||||
style={{
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
}}
|
|
||||||
source={focused ? require('./res/settings-fill.png') : require('./res/settings.png')} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const NowPlayingIcon: React.FC<{ focused: boolean }> = ({ focused }) => (
|
|
||||||
<Image
|
|
||||||
style={{
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
}}
|
|
||||||
source={focused ? require('./res/music_notes-fill.png') : require('./res/music_notes.png')} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const ArtistsIcon: React.FC<{ focused: boolean }> = ({ focused }) => (
|
|
||||||
<Image
|
|
||||||
style={{
|
|
||||||
height: 32,
|
|
||||||
width: 32,
|
|
||||||
}}
|
|
||||||
source={focused ? require('./res/mic_on-fill.png') : require('./res/mic_on.png')} />
|
|
||||||
);
|
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator();
|
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<Tab.Navigator>
|
<RootNavigator />
|
||||||
<Tab.Screen
|
|
||||||
name='Settings'
|
|
||||||
component={SettingsView}
|
|
||||||
options={{
|
|
||||||
tabBarIcon: SettingsIcon,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name='Artists'
|
|
||||||
component={ArtistsList}
|
|
||||||
options={{
|
|
||||||
tabBarIcon: ArtistsIcon,
|
|
||||||
tabBarVisibilityAnimationConfig: {
|
|
||||||
show: {
|
|
||||||
animation: 'spring',
|
|
||||||
},
|
|
||||||
hide: {
|
|
||||||
animation: 'spring',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tab.Screen
|
|
||||||
name='Now Playing'
|
|
||||||
component={NowPlayingLayout}
|
|
||||||
options={{
|
|
||||||
tabBarIcon: NowPlayingIcon,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tab.Navigator>
|
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
);
|
);
|
||||||
|
|||||||
BIN
android/app/src/main/assets/fonts/Rubik-VariableFont_wght.ttf
Normal file
BIN
assets/fonts/Rubik-VariableFont_wght.ttf
Normal file
@ -12,6 +12,7 @@
|
|||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||||
|
E40802F2F4B64B50BC3D4A71 /* Rubik-VariableFont_wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6882041C5ED04E249BB58448 /* Rubik-VariableFont_wght.ttf */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -36,6 +37,7 @@
|
|||||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = SubSonify/main.m; sourceTree = "<group>"; };
|
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = SubSonify/main.m; sourceTree = "<group>"; };
|
||||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = SubSonify/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = SubSonify/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||||
|
6882041C5ED04E249BB58448 /* Rubik-VariableFont_wght.ttf */ = {isa = PBXFileReference; name = "Rubik-VariableFont_wght.ttf"; path = "../assets/fonts/Rubik-VariableFont_wght.ttf"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -109,6 +111,7 @@
|
|||||||
00E356EF1AD99517003FC87E /* SubSonifyTests */,
|
00E356EF1AD99517003FC87E /* SubSonifyTests */,
|
||||||
83CBBA001A601CBA00E9B192 /* Products */,
|
83CBBA001A601CBA00E9B192 /* Products */,
|
||||||
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
2D16E6871FA4F8E400B85C8A /* Frameworks */,
|
||||||
|
4E728597FECE42D1A816E9C1 /* Resources */,
|
||||||
);
|
);
|
||||||
indentWidth = 2;
|
indentWidth = 2;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -124,6 +127,15 @@
|
|||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4E728597FECE42D1A816E9C1 /* Resources */ = {
|
||||||
|
isa = "PBXGroup";
|
||||||
|
children = (
|
||||||
|
6882041C5ED04E249BB58448 /* Rubik-VariableFont_wght.ttf */,
|
||||||
|
);
|
||||||
|
name = Resources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
path = "";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@ -214,6 +226,7 @@
|
|||||||
files = (
|
files = (
|
||||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||||
|
E40802F2F4B64B50BC3D4A71 /* Rubik-VariableFont_wght.ttf in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string></string>
|
<string/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
@ -51,5 +51,9 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>UIAppFonts</key>
|
||||||
|
<array>
|
||||||
|
<string>Rubik-VariableFont_wght.ttf</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
7
react-native.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
project: {
|
||||||
|
ios: {},
|
||||||
|
android: {},
|
||||||
|
},
|
||||||
|
assets: ['./assets/fonts']
|
||||||
|
};
|
||||||
BIN
res/chevron_right-fill.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
res/chevron_right.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
res/home-fill.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
res/home.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
res/library-fill.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
res/library.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
res/search-fill.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
res/search.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
@ -29,7 +29,7 @@ const List = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ArtistsList = () => {
|
const ListPlusControls = () => {
|
||||||
const resetArtists = useResetRecoilState(artistsState);
|
const resetArtists = useResetRecoilState(artistsState);
|
||||||
const updateArtists = useUpdateArtists();
|
const updateArtists = useUpdateArtists();
|
||||||
|
|
||||||
@ -43,11 +43,17 @@ const ArtistsList = () => {
|
|||||||
title='Update from API'
|
title='Update from API'
|
||||||
onPress={updateArtists}
|
onPress={updateArtists}
|
||||||
/>
|
/>
|
||||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
|
||||||
<List />
|
<List />
|
||||||
</React.Suspense>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ArtistsList = () => (
|
||||||
|
<View>
|
||||||
|
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||||
|
<ListPlusControls />
|
||||||
|
</React.Suspense>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
export default ArtistsList;
|
export default ArtistsList;
|
||||||
|
|||||||
25
src/components/FocusableIcon.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Image, ImageSourcePropType } from 'react-native';
|
||||||
|
|
||||||
|
export type FocusableIconProps = {
|
||||||
|
focused: boolean,
|
||||||
|
source: ImageSourcePropType;
|
||||||
|
focusedSource?: ImageSourcePropType;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FocusableIcon: React.FC<FocusableIconProps> = (props) => {
|
||||||
|
props.focusedSource = props.focusedSource || props.source;
|
||||||
|
props.width = props.width || 32;
|
||||||
|
props.height = props.height || 32;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
style={{ height: props.height, width: props.width }}
|
||||||
|
source={props.focused ? props.focusedSource : props.source}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FocusableIcon;
|
||||||
48
src/components/Library.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Text, View, Image } from 'react-native';
|
||||||
|
|
||||||
|
const SectionHeader: React.FC<{ title: string }> = ({ title }) => {
|
||||||
|
return (
|
||||||
|
<View style={{
|
||||||
|
height: 60,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
// backgroundColor: 'green',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingLeft: 15,
|
||||||
|
paddingRight: 15,
|
||||||
|
}}>
|
||||||
|
<Text style={{
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 34,
|
||||||
|
fontWeight: 'normal',
|
||||||
|
fontFamily: 'Rubik-VariableFont_wght',
|
||||||
|
}}>{title}</Text>
|
||||||
|
<Image
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
tintColor: 'white',
|
||||||
|
}}
|
||||||
|
source={require('../../res/chevron_right.png')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlbumCoverList = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const Library = () => (
|
||||||
|
<View style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#3b3b3b',
|
||||||
|
}}>
|
||||||
|
<SectionHeader title='Albums' />
|
||||||
|
<SectionHeader title='Artists' />
|
||||||
|
<SectionHeader title='Playlists' />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Library;
|
||||||
23
src/components/RootNavigator.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
import TabNavigator from './TabNavigator';
|
||||||
|
import NowPlayingLayout from './NowPlayingLayout';
|
||||||
|
|
||||||
|
const RootStack = createStackNavigator();
|
||||||
|
|
||||||
|
const RootNavigator = () => (
|
||||||
|
<RootStack.Navigator>
|
||||||
|
<RootStack.Screen
|
||||||
|
name='Main'
|
||||||
|
component={TabNavigator}
|
||||||
|
options={{ headerShown: false }}
|
||||||
|
/>
|
||||||
|
<RootStack.Screen
|
||||||
|
name='Now Playing'
|
||||||
|
component={NowPlayingLayout}
|
||||||
|
options={{ headerShown: false }}
|
||||||
|
/>
|
||||||
|
</RootStack.Navigator>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RootNavigator;
|
||||||
@ -7,6 +7,7 @@ import { musicDb, settingsDb } from '../clients';
|
|||||||
import { appSettingsState, serversState } from '../state/settings';
|
import { appSettingsState, serversState } from '../state/settings';
|
||||||
import { DbStorage } from '../storage/db';
|
import { DbStorage } from '../storage/db';
|
||||||
import { StackScreenProps } from '@react-navigation/stack';
|
import { StackScreenProps } from '@react-navigation/stack';
|
||||||
|
import { useNavigation } from '@react-navigation/core';
|
||||||
|
|
||||||
const RecreateDbButton: React.FC<{ db: DbStorage, title: string }> = ({ db, title }) => {
|
const RecreateDbButton: React.FC<{ db: DbStorage, title: string }> = ({ db, title }) => {
|
||||||
const [inProgress, setInProgress] = useState(false);
|
const [inProgress, setInProgress] = useState(false);
|
||||||
@ -31,10 +32,15 @@ const RecreateDbButton: React.FC<{ db: DbStorage, title: string }> = ({ db, titl
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DbControls = () => {
|
const DbControls = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<RecreateDbButton db={musicDb} title='Music' />
|
<RecreateDbButton db={musicDb} title='Music' />
|
||||||
<RecreateDbButton db={settingsDb} title='Settings' />
|
<RecreateDbButton db={settingsDb} title='Settings' />
|
||||||
|
<Button
|
||||||
|
title='Now Playing'
|
||||||
|
onPress={() => navigation.navigate('Now Playing')}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/components/TabNavigator.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
|
import SettingsView from './Settings';
|
||||||
|
import NowPlayingLayout from './NowPlayingLayout';
|
||||||
|
import ArtistsList from './ArtistsList';
|
||||||
|
import FocusableIcon from './FocusableIcon';
|
||||||
|
import Library from './Library';
|
||||||
|
|
||||||
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
|
const TabNavigator = () => {
|
||||||
|
return (
|
||||||
|
<Tab.Navigator>
|
||||||
|
<Tab.Screen
|
||||||
|
name='Home'
|
||||||
|
component={ArtistsList}
|
||||||
|
options={{
|
||||||
|
tabBarIcon: ({ focused }) => FocusableIcon({ focused,
|
||||||
|
source: require('../../res/home.png'),
|
||||||
|
focusedSource: require('../../res/home-fill.png'),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name='Library'
|
||||||
|
component={Library}
|
||||||
|
options={{
|
||||||
|
tabBarIcon: ({ focused }) => FocusableIcon({ focused,
|
||||||
|
source: require('../../res/library.png'),
|
||||||
|
focusedSource: require('../../res/library-fill.png'),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name='Search'
|
||||||
|
component={NowPlayingLayout}
|
||||||
|
options={{
|
||||||
|
tabBarIcon: ({ focused }) => FocusableIcon({ focused,
|
||||||
|
source: require('../../res/search.png'),
|
||||||
|
focusedSource: require('../../res/search-fill.png'),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name='Settings'
|
||||||
|
component={SettingsView}
|
||||||
|
options={{
|
||||||
|
tabBarIcon: ({ focused }) => FocusableIcon({ focused,
|
||||||
|
source: require('../../res/settings.png'),
|
||||||
|
focusedSource: require('../../res/settings-fill.png'),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tab.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabNavigator;
|
||||||