From b21766a352e8578facc7959a3b35377b781285d4 Mon Sep 17 00:00:00 2001 From: Lei Nelissen Date: Tue, 10 May 2022 23:52:58 +0200 Subject: [PATCH] Revamp the pop-up player modal --- ios/Podfile.lock | 12 +-- package-lock.json | 32 +++--- package.json | 2 +- src/assets/icons/airplay-audio.svg | 1 + src/assets/icons/backward-end.svg | 10 ++ src/assets/icons/forward-end.svg | 10 ++ src/components/Button.tsx | 9 +- src/components/Colors.tsx | 8 +- src/components/Divider.tsx | 18 ++++ src/components/DownloadManager.ts | 4 +- .../Music/stacks/components/TrackListView.tsx | 4 +- .../Player/components/Casting.android.tsx | 5 +- .../modals/Player/components/Casting.ios.tsx | 63 ++++++++--- .../Player/components/MediaControls.tsx | 69 ++---------- .../modals/Player/components/ProgressBar.tsx | 2 +- .../modals/Player/components/Queue.tsx | 102 +++++++++++++++--- .../modals/Player/components/StreamStatus.tsx | 61 +++++++++++ src/screens/modals/Player/index.tsx | 2 + src/utility/JellyfinApi.ts | 1 + src/utility/ticksToDuration.ts | 9 ++ src/utility/usePlayTracks.ts | 2 +- 21 files changed, 295 insertions(+), 131 deletions(-) create mode 100644 src/assets/icons/airplay-audio.svg create mode 100644 src/assets/icons/backward-end.svg create mode 100644 src/assets/icons/forward-end.svg create mode 100644 src/components/Divider.tsx create mode 100644 src/screens/modals/Player/components/StreamStatus.tsx create mode 100644 src/utility/ticksToDuration.ts diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9439ba9..63e1afc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -114,6 +114,8 @@ PODS: - React-RCTSettings (= 0.68.1) - React-RCTText (= 0.68.1) - React-RCTVibration (= 0.68.1) + - react-airplay (1.1.2): + - React-Core - React-callinvoker (0.68.1) - React-Codegen (0.68.1): - FBReactNativeSpec (= 0.68.1) @@ -291,8 +293,6 @@ PODS: - React-jsinspector (0.68.1) - React-logger (0.68.1): - glog - - react-native-airplay-button (1.1.0): - - React-Core - react-native-blur (0.8.0): - React - react-native-flipper (0.127.0): @@ -504,6 +504,7 @@ DEPENDENCIES: - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - React (from `../node_modules/react-native/`) + - react-airplay (from `../node_modules/react-airplay`) - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) - React-Codegen (from `build/generated/ios`) - React-Core (from `../node_modules/react-native/`) @@ -515,7 +516,6 @@ DEPENDENCIES: - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - - react-native-airplay-button (from `../node_modules/react-native-airplay-button`) - "react-native-blur (from `../node_modules/@react-native-community/blur`)" - react-native-flipper (from `../node_modules/react-native-flipper`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" @@ -591,6 +591,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/Libraries/TypeSafety" React: :path: "../node_modules/react-native/" + react-airplay: + :path: "../node_modules/react-airplay" React-callinvoker: :path: "../node_modules/react-native/ReactCommon/callinvoker" React-Codegen: @@ -609,8 +611,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsinspector" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" - react-native-airplay-button: - :path: "../node_modules/react-native-airplay-button" react-native-blur: :path: "../node_modules/@react-native-community/blur" react-native-flipper: @@ -700,6 +700,7 @@ SPEC CHECKSUMS: RCTRequired: 00581111c53531e39e3c6346ef0d2c0cf52a5a37 RCTTypeSafety: 07e03ee7800e7dd65cba8e52ad0c2edb06c96604 React: e61f4bf3c573d0c61c56b53dc3eb1d9daf0768a0 + react-airplay: 19bc646b4cc698c00772f4cb0e45ca8e280d4c6e React-callinvoker: 047d47230bb6fd66827f8cb0bea4e944ffd1309b React-Codegen: bb0403cde7374af091530e84e492589485aab480 React-Core: a4a3a8e10d004b08e013c3d0438259dd89a3894c @@ -709,7 +710,6 @@ SPEC CHECKSUMS: React-jsiexecutor: 4a4bae5671b064a2248a690cf75957669489d08c React-jsinspector: 218a2503198ff28a085f8e16622a8d8f507c8019 React-logger: f79dd3cc0f9b44f5611c6c7862badd891a862cf8 - react-native-airplay-button: 90c7ba52402c8e92342003b8a1ff78dfb4357a9e react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c react-native-flipper: b9e2e817604af8da0d5a9ba20a8516e780e30f3c react-native-netinfo: 3671b091c4843fda5e153612866ef4024b8f5d62 diff --git a/package-lock.json b/package-lock.json index 0abc9b8..9500a8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,8 +30,8 @@ "i18n-js": "^3.9.2", "lodash": "^4.17.21", "react": "^17.0.2", + "react-airplay": "^1.1.2", "react-native": "^0.68.1", - "react-native-airplay-button": "^1.1.0", "react-native-collapsible": "^1.6.0", "react-native-dotenv": "^3.3.1", "react-native-fast-image": "^8.5.11", @@ -14241,6 +14241,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-airplay": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/react-airplay/-/react-airplay-1.1.2.tgz", + "integrity": "sha512-elsnY0VLF5N0JqY+jdqFd28BmTnzhda0ljV4vTexFcdhrB6uht70J5QUN2E25U4Y0DWlcgprlxAYfWDbUOjTCQ==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-devtools-core": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.4.tgz", @@ -14314,15 +14323,6 @@ "react": "17.0.2" } }, - "node_modules/react-native-airplay-button": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/react-native-airplay-button/-/react-native-airplay-button-1.1.0.tgz", - "integrity": "sha512-UifB3XLh7AS9jWSz4Rn5kzFqmuXNa9NiZqn7BUNGjyie/4AO2omyXfeE3RVjO3rEZz7zBcP3/Wgd0QDBcaSt2Q==", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/react-native-codegen": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.0.16.tgz", @@ -28093,6 +28093,12 @@ "object-assign": "^4.1.1" } }, + "react-airplay": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/react-airplay/-/react-airplay-1.1.2.tgz", + "integrity": "sha512-elsnY0VLF5N0JqY+jdqFd28BmTnzhda0ljV4vTexFcdhrB6uht70J5QUN2E25U4Y0DWlcgprlxAYfWDbUOjTCQ==", + "requires": {} + }, "react-devtools-core": { "version": "4.24.4", "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.24.4.tgz", @@ -28732,12 +28738,6 @@ } } }, - "react-native-airplay-button": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/react-native-airplay-button/-/react-native-airplay-button-1.1.0.tgz", - "integrity": "sha512-UifB3XLh7AS9jWSz4Rn5kzFqmuXNa9NiZqn7BUNGjyie/4AO2omyXfeE3RVjO3rEZz7zBcP3/Wgd0QDBcaSt2Q==", - "requires": {} - }, "react-native-codegen": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/react-native-codegen/-/react-native-codegen-0.0.16.tgz", diff --git a/package.json b/package.json index ba794b9..1d4a8cd 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "i18n-js": "^3.9.2", "lodash": "^4.17.21", "react": "^17.0.2", + "react-airplay": "^1.1.2", "react-native": "^0.68.1", - "react-native-airplay-button": "^1.1.0", "react-native-collapsible": "^1.6.0", "react-native-dotenv": "^3.3.1", "react-native-fast-image": "^8.5.11", diff --git a/src/assets/icons/airplay-audio.svg b/src/assets/icons/airplay-audio.svg new file mode 100644 index 0000000..46737ec --- /dev/null +++ b/src/assets/icons/airplay-audio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/backward-end.svg b/src/assets/icons/backward-end.svg new file mode 100644 index 0000000..f9ec689 --- /dev/null +++ b/src/assets/icons/backward-end.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/forward-end.svg b/src/assets/icons/forward-end.svg new file mode 100644 index 0000000..d7f1848 --- /dev/null +++ b/src/assets/icons/forward-end.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 05179bc..cc58a91 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -30,10 +30,6 @@ const ButtonText = styled.Text<{ active?: boolean }>` color: ${THEME_COLOR}; font-weight: 500; font-size: 14px; - - ${props => props.active && css` - color: white; - `} `; const Button = React.forwardRef(function Button(props, ref) { @@ -47,20 +43,19 @@ const Button = React.forwardRef(function Button(props, ref) { {Icon && + ); +} + +export default Divider; \ No newline at end of file diff --git a/src/components/DownloadManager.ts b/src/components/DownloadManager.ts index e700851..8bd7dde 100644 --- a/src/components/DownloadManager.ts +++ b/src/components/DownloadManager.ts @@ -67,8 +67,6 @@ function DownloadManager () { return; } - console.log(ids); - /** * Whenever the store is cleared, existing downloads get "lost" because * the only reference we have is the store. This function checks for @@ -94,7 +92,7 @@ function DownloadManager () { dispatch(completeDownload({ id, location: file.path, - size: Number.parseInt(file.size), + size: file.size, })); }); } diff --git a/src/screens/Music/stacks/components/TrackListView.tsx b/src/screens/Music/stacks/components/TrackListView.tsx index 3a2b6da..25e7eae 100644 --- a/src/screens/Music/stacks/components/TrackListView.tsx +++ b/src/screens/Music/stacks/components/TrackListView.tsx @@ -25,6 +25,7 @@ import { Header, SubHeader } from 'components/Typography'; import { Text } from 'components/Typography'; import CoverImage from 'components/CoverImage'; +import ticksToDuration from 'utility/ticksToDuration'; const styles = StyleSheet.create({ index: { @@ -170,8 +171,7 @@ const TrackListView: React.FC = ({ ]} numberOfLines={1} > - {Math.round(tracks[trackId]?.RunTimeTicks / 10000000 / 60)} - :{Math.round(tracks[trackId]?.RunTimeTicks / 10000000 % 60).toString().padStart(2, '0')} + {ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)} diff --git a/src/screens/modals/Player/components/Casting.android.tsx b/src/screens/modals/Player/components/Casting.android.tsx index b923881..31a4fa1 100644 --- a/src/screens/modals/Player/components/Casting.android.tsx +++ b/src/screens/modals/Player/components/Casting.android.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Text } from 'react-native'; import styled from 'styled-components/native'; const Button = styled.View` @@ -7,7 +8,9 @@ const Button = styled.View` function Casting() { return ( - ); } diff --git a/src/screens/modals/Player/components/Casting.ios.tsx b/src/screens/modals/Player/components/Casting.ios.tsx index 60c10a8..89c22cc 100644 --- a/src/screens/modals/Player/components/Casting.ios.tsx +++ b/src/screens/modals/Player/components/Casting.ios.tsx @@ -1,24 +1,57 @@ +import { Text } from 'components/Typography'; import { THEME_COLOR } from 'CONSTANTS'; -import React from 'react'; -import AirPlayButton from 'react-native-airplay-button'; -import styled from 'styled-components/native'; -import { CastingProps } from './Casting'; +import React, { useCallback } from 'react'; +import { showRoutePicker, useAirplayRoutes } from 'react-airplay'; +import { TouchableOpacity } from 'react-native'; +import styled, { css } from 'styled-components/native'; +import AirplayAudioIcon from 'assets/icons/airplay-audio.svg'; +import useDefaultStyles from 'components/Colors'; -const Button = styled.View` - margin: 20px 40px; +const Container = styled.View<{ active?: boolean }>` + display: flex; + flex-direction: row; + align-items: center; + flex: 1 1 auto; + + ${(props) => props.active && css` + padding: 8px; + margin: -8px 0; + border-radius: 8px; + `} `; -function Casting({ fill }: CastingProps) { +const Label = styled(Text)<{ active?: boolean }>` + margin-left: 8px; + opacity: 0.5; + font-size: 13px; + + ${(props) => props.active && css` + color: ${THEME_COLOR}; + opacity: 1; + `} +`; + +function Casting() { + const defaultStyles = useDefaultStyles(); + const routes = useAirplayRoutes(); + const handleClick = useCallback(() => showRoutePicker({}), []); + return ( - <> - - + + + ); } diff --git a/src/screens/modals/Player/components/MediaControls.tsx b/src/screens/modals/Player/components/MediaControls.tsx index 15afb4d..99e53a8 100644 --- a/src/screens/modals/Player/components/MediaControls.tsx +++ b/src/screens/modals/Player/components/MediaControls.tsx @@ -1,19 +1,14 @@ -import React, { useState, useCallback } from 'react'; -import TrackPlayer, { Event, State, usePlaybackState, useTrackPlayerEvents } from 'react-native-track-player'; +import React from 'react'; +import TrackPlayer, { State, usePlaybackState } from 'react-native-track-player'; import { TouchableOpacity, useColorScheme } from 'react-native'; import styled from 'styled-components/native'; import { useHasNextQueue, useHasPreviousQueue } from 'utility/useQueue'; -import ForwardIcon from 'assets/icons/forwards.svg'; -import BackwardIcon from 'assets/icons/backwards.svg'; +import ForwardIcon from 'assets/icons/forward-end.svg'; +import BackwardIcon from 'assets/icons/backward-end.svg'; import PlayIcon from 'assets/icons/play.svg'; import PauseIcon from 'assets/icons/pause.svg'; -import RepeatIcon from 'assets/icons/repeat.svg'; -// import ShuffleIcon from 'assets/icons/shuffle.svg'; -import { THEME_COLOR } from 'CONSTANTS'; -import Casting from './Casting'; const BUTTON_SIZE = 40; -const BUTTON_SIZE_SMALL = 25; const pause = TrackPlayer.pause; const play = TrackPlayer.play; @@ -22,7 +17,7 @@ const previous = TrackPlayer.skipToPrevious; const Container = styled.View` align-items: center; - margin: 20px 0; + margin-top: 40px; `; const Buttons = styled.View` @@ -33,7 +28,8 @@ const Buttons = styled.View` `; const Button = styled.View` - margin: 20px 40px; + margin: 0 40px; + opacity: 0.75; `; export default function MediaControls() { @@ -51,12 +47,6 @@ export default function MediaControls() { - - - - ); } @@ -81,51 +71,6 @@ export function NextButton({ fill }: { fill: string }) { ); } -export function RepeatButton({ fill }: { fill: string}) { - const [isRepeating, setRepeating] = useState(false); - const handlePress = useCallback(() => setRepeating(!isRepeating), [isRepeating, setRepeating]); - - // The callback that should determine whether we need to repeeat or not - useTrackPlayerEvents([Event.PlaybackQueueEnded], async () => { - if (isRepeating) { - // Skip to the first track - await TrackPlayer.skip(0); - - // Cautiously reset the seek time, as there might only be a single - // item in queue. - await TrackPlayer.seekTo(0); - - // Then play the item - await TrackPlayer.play(); - } - }); - - return ( - - - - ); -} - -// export function ShuffleButton({ fill }: { fill: string}) { -// const [isShuffling, setShuffling] = useState(false); -// const handlePress = useCallback(() => setShuffling(!isShuffling), [isShuffling, setShuffling]); - -// return ( -// -// -// -// ); -// } - export function MainButton({ fill }: { fill: string }) { const state = usePlaybackState(); diff --git a/src/screens/modals/Player/components/ProgressBar.tsx b/src/screens/modals/Player/components/ProgressBar.tsx index 0405843..a26060e 100644 --- a/src/screens/modals/Player/components/ProgressBar.tsx +++ b/src/screens/modals/Player/components/ProgressBar.tsx @@ -20,7 +20,7 @@ import Reanimated, { import ReText from 'components/ReText'; const DRAG_HANDLE_SIZE = 20; -const PADDING_TOP = 14; +const PADDING_TOP = 12; const Container = styled.View` padding-top: ${PADDING_TOP}px; diff --git a/src/screens/modals/Player/components/Queue.tsx b/src/screens/modals/Player/components/Queue.tsx index a3dd233..7efe3d5 100644 --- a/src/screens/modals/Player/components/Queue.tsx +++ b/src/screens/modals/Player/components/Queue.tsx @@ -1,29 +1,53 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import useQueue from 'utility/useQueue'; 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 TrackPlayer, { RepeatMode } from 'react-native-track-player'; import { t } from '@localisation'; import useDefaultStyles from 'components/Colors'; import { Text } from 'components/Typography'; - +import RepeatIcon from 'assets/icons/repeat.svg'; import Button from 'components/Button'; import { THEME_COLOR } from 'CONSTANTS'; import DownloadIcon from 'components/DownloadIcon'; +import Divider from 'components/Divider'; +import ticksToDuration from 'utility/ticksToDuration'; + +const ICON_SIZE = 16; + +const Container = styled.View` + margin-top: 56px; +`; + +const Header = styled.View` + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 8px; +`; + +const IconButton = styled.TouchableOpacity` + padding: 8px; + border-radius: 4px; +`; + +const TextHalfOpacity = styled(Text)` + opacity: 0.5; +`; const QueueItem = styled.View<{ active?: boolean, alreadyPlayed?: boolean, isDark?: boolean }>` - padding: 10px; - border-bottom-width: 1px; + padding: 8px 0; flex: 0 0 auto; flex-direction: row; align-items: center; ${props => props.active && css` font-weight: 900; - padding: 20px 35px; - margin: 0 -25px; + padding: 8px 18px; + margin: 0 -18px; + border-radius: 8px; `} ${props => props.alreadyPlayed && css` @@ -44,18 +68,49 @@ const styles = StyleSheet.create({ export default function Queue() { const defaultStyles = useDefaultStyles(); const queue = useQueue(); + const [isRepeating, setIsRepeating] = useState(false); const { index: currentIndex } = useCurrentTrack(); + const playTrack = useCallback(async (index: number) => { await TrackPlayer.skip(index); await TrackPlayer.play(); }, []); + const clearQueue = useCallback(async () => { await TrackPlayer.reset(); }, []); + const toggleLoop = useCallback(() => { + setIsRepeating((prev) => { + TrackPlayer.setRepeatMode(prev ? RepeatMode.Off : RepeatMode.Queue); + return !prev; + }); + }, []); + + // Retrieve the repeat mode and assign it to the state on component mount + useEffect(() => { + TrackPlayer.getRepeatMode() + .then((mode) => { + setIsRepeating(mode === RepeatMode.Queue); + }); + }, []); + return ( - - {t('queue')} + +
+ {t('queue')} + + + + +
{queue.map((track, i) => ( - - {track.title} - {track.artist} + + + {track.title} + + + {track.artist}{track.album && ' — ' + track.album} + - - + + + {ticksToDuration(track.duration || 0)} + + + + @@ -80,6 +152,6 @@ export default function Queue() {