diff --git a/android/app/build.gradle b/android/app/build.gradle index c13dee7..9b2dd47 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -135,7 +135,7 @@ android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { - applicationId "com.rndiffapp" + applicationId "com.jellyfinaudioplayer" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 8 @@ -300,6 +300,10 @@ if (isNewArchitectureEnabled()) { substitute(module("com.facebook.react:react-native")) .using(project(":ReactAndroid")).because("On New Architecture we're building React Native from source") } + resolutionStrategy { + force 'com.google.android.exoplayer:exoplayer-core:2.11.4' + } + } } diff --git a/android/app/src/main/java/com/jellyfinaudioplayer/MainApplication.java b/android/app/src/main/java/com/jellyfinaudioplayer/MainApplication.java index 1aeeec7..3da7d92 100644 --- a/android/app/src/main/java/com/jellyfinaudioplayer/MainApplication.java +++ b/android/app/src/main/java/com/jellyfinaudioplayer/MainApplication.java @@ -11,6 +11,9 @@ import com.facebook.soloader.SoLoader; import java.lang.reflect.InvocationTargetException; import java.util.List; +import com.facebook.react.bridge.JSIModulePackage; +import com.swmansion.reanimated.ReanimatedJSIModulePackage; + public class MainApplication extends Application implements ReactApplication { private final ReactNativeHost mReactNativeHost = @@ -33,6 +36,11 @@ public class MainApplication extends Application implements ReactApplication { protected String getJSMainModuleName() { return "index"; } + + @Override + protected JSIModulePackage getJSIModulePackage() { + return new ReanimatedJSIModulePackage(); + } }; @Override diff --git a/android/build.gradle b/android/build.gradle index d2786b4..19b45a5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -57,6 +57,7 @@ allprojects { jcenter() { content { includeGroup("com.google.android.exoplayer") + includeGroupByRegex("com.eightbitlab.*") } } } diff --git a/babel.config.js b/babel.config.js index 13ea02c..096f8c5 100644 --- a/babel.config.js +++ b/babel.config.js @@ -31,6 +31,7 @@ module.exports = { ], [ 'module:react-native-dotenv' - ] + ], + 'react-native-reanimated/plugin' ] }; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a8403a1..9439ba9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -422,6 +422,33 @@ PODS: - React-Core - RNLocalize (2.2.1): - React-Core + - RNReanimated (2.8.0): + - DoubleConversion + - FBLazyVector + - FBReactNativeSpec + - glog + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core/DevSupport + - React-Core/RCTWebSocket + - React-CoreModules + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-RCTActionSheet + - React-RCTAnimation + - React-RCTBlob + - React-RCTImage + - React-RCTLinking + - React-RCTNetwork + - React-RCTSettings + - React-RCTText + - ReactCommon/turbomodule/core + - Yoga - RNScreens (3.13.1): - React-Core - React-RCTImage @@ -516,6 +543,7 @@ DEPENDENCIES: - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNLocalize (from `../node_modules/react-native-localize`) + - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - "RNSentry (from `../node_modules/@sentry/react-native`)" - RNSVG (from `../node_modules/react-native-svg`) @@ -637,6 +665,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-gesture-handler" RNLocalize: :path: "../node_modules/react-native-localize" + RNReanimated: + :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" RNSentry: @@ -707,6 +737,7 @@ SPEC CHECKSUMS: RNFS: fc610f78fdf8bfc89a9e5cc2f898519f4dba1002 RNGestureHandler: 4f4986408310a43f1606c391f38f76e0d6e790d5 RNLocalize: cbcb55d0e19c78086ea4eea20e03fe8000bbbced + RNReanimated: 64573e25e078ae6bec03b891586d50b9ec284393 RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19 RNSentry: 2cd1daa124b0d9fd0dfc2cb6094fdd168cb579bc RNSVG: 302bfc9905bd8122f08966dc2ce2d07b7b52b9f8 diff --git a/package-lock.json b/package-lock.json index 5619bca..f96afa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "date-fns": "^2.28.0", "events": "^3.3.0", "fuse.js": "^6.6.0", + "hermes-engine": "^0.11.0", "i18n-js": "^3.9.2", "lodash": "^4.17.21", "react": "^17.0.2", @@ -38,12 +39,13 @@ "react-native-fs": "^2.19.0", "react-native-gesture-handler": "^2.4.1", "react-native-localize": "^2.2.1", + "react-native-reanimated": "^2.8.0", "react-native-safe-area-context": "^4.2.5", "react-native-screens": "^3.13.1", "react-native-shadow-2": "^6.0.5", "react-native-svg": "^12.3.0", "react-native-svg-transformer": "^1.0.0", - "react-native-track-player": "^2.1.3", + "react-native-track-player": "^2.2.0-rc3", "react-native-webview": "^11.18.2", "react-redux": "^7.2.6", "redux": "^4.2.0", @@ -4911,8 +4913,7 @@ "node_modules/@types/invariant": { "version": "2.2.35", "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==", - "peer": true + "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.3", @@ -8772,6 +8773,11 @@ "node": ">=0.10.0" } }, + "node_modules/hermes-engine": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz", + "integrity": "sha512-7aMUlZja2IyLYAcZ69NBnwJAR5ZOYlSllj0oMpx08a8HzxHOys0eKCzfphrf6D0vX1JGO1QQvVsQKe6TkYherw==" + }, "node_modules/hermes-estree": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.5.0.tgz", @@ -12456,8 +12462,7 @@ "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "peer": true + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -14427,7 +14432,6 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-2.8.0.tgz", "integrity": "sha512-kJvf/UWLBMaGCs9X66MKq5zdFMgwx8D0nHnolbHR7s8ZnbLdb7TlQ/yuzIXqn/9wABfnwtNRI3CyaP1aHWMmZg==", - "peer": true, "dependencies": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", @@ -14505,9 +14509,9 @@ } }, "node_modules/react-native-track-player": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/react-native-track-player/-/react-native-track-player-2.1.3.tgz", - "integrity": "sha512-JWKFRu+hr1ECN339RH+c+XDM7HfwvdvS4H1p4cJbhg/9b1CQGPJSrYXEhYkngN0msoxBxAjFyFIhjT2fWDCltA==", + "version": "2.2.0-rc3", + "resolved": "https://registry.npmjs.org/react-native-track-player/-/react-native-track-player-2.2.0-rc3.tgz", + "integrity": "sha512-Pvmum3MQ5PE8/yOIIPsk00zZk3EzdocUuVUwuBKSCmdKK/3O9YhnZRC3EuT59XCDm23pZZJZDSR44VeXN6Gamg==", "peerDependencies": { "react": ">=16.8.6", "react-native": ">=0.60.0-rc.2", @@ -14806,11 +14810,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-native/node_modules/hermes-engine": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz", - "integrity": "sha512-7aMUlZja2IyLYAcZ69NBnwJAR5ZOYlSllj0oMpx08a8HzxHOys0eKCzfphrf6D0vX1JGO1QQvVsQKe6TkYherw==" - }, "node_modules/react-native/node_modules/hermes-parser": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.5.0.tgz", @@ -15879,8 +15878,7 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "peer": true + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "node_modules/setprototypeof": { "version": "1.1.1", @@ -16385,8 +16383,7 @@ "node_modules/string-hash-64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string-hash-64/-/string-hash-64-1.0.3.tgz", - "integrity": "sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==", - "peer": true + "integrity": "sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==" }, "node_modules/string-length": { "version": "4.0.2", @@ -20895,8 +20892,7 @@ "@types/invariant": { "version": "2.2.35", "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz", - "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==", - "peer": true + "integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==" }, "@types/istanbul-lib-coverage": { "version": "2.0.3", @@ -23834,6 +23830,11 @@ } } }, + "hermes-engine": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz", + "integrity": "sha512-7aMUlZja2IyLYAcZ69NBnwJAR5ZOYlSllj0oMpx08a8HzxHOys0eKCzfphrf6D0vX1JGO1QQvVsQKe6TkYherw==" + }, "hermes-estree": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.5.0.tgz", @@ -26684,8 +26685,7 @@ "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "peer": true + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "lodash.merge": { "version": "4.6.2", @@ -28377,11 +28377,6 @@ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==" }, - "hermes-engine": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.11.0.tgz", - "integrity": "sha512-7aMUlZja2IyLYAcZ69NBnwJAR5ZOYlSllj0oMpx08a8HzxHOys0eKCzfphrf6D0vX1JGO1QQvVsQKe6TkYherw==" - }, "hermes-parser": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.5.0.tgz", @@ -28817,7 +28812,6 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-2.8.0.tgz", "integrity": "sha512-kJvf/UWLBMaGCs9X66MKq5zdFMgwx8D0nHnolbHR7s8ZnbLdb7TlQ/yuzIXqn/9wABfnwtNRI3CyaP1aHWMmZg==", - "peer": true, "requires": { "@babel/plugin-transform-object-assign": "^7.16.7", "@babel/preset-typescript": "^7.16.7", @@ -28871,9 +28865,9 @@ } }, "react-native-track-player": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/react-native-track-player/-/react-native-track-player-2.1.3.tgz", - "integrity": "sha512-JWKFRu+hr1ECN339RH+c+XDM7HfwvdvS4H1p4cJbhg/9b1CQGPJSrYXEhYkngN0msoxBxAjFyFIhjT2fWDCltA==", + "version": "2.2.0-rc3", + "resolved": "https://registry.npmjs.org/react-native-track-player/-/react-native-track-player-2.2.0-rc3.tgz", + "integrity": "sha512-Pvmum3MQ5PE8/yOIIPsk00zZk3EzdocUuVUwuBKSCmdKK/3O9YhnZRC3EuT59XCDm23pZZJZDSR44VeXN6Gamg==", "requires": {} }, "react-native-webview": { @@ -29401,8 +29395,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "peer": true + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "setprototypeof": { "version": "1.1.1", @@ -29816,8 +29809,7 @@ "string-hash-64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/string-hash-64/-/string-hash-64-1.0.3.tgz", - "integrity": "sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==", - "peer": true + "integrity": "sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==" }, "string-length": { "version": "4.0.2", diff --git a/package.json b/package.json index 0a7d57c..2df2cf3 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "date-fns": "^2.28.0", "events": "^3.3.0", "fuse.js": "^6.6.0", + "hermes-engine": "^0.11.0", "i18n-js": "^3.9.2", "lodash": "^4.17.21", "react": "^17.0.2", @@ -42,12 +43,13 @@ "react-native-fs": "^2.19.0", "react-native-gesture-handler": "^2.4.1", "react-native-localize": "^2.2.1", + "react-native-reanimated": "^2.8.0", "react-native-safe-area-context": "^4.2.5", "react-native-screens": "^3.13.1", "react-native-shadow-2": "^6.0.5", "react-native-svg": "^12.3.0", "react-native-svg-transformer": "^1.0.0", - "react-native-track-player": "^2.1.3", + "react-native-track-player": "^2.2.0-rc3", "react-native-webview": "^11.18.2", "react-redux": "^7.2.6", "redux": "^4.2.0", diff --git a/src/components/App.tsx b/src/components/App.tsx index 712c699..f56b808 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -12,6 +12,7 @@ import { import { useColorScheme } from 'react-native'; import { ColorSchemeContext, themes } from './Colors'; import DownloadManager from './DownloadManager'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; // import ErrorReportingAlert from 'utility/ErrorReportingAlert'; export default function App(): JSX.Element { @@ -30,7 +31,8 @@ export default function App(): JSX.Element { Capability.SkipToPrevious, Capability.Stop, Capability.SeekTo, - ] + ], + stopWithApp: true }); } setupTrackPlayer(); @@ -41,8 +43,10 @@ export default function App(): JSX.Element { - - + + + + diff --git a/src/components/Progresstrack.tsx b/src/components/Progresstrack.tsx new file mode 100644 index 0000000..31ed454 --- /dev/null +++ b/src/components/Progresstrack.tsx @@ -0,0 +1,52 @@ +import { THEME_COLOR } from 'CONSTANTS'; +import styled from 'styled-components/native'; +import Animated from 'react-native-reanimated'; + +export function getSeconds(seconds: number): string { + 'worklet'; + return Math.floor(seconds % 60).toString().padStart(2, '0'); +} + +export function getMinutes(seconds: number): number { + 'worklet'; + return Math.floor(seconds / 60); +} + +export function calculateProgressTranslation( + position: number, + reference: number, + width: number, +) { + 'worklet'; + const completion = position / reference; + const output = (1 - (completion || 0)) * -1 * width; + return output; +} + +export const ProgressTrackContainer = styled.View` + overflow: hidden; + height: 5px; + flex: 1; + flex-direction: row; + align-items: center; + position: relative; + border-radius: 6px; +`; + +export interface ProgressTrackProps { + opacity?: number; + stroke?: number; +} + +const ProgressTrack = styled(Animated.View)` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: ${(props) => props.stroke ? props.stroke + 'px' : '100%'}; + background-color: ${THEME_COLOR}; + opacity: ${(props) => props.opacity || 1}; + border-radius: 99px; +`; + +export default ProgressTrack; \ No newline at end of file diff --git a/src/components/ReText.tsx b/src/components/ReText.tsx new file mode 100644 index 0000000..51db0d3 --- /dev/null +++ b/src/components/ReText.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type { TextProps as RNTextProps } from 'react-native'; +import { StyleSheet, TextInput } from 'react-native'; +import Animated, { useAnimatedProps } from 'react-native-reanimated'; + +const styles = StyleSheet.create({ + baseStyle: { + color: 'black', + }, +}); +Animated.addWhitelistedNativeProps({ text: true }); + +interface TextProps { + text: Animated.SharedValue; + style?: Animated.AnimateProps['style']; +} + +const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); + +const ReText = (props: TextProps) => { + const { text, style } = { style: {}, ...props }; + const animatedProps = useAnimatedProps(() => { + return { + text: text.value, + // Here we use any because the text prop is not available in the type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + }); + return ( + + ); +}; + +export default ReText; \ No newline at end of file diff --git a/src/components/Typography.ts b/src/components/Typography.ts index abad679..ae6416d 100644 --- a/src/components/Typography.ts +++ b/src/components/Typography.ts @@ -3,12 +3,12 @@ import Text from './Text'; export const Header = styled(Text)` margin: 0 0 6px 0; - font-size: 24px; + font-size: 28px; font-weight: 400; `; export const SubHeader = styled(Text)` - font-size: 14px; + font-size: 16px; margin: 0 0 6px 0; font-weight: 400; opacity: 0.5; diff --git a/src/screens/Music/overlays/NowPlaying/index.tsx b/src/screens/Music/overlays/NowPlaying/index.tsx index 50b1251..c8a9982 100644 --- a/src/screens/Music/overlays/NowPlaying/index.tsx +++ b/src/screens/Music/overlays/NowPlaying/index.tsx @@ -7,12 +7,13 @@ import PlayIcon from 'assets/icons/play.svg'; import PauseIcon from 'assets/icons/pause.svg'; import useCurrentTrack from 'utility/useCurrentTrack'; import TrackPlayer, { State, usePlaybackState, useProgress } from 'react-native-track-player'; -import { THEME_COLOR } from 'CONSTANTS'; import { Shadow } from 'react-native-shadow-2'; import usePrevious from 'utility/usePrevious'; import Text from 'components/Text'; import useDefaultStyles, { ColoredBlurView } from 'components/Colors'; import { useNavigation } from '@react-navigation/native'; +import { calculateProgressTranslation } from 'components/Progresstrack'; +import { THEME_COLOR } from 'CONSTANTS'; const NOW_PLAYING_POPOVER_MARGIN = 6; const NOW_PLAYING_POPOVER_WIDTH = Dimensions.get('screen').width - 2 * NOW_PLAYING_POPOVER_MARGIN; @@ -38,6 +39,17 @@ const InnerContainer = styled.Pressable` align-items: center; `; +const ProgressTrack = styled(Animated.View)<{ stroke?: number; opacity?: number}>` + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: ${(props) => props.stroke ? props.stroke + 'px' : '100%'}; + background-color: ${THEME_COLOR}; + opacity: ${(props) => props.opacity || 1}; + border-radius: 99px; +`; + const ShadowOverlay = styled.View` position: absolute; top: 0; @@ -61,21 +73,6 @@ const ActionButton = styled.Pressable` margin-right: 8px; `; -interface ProgressTrackProps { - opacity?: number; -} - -const ProgressTrack = styled(Animated.View)` - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 2px; - background-color: ${THEME_COLOR}; - opacity: ${(props) => props.opacity || 1}; - border-radius: 99px; -`; - function SelectActionButton() { const state = usePlaybackState(); const defaultStyles = useDefaultStyles(); @@ -108,11 +105,6 @@ function SelectActionButton() { } } -function calculateProgressTranslation(position: number, reference: number) { - const completion = position / reference; - return (1 - (completion || 0)) * -1 * NOW_PLAYING_POPOVER_WIDTH; -} - function NowPlaying() { const { index, track } = useCurrentTrack(); const { buffered, duration, position } = useProgress(); @@ -130,13 +122,13 @@ function NowPlaying() { const hasChangedTrack = previousIndex !== index || duration === 0; Animated.timing(bufferAnimation.current, { - toValue: calculateProgressTranslation(buffered, duration), + toValue: calculateProgressTranslation(buffered, duration, NOW_PLAYING_POPOVER_WIDTH), duration: hasChangedTrack ? 0 : 500, useNativeDriver: true, easing: Easing.ease, }).start(); Animated.timing(progressAnimation.current, { - toValue: calculateProgressTranslation(position, duration), + toValue: calculateProgressTranslation(position, duration, NOW_PLAYING_POPOVER_WIDTH), duration: hasChangedTrack ? 0 : 500, useNativeDriver: true, }).start(); @@ -168,9 +160,11 @@ function NowPlaying() { diff --git a/src/screens/modals/Player/components/ProgressBar.tsx b/src/screens/modals/Player/components/ProgressBar.tsx index e74791d..d925a5d 100644 --- a/src/screens/modals/Player/components/ProgressBar.tsx +++ b/src/screens/modals/Player/components/ProgressBar.tsx @@ -1,95 +1,187 @@ -import React, { Component } from 'react'; -import TrackPlayer from 'react-native-track-player'; +import React, { useEffect } from 'react'; +import TrackPlayer, { useProgress } from 'react-native-track-player'; import styled from 'styled-components/native'; -import { Text, Platform } from 'react-native'; -import Slider from '@react-native-community/slider'; +import ProgressTrack, { + calculateProgressTranslation, + getMinutes, + getSeconds, + ProgressTrackContainer +} from 'components/Progresstrack'; +import { Gesture, GestureDetector, gestureHandlerRootHOC } from 'react-native-gesture-handler'; import { THEME_COLOR } from 'CONSTANTS'; -import { DefaultStylesProvider } from 'components/Colors'; +import Reanimated, { + useSharedValue, + useAnimatedStyle, + withTiming, + Easing, + useDerivedValue, + runOnJS, +} from 'react-native-reanimated'; +import ReText from 'components/ReText'; + +const DRAG_HANDLE_SIZE = 20; + +const Container = styled.View` + margin-top: 28px; +`; const NumberBar = styled.View` flex-direction: row; justify-content: space-between; width: 100%; - padding: 20px 0; + padding: 8px 0; `; -function getSeconds(seconds: number): string { - return Math.floor(seconds % 60).toString().padStart(2, '0'); -} +const Number = styled(ReText)` + font-size: 13px; +`; -function getMinutes(seconds: number): number { - return Math.floor(seconds / 60); -} +const DragHandle = styled(Reanimated.View)` + width: ${DRAG_HANDLE_SIZE}px; + height: ${DRAG_HANDLE_SIZE}px; + border-radius: ${DRAG_HANDLE_SIZE}px; + background-color: ${THEME_COLOR}; + position: absolute; + left: -${DRAG_HANDLE_SIZE / 2}px; + top: -${DRAG_HANDLE_SIZE / 2 - 2.5}px; + z-index: 14; +`; -interface State { - position: number; - duration: number; - gesture?: number; -} +function ProgressBar() { + const { position, buffered, duration } = useProgress(); -export default class ProgressBar extends Component<{}, State> { - state: State = { - position: 0, - duration: 0, - }; + const width = useSharedValue(0); + const pos = useSharedValue(0); + const buf = useSharedValue(0); + const dur = useSharedValue(0); - timer: number | null = null; + const isDragging = useSharedValue(false); + const offset = useSharedValue(0); - componentDidMount() { - this.timer = setInterval(this.updateProgress, 500); - } + const bufferAnimation = useDerivedValue(() => { + return calculateProgressTranslation(buf.value, dur.value, width.value); + }, [[dur, buf, width.value]]); - componentWillUnmount() { - if (this.timer) { - clearInterval(this.timer); + const progressAnimation = useDerivedValue(() => { + if (isDragging.value) { + return calculateProgressTranslation(offset.value, width.value, width.value); + } else { + return calculateProgressTranslation(pos.value, dur.value, width.value); } - } + }); - updateProgress = async () => { - const [position, duration] = await Promise.all([ - TrackPlayer.getPosition(), - TrackPlayer.getDuration(), - ]); + const timePassed = useDerivedValue(() => { + if (isDragging.value) { + const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value; + return getMinutes(currentPosition) + ':' + getSeconds(currentPosition); + } else { + return getMinutes(pos.value) + ':' + getSeconds(pos.value); + } + }, [pos]); - this.setState({ position, duration }); - }; + const timeRemaining = useDerivedValue(() => { + if (isDragging.value) { + const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value; + const remaining = (currentPosition - dur.value) * -1; + return `-${getMinutes(remaining)}:${getSeconds((remaining))}`; + } else { + const remaining = (pos.value - dur.value) * -1; + return `-${getMinutes(remaining)}:${getSeconds((remaining))}`; + } + }, [pos, dur]); + + const gesture = Gesture.Pan() + .onBegin(() => { + isDragging.value = true; + }).onUpdate((e) => { + offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2); + }).onFinalize(() => { + pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value; + isDragging.value = false; + runOnJS(TrackPlayer.seekTo)(pos.value); + }); - handleGesture = async (gesture: number) => { - // Set relative translation in state - this.setState({ gesture }); - }; + useEffect(() => { + pos.value = position; + buf.value = buffered; + dur.value = duration; + }, [position, buffered, duration]); - handleEndOfGesture = (position: number) => { - // Calculate and set the new position - TrackPlayer.seekTo(position); - this.setState({ gesture: undefined, position }); - }; - - render() { - const { position, duration, gesture } = this.state; - - return ( - - {defaultStyle => ( - <> - - - {getMinutes(gesture || position)}:{getSeconds(gesture || position)} - {getMinutes(duration)}:{getSeconds(duration)} - - - ) + const dragHandleStyles = useAnimatedStyle(() => { + return { + transform: [ + { translateX: offset.value }, + { + scale: withTiming(isDragging.value ? 1 : 0.05, { + duration: 100, + easing: Easing.out(Easing.ease), + }) } - - ); - } -} \ No newline at end of file + ], + }; + }); + + const bufferStyles = useAnimatedStyle(() => ({ + transform: [ + { translateX: bufferAnimation.value } + ] + })); + + const progressStyles = useAnimatedStyle(() => { + return { + transform: [ + { translateX: progressAnimation.value } + ] + }; + }); + + const timePassedStyles = useAnimatedStyle(() => { + return { + transform: [ + { translateY: withTiming(isDragging.value && offset.value < 48 ? 12 : 0, { + duration: 145, + easing: Easing.ease + }) }, + ], + }; + }); + + const timeRemainingStyles = useAnimatedStyle(() => { + return { + transform: [ + { translateY: withTiming(isDragging.value && offset.value > width.value - 48 ? 12 : 0, { + duration: 150, + easing: Easing.ease + }) }, + ], + }; + }); + + return ( + { width.value = e.nativeEvent.layout.width; }}> + + <> + + + + + + + + + + + + + + ); +} + +export default gestureHandlerRootHOC(ProgressBar); diff --git a/src/screens/modals/Player/index.tsx b/src/screens/modals/Player/index.tsx index fbefb89..e545e3f 100644 --- a/src/screens/modals/Player/index.tsx +++ b/src/screens/modals/Player/index.tsx @@ -21,8 +21,8 @@ export default function Player() { - + );