Compare commits

...

7 Commits

Author SHA1 Message Date
Lei Nelissen
4635266273 Add screens for opting into Sentry error tracking 2021-02-13 15:34:43 +01:00
Lei Nelissen
8dc287e56a Make theming somewhat more performant 2021-02-13 12:14:57 +01:00
Lei Nelissen
6cfa8f7624 Attempt fix for JELLYFIN-AUDIO-PLAYER-M 2021-02-11 23:55:49 +01:00
Lei Nelissen
6cf5b8167b Release build 9 2021-02-11 23:47:42 +01:00
Lei Nelissen
dc76ea27d3 Double-check whether an album has tracks 2021-02-11 23:46:03 +01:00
Lei Nelissen
7adc96ba12 Fancy new buttons and more consistent colors 2021-02-11 23:43:21 +01:00
Lei Nelissen
42eb7a169b Release build number 8 2021-02-09 23:33:34 +01:00
52 changed files with 15555 additions and 3780 deletions

View File

@@ -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'
]
}
};

View File

@@ -7,17 +7,17 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.1.0)
aws-partitions (1.421.0)
aws-sdk-core (3.111.2)
aws-partitions (1.426.0)
aws-sdk-core (3.112.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.41.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sdk-kms (1.42.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.87.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sdk-s3 (1.88.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2)
@@ -36,7 +36,7 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.1)
excon (0.78.1)
excon (0.79.0)
faraday (1.3.0)
faraday-net_http (~> 1.0)
multipart-post (>= 1.2, < 3)
@@ -47,8 +47,8 @@ GEM
faraday-net_http (1.0.1)
faraday_middleware (1.0.0)
faraday (~> 1.0)
fastimage (2.2.1)
fastlane (2.172.0)
fastimage (2.2.2)
fastlane (2.174.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0)
@@ -107,7 +107,7 @@ GEM
webrick
google-apis-iamcredentials_v1 (0.1.0)
google-apis-core (~> 0.1)
google-apis-storage_v1 (0.1.0)
google-apis-storage_v1 (0.2.0)
google-apis-core (~> 0.1)
google-cloud-core (1.5.0)
google-cloud-env (~> 1.0)
@@ -123,7 +123,7 @@ GEM
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.15.0)
googleauth (0.15.1)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)

View File

@@ -4,14 +4,8 @@ import TrackPlayer from 'react-native-track-player';
import App from './src/components/App';
import { name as appName } from './app.json';
import PlaybackService from './src/utility/PlaybackService';
import * as Sentry from '@sentry/react-native';
import { SENTRY_DSN } from '@env';
if (SENTRY_DSN) {
Sentry.init({
dsn: SENTRY_DSN
});
}
import { setupSentry } from 'utility/Sentry';
setupSentry();
AppRegistry.registerComponent(appName, () => App);
TrackPlayer.registerPlaybackService(() => PlaybackService);

View File

@@ -504,7 +504,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = 238P3C58WC;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -535,7 +535,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 7;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = 238P3C58WC;
INFOPLIST_FILE = JellyfinAudioPlayer/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>7</string>
<string>9</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>

View File

@@ -19,6 +19,6 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>7</string>
<string>9</string>
</dict>
</plist>

View File

@@ -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

18046
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,9 +28,9 @@
"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-collapsible": "^1.5.3",
"react-native-dotenv": "^2.5.1",
"react-native-fast-image": "^8.3.4",
"react-native-gesture-handler": "^1.9.0",
@@ -49,14 +49,15 @@
"styled-components": "^5.2.1"
},
"devDependencies": {
"@babel/core": "^7.12.13",
"@babel/runtime": "^7.12.13",
"@babel/core": "^7.8.4",
"@babel/runtime": "^7.8.4",
"@react-native-community/eslint-config": "^2.0.0",
"@types/i18n-js": "^3.8.0",
"@types/jest": "^26.0.20",
"@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",
@@ -64,7 +65,7 @@
"eslint": "^7.19.0",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^26.6.3",
"metro-react-native-babel-preset": "^0.65.0",
"metro-react-native-babel-preset": "^0.59.0",
"react-test-renderer": "^17.0.1",
"typescript": "^4.1.3"
},

