Fancy new buttons and more consistent colors
This commit is contained in:
15
.eslintrc.js
15
.eslintrc.js
@@ -8,7 +8,7 @@ module.exports = {
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
// "plugin:@typescript-eslint/recommended"
|
||||
// 'plugin:@typescript-eslint/recommended'
|
||||
],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
@@ -28,14 +28,15 @@ module.exports = {
|
||||
'react-hooks'
|
||||
],
|
||||
rules: {
|
||||
indent: [
|
||||
indent: 'off',
|
||||
'@typescript-eslint/indent': [
|
||||
'error',
|
||||
4,
|
||||
{
|
||||
SwitchCase: 1,
|
||||
}
|
||||
],
|
||||
"linebreak-style": [
|
||||
'linebreak-style': [
|
||||
'error',
|
||||
'unix'
|
||||
],
|
||||
@@ -47,10 +48,10 @@ module.exports = {
|
||||
'error',
|
||||
'always'
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"react/prop-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error"
|
||||
'no-unused-vars': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error'
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -245,15 +245,13 @@ PODS:
|
||||
- React-cxxreact (= 0.63.4)
|
||||
- React-jsi (= 0.63.4)
|
||||
- React-jsinspector (0.63.4)
|
||||
- react-native-appearance (0.3.4):
|
||||
- React
|
||||
- react-native-safe-area-context (3.1.9):
|
||||
- React-Core
|
||||
- react-native-slider (3.0.3):
|
||||
- React
|
||||
- react-native-track-player (1.2.3):
|
||||
- React
|
||||
- react-native-webview (11.2.1):
|
||||
- react-native-webview (11.2.3):
|
||||
- React-Core
|
||||
- React-RCTActionSheet (0.63.4):
|
||||
- React-Core/RCTActionSheetHeaders (= 0.63.4)
|
||||
@@ -325,7 +323,7 @@ PODS:
|
||||
- React-Core
|
||||
- SDWebImage (~> 5.8)
|
||||
- SDWebImageWebPCoder (~> 0.6.1)
|
||||
- RNGestureHandler (1.9.0):
|
||||
- RNGestureHandler (1.10.0):
|
||||
- React-Core
|
||||
- RNLocalize (2.0.1):
|
||||
- React-Core
|
||||
@@ -388,7 +386,6 @@ DEPENDENCIES:
|
||||
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
|
||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
||||
- react-native-appearance (from `../node_modules/react-native-appearance`)
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
|
||||
- react-native-track-player (from `../node_modules/react-native-track-player`)
|
||||
@@ -465,8 +462,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
|
||||
React-jsinspector:
|
||||
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
||||
react-native-appearance:
|
||||
:path: "../node_modules/react-native-appearance"
|
||||
react-native-safe-area-context:
|
||||
:path: "../node_modules/react-native-safe-area-context"
|
||||
react-native-slider:
|
||||
@@ -546,11 +541,10 @@ SPEC CHECKSUMS:
|
||||
React-jsi: a0418934cf48f25b485631deb27c64dc40fb4c31
|
||||
React-jsiexecutor: 93bd528844ad21dc07aab1c67cb10abae6df6949
|
||||
React-jsinspector: 58aef7155bc9a9683f5b60b35eccea8722a4f53a
|
||||
react-native-appearance: fc2014516054585d531e07aa0b40ab0de1d2be85
|
||||
react-native-safe-area-context: 86612d2c9a9e94e288319262d10b5f93f0b395f5
|
||||
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
|
||||
react-native-track-player: ba2416753b58f3cdf9db5a07daa65876d659f925
|
||||
react-native-webview: dbe6c1ad149740f0e2d84a963f1d3c3e77f2d99c
|
||||
react-native-safe-area-context: b6e0e284002381d2ff29fa4fff42b4d8282e3c94
|
||||
react-native-slider: e99fc201cefe81270fc9d81714a7a0f5e566b168
|
||||
react-native-track-player: 6ff21d21eb70ecbd2a6bad29822ecf6c8609a3aa
|
||||
react-native-webview: 6520e3e7b4933de76b95ef542c8d7115cf45b68e
|
||||
React-RCTActionSheet: 89a0ca9f4a06c1f93c26067af074ccdce0f40336
|
||||
React-RCTAnimation: 1bde3ecc0c104c55df246eda516e0deb03c4e49b
|
||||
React-RCTBlob: a97d378b527740cc667e03ebfa183a75231ab0f0
|
||||
@@ -561,12 +555,12 @@ SPEC CHECKSUMS:
|
||||
React-RCTText: 5c51df3f08cb9dedc6e790161195d12bac06101c
|
||||
React-RCTVibration: ae4f914cfe8de7d4de95ae1ea6cc8f6315d73d9d
|
||||
ReactCommon: 73d79c7039f473b76db6ff7c6b159c478acbbb3b
|
||||
RNCAsyncStorage: cb9a623793918c6699586281f0b51cbc38f046f9
|
||||
RNCMaskedView: f5c7d14d6847b7b44853f7acb6284c1da30a3459
|
||||
RNCAsyncStorage: b03032fdbdb725bea0bd9e5ec5a7272865ae7398
|
||||
RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f
|
||||
RNCPicker: 914b557e20b3b8317b084aca9ff4b4edb95f61e4
|
||||
RNFastImage: d4870d58f5936111c56218dbd7fcfc18e65b58ff
|
||||
RNGestureHandler: 9b7e605a741412e20e13c512738a31bd1611759b
|
||||
RNLocalize: dcf0fdb332b37b0d24178e876a7ce4dbbc9c838d
|
||||
RNGestureHandler: 03f587ba19a7e2d9fe48fb9aa6e33ace5fd07dd5
|
||||
RNLocalize: 41026b7c14878f1a1b381bc79f668f1fbf841adb
|
||||
RNReanimated: e03f7425cb7a38dcf1b644d680d1bfc91c3337ad
|
||||
RNScreens: b6c9607e6fe47c1b6e2f1910d2acd46dd7ecea3a
|
||||
RNSentry: 6aeba1adc242fd22a6826acae92f430697b47a9c
|
||||
|
||||
15216
package-lock.json
generated
15216
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,9 +28,8 @@
|
||||
"fuse.js": "^6.4.6",
|
||||
"i18n-js": "^3.8.0",
|
||||
"lodash": "^4.17.20",
|
||||
"react": "^17.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-native": "^0.63.4",
|
||||
"react-native-appearance": "^0.3.4",
|
||||
"react-native-dotenv": "^2.5.1",
|
||||
"react-native-fast-image": "^8.3.4",
|
||||
"react-native-gesture-handler": "^1.9.0",
|
||||
@@ -57,6 +56,7 @@
|
||||
"@types/react-native": "^0.63.48",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-test-renderer": "^17.0.0",
|
||||
"@types/styled-components-react-native": "^5.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.2",
|
||||
"@typescript-eslint/parser": "^4.14.2",
|
||||
"babel-jest": "^26.6.3",
|
||||
|
||||
1
src/assets/queue-append.svg
Normal file
1
src/assets/queue-append.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.39 84.08"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="Regular-M"><path d="M3.66,0A3.62,3.62,0,0,0,0,3.66,3.67,3.67,0,0,0,3.66,7.37h91a3.75,3.75,0,0,0,3.76-3.71A3.71,3.71,0,0,0,94.63,0Zm0,25.54A3.67,3.67,0,0,0,0,29.25a3.62,3.62,0,0,0,3.66,3.66h91a3.71,3.71,0,0,0,3.76-3.66,3.75,3.75,0,0,0-3.76-3.71ZM.05,50.68v11c0,6.93,4.88,11.18,11.77,11.18h14.4V78c0,4,3.76,5.47,6.84,3l13.23-10.6a3.71,3.71,0,0,0,0-6L33.06,53.86c-3-2.35-6.84-.88-6.84,3v4.88H12.89A1.51,1.51,0,0,1,11.23,60V50.68c0-4-2.05-6.44-5.61-6.44S.05,46.73.05,50.68Zm61.18.44a3.69,3.69,0,0,0,0,7.38h33.4a3.69,3.69,0,1,0,0-7.38Zm0,25.59a3.69,3.69,0,0,0,0,7.37h33.4a3.69,3.69,0,1,0,0-7.37Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 758 B |
1
src/assets/queue-prepend.svg
Normal file
1
src/assets/queue-prepend.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.39 84.08"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="Regular-M"><path d="M61.23,7.37h33.4a3.72,3.72,0,0,0,3.76-3.71A3.71,3.71,0,0,0,94.63,0H61.23a3.62,3.62,0,0,0-3.66,3.66A3.64,3.64,0,0,0,61.23,7.37ZM.05,33.4c0,4,2,6.44,5.57,6.44s5.61-2.49,5.61-6.44V24.07a1.51,1.51,0,0,1,1.66-1.71H26.22v4.89c0,3.85,3.86,5.32,6.84,3L46.29,19.68a3.71,3.71,0,0,0,0-6L33.06,3.12c-3.08-2.49-6.84-1-6.84,3v5.08H11.82C4.93,11.18.05,15.43.05,22.36ZM61.23,33h33.4a3.69,3.69,0,1,0,0-7.37H61.23a3.69,3.69,0,0,0,0,7.37ZM3.66,58.54h91a3.75,3.75,0,0,0,3.76-3.71,3.71,3.71,0,0,0-3.76-3.66h-91A3.62,3.62,0,0,0,0,54.83,3.67,3.67,0,0,0,3.66,58.54Zm0,25.54h91a3.71,3.71,0,0,0,3.76-3.66,3.75,3.75,0,0,0-3.76-3.71h-91A3.67,3.67,0,0,0,0,80.42,3.62,3.62,0,0,0,3.66,84.08Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 848 B |
1
src/assets/queue.svg
Normal file
1
src/assets/queue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.39 74.76"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="Regular-M"><path d="M4,15.17,12.65,10a2.45,2.45,0,0,0,0-4.11L4,.62C2-.6,0,0,0,2.33V13.46C0,15.71,2.05,16.29,4,15.17ZM31.3,11.75H94.43a3.93,3.93,0,1,0,0-7.86H31.3a3.88,3.88,0,0,0-4,4A3.87,3.87,0,0,0,31.3,11.75ZM4,44.61l8.69-5.17a2.44,2.44,0,0,0,0-4.1L4,30.06c-2-1.17-4-.58-4,1.71V42.91C0,45.15,2.05,45.74,4,44.61ZM31.3,41.29H94.43a3.93,3.93,0,1,0,0-7.86H31.3a3.93,3.93,0,1,0,0,7.86ZM4,74.2,12.65,69a2.44,2.44,0,0,0,0-4.1L4,59.65C2,58.48,0,59,0,61.36V72.5C0,74.74,2.05,75.33,4,74.2ZM31.3,70.79H94.43a3.93,3.93,0,1,0,0-7.86H31.3a3.93,3.93,0,1,0,0,7.86Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 717 B |
@@ -9,10 +9,10 @@ import {
|
||||
TapGestureHandlerGestureEvent
|
||||
} from 'react-native-gesture-handler';
|
||||
|
||||
interface LetterContainerProps {
|
||||
onPress: (letter: string) => void;
|
||||
letter: string;
|
||||
}
|
||||
// interface LetterContainerProps {
|
||||
// onPress: (letter: string) => void;
|
||||
// letter: string;
|
||||
// }
|
||||
|
||||
const Container = styled.View`
|
||||
position: absolute;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import { AppearanceProvider, Appearance, AppearanceListener } from 'react-native-appearance';
|
||||
import Routes from '../screens';
|
||||
import store, { persistedStore } from 'store';
|
||||
import {
|
||||
@@ -10,60 +9,36 @@ import {
|
||||
DefaultTheme,
|
||||
DarkTheme,
|
||||
} from '@react-navigation/native';
|
||||
import { useColorScheme } from 'react-native';
|
||||
|
||||
interface State {
|
||||
isReady: boolean;
|
||||
colorScheme?: string;
|
||||
}
|
||||
export default function App(): JSX.Element {
|
||||
const colorScheme = useColorScheme();
|
||||
// const colorScheme = 'dark';
|
||||
|
||||
export default class App extends Component<{}, State> {
|
||||
state: State = {
|
||||
isReady: false,
|
||||
};
|
||||
|
||||
subscription = null;
|
||||
|
||||
async componentDidMount() {
|
||||
await TrackPlayer.setupPlayer();
|
||||
await TrackPlayer.updateOptions({
|
||||
capabilities: [
|
||||
TrackPlayer.CAPABILITY_PLAY,
|
||||
TrackPlayer.CAPABILITY_PAUSE,
|
||||
TrackPlayer.CAPABILITY_SKIP_TO_NEXT,
|
||||
TrackPlayer.CAPABILITY_SKIP_TO_PREVIOUS,
|
||||
TrackPlayer.CAPABILITY_STOP,
|
||||
TrackPlayer.CAPABILITY_SEEK_TO,
|
||||
]
|
||||
});
|
||||
this.subscription = Appearance.addChangeListener(this.setScheme);
|
||||
this.setState({ isReady: true, colorScheme: Appearance.getColorScheme() });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription?.remove();
|
||||
}
|
||||
|
||||
setScheme: AppearanceListener = ({ colorScheme }) => {
|
||||
this.setState({ colorScheme });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isReady, colorScheme } = this.state;
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
useEffect(() => {
|
||||
async function setupTrackPlayer() {
|
||||
await TrackPlayer.setupPlayer();
|
||||
await TrackPlayer.updateOptions({
|
||||
capabilities: [
|
||||
TrackPlayer.CAPABILITY_PLAY,
|
||||
TrackPlayer.CAPABILITY_PAUSE,
|
||||
TrackPlayer.CAPABILITY_SKIP_TO_NEXT,
|
||||
TrackPlayer.CAPABILITY_SKIP_TO_PREVIOUS,
|
||||
TrackPlayer.CAPABILITY_STOP,
|
||||
TrackPlayer.CAPABILITY_SEEK_TO,
|
||||
]
|
||||
});
|
||||
}
|
||||
setupTrackPlayer();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistedStore}>
|
||||
<AppearanceProvider>
|
||||
<NavigationContainer theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Routes />
|
||||
</NavigationContainer>
|
||||
</AppearanceProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistedStore}>
|
||||
<NavigationContainer theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
||||
<Routes />
|
||||
</NavigationContainer>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
69
src/components/Button.tsx
Normal file
69
src/components/Button.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { SvgProps } from 'react-native-svg';
|
||||
import {
|
||||
PressableProps,
|
||||
} from 'react-native';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import useDefaultStyles from './Colors';
|
||||
|
||||
interface ButtonProps extends PressableProps {
|
||||
icon?: React.FC<SvgProps>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface PressableStyleProps {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const BaseButton = styled.Pressable<PressableStyleProps>`
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
|
||||
${props => props.active && css`
|
||||
background-color: ${THEME_COLOR};
|
||||
`}
|
||||
`;
|
||||
|
||||
const ButtonText = styled.Text<{ active?: boolean }>`
|
||||
color: ${THEME_COLOR};
|
||||
font-weight: 600;
|
||||
|
||||
${props => props.active && css`
|
||||
color: white;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default function Button(props: ButtonProps) {
|
||||
const { icon: Icon, title, ...rest } = props;
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const [isPressed, setPressed] = useState(false);
|
||||
const handlePressIn = useCallback(() => setPressed(true), []);
|
||||
const handlePressOut = useCallback(() => setPressed(false), []);
|
||||
|
||||
return (
|
||||
<BaseButton
|
||||
{...rest}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
active={isPressed}
|
||||
style={[ defaultStyles.button, props.style ]}
|
||||
>
|
||||
{Icon &&
|
||||
<Icon
|
||||
width={12}
|
||||
height={12}
|
||||
fill={isPressed ? '#fff' : THEME_COLOR}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<ButtonText active={isPressed}>{title}</ButtonText>
|
||||
</BaseButton>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { StyleSheet, PlatformColor } from 'react-native';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
|
||||
export const colors = StyleSheet.create({
|
||||
text: {
|
||||
color: PlatformColor('?attr/colorOnBackground'),
|
||||
},
|
||||
view: {
|
||||
backgroundColor: PlatformColor('?android:colorBackground'),
|
||||
},
|
||||
border: {
|
||||
borderColor: '#88888844'
|
||||
},
|
||||
activeBackground: {
|
||||
backgroundColor: `${THEME_COLOR}44`,
|
||||
},
|
||||
imageBackground: {
|
||||
backgroundColor: PlatformColor('?attr/colorBackgroundFloating'),
|
||||
},
|
||||
modal: {
|
||||
backgroundColor: PlatformColor('?attr/colorBackgroundFloating'),
|
||||
},
|
||||
input: {
|
||||
backgroundColor: PlatformColor('?attr/colorBackgroundFloating'),
|
||||
}
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { StyleSheet, PlatformColor, DynamicColorIOS } from 'react-native';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
|
||||
export const colors = StyleSheet.create({
|
||||
text: {
|
||||
color: PlatformColor('label'),
|
||||
},
|
||||
view: {
|
||||
backgroundColor: PlatformColor('systemBackground'),
|
||||
},
|
||||
border: {
|
||||
borderColor: PlatformColor('systemGray5Color'),
|
||||
},
|
||||
activeBackground: {
|
||||
backgroundColor: DynamicColorIOS({ light: `${THEME_COLOR}16`, dark: `${THEME_COLOR}66` })
|
||||
},
|
||||
imageBackground: {
|
||||
backgroundColor: PlatformColor('systemGray5Color')
|
||||
},
|
||||
modal: {
|
||||
backgroundColor: DynamicColorIOS({ light: '#eeeeeeee', dark: '#222222ee' })
|
||||
},
|
||||
input: {
|
||||
backgroundColor: PlatformColor('systemGray5Color'),
|
||||
color: PlatformColor('label'),
|
||||
}
|
||||
});
|
||||
55
src/components/Colors.ts
Normal file
55
src/components/Colors.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { StyleSheet, useColorScheme } from 'react-native';
|
||||
|
||||
export default function useDefaultStyles() {
|
||||
const scheme = useColorScheme();
|
||||
// const scheme = 'dark';
|
||||
|
||||
return StyleSheet.create({
|
||||
text: {
|
||||
color: scheme === 'dark' ? '#fff' : '#000',
|
||||
},
|
||||
textHalfOpacity: {
|
||||
color: scheme === 'dark' ? '#ffffff88' : '#00000088',
|
||||
},
|
||||
view: {
|
||||
backgroundColor: scheme === 'dark' ? '#111' : '#eee',
|
||||
},
|
||||
border: {
|
||||
borderColor: scheme === 'dark' ? '#262626' : '#ddd',
|
||||
},
|
||||
activeBackground: {
|
||||
backgroundColor: `${THEME_COLOR}${scheme === 'dark' ? '66' : '16'}`,
|
||||
},
|
||||
imageBackground: {
|
||||
backgroundColor: scheme === 'dark' ? '#333' : '#ddd',
|
||||
},
|
||||
modal: {
|
||||
backgroundColor: scheme === 'dark' ? '#222222ee' : '#eeeeeeee',
|
||||
},
|
||||
modalInner: {
|
||||
backgroundColor: scheme === 'dark' ? '#000' : '#fff',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: scheme === 'dark' ? '#161616' : '#e6e6e6',
|
||||
},
|
||||
input: {
|
||||
backgroundColor: scheme === 'dark' ? '#161616' : '#e6e6e6',
|
||||
color: scheme === 'dark' ? '#fff' : '#000',
|
||||
},
|
||||
sectionHeading: {
|
||||
backgroundColor: scheme === 'dark' ? '#111' : '#eee',
|
||||
borderColor: scheme === 'dark' ? '#333' : '#ddd',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface DefaultStylesProviderProps {
|
||||
children: (defaultStyles: ReturnType<typeof useDefaultStyles>) => JSX.Element;
|
||||
}
|
||||
|
||||
export function DefaultStylesProvider(props: DefaultStylesProviderProps) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
return props.children(defaultStyles);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components/native';
|
||||
|
||||
const Input = styled.TextInput`
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacityProps, Text } from 'react-native';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { TouchableOpacityProps } from 'react-native';
|
||||
import ChevronRight from 'assets/chevron-right.svg';
|
||||
import styled from 'styled-components/native';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { colors } from './Colors';
|
||||
import useDefaultStyles from './Colors';
|
||||
|
||||
const BUTTON_SIZE = 14;
|
||||
|
||||
const Container = styled.TouchableOpacity`
|
||||
padding: 18px 0;
|
||||
const Container = styled.Pressable<{ active?: boolean }>`
|
||||
padding: 18px 20px;
|
||||
border-bottom-width: 1px;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
${props => props.active && css`
|
||||
background-color: ${THEME_COLOR};
|
||||
`}
|
||||
`;
|
||||
|
||||
const Label = styled.Text<{ active?: boolean }>`
|
||||
color: ${THEME_COLOR};
|
||||
font-size: 16px;
|
||||
|
||||
${props => props.active && css`
|
||||
color: white;
|
||||
`}
|
||||
`;
|
||||
|
||||
const ListButton: React.FC<TouchableOpacityProps> = ({ children, ...props }) => {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const [isPressed, setPressed] = useState(false);
|
||||
const handlePressIn = useCallback(() => setPressed(true), []);
|
||||
const handlePressOut = useCallback(() => setPressed(false), []);
|
||||
|
||||
return (
|
||||
<Container {...props} style={colors.border}>
|
||||
<Text style={{ color: THEME_COLOR, fontSize: 16 }}>{children}</Text>
|
||||
<ChevronRight width={BUTTON_SIZE} height={BUTTON_SIZE} fill={THEME_COLOR} />
|
||||
<Container
|
||||
{...props}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
style={defaultStyles.border}
|
||||
active={isPressed}
|
||||
>
|
||||
<Label active={isPressed}>{children}</Label>
|
||||
<ChevronRight width={BUTTON_SIZE} height={BUTTON_SIZE} fill={isPressed ? '#fff' : THEME_COLOR} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import { SafeAreaView, Pressable } from 'react-native';
|
||||
import { colors } from './Colors';
|
||||
import { useNavigation, StackActions } from '@react-navigation/native';
|
||||
import useDefaultStyles from './Colors';
|
||||
|
||||
interface Props {
|
||||
fullSize?: boolean;
|
||||
@@ -24,15 +24,16 @@ const Container = styled(Pressable)<Pick<Props, 'fullSize'>>`
|
||||
`;
|
||||
|
||||
const Modal: React.FC<Props> = ({ children, fullSize = true }) => {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const navigation = useNavigation();
|
||||
const closeModal = useCallback(() => {
|
||||
navigation.dispatch(StackActions.popToTop());
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<Background style={colors.modal} onPress={closeModal}>
|
||||
<Background style={defaultStyles.modal} onPress={closeModal}>
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<Container style={colors.view} fullSize={fullSize}>
|
||||
<Container style={defaultStyles.modalInner} fullSize={fullSize}>
|
||||
{children}
|
||||
</Container>
|
||||
</SafeAreaView>
|
||||
|
||||
11
src/components/Text.tsx
Normal file
11
src/components/Text.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { Text as BaseText, TextProps } from 'react-native';
|
||||
import useDefaultStyles from './Colors';
|
||||
|
||||
export default function Text(props: PropsWithChildren<TextProps>) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
return (
|
||||
<BaseText {...props} style={[defaultStyles.text, props.style]} />
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import styled from 'styled-components/native';
|
||||
import Text from './Text';
|
||||
|
||||
export const Header = styled.Text`
|
||||
export const Header = styled(Text)`
|
||||
margin: 24px 0 12px 0;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const SubHeader = styled.Text`
|
||||
export const SubHeader = styled(Text)`
|
||||
font-size: 24px;
|
||||
margin: 12px 0;
|
||||
`;
|
||||
@@ -1,26 +1,26 @@
|
||||
export type LocaleKeys = 'play-next'
|
||||
| 'play-album'
|
||||
| 'queue'
|
||||
| 'add-to-queue'
|
||||
| 'clear-queue'
|
||||
| 'no-results'
|
||||
| 'album'
|
||||
| 'albums'
|
||||
| 'all-albums'
|
||||
| 'search'
|
||||
| 'music'
|
||||
| 'now-playing'
|
||||
| 'onboarding-welcome'
|
||||
| 'onboarding-intro'
|
||||
| 'onboarding-cta'
|
||||
| 'set-jellyfin-server'
|
||||
| 'set-jellyfin-server-instruction'
|
||||
| 'settings'
|
||||
| 'jellyfin-library'
|
||||
| 'jellyfin-server-url'
|
||||
| 'jellyfin-access-token'
|
||||
| 'jellyfin-user-id'
|
||||
| 'setting-cache'
|
||||
| 'setting-cache-description'
|
||||
| 'reset-cache'
|
||||
| 'recent-albums'
|
||||
| 'play-album'
|
||||
| 'queue'
|
||||
| 'add-to-queue'
|
||||
| 'clear-queue'
|
||||
| 'no-results'
|
||||
| 'album'
|
||||
| 'albums'
|
||||
| 'all-albums'
|
||||
| 'search'
|
||||
| 'music'
|
||||
| 'now-playing'
|
||||
| 'onboarding-welcome'
|
||||
| 'onboarding-intro'
|
||||
| 'onboarding-cta'
|
||||
| 'set-jellyfin-server'
|
||||
| 'set-jellyfin-server-instruction'
|
||||
| 'settings'
|
||||
| 'jellyfin-library'
|
||||
| 'jellyfin-server-url'
|
||||
| 'jellyfin-access-token'
|
||||
| 'jellyfin-user-id'
|
||||
| 'setting-cache'
|
||||
| 'setting-cache-description'
|
||||
| 'reset-cache'
|
||||
| 'recent-albums'
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { StackParams } from '../types';
|
||||
import { Text, ScrollView, Dimensions, Button, RefreshControl, StyleSheet } from 'react-native';
|
||||
import { Text, ScrollView, Dimensions, RefreshControl, StyleSheet, View } from 'react-native';
|
||||
import { useGetImage } from 'utility/JellyfinApi';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
|
||||
@@ -13,9 +13,11 @@ import { ALBUM_CACHE_AMOUNT_OF_DAYS, THEME_COLOR } from 'CONSTANTS';
|
||||
import usePlayAlbum from 'utility/usePlayAlbum';
|
||||
import TouchableHandler from 'components/TouchableHandler';
|
||||
import useCurrentTrack from 'utility/useCurrentTrack';
|
||||
import { colors } from 'components/Colors';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import { t } from '@localisation';
|
||||
import Button from 'components/Button';
|
||||
import Play from 'assets/play.svg';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
|
||||
type Route = RouteProp<StackParams, 'Album'>;
|
||||
|
||||
@@ -23,18 +25,15 @@ const Screen = Dimensions.get('screen');
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
name: {
|
||||
...colors.text,
|
||||
fontSize: 36,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
artist: {
|
||||
...colors.text,
|
||||
fontSize: 24,
|
||||
opacity: 0.5,
|
||||
marginBottom: 24
|
||||
},
|
||||
index: {
|
||||
...colors.text,
|
||||
width: 20,
|
||||
opacity: 0.5,
|
||||
marginRight: 5
|
||||
@@ -61,6 +60,8 @@ const TrackContainer = styled.View<{isPlaying: boolean}>`
|
||||
`;
|
||||
|
||||
const Album: React.FC = () => {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
// Retrieve state
|
||||
const { params: { id } } = useRoute<Route>();
|
||||
const tracks = useTypedSelector((state) => state.music.tracks.entities);
|
||||
@@ -112,25 +113,27 @@ const Album: React.FC = () => {
|
||||
<RefreshControl refreshing={isLoading} onRefresh={refresh} />
|
||||
}
|
||||
>
|
||||
<AlbumImage source={{ uri: getImage(album?.Id) }} style={colors.imageBackground} />
|
||||
<Text style={styles.name} >{album?.Name}</Text>
|
||||
<Text style={styles.artist}>{album?.AlbumArtist}</Text>
|
||||
<Button title={t('play-album')} onPress={selectAlbum} color={THEME_COLOR} />
|
||||
{album?.Tracks?.length ? album.Tracks.map((trackId) =>
|
||||
<TouchableHandler
|
||||
key={trackId}
|
||||
id={trackId}
|
||||
onPress={selectTrack}
|
||||
onLongPress={longPressTrack}
|
||||
>
|
||||
<TrackContainer isPlaying={currentTrack?.id.startsWith(trackId) || false} style={colors.border}>
|
||||
<Text style={styles.index}>
|
||||
{tracks[trackId]?.IndexNumber}
|
||||
</Text>
|
||||
<Text style={colors.text}>{tracks[trackId]?.Name}</Text>
|
||||
</TrackContainer>
|
||||
</TouchableHandler>
|
||||
) : undefined}
|
||||
<AlbumImage source={{ uri: getImage(album?.Id) }} style={defaultStyles.imageBackground} />
|
||||
<Text style={[ defaultStyles.text, styles.name ]} >{album?.Name}</Text>
|
||||
<Text style={[ defaultStyles.text, styles.artist ]}>{album?.AlbumArtist}</Text>
|
||||
<Button title={t('play-album')} icon={Play} onPress={selectAlbum} />
|
||||
<View style={{ marginTop: 15 }}>
|
||||
{album?.Tracks?.length ? album.Tracks.map((trackId) =>
|
||||
<TouchableHandler
|
||||
key={trackId}
|
||||
id={trackId}
|
||||
onPress={selectTrack}
|
||||
onLongPress={longPressTrack}
|
||||
>
|
||||
<TrackContainer isPlaying={currentTrack?.id.startsWith(trackId) || false} style={defaultStyles.border}>
|
||||
<Text style={[ defaultStyles.text, styles.index ]}>
|
||||
{tracks[trackId]?.IndexNumber}
|
||||
</Text>
|
||||
<Text style={defaultStyles.text}>{tracks[trackId]?.Name}</Text>
|
||||
</TrackContainer>
|
||||
</TouchableHandler>
|
||||
) : undefined}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useRef, PureComponent, ReactText } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, ReactText } from 'react';
|
||||
import { useGetImage } from 'utility/JellyfinApi';
|
||||
import { Album, NavigationProp } from '../types';
|
||||
import { Text, SafeAreaView, SectionList, View } from 'react-native';
|
||||
@@ -9,13 +9,12 @@ import { useTypedSelector } from 'store';
|
||||
import { fetchAllAlbums } from 'store/music/actions';
|
||||
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS';
|
||||
import TouchableHandler from 'components/TouchableHandler';
|
||||
import ListContainer from './components/ListContainer';
|
||||
import AlbumImage, { AlbumItem } from './components/AlbumImage';
|
||||
import { selectAlbumsByAlphabet, SectionedId } from 'store/music/selectors';
|
||||
import AlphabetScroller from 'components/AlphabetScroller';
|
||||
import { EntityId } from '@reduxjs/toolkit';
|
||||
import styled from 'styled-components/native';
|
||||
import { colors } from 'components/Colors';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
|
||||
interface VirtualizedItemInfo {
|
||||
section: SectionedId,
|
||||
@@ -51,19 +50,16 @@ const SectionText = styled.Text`
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const sectionStyles = { ...colors.view, ...colors.border };
|
||||
const SectionHeading = React.memo(function SectionHeading(props: { label: string }) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const { label } = props;
|
||||
|
||||
class SectionHeading extends PureComponent<{ label: string }> {
|
||||
render() {
|
||||
const { label } = this.props;
|
||||
|
||||
return (
|
||||
<SectionContainer style={sectionStyles}>
|
||||
<SectionText style={colors.text}>{label}</SectionText>
|
||||
</SectionContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<SectionContainer style={defaultStyles.sectionHeading}>
|
||||
<SectionText style={defaultStyles.text}>{label}</SectionText>
|
||||
</SectionContainer>
|
||||
);
|
||||
});
|
||||
|
||||
interface GeneratedAlbumItemProps {
|
||||
id: ReactText;
|
||||
@@ -77,21 +73,20 @@ const HalfOpacity = styled.Text`
|
||||
opacity: 0.5;
|
||||
`;
|
||||
|
||||
class GeneratedAlbumItem extends PureComponent<GeneratedAlbumItemProps> {
|
||||
render() {
|
||||
const { id, imageUrl, name, artist, onPress } = this.props;
|
||||
const GeneratedAlbumItem = React.memo(function GeneratedAlbumItem(props: GeneratedAlbumItemProps) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const { id, imageUrl, name, artist, onPress } = props;
|
||||
|
||||
return (
|
||||
<TouchableHandler id={id as string} onPress={onPress}>
|
||||
<AlbumItem>
|
||||
<AlbumImage source={{ uri: imageUrl }} style={colors.imageBackground} />
|
||||
<Text numberOfLines={1} style={colors.text}>{name}</Text>
|
||||
<HalfOpacity style={colors.text} numberOfLines={1}>{artist}</HalfOpacity>
|
||||
</AlbumItem>
|
||||
</TouchableHandler>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<TouchableHandler id={id as string} onPress={onPress}>
|
||||
<AlbumItem>
|
||||
<AlbumImage source={{ uri: imageUrl }} style={defaultStyles.imageBackground} />
|
||||
<Text numberOfLines={1} style={defaultStyles.text}>{name}</Text>
|
||||
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>{artist}</HalfOpacity>
|
||||
</AlbumItem>
|
||||
</TouchableHandler>
|
||||
);
|
||||
});
|
||||
|
||||
const Albums: React.FC = () => {
|
||||
// Retrieve data from store
|
||||
|
||||
@@ -12,31 +12,36 @@ import AlbumImage, { AlbumItem } from './components/AlbumImage';
|
||||
import { useRecentAlbums } from 'store/music/selectors';
|
||||
import { Header } from 'components/Typography';
|
||||
import ListButton from 'components/ListButton';
|
||||
import { colors } from 'components/Colors';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
artist: {
|
||||
...colors.text,
|
||||
opacity: 0.5,
|
||||
columnWrapper: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10
|
||||
}
|
||||
});
|
||||
|
||||
const NavigationHeader: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]);
|
||||
const handleSearchClick = useCallback(() => { navigation.navigate('Search'); }, [navigation]);
|
||||
|
||||
return (
|
||||
<ListContainer>
|
||||
<>
|
||||
<ListButton onPress={handleAllAlbumsClick}>{t('all-albums')}</ListButton>
|
||||
<ListButton onPress={handleSearchClick}>{t('search')}</ListButton>
|
||||
<Header style={colors.text}>{t('recent-albums')}</Header>
|
||||
</ListContainer>
|
||||
<ListContainer>
|
||||
<Header style={defaultStyles.text}>{t('recent-albums')}</Header>
|
||||
</ListContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const RecentAlbums: React.FC = () => {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
// Retrieve data from store
|
||||
const { entities: albums } = useTypedSelector((state) => state.music.albums);
|
||||
const recentAlbums = useRecentAlbums(24);
|
||||
@@ -56,25 +61,24 @@ const RecentAlbums: React.FC = () => {
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<ListContainer>
|
||||
<FlatList
|
||||
data={recentAlbums as string[]}
|
||||
refreshing={isLoading}
|
||||
onRefresh={retrieveData}
|
||||
numColumns={2}
|
||||
keyExtractor={d => d}
|
||||
ListHeaderComponent={NavigationHeader}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableHandler id={item} onPress={selectAlbum}>
|
||||
<AlbumItem>
|
||||
<AlbumImage source={{ uri: getImage(item) }} style={colors.imageBackground} />
|
||||
<Text style={colors.text} numberOfLines={1}>{albums[item]?.Name}</Text>
|
||||
<Text style={styles.artist} numberOfLines={1}>{albums[item]?.AlbumArtist}</Text>
|
||||
</AlbumItem>
|
||||
</TouchableHandler>
|
||||
)}
|
||||
/>
|
||||
</ListContainer>
|
||||
<FlatList
|
||||
data={recentAlbums as string[]}
|
||||
refreshing={isLoading}
|
||||
onRefresh={retrieveData}
|
||||
numColumns={2}
|
||||
keyExtractor={d => d}
|
||||
columnWrapperStyle={styles.columnWrapper}
|
||||
ListHeaderComponent={NavigationHeader}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableHandler id={item} onPress={selectAlbum}>
|
||||
<AlbumItem>
|
||||
<AlbumImage source={{ uri: getImage(item) }} style={defaultStyles.imageBackground} />
|
||||
<Text style={defaultStyles.text} numberOfLines={1}>{albums[item]?.Name}</Text>
|
||||
<Text style={defaultStyles.textHalfOpacity} numberOfLines={1}>{albums[item]?.AlbumArtist}</Text>
|
||||
</AlbumItem>
|
||||
</TouchableHandler>
|
||||
)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useNavigation } from '@react-navigation/native';
|
||||
import { useGetImage } from 'utility/JellyfinApi';
|
||||
import { NavigationProp } from '../types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { colors } from 'components/Colors';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
|
||||
const Container = styled.View`
|
||||
padding: 0 20px;
|
||||
@@ -47,6 +47,8 @@ const fuseOptions = {
|
||||
};
|
||||
|
||||
export default function Search() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
// Prepare state
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const albums = useTypedSelector(state => state.music.albums.entities);
|
||||
@@ -89,10 +91,10 @@ export default function Search() {
|
||||
|
||||
const HeaderComponent = React.useMemo(() => (
|
||||
<Container>
|
||||
<Input value={searchTerm} onChangeText={setSearchTerm} style={colors.input} placeholder={t('search') + '...'} />
|
||||
{(searchTerm.length && !results.length) ? <Text>{t('no-results')}</Text> : null}
|
||||
<Input value={searchTerm} onChangeText={setSearchTerm} style={defaultStyles.input} placeholder={t('search') + '...'} />
|
||||
{(searchTerm.length && !results.length) ? <Text style={{ textAlign: 'center' }}>{t('no-results')}</Text> : null}
|
||||
</Container>
|
||||
), [searchTerm, results, setSearchTerm]);
|
||||
), [searchTerm, results, setSearchTerm, defaultStyles]);
|
||||
|
||||
// GUARD: We cannot search for stuff unless Fuse is loaded with results.
|
||||
// Therefore we delay rendering to when we are certain it's there.
|
||||
@@ -105,13 +107,13 @@ export default function Search() {
|
||||
data={results}
|
||||
renderItem={({ item: { item: album } }) =>(
|
||||
<TouchableHandler id={album.Id} onPress={selectAlbum}>
|
||||
<SearchResult style={colors.border}>
|
||||
<SearchResult style={defaultStyles.border}>
|
||||
<AlbumImage source={{ uri: getImage(album.Id) }} />
|
||||
<View>
|
||||
<Text numberOfLines={1} ellipsizeMode="tail" style={colors.text}>
|
||||
<Text numberOfLines={1} ellipsizeMode="tail" style={defaultStyles.text}>
|
||||
{album.Name} - {album.AlbumArtist}
|
||||
</Text>
|
||||
<HalfOpacity style={colors.text}>{t('album')}</HalfOpacity>
|
||||
<HalfOpacity style={defaultStyles.text}>{t('album')}</HalfOpacity>
|
||||
</View>
|
||||
</SearchResult>
|
||||
</TouchableHandler>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import TrackPlayer, { usePlaybackState, STATE_PLAYING, STATE_PAUSED } from 'react-native-track-player';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import { TouchableOpacity, useColorScheme } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import { useHasQueue } from 'utility/useQueue';
|
||||
import ForwardIcon from 'assets/forwards.svg';
|
||||
@@ -9,7 +9,6 @@ import PlayIcon from 'assets/play.svg';
|
||||
import PauseIcon from 'assets/pause.svg';
|
||||
import RepeatIcon from 'assets/repeat.svg';
|
||||
// import ShuffleIcon from 'assets/shuffle.svg';
|
||||
import { useColorScheme } from 'react-native-appearance';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
|
||||
const BUTTON_SIZE = 40;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Text, Dimensions, View, StyleSheet } from 'react-native';
|
||||
import { Dimensions, View, StyleSheet } from 'react-native';
|
||||
import useCurrentTrack from 'utility/useCurrentTrack';
|
||||
import styled from 'styled-components/native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { colors } from 'components/Colors';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import Text from 'components/Text';
|
||||
|
||||
const Screen = Dimensions.get('screen');
|
||||
|
||||
@@ -16,13 +17,11 @@ const Artwork = styled(FastImage)`
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
artist: {
|
||||
...colors.text,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 24,
|
||||
marginBottom: 12,
|
||||
},
|
||||
title: {
|
||||
...colors.text,
|
||||
fontSize: 18,
|
||||
marginBottom: 12,
|
||||
textAlign: 'center',
|
||||
@@ -34,17 +33,18 @@ const styles = StyleSheet.create({
|
||||
|
||||
export default function NowPlaying() {
|
||||
const track = useCurrentTrack();
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
return (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Artwork
|
||||
style={colors.imageBackground}
|
||||
style={defaultStyles.imageBackground}
|
||||
source={{
|
||||
uri: track?.artwork,
|
||||
priority: FastImage.priority.high,
|
||||
}}
|
||||
/>
|
||||
<Text style={styles.artist} >{track?.artist}</Text>
|
||||
<Text style={styles.artist}>{track?.artist}</Text>
|
||||
<Text style={styles.title}>{track?.title}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import styled from 'styled-components/native';
|
||||
import { Text, Platform } from 'react-native';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { colors } from 'components/Colors';
|
||||
import { DefaultStylesProvider } from 'components/Colors';
|
||||
|
||||
const NumberBar = styled.View`
|
||||
flex-direction: row;
|
||||
@@ -67,22 +67,27 @@ export default class ProgressBar extends Component<{}, State> {
|
||||
const { position, duration, gesture } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Slider
|
||||
value={gesture || position}
|
||||
minimumValue={0}
|
||||
maximumValue={duration || 0}
|
||||
onValueChange={this.handleGesture}
|
||||
onSlidingComplete={this.handleEndOfGesture}
|
||||
minimumTrackTintColor={THEME_COLOR}
|
||||
thumbTintColor={Platform.OS === 'android' ? THEME_COLOR : undefined}
|
||||
disabled={!duration}
|
||||
/>
|
||||
<NumberBar>
|
||||
<Text style={colors.text}>{getMinutes(gesture || position)}:{getSeconds(gesture || position)}</Text>
|
||||
<Text style={colors.text}>{getMinutes(duration)}:{getSeconds(duration)}</Text>
|
||||
</NumberBar>
|
||||
</>
|
||||
<DefaultStylesProvider>
|
||||
{defaultStyle => (
|
||||
<>
|
||||
<Slider
|
||||
value={gesture || position}
|
||||
minimumValue={0}
|
||||
maximumValue={duration || 0}
|
||||
onValueChange={this.handleGesture}
|
||||
onSlidingComplete={this.handleEndOfGesture}
|
||||
minimumTrackTintColor={THEME_COLOR}
|
||||
thumbTintColor={Platform.OS === 'android' ? THEME_COLOR : undefined}
|
||||
disabled={!duration}
|
||||
/>
|
||||
<NumberBar>
|
||||
<Text style={defaultStyle.text}>{getMinutes(gesture || position)}:{getSeconds(gesture || position)}</Text>
|
||||
<Text style={defaultStyle.text}>{getMinutes(duration)}:{getSeconds(duration)}</Text>
|
||||
</NumberBar>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</DefaultStylesProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import useQueue from 'utility/useQueue';
|
||||
import { View, Text, StyleSheet, Button } from 'react-native';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import useCurrentTrack from 'utility/useCurrentTrack';
|
||||
import TouchableHandler from 'components/TouchableHandler';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { colors } from 'components/Colors';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import Text from 'components/Text';
|
||||
import Button from 'components/Button';
|
||||
|
||||
const QueueItem = styled.View<{ active?: boolean, alreadyPlayed?: boolean, isDark?: boolean }>`
|
||||
padding: 10px;
|
||||
@@ -29,17 +30,13 @@ const ClearQueue = styled.View`
|
||||
`;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
title: {
|
||||
...colors.text,
|
||||
marginBottom: 2,
|
||||
},
|
||||
artist: {
|
||||
...colors.text,
|
||||
opacity: 0.5,
|
||||
trackTitle: {
|
||||
marginBottom: 2
|
||||
}
|
||||
});
|
||||
|
||||
export default function Queue() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const queue = useQueue();
|
||||
const currentTrack = useCurrentTrack();
|
||||
const currentIndex = queue.findIndex(d => d.id === currentTrack?.id);
|
||||
@@ -60,18 +57,18 @@ export default function Queue() {
|
||||
active={currentTrack?.id === track.id}
|
||||
key={i}
|
||||
alreadyPlayed={i < currentIndex}
|
||||
style={{
|
||||
...colors.border,
|
||||
...currentTrack?.id === track.id ? colors.activeBackground : {},
|
||||
}}
|
||||
style={[
|
||||
defaultStyles.border,
|
||||
currentTrack?.id === track.id ? defaultStyles.activeBackground : {},
|
||||
]}
|
||||
>
|
||||
<Text style={styles.title}>{track.title}</Text>
|
||||
<Text style={styles.artist}>{track.artist}</Text>
|
||||
<Text style={styles.trackTitle}>{track.title}</Text>
|
||||
<Text style={defaultStyles.textHalfOpacity}>{track.artist}</Text>
|
||||
</QueueItem>
|
||||
</TouchableHandler>
|
||||
))}
|
||||
<ClearQueue>
|
||||
<Button title={t('clear-queue')} color={THEME_COLOR} onPress={clearQueue} />
|
||||
<Button title={t('clear-queue')} onPress={clearQueue} />
|
||||
</ClearQueue>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -4,18 +4,19 @@ import MediaControls from './components/MediaControls';
|
||||
import ProgressBar from './components/ProgressBar';
|
||||
import NowPlaying from './components/NowPlaying';
|
||||
import Queue from './components/Queue';
|
||||
import { colors } from 'components/Colors';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: colors.view,
|
||||
inner: {
|
||||
padding: 25,
|
||||
}
|
||||
});
|
||||
|
||||
export default function Player() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.inner} style={styles.outer}>
|
||||
<ScrollView contentContainerStyle={styles.inner} style={defaultStyles.view}>
|
||||
<NowPlaying />
|
||||
<MediaControls />
|
||||
<ProgressBar />
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import { SubHeader } from 'components/Typography';
|
||||
import { Text, Button } from 'react-native';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import music from 'store/music';
|
||||
import { t } from '@localisation';
|
||||
import Button from 'components/Button';
|
||||
import styled from 'styled-components/native';
|
||||
import Text from 'components/Text';
|
||||
|
||||
const ClearCache = styled(Button)`
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
export default function CacheSettings() {
|
||||
const dispatch = useDispatch();
|
||||
@@ -21,7 +26,7 @@ export default function CacheSettings() {
|
||||
<>
|
||||
<SubHeader>{t('setting-cache')}</SubHeader>
|
||||
<Text>{t('setting-cache-description')}</Text>
|
||||
<Button title={t('reset-cache')} onPress={handleClearCache} color={THEME_COLOR} />
|
||||
<ClearCache title={t('reset-cache')} onPress={handleClearCache} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import styled from 'styled-components/native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Text, Button } from 'react-native';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { SubHeader } from 'components/Typography';
|
||||
import { colors } from 'components/Colors';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import { NavigationProp } from '../..';
|
||||
import { useTypedSelector } from 'store';
|
||||
import { t } from '@localisation';
|
||||
import Button from 'components/Button';
|
||||
import Text from 'components/Text';
|
||||
|
||||
const InputContainer = styled.View`
|
||||
margin: 10px 0;
|
||||
@@ -20,6 +20,7 @@ const Input = styled.TextInput`
|
||||
`;
|
||||
|
||||
export default function LibrarySettings() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const { jellyfin } = useTypedSelector(state => state.settings);
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const handleSetLibrary = useCallback(() => navigation.navigate('SetJellyfinServer'), [navigation]);
|
||||
@@ -28,18 +29,18 @@ export default function LibrarySettings() {
|
||||
<>
|
||||
<SubHeader>{t('jellyfin-library')}</SubHeader>
|
||||
<InputContainer>
|
||||
<Text style={colors.text}>{t('jellyfin-server-url')}</Text>
|
||||
<Input placeholder="https://jellyfin.yourserver.com/" value={jellyfin?.uri} editable={false} style={colors.input} />
|
||||
<Text style={defaultStyles.text}>{t('jellyfin-server-url')}</Text>
|
||||
<Input placeholder="https://jellyfin.yourserver.com/" value={jellyfin?.uri} editable={false} style={defaultStyles.input} />
|
||||
</InputContainer>
|
||||
<InputContainer>
|
||||
<Text style={colors.text}>{t('jellyfin-access-token')}</Text>
|
||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.access_token} editable={false} style={colors.input} />
|
||||
<Text style={defaultStyles.text}>{t('jellyfin-access-token')}</Text>
|
||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.access_token} editable={false} style={defaultStyles.input} />
|
||||
</InputContainer>
|
||||
<InputContainer>
|
||||
<Text style={colors.text}>{t('jellyfin-user-id')}</Text>
|
||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.user_id} editable={false} style={colors.input} />
|
||||
<Text style={defaultStyles.text}>{t('jellyfin-user-id')}</Text>
|
||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.user_id} editable={false} style={defaultStyles.input} />
|
||||
</InputContainer>
|
||||
<Button title={t('set-jellyfin-server')} onPress={handleSetLibrary} color={THEME_COLOR} />
|
||||
<Button title={t('set-jellyfin-server')} onPress={handleSetLibrary} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
import React from 'react';
|
||||
import { View, SafeAreaView, ScrollView } from 'react-native';
|
||||
import { Header } from 'components/Typography';
|
||||
import { colors } from 'components/Colors';
|
||||
import Library from './components/Library';
|
||||
import Cache from './components/Cache';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import { Header } from 'components/Typography';
|
||||
import { t } from '@localisation';
|
||||
|
||||
export default function Settings() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<SafeAreaView>
|
||||
<View style={{ padding: 20 }}>
|
||||
<Header style={colors.text}>{t('settings')}</Header>
|
||||
<Header style={defaultStyles.text}>{t('settings')}</Header>
|
||||
<Library />
|
||||
<Cache />
|
||||
</View>
|
||||
|
||||
@@ -93,6 +93,6 @@ export default function Routes() {
|
||||
}
|
||||
|
||||
export type NavigationProp = CompositeNavigationProp<
|
||||
StackNavigationProp<Routes>,
|
||||
BottomTabNavigationProp<Screens>
|
||||
StackNavigationProp<Routes>,
|
||||
BottomTabNavigationProp<Screens>
|
||||
>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Text, Button, View } from 'react-native';
|
||||
import { Button, View } from 'react-native';
|
||||
import Modal from 'components/Modal';
|
||||
import Input from 'components/Input';
|
||||
import { setJellyfinCredentials } from 'store/settings/actions';
|
||||
@@ -7,10 +7,12 @@ import { useDispatch } from 'react-redux';
|
||||
import { useNavigation, StackActions } from '@react-navigation/native';
|
||||
import CredentialGenerator from './components/CredentialGenerator';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { colors } from 'components/Colors';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import Text from 'components/Text';
|
||||
|
||||
export default function SetJellyfinServer() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
// State for first screen
|
||||
const [serverUrl, setServerUrl] = useState<string>();
|
||||
const [isLogginIn, setIsLogginIn] = useState<boolean>(false);
|
||||
@@ -34,7 +36,7 @@ export default function SetJellyfinServer() {
|
||||
/>
|
||||
) : (
|
||||
<View style={{ padding: 20, flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text style={colors.text}>
|
||||
<Text>
|
||||
{t('set-jellyfin-server-instruction')}
|
||||
</Text>
|
||||
<Input
|
||||
@@ -44,7 +46,7 @@ export default function SetJellyfinServer() {
|
||||
keyboardType="url"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
style={{ ...colors.input, width: '100%' }}
|
||||
style={[ defaultStyles.input, { width: '100%' } ]}
|
||||
/>
|
||||
<Button
|
||||
title={t('set-jellyfin-server')}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import { Text, Button } from 'react-native';
|
||||
import { useNavigation, StackActions, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { ModalStackParams } from 'screens/types';
|
||||
import { useTypedSelector } from 'store';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { SubHeader } from 'components/Typography';
|
||||
import styled from 'styled-components/native';
|
||||
import usePlayTrack from 'utility/usePlayTrack';
|
||||
import { t } from '@localisation';
|
||||
import Button from 'components/Button';
|
||||
import PlayIcon from 'assets/play.svg';
|
||||
import QueueAppendIcon from 'assets/queue-append.svg';
|
||||
import Text from 'components/Text';
|
||||
|
||||
type Route = RouteProp<ModalStackParams, 'TrackPopupMenu'>;
|
||||
|
||||
@@ -19,7 +21,11 @@ const Container = styled.View`
|
||||
const Buttons = styled.View`
|
||||
margin-top: 20px;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
/* justify-content: space-around; */
|
||||
`;
|
||||
|
||||
const ButtonSpacing = styled.View`
|
||||
width: 8px;
|
||||
`;
|
||||
|
||||
function TrackPopupMenu() {
|
||||
@@ -49,8 +55,9 @@ function TrackPopupMenu() {
|
||||
<SubHeader>{track?.Name}</SubHeader>
|
||||
<Text>{track?.Album} - {track?.AlbumArtist}</Text>
|
||||
<Buttons>
|
||||
<Button title={t('play-next')} color={THEME_COLOR} onPress={handlePlayNext} />
|
||||
<Button title={t('add-to-queue')} color={THEME_COLOR} onPress={handleAddToQueue} />
|
||||
<Button title={t('play-next')} icon={PlayIcon} onPress={handlePlayNext} />
|
||||
<ButtonSpacing />
|
||||
<Button title={t('add-to-queue')} icon={QueueAppendIcon} onPress={handleAddToQueue} />
|
||||
</Buttons>
|
||||
</Container>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user