Revamp the pop-up player modal
This commit is contained in:
@@ -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
|
||||
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
src/assets/icons/airplay-audio.svg
Normal file
1
src/assets/icons/airplay-audio.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 56 56"><defs><clipPath id="clip-path"><path d="M39.31 44.79a.73.73 0 0 1-.54 1.21H17.23a.73.73 0 0 1-.54-1.21l10.68-12.3a.84.84 0 0 1 1.26 0Z"/></clipPath><clipPath id="clip-path-2"><path style="fill:none" d="M-4-3h64v64H-4z"/></clipPath><clipPath id="clip-path-3"><path d="M28 22a6 6 0 0 0-3.65 10.76.16.16 0 0 1 0 .23l-.86 1-.19.21a.18.18 0 0 1-.27 0l-.32-.2a8 8 0 1 1 10.56 0l-.31.25a.18.18 0 0 1-.27 0l-.2-.22-.85-1a.17.17 0 0 1 0-.24A6 6 0 0 0 28 22Zm0-5a11 11 0 0 0-7 19.49l.1.07a.15.15 0 0 1 0 .21L21 37l-.88 1a.2.2 0 0 1-.28 0h-.14a13 13 0 1 1 16.6 0l-.12.09a.14.14 0 0 1-.2 0L35.9 38l-.89-1-.14-.16a.14.14 0 0 1 0-.21l.14-.11A11 11 0 0 0 28 17ZM17.82 40.33a.21.21 0 0 1 0 .29l-.06.07-.92 1.07-.06.07a.2.2 0 0 1-.28 0l-.08-.07a18 18 0 1 1 23 .07.16.16 0 0 1-.21 0l-1-1.2-.05-.05a.16.16 0 0 1 0-.21h.06a16 16 0 1 0-20.49-.07Z"/></clipPath></defs><path d="M11.51 27.21h32.97V51H11.51z" style="clip-path:url(#clip-path)"/><path d="M5 5h46v41.92H5z" style="clip-path:url(#clip-path-3)"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
10
src/assets/icons/backward-end.svg
Normal file
10
src/assets/icons/backward-end.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3_22)">
|
||||
<path d="M0.689175 29.9526H3.63979C4.83259 29.9526 5.44782 29.3373 5.44782 28.1445V20.0084C5.3976 19.8451 5.38505 19.6819 5.38505 19.4936C5.38505 19.3178 5.3976 19.142 5.44782 18.9913V10.8677C5.44782 9.64983 4.83259 9.07226 3.63979 9.05971H0.689175C-0.503626 9.05971 -1.11886 9.67494 -1.11886 10.8677V28.1445C-1.13142 29.3373 -0.516182 29.9526 0.689175 29.9526ZM21.243 29.6136C22.3479 29.6136 23.2771 28.7849 23.2771 27.2531V20.0084C23.4403 20.6236 23.9049 21.1384 24.6456 21.5778L37.5153 29.1364C38.0552 29.4503 38.5324 29.6136 39.0848 29.6136C40.1897 29.6136 41.1189 28.7849 41.1189 27.2531V11.7592C41.1189 10.2274 40.1897 9.39871 39.0848 9.39871C38.5324 9.39871 38.0552 9.56194 37.5153 9.87583L24.6456 17.4344C23.8923 17.8864 23.4403 18.3761 23.2771 18.9913V11.7592C23.2771 10.2274 22.3605 9.39871 21.2556 9.39871C20.7031 9.39871 20.226 9.56194 19.6735 9.87583L6.80385 17.4344C6.06306 17.8864 5.59849 18.3761 5.44782 18.9913V20.0084C5.61105 20.6236 6.06306 21.1384 6.80385 21.5778L19.6735 29.1364C20.226 29.4503 20.6906 29.6136 21.243 29.6136Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3_22">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
10
src/assets/icons/forward-end.svg
Normal file
10
src/assets/icons/forward-end.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3_26)">
|
||||
<path d="M0.915182 29.5759C1.46764 29.5759 1.94476 29.4127 2.48466 29.0988L15.3544 21.5402C16.0951 21.1007 16.5597 20.5859 16.7104 19.9707V27.2154C16.7104 28.7347 17.6395 29.5759 18.7444 29.5759C19.2969 29.5759 19.774 29.4127 20.3139 29.0988L33.1836 21.5402C33.9369 21.1007 34.389 20.5859 34.5522 19.9707V28.1445C34.5522 29.3373 35.1549 29.9526 36.3602 29.9526H39.3108C40.5036 29.9526 41.1189 29.3373 41.1189 28.1445V10.8677C41.1189 9.64983 40.5036 9.05971 39.3108 9.05971H36.3602C35.1674 9.05971 34.5522 9.67494 34.5522 10.8677V18.9537C34.389 18.3384 33.9369 17.8488 33.1836 17.3968L20.3139 9.83817C19.774 9.52427 19.2969 9.36105 18.7444 9.36105C17.6395 9.36105 16.7104 10.1897 16.7104 11.7215V18.9537C16.5597 18.3384 16.1077 17.8488 15.3544 17.3968L2.48466 9.83817C1.9322 9.52427 1.46764 9.36105 0.902626 9.36105C-0.202285 9.36105 -1.11886 10.1897 -1.11886 11.7215V27.2154C-1.11886 28.7347 -0.189729 29.5759 0.915182 29.5759Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3_26">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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<View, ButtonProps>(function Button(props, ref) {
|
||||
@@ -47,20 +43,19 @@ const Button = React.forwardRef<View, ButtonProps>(function Button(props, ref) {
|
||||
<BaseButton
|
||||
{...rest}
|
||||
disabled={disabled}
|
||||
// @ts-expect-error styled-components has outdated react-native typings
|
||||
ref={ref}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
style={[
|
||||
props.style,
|
||||
{ backgroundColor: isPressed ? THEME_COLOR : defaultStyles.button.backgroundColor }
|
||||
{ backgroundColor: isPressed ? defaultStyles.activeBackground.backgroundColor : defaultStyles.button.backgroundColor }
|
||||
]}
|
||||
>
|
||||
{Icon &&
|
||||
<Icon
|
||||
width={14}
|
||||
height={14}
|
||||
fill={isPressed ? '#fff' : THEME_COLOR}
|
||||
fill={THEME_COLOR}
|
||||
style={{
|
||||
marginRight: 8,
|
||||
}}
|
||||
|
||||
@@ -57,7 +57,13 @@ function generateStyles(scheme: ColorSchemeName) {
|
||||
},
|
||||
stackHeader: {
|
||||
color: scheme === 'dark' ? 'white' : 'black'
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
color: scheme === 'dark' ? '#ffffff4d' : '#0000004d',
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: scheme === 'dark' ? '#333' : '#f6f6f6',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
18
src/components/Divider.tsx
Normal file
18
src/components/Divider.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ViewProps } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import useDefaultStyles from './Colors';
|
||||
|
||||
const Container = styled.View`
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
function Divider({ style }: ViewProps) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
return (
|
||||
<Container style={[defaultStyles.divider, style]} />
|
||||
);
|
||||
}
|
||||
|
||||
export default Divider;
|
||||
@@ -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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<TrackListViewProps> = ({
|
||||
]}
|
||||
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)}
|
||||
</Text>
|
||||
<DownloadIcon trackId={trackId} fill={currentTrack?.backendId === trackId ? `${THEME_COLOR}44` : undefined} />
|
||||
</View>
|
||||
|
||||
@@ -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 (
|
||||
<Button />
|
||||
<Button>
|
||||
<Text>Local Playback</Text>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Button>
|
||||
<AirPlayButton
|
||||
activeTintColor={THEME_COLOR}
|
||||
tintColor={fill}
|
||||
style={{ width: 40, height: 40 }}
|
||||
<TouchableOpacity onPress={handleClick} activeOpacity={0.6}>
|
||||
<Container style={routes.length ? defaultStyles.activeBackground : undefined} active={routes.length > 0}>
|
||||
<AirplayAudioIcon
|
||||
width={20}
|
||||
height={20}
|
||||
fill={routes.length > 0 ? THEME_COLOR : defaultStyles.textHalfOpacity.color}
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
<Label active={routes.length > 0} numberOfLines={1}>
|
||||
{routes.length > 0
|
||||
? `Playing on ${routes.map((route) => route.portName).join(', ')}`
|
||||
: 'Local Playback'
|
||||
}
|
||||
</Label>
|
||||
</Container>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
<NextButton fill={fill} />
|
||||
</Button>
|
||||
</Buttons>
|
||||
<Buttons>
|
||||
<Button>
|
||||
<RepeatButton fill={fill} />
|
||||
</Button>
|
||||
<Casting fill={fill} />
|
||||
</Buttons>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<TouchableOpacity onPress={handlePress} style={{ opacity: isRepeating ? 1 : 0.5 }}>
|
||||
<RepeatIcon
|
||||
width={BUTTON_SIZE_SMALL}
|
||||
height={BUTTON_SIZE_SMALL}
|
||||
fill={isRepeating ? THEME_COLOR : fill}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// export function ShuffleButton({ fill }: { fill: string}) {
|
||||
// const [isShuffling, setShuffling] = useState(false);
|
||||
// const handlePress = useCallback(() => setShuffling(!isShuffling), [isShuffling, setShuffling]);
|
||||
|
||||
// return (
|
||||
// <TouchableOpacity onPress={handlePress} style={{ opacity: isShuffling ? 1 : 0.5 }}>
|
||||
// <ShuffleIcon
|
||||
// width={BUTTON_SIZE_SMALL}
|
||||
// height={BUTTON_SIZE_SMALL}
|
||||
// fill={isShuffling ? THEME_COLOR : fill}
|
||||
// />
|
||||
// </TouchableOpacity>
|
||||
// );
|
||||
// }
|
||||
|
||||
export function MainButton({ fill }: { fill: string }) {
|
||||
const state = usePlaybackState();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<View>
|
||||
<Text style={{ marginTop: 20, marginBottom: 20 }}>{t('queue')}</Text>
|
||||
<Container>
|
||||
<Header>
|
||||
<Text>{t('queue')}</Text>
|
||||
<Divider style={{ marginHorizontal: 18 }} />
|
||||
<IconButton
|
||||
style={isRepeating ? defaultStyles.activeBackground : undefined}
|
||||
onPress={toggleLoop}
|
||||
>
|
||||
<RepeatIcon
|
||||
fill={isRepeating ? THEME_COLOR : defaultStyles.textHalfOpacity.color}
|
||||
width={ICON_SIZE}
|
||||
height={ICON_SIZE}
|
||||
/>
|
||||
</IconButton>
|
||||
</Header>
|
||||
{queue.map((track, i) => (
|
||||
<TouchableHandler id={i} onPress={playTrack} key={i}>
|
||||
<QueueItem
|
||||
@@ -67,12 +122,29 @@ export default function Queue() {
|
||||
currentIndex === i ? defaultStyles.activeBackground : {},
|
||||
]}
|
||||
>
|
||||
<View>
|
||||
<Text style={currentIndex === i ? { color: THEME_COLOR, fontWeight: '700' } : styles.trackTitle}>{track.title}</Text>
|
||||
<Text style={currentIndex === i ? { color: THEME_COLOR, fontWeight: '400' } : defaultStyles.textHalfOpacity}>{track.artist}</Text>
|
||||
<View style={{ flex: 1, marginRight: 16 }}>
|
||||
<Text
|
||||
style={[currentIndex === i ? { color: THEME_COLOR, fontWeight: '500' } : styles.trackTitle, { marginBottom: 2 }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{track.title}
|
||||
</Text>
|
||||
<TextHalfOpacity
|
||||
style={currentIndex === i ? { color: THEME_COLOR, fontWeight: '400' } : undefined}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{track.artist}{track.album && ' — ' + track.album}
|
||||
</TextHalfOpacity>
|
||||
</View>
|
||||
<View style={{ marginLeft: 'auto' }}>
|
||||
<DownloadIcon trackId={track.backendId} />
|
||||
<View style={{ marginLeft: 'auto', marginRight: 8 }}>
|
||||
<TextHalfOpacity
|
||||
style={currentIndex === i ? { color: THEME_COLOR, fontWeight: '400' } : undefined}
|
||||
>
|
||||
{ticksToDuration(track.duration || 0)}
|
||||
</TextHalfOpacity>
|
||||
</View>
|
||||
<View>
|
||||
<DownloadIcon trackId={track.backendId} fill={currentIndex === i ? THEME_COLOR + '80' : undefined} />
|
||||
</View>
|
||||
</QueueItem>
|
||||
</TouchableHandler>
|
||||
@@ -80,6 +152,6 @@ export default function Queue() {
|
||||
<ClearQueue>
|
||||
<Button title={t('clear-queue')} onPress={clearQueue} />
|
||||
</ClearQueue>
|
||||
</View>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
61
src/screens/modals/Player/components/StreamStatus.tsx
Normal file
61
src/screens/modals/Player/components/StreamStatus.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import useCurrentTrack from 'utility/useCurrentTrack';
|
||||
import CloudIcon from 'assets/icons/cloud.svg';
|
||||
import InternalDriveIcon from 'assets/icons/internal-drive.svg';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import { Text } from 'components/Typography';
|
||||
import styled from 'styled-components/native';
|
||||
import Casting from './Casting';
|
||||
|
||||
const ICON_SIZE = 16;
|
||||
|
||||
const Container = styled.View`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
margin-top: 24px;
|
||||
`;
|
||||
|
||||
const Group = styled.View`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: 8px;
|
||||
flex: 1 1 auto;
|
||||
`;
|
||||
|
||||
const Label = styled(Text)`
|
||||
margin-left: 8px;
|
||||
opacity: 0.5;
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
function StreamStatus() {
|
||||
const { track } = useCurrentTrack();
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
const isLocalPlay = useMemo(() => {
|
||||
const url = track?.url;
|
||||
return typeof url === 'string' ? url.startsWith('file://') : false;
|
||||
}, [track?.url]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Group>
|
||||
{isLocalPlay ? (
|
||||
<>
|
||||
<InternalDriveIcon width={ICON_SIZE} height={ICON_SIZE} fill={defaultStyles.icon.color} />
|
||||
<Label numberOfLines={1}>Local Playback</Label>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CloudIcon width={ICON_SIZE} height={ICON_SIZE} fill={defaultStyles.icon.color} />
|
||||
<Label numberOfLines={1}>Streaming</Label>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
<Casting />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default StreamStatus;
|
||||
@@ -8,6 +8,7 @@ import useDefaultStyles from 'components/Colors';
|
||||
import ConnectionNotice from './components/ConnectionNotice';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import StreamStatus from './components/StreamStatus';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
inner: {
|
||||
@@ -23,6 +24,7 @@ export default function Player() {
|
||||
<ScrollView contentContainerStyle={styles.inner} style={defaultStyles.view}>
|
||||
<NowPlaying />
|
||||
<ConnectionNotice />
|
||||
<StreamStatus />
|
||||
<ProgressBar />
|
||||
<MediaControls />
|
||||
<Queue />
|
||||
|
||||
@@ -42,6 +42,7 @@ export function generateTrack(track: AlbumTrack, credentials: Credentials): Trac
|
||||
title: track.Name,
|
||||
artist: track.Artists.join(', '),
|
||||
album: track.Album,
|
||||
duration: track.RunTimeTicks,
|
||||
artwork: track.AlbumId
|
||||
? getImage(track.AlbumId, credentials)
|
||||
: getImage(track.Id, credentials),
|
||||
|
||||
9
src/utility/ticksToDuration.ts
Normal file
9
src/utility/ticksToDuration.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
function ticksToDuration(ticks: number) {
|
||||
const seconds = Math.round(ticks / 10000000);
|
||||
const minutes = Math.round(seconds / 60);
|
||||
const hours = Math.round(minutes / 60);
|
||||
|
||||
return `${hours > 0 ? hours + ':' : ''}${minutes}:${(seconds % 60).toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export default ticksToDuration;
|
||||
@@ -57,7 +57,7 @@ export default function usePlayTracks() {
|
||||
// Check if a downloaded version exists, and if so rewrite the URL
|
||||
const download = downloads[trackId];
|
||||
if (download?.location) {
|
||||
generatedTrack.url = download.location;
|
||||
generatedTrack.url = 'file://' + download.location;
|
||||
}
|
||||
|
||||
return generatedTrack;
|
||||
|
||||
Reference in New Issue
Block a user