View 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

View 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
View 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

View File

@@ -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;

View File

@@ -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,41 @@ import {
DefaultTheme,
DarkTheme,
} from '@react-navigation/native';
import { useColorScheme } from 'react-native';
import { ColorSchemeContext, themes } from './Colors';
import ErrorReportingAlert from 'utility/ErrorReportingAlert';
interface State {
isReady: boolean;
colorScheme?: string;
}
export default function App(): JSX.Element {
const colorScheme = useColorScheme();
// const colorScheme = 'dark';
const theme = themes[colorScheme || 'light'];
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}>
<ColorSchemeContext.Provider value={theme}>
<NavigationContainer theme={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Routes />
</NavigationContainer>
</ColorSchemeContext.Provider>
</PersistGate>
</Provider>
);
}

70
src/components/Button.tsx Normal file
View File

@@ -0,0 +1,70 @@
import React, { useCallback, useState } from 'react';
import { SvgProps } from 'react-native-svg';
import {
PressableProps, ViewProps,
} 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;
style?: ViewProps['style'];
}
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>
);
}

View File

@@ -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'),
}
});

View File

@@ -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'),
}
});

80
src/components/Colors.ts Normal file
View File

@@ -0,0 +1,80 @@
import { THEME_COLOR } from 'CONSTANTS';
import React from 'react';
import { useContext } from 'react';
import { ColorSchemeName, StyleSheet } from 'react-native';
/**
* Function for generating both the dark and light stylesheets, so that they
* don't have to be generate on every individual component render
*/
function generateStyles(scheme: ColorSchemeName) {
return StyleSheet.create({
text: {
color: scheme === 'dark' ? '#fff' : '#000',
},
textHalfOpacity: {
color: scheme === 'dark' ? '#ffffff88' : '#00000088',
},
view: {
backgroundColor: scheme === 'dark' ? '#111' : '#f6f6f6',
},
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',
},
stackHeader: {
color: scheme === 'dark' ? 'white' : 'black'
}
});
}
// Prerender both stylesheets
export const themes: Record<'dark' | 'light', ReturnType<typeof generateStyles>> = {
'dark': generateStyles('dark'),
'light': generateStyles('light'),
};
// Create context for supplying the theming information
export const ColorSchemeContext = React.createContext(themes.dark);
/**
* Retrieves the default styles object in hook form
*/
export default function useDefaultStyles() {
return useContext(ColorSchemeContext);
}
interface DefaultStylesProviderProps {
children: (defaultStyles: ReturnType<typeof useDefaultStyles>) => JSX.Element;
}
/**
* A render props component to supply the defaultStyles object.
*/
export function DefaultStylesProvider(props: DefaultStylesProviderProps) {
const defaultStyles = useDefaultStyles();
return props.children(defaultStyles);
}

View File

@@ -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;
`;

View File

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

View File

@@ -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
View 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]} />
);
}

View File

@@ -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;
`;

4
src/custom.d.ts vendored
View File

@@ -2,4 +2,8 @@ declare module '*.svg' {
import { SvgProps } from 'react-native-svg';
const content: React.FC<SvgProps>;
export default content;
}
declare module '@env' {
export const SENTRY_DSN: string;
}

View File

@@ -24,5 +24,19 @@
"setting-cache": "Cache",
"setting-cache-description": "If you have updated your Jellyfin library, but the app is holding on to cached assets, you can forcefully clear the cache using this button. This will force the app to fetch the library from scratch.",
"reset-cache": "Reset Cache",
"recent-albums": "Recent Albums"
"recent-albums": "Recent Albums",
"error-reporting": "Error Reporting",
"error-reporting-description": "During use of this app, you may encounter errors. Reporting these errors helps in creating a more secure and stable app experience.",
"error-reporting-rationale": "When you enable error reporting, every time an error occurs, a report is automatically created and sent to a server, along with helpful debugging information such as devices, versions and the specific error.",
"why-use-tracking": "Why use tracking?",
"why-use-tracking-description": "Tracking helps speed up development for this app by reporting weird edge cases and oversights. This helps make the app more stable and robust, thus increasing the app experience for everyone.",
"what-data-is-gathered": "What data is gathered?",
"what-data-is-gathered-description": "We log the error, device type, OS version, app version and device id. No application state is sent in any error reporting. The device id is a unique hash that can be reset in your device settings, and we cannot deduce any personal information from this identifier.",
"where-is-data-stored": "Where is data stored?",
"where-is-data-stored-description": "The Sentry backend is self-hosted on our own infrastructure. No-one but us has access to the servers, databases, application and data logs, least of all any Sentry staff. The infrastructure is hosted in the European Union.",
"enable-error-reporting": "Do you want to enable error reporting?",
"enable-error-reporting-description": "This helps improve the app experience by sending crash and error reports to us.",
"enable": "Enable",
"disable": "Disable",
"more-info": "More Info"
}

View File

@@ -24,5 +24,19 @@
"setting-cache": "Cache",
"setting-cache-description": "Als je Jellyfin bibliotheek geüpdatet hebt, maar de app nog aan elementen uit de cache vasthoudt, kun je de cache geforceerd leegmaken met deze knop. Dit forceert de app om de bibliotheek weer vanaf het begin op te bouwen.",
"reset-cache": "Leeg Cache",
"recent-albums": "Recente Albums"
"recent-albums": "Recente Albums",
"error-reporting": "Foutmeldingen Rapporteren",
"error-reporting-description": "Gedurende het gebruik van deze app kun je tegen fouten aanlopen. Het rapporteren van deze foutmeldingen helpt om een veiligere en stabielere app-ervaring te maken.",
"error-reporting-rationale": "Wanneer je foutmeldingen aanzet, wordt er elke keer als je een foutmelding krijgt een automatisch rapport gegenereerd en naar onze server gestuurd. Deze bevat behulpzame informatie, zoals apparaat, versies en de specifieke foutmelding.",
"why-use-tracking": "Waarom tracking gebruiken?",
"why-use-tracking-description": "Tracking helpt het versnellen van het ontwikkeltempo doordat we beter inzicht hebben op rare gevallen en dingen die we gemist hebben. Dit helpt met het stabieler en veiliger maken van de voor iedereen.",
"what-data-is-gathered": "Welke data wordt er verzameld?",
"what-data-is-gathered-description": "We loggen de foutmeldingen, je apparaattype, versienummers en een device id. Geen applicatiestaat wordt gedeeld in de rapportage. Het device id is een unieke hash die gereset kan worden in de instellingen van je apparaat. Wij kunnen geen verdere informatie afleiden van deze identifier.",
"where-is-data-stored": "Waar wordt de data opgeslagen?",
"where-is-data-stored-description": "De Sentry backend wordt door onszelf gehost op onze eigen infrastructuur. Niemand behalve wij heeft toegang tot de servers, databases, applicaties en data logs, en zeker geen Sentry-werknemers. De infrastructuur wordt gehost in de Europese Unie.",
"enable-error-reporting": "Wil je foutmeldingen rapporteren?",
"enable-error-reporting-description": "Dit helpt de appervaring te verbeteren door ons rapportages te sturen van crashes en andere foutmeldingen.",
"enable": "Zet aan",
"disable": "Zet uit",
"more-info": "Meer informatie"
}

View File

@@ -1,26 +1,40 @@
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'
| 'error-reporting'
| 'error-reporting-description'
| 'error-reporting-rationale'
| 'why-use-tracking'
| 'why-use-tracking-description'
| 'what-data-is-gathered'
| 'what-data-is-gathered-description'
| 'where-is-data-stored'
| 'where-is-data-stored-description'
| 'enable-error-reporting'
| 'enable-error-reporting-description'
| 'enable'
| 'disable'
| 'more-info'

View File

@@ -7,17 +7,18 @@ import RecentAlbums from './stacks/RecentAlbums';
import Search from './stacks/Search';
import { THEME_COLOR } from 'CONSTANTS';
import { t } from '@localisation';
import useDefaultStyles from 'components/Colors';
const Stack = createStackNavigator<StackParams>();
const navigationOptions = {
headerTintColor: THEME_COLOR,
headerTitleStyle: { color: 'black' }
};
function MusicStack() {
const defaultStyles = useDefaultStyles();
return (
<Stack.Navigator initialRouteName="RecentAlbums" screenOptions={navigationOptions}>
<Stack.Navigator initialRouteName="RecentAlbums" screenOptions={{
headerTintColor: THEME_COLOR,
headerTitleStyle: defaultStyles.stackHeader
}}>
<Stack.Screen name="RecentAlbums" component={RecentAlbums} options={{ headerTitle: t('recent-albums') }} />
<Stack.Screen name="Albums" component={Albums} options={{ headerTitle: t('albums') }} />
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />

View File

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

View File

@@ -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

View File

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

View File

@@ -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>

View File

@@ -70,6 +70,7 @@ export type StackParams = {
Albums: undefined;
Album: { id: string, album: Album };
RecentAlbums: undefined;
Search: undefined;
};
export type NavigationProp = StackNavigationProp<StackParams>;

View File

@@ -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;

View File

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

View File

@@ -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;
@@ -65,24 +65,29 @@ export default class ProgressBar extends Component<{}, State> {
render() {
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>
);
}
}

View File

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

View File

@@ -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 />

View File

@@ -1,11 +1,19 @@
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;
`;
const Container = styled.ScrollView`
padding: 24px;
`;
export default function CacheSettings() {
const dispatch = useDispatch();
@@ -18,10 +26,9 @@ export default function CacheSettings() {
}, [dispatch]);
return (
<>
<SubHeader>{t('setting-cache')}</SubHeader>
<Container>
<Text>{t('setting-cache-description')}</Text>
<Button title={t('reset-cache')} onPress={handleClearCache} color={THEME_COLOR} />
</>
<ClearCache title={t('reset-cache')} onPress={handleClearCache} />
</Container>
);
}

View File

@@ -1,13 +1,12 @@
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;
@@ -19,27 +18,31 @@ const Input = styled.TextInput`
border-radius: 5px;
`;
const Container = styled.ScrollView`
padding: 24px;
`;
export default function LibrarySettings() {
const defaultStyles = useDefaultStyles();
const { jellyfin } = useTypedSelector(state => state.settings);
const navigation = useNavigation<NavigationProp>();
const handleSetLibrary = useCallback(() => navigation.navigate('SetJellyfinServer'), [navigation]);
return (
<>
<SubHeader>{t('jellyfin-library')}</SubHeader>
<Container>
<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} />
</Container>
);
}

View File

@@ -0,0 +1,124 @@
import Text from 'components/Text';
import React, { useEffect, useState } from 'react';
import { Switch } from 'react-native-gesture-handler';
import styled, { css } from 'styled-components/native';
import { isSentryEnabled, setSentryStatus } from 'utility/Sentry';
import Accordion from 'react-native-collapsible/Accordion';
import ChevronIcon from 'assets/chevron-right.svg';
import { THEME_COLOR } from 'CONSTANTS';
import useDefaultStyles, { DefaultStylesProvider } from 'components/Colors';
import { t } from '@localisation';
const Container = styled.ScrollView`
padding: 24px;
`;
const SwitchContainer = styled.View`
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 16px 0;
`;
const HeaderContainer = styled.View<{ isActive?: boolean }>`
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 16px 0 4px 0;
${props => props.isActive && css`
background-color: ${THEME_COLOR};
`}
`;
const HeaderText = styled(Text)`
font-size: 16px;
`;
const ContentContainer = styled.View`
margin-top: 8px;
`;
const Label = styled(Text)`
font-size: 16px;
`;
const Chevron = styled(ChevronIcon)<{ isActive?: boolean }>`
width: 14px;
height: 14px;
transform: rotate(-90deg);
${props => props.isActive && css`
transform: rotate(90deg);
`}
`;
type Question = { title: string, content: string };
const questions: Question[] = [
{
title: t('why-use-tracking'),
content: t('why-use-tracking-description')
},
{
title: t('what-data-is-gathered'),
content: t('what-data-is-gathered-description')
},
{
title: t('where-is-data-stored'),
content: t('where-is-data-stored-description')
}
];
function renderHeader(question: Question, index: number, isActive: boolean) {
return (
<HeaderContainer>
<HeaderText>{question.title}</HeaderText>
<DefaultStylesProvider>
{styles =>
<Chevron fill={styles.text.color} isActive={isActive} />
}
</DefaultStylesProvider>
</HeaderContainer>
);
}
function renderContent(question: Question) {
return (
<ContentContainer>
<Text>{question.content}</Text>
</ContentContainer>
);
}
export default function Sentry() {
const defaultStyles = useDefaultStyles();
const [isReportingEnabled, setReporting] = useState(isSentryEnabled);
const [activeSections, setActiveSections] = useState<number[]>([]);
const toggleSwitch = () => setReporting(previousState => !previousState);
useEffect(() => {
setSentryStatus(isReportingEnabled);
});
return (
<Container>
<Text>{t('error-reporting-description')}</Text>
<Text />
<Text>{t('error-reporting-rationale')}</Text>
<SwitchContainer>
<Label>{t('error-reporting')}</Label>
<Switch value={isReportingEnabled} onValueChange={toggleSwitch} />
</SwitchContainer>
<Accordion
sections={questions}
renderHeader={renderHeader}
renderContent={renderContent}
activeSections={activeSections}
onChange={setActiveSections}
underlayColor={defaultStyles.activeBackground.backgroundColor}
/>
</Container>
);
}

View File

@@ -1,21 +1,46 @@
import React from 'react';
import { View, SafeAreaView, ScrollView } from 'react-native';
import { Header } from 'components/Typography';
import { colors } from 'components/Colors';
import React, { useCallback } from 'react';
import { SafeAreaView, ScrollView } from 'react-native';
import Library from './components/Library';
import Cache from './components/Cache';
import useDefaultStyles from 'components/Colors';
import { t } from '@localisation';
import { createStackNavigator } from '@react-navigation/stack';
import { useNavigation } from '@react-navigation/native';
import ListButton from 'components/ListButton';
import { THEME_COLOR } from 'CONSTANTS';
import Sentry from './components/Sentry';
export function SettingsList() {
const navigation = useNavigation();
const handleLibraryClick = useCallback(() => { navigation.navigate('Library'); }, [navigation]);
const handleCacheClick = useCallback(() => { navigation.navigate('Cache'); }, [navigation]);
const handleSentryClick = useCallback(() => { navigation.navigate('Sentry'); }, [navigation]);
export default function Settings() {
return (
<ScrollView>
<SafeAreaView>
<View style={{ padding: 20 }}>
<Header style={colors.text}>{t('settings')}</Header>
<Library />
<Cache />
</View>
<ListButton onPress={handleLibraryClick}>{t('jellyfin-library')}</ListButton>
<ListButton onPress={handleCacheClick}>{t('setting-cache')}</ListButton>
<ListButton onPress={handleSentryClick}>{t('error-reporting')}</ListButton>
</SafeAreaView>
</ScrollView>
);
}
const Stack = createStackNavigator();
export default function Settings() {
const defaultStyles = useDefaultStyles();
return (
<Stack.Navigator initialRouteName="SettingList" screenOptions={{
headerTintColor: THEME_COLOR,
headerTitleStyle: defaultStyles.stackHeader
}}>
<Stack.Screen name="SettingList" component={SettingsList} options={{ headerTitle: t('settings') }} />
<Stack.Screen name="Library" component={Library} options={{ headerTitle: t('jellyfin-library') }} />
<Stack.Screen name="Cache" component={Cache} options={{ headerTitle: t('setting-cache') }} />
<Stack.Screen name="Sentry" component={Sentry} options={{ headerTitle: t('error-reporting') }} />
</Stack.Navigator>
);
}

View File

@@ -15,6 +15,8 @@ import Onboarding from './Onboarding';
import TrackPopupMenu from './modals/TrackPopupMenu';
import { ModalStackParams } from './types';
import { t } from '@localisation';
import ErrorReportingAlert from 'utility/ErrorReportingAlert';
import ErrorReportingPopup from './modals/ErrorReportingPopup';
const Stack = createStackNavigator<ModalStackParams>();
const Tab = createBottomTabNavigator();
@@ -48,27 +50,30 @@ function Screens() {
}
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: function TabBarIcon({ color, size }) {
const Icon = getIcon(route.name);
<>
<ErrorReportingAlert />
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: function TabBarIcon({ color, size }) {
const Icon = getIcon(route.name);
if (!Icon) {
return null;
if (!Icon) {
return null;
}
return <Icon fill={color} width={size} height={size} />;
}
return <Icon fill={color} width={size} height={size} />;
}
})}
tabBarOptions={{
activeTintColor: THEME_COLOR,
inactiveTintColor: 'gray',
}}
>
<Tab.Screen name="NowPlaying" component={Player} options={{ tabBarLabel: t('now-playing') }} />
<Tab.Screen name="Music" component={Music} options={{ tabBarLabel: t('music') }} />
<Tab.Screen name="Settings" component={Settings} options={{ tabBarLabel: t('settings') }} />
</Tab.Navigator>
})}
tabBarOptions={{
activeTintColor: THEME_COLOR,
inactiveTintColor: 'gray',
}}
>
<Tab.Screen name="NowPlaying" component={Player} options={{ tabBarLabel: t('now-playing') }} />
<Tab.Screen name="Music" component={Music} options={{ tabBarLabel: t('music') }} />
<Tab.Screen name="Settings" component={Settings} options={{ tabBarLabel: t('settings') }} />
</Tab.Navigator>
</>
);
}
@@ -88,11 +93,12 @@ export default function Routes() {
<Stack.Screen name="Screens" component={Screens} />
<Stack.Screen name="SetJellyfinServer" component={SetJellyfinServer} />
<Stack.Screen name="TrackPopupMenu" component={TrackPopupMenu} />
<Stack.Screen name="ErrorReporting" component={ErrorReportingPopup} />
</Stack.Navigator>
);
}
export type NavigationProp = CompositeNavigationProp<
StackNavigationProp<Routes>,
BottomTabNavigationProp<Screens>
StackNavigationProp<Routes>,
BottomTabNavigationProp<Screens>
>;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import Modal from 'components/Modal';
import Sentry from 'screens/Settings/components/Sentry';
export default function ErrorReportingPopup() {
return (
<Modal fullSize={false}>
<Sentry />
</Modal>
);
}

View File

@@ -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')}

View File

@@ -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>

View File

@@ -60,6 +60,10 @@ const music = createSlice({
* Fetch tracks by album
*/
builder.addCase(fetchTracksByAlbum.fulfilled, (state, { payload }) => {
if (!payload.length) {
return;
}
trackAdapter.upsertMany(state.tracks, payload);
// Also store all the track ids in the album

View File

@@ -2,4 +2,5 @@ import { createAction } from '@reduxjs/toolkit';
export const setJellyfinCredentials = createAction<{ access_token: string, user_id: string, uri: string, deviced_id: string; }>('SET_JELLYFIN_CREDENTIALS');
export const setBitrate = createAction<number>('SET_BITRATE');
export const setOnboardingStatus = createAction<boolean>('SET_ONBOARDING_STATUS');
export const setOnboardingStatus = createAction<boolean>('SET_ONBOARDING_STATUS');
export const setReceivedErrorReportingAlert = createAction<void>('SET_RECEIVED_ERROR_REPORTING_ALERT');

View File

@@ -1,5 +1,5 @@
import { createReducer } from '@reduxjs/toolkit';
import { setBitrate, setJellyfinCredentials, setOnboardingStatus } from './actions';
import { setReceivedErrorReportingAlert, setBitrate, setJellyfinCredentials, setOnboardingStatus } from './actions';
interface State {
jellyfin?: {
@@ -10,11 +10,13 @@ interface State {
}
bitrate: number;
isOnboardingComplete: boolean;
hasReceivedErrorReportingAlert: boolean;
}
const initialState: State = {
bitrate: 140000000,
isOnboardingComplete: false,
hasReceivedErrorReportingAlert: false,
};
const settings = createReducer(initialState, {
@@ -29,6 +31,10 @@ const settings = createReducer(initialState, {
[setOnboardingStatus.type]: (state, action) => ({
...state,
isOnboardingComplete: action.payload,
}),
[setReceivedErrorReportingAlert.type]: (state) => ({
...state,
hasReceivedErrorReportingAlert: true,
})
});

View File

@@ -0,0 +1,59 @@
import { useEffect } from 'react';
import { Alert } from 'react-native';
import { useTypedSelector } from 'store';
import { t } from '@localisation';
import { setReceivedErrorReportingAlert } from 'store/settings/actions';
import { setSentryStatus } from './Sentry';
import { useNavigation } from '@react-navigation/native';
import { useDispatch } from 'react-redux';
/**
* This will send out an alert message asking the user if they want to enable
* error reporting.
*/
export default function ErrorReportingAlert() {
const { hasReceivedErrorReportingAlert } = useTypedSelector(state => state.settings);
const navigation = useNavigation();
const dispatch = useDispatch();
useEffect(() => {
// Only send out alert if we haven't done so ever
if (!hasReceivedErrorReportingAlert) {
// Generate the alert
Alert.alert(
t('enable-error-reporting'),
t('enable-error-reporting-description'),
[
{
text: t('enable'),
style: 'default',
onPress: () => {
setSentryStatus(true);
}
},
{
text: t('disable'),
style: 'destructive',
onPress: () => {
setSentryStatus(false);
}
},
{
text: t('more-info'),
style: 'cancel',
onPress: () => {
navigation.navigate('ErrorReporting');
}
}
]
);
// Store the flag that we have sent out the alert, so that we don't
// have to do so anymore in the future.
dispatch(setReceivedErrorReportingAlert());
}
}, []);
return null;
}

50
src/utility/Sentry.ts Normal file
View File

@@ -0,0 +1,50 @@
import { SENTRY_DSN } from '@env';
import AsyncStorage from '@react-native-community/async-storage';
import * as Sentry from '@sentry/react-native';
const SENTRY_ASYNC__ITEM_STRING = 'sentry_enabled';
export let isSentryEnabled = false;
/**
* Setup Sentry based on what value is stored in AsyncStorage.
*/
export async function setupSentry(): Promise<void> {
// First, we'll retrieve the user settings. This delays Sentry being active
// slightly, at the bonus of acutally being able to send off reports for
// start-up stuff.
isSentryEnabled = (await AsyncStorage.getItem(SENTRY_ASYNC__ITEM_STRING)) === 'true';
// Make sure the DSN is actually set, in order to prevent weird erros.
if (SENTRY_DSN) {
Sentry.init({
dsn: SENTRY_DSN,
// Before we send any event, check whether the Sentry SDK should be
// enabled based on user settings.
beforeSend(event) {
// If so, pass off the event to the back-end
if (isSentryEnabled) {
return event;
}
// If not, don't sent a thing
return null;
}
});
}
}
/**
* Helper function to enable or disable the Sentry SDK for this app.
*/
export async function setSentryStatus(isEnabled: boolean): Promise<void> {
// GUARD: If nothing's changed, change nothing
if (isEnabled === isSentryEnabled) {
return;
}
// First, store the value in Async Storage
await AsyncStorage.setItem(SENTRY_ASYNC__ITEM_STRING, isEnabled.toString());
// Then, assign it to the variable
isSentryEnabled = isEnabled;
}

View File

@@ -24,8 +24,15 @@ export default function useCurrentTrack(): Track | undefined {
}
// If it is different, retrieve the track and save it
const currentTrack = await TrackPlayer.getTrack(currentTrackId);
setTrack(currentTrack);
try {
const currentTrack = await TrackPlayer.getTrack(currentTrackId);
setTrack(currentTrack);
} catch {
// Due to the async nature, a track might be removed at the
// point when we try to retrieve it. If this happens, we'll just
// smother the error and wait for a new track update to
// finish.
}
};
fetchTrack();