diff --git a/android/app/src/main/assets/fonts/Inter-VariableFont_slnt,wght.ttf b/android/app/src/main/assets/fonts/Inter-VariableFont_slnt,wght.ttf new file mode 100644 index 0000000..969a990 Binary files /dev/null and b/android/app/src/main/assets/fonts/Inter-VariableFont_slnt,wght.ttf differ diff --git a/ios/JellyfinAudioPlayer.xcodeproj/project.pbxproj b/ios/JellyfinAudioPlayer.xcodeproj/project.pbxproj index eb499d2..6fa181d 100644 --- a/ios/JellyfinAudioPlayer.xcodeproj/project.pbxproj +++ b/ios/JellyfinAudioPlayer.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 463612208457EBB4B723000A /* libPods-JellyfinAudioPlayer-JellyfinAudioPlayerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 842AB597D56E84A4ACDC4735 /* libPods-JellyfinAudioPlayer-JellyfinAudioPlayerTests.a */; }; 4FA1B23D2550A94C007A035E /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA1B23C2550A94C007A035E /* File.swift */; }; A807E2BB233D6F9347D8A95C /* libPods-JellyfinAudioPlayer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 71370E61E2CC6BD9372ADCF3 /* libPods-JellyfinAudioPlayer.a */; }; + 4C04FC6E055249ABB204D3BC /* Inter-VariableFont_slnt,wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4B4A0465FF364579B28CF5D7 /* Inter-VariableFont_slnt,wght.ttf */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -54,6 +55,7 @@ E35451F7979C52C1692C4C9F /* libPods-JellyfinAudioPlayer-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JellyfinAudioPlayer-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; + 4B4A0465FF364579B28CF5D7 /* Inter-VariableFont_slnt,wght.ttf */ = {isa = PBXFileReference; name = "Inter-VariableFont_slnt,wght.ttf"; path = "../src/assets/fonts/Inter-VariableFont_slnt,wght.ttf"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = unknown; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -152,6 +154,7 @@ 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, 46001D7383D71A837AAF6E07 /* Pods */, + CFCEB457E84E4C5195253CD7 /* Resources */, ); indentWidth = 2; sourceTree = ""; @@ -167,6 +170,15 @@ name = Products; sourceTree = ""; }; + CFCEB457E84E4C5195253CD7 /* Resources */ = { + isa = "PBXGroup"; + children = ( + 4B4A0465FF364579B28CF5D7 /* Inter-VariableFont_slnt,wght.ttf */, + ); + name = Resources; + sourceTree = ""; + path = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -268,6 +280,7 @@ files = ( 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + 4C04FC6E055249ABB204D3BC /* Inter-VariableFont_slnt,wght.ttf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/JellyfinAudioPlayer/Info.plist b/ios/JellyfinAudioPlayer/Info.plist index 582645b..50d2a4e 100644 --- a/ios/JellyfinAudioPlayer/Info.plist +++ b/ios/JellyfinAudioPlayer/Info.plist @@ -30,7 +30,7 @@ NSLocationWhenInUseUsageDescription - + UIBackgroundModes audio @@ -50,5 +50,9 @@ UIViewControllerBasedStatusBarAppearance + UIAppFonts + + Inter-VariableFont_slnt,wght.ttf + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b2912b7..3b360c8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -283,6 +283,8 @@ PODS: - glog - react-native-airplay-button (1.1.0): - React-Core + - react-native-blur (0.8.0): + - React - react-native-flipper (0.127.0): - React-Core - react-native-netinfo (7.1.7): @@ -441,6 +443,7 @@ DEPENDENCIES: - 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`)" - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -530,6 +533,8 @@ EXTERNAL SOURCES: :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: :path: "../node_modules/react-native-flipper" react-native-netinfo: @@ -622,6 +627,7 @@ SPEC CHECKSUMS: React-jsinspector: d0374f7509d407d2264168b6d0fad0b54e300b85 React-logger: 933f80c97c633ee8965d609876848148e3fef438 react-native-airplay-button: 90c7ba52402c8e92342003b8a1ff78dfb4357a9e + react-native-blur: cad4d93b364f91e7b7931b3fa935455487e5c33c react-native-flipper: b9e2e817604af8da0d5a9ba20a8516e780e30f3c react-native-netinfo: 27f287f2d191693f3b9d01a4273137fcf91c3b5d react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057 diff --git a/package-lock.json b/package-lock.json index 7e31247..bc0c172 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.2.4", "dependencies": { "@react-native-community/async-storage": "^1.12.1", + "@react-native-community/blur": "^3.6.0", "@react-native-community/masked-view": "^0.1.11", "@react-native-community/netinfo": "^7.1.7", "@react-native-community/picker": "^1.8.1", @@ -36,6 +37,7 @@ "react-native-localize": "^2.1.7", "react-native-safe-area-context": "^3.3.2", "react-native-screens": "^3.10.1", + "react-native-shadow-2": "^6.0.3", "react-native-svg": "^12.2.0", "react-native-svg-transformer": "^1.0.0", "react-native-track-player": "^2.1.2", @@ -2755,6 +2757,14 @@ "react-native": ">=0.59" } }, + "node_modules/@react-native-community/blur": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-3.6.0.tgz", + "integrity": "sha512-GtDBhpX2pQcjl4VopOC8FktrVufrEfYRwVeMQ2WWckqKIv2BdwvlvWvj88L1WmEdBr9UNcm3rtgz+d+YXkmirA==", + "dependencies": { + "prop-types": "^15.5.10" + } + }, "node_modules/@react-native-community/cli": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-6.3.1.tgz", @@ -13700,6 +13710,17 @@ "node": ">=6" } }, + "node_modules/polished": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.1.4.tgz", + "integrity": "sha512-Nq5Mbza+Auo7N3sQb1QMFaQiDO+4UexWuSGR7Cjb4Sw11SZIJcrrFtiZ+L0jT9MBsUsxDboHVASbCLbE1rnECg==", + "dependencies": { + "@babel/runtime": "^7.16.7" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -14082,6 +14103,18 @@ "react-native": "*" } }, + "node_modules/react-native-shadow-2": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/react-native-shadow-2/-/react-native-shadow-2-6.0.3.tgz", + "integrity": "sha512-gJP3tEEBvCus/jpclEI1RNKGsl0/nTwFzqFDdmSbqt7tLgDuy8r2oNYdn4hP1WAeT1fTH0EOh9bXDGB5ZfCV0A==", + "dependencies": { + "polished": "^4.1.4" + }, + "peerDependencies": { + "react-native": "*", + "react-native-svg": "^12.1.0" + } + }, "node_modules/react-native-svg": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-12.2.0.tgz", @@ -18573,6 +18606,14 @@ "deep-assign": "^3.0.0" } }, + "@react-native-community/blur": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@react-native-community/blur/-/blur-3.6.0.tgz", + "integrity": "sha512-GtDBhpX2pQcjl4VopOC8FktrVufrEfYRwVeMQ2WWckqKIv2BdwvlvWvj88L1WmEdBr9UNcm3rtgz+d+YXkmirA==", + "requires": { + "prop-types": "^15.5.10" + } + }, "@react-native-community/cli": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-6.3.1.tgz", @@ -27045,6 +27086,14 @@ "xmlbuilder": "^9.0.7" } }, + "polished": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.1.4.tgz", + "integrity": "sha512-Nq5Mbza+Auo7N3sQb1QMFaQiDO+4UexWuSGR7Cjb4Sw11SZIJcrrFtiZ+L0jT9MBsUsxDboHVASbCLbE1rnECg==", + "requires": { + "@babel/runtime": "^7.16.7" + } + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -27340,6 +27389,14 @@ "warn-once": "^0.1.0" } }, + "react-native-shadow-2": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/react-native-shadow-2/-/react-native-shadow-2-6.0.3.tgz", + "integrity": "sha512-gJP3tEEBvCus/jpclEI1RNKGsl0/nTwFzqFDdmSbqt7tLgDuy8r2oNYdn4hP1WAeT1fTH0EOh9bXDGB5ZfCV0A==", + "requires": { + "polished": "^4.1.4" + } + }, "react-native-svg": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-12.2.0.tgz", diff --git a/package.json b/package.json index 677d636..5c6f4b7 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@react-native-community/async-storage": "^1.12.1", + "@react-native-community/blur": "^3.6.0", "@react-native-community/masked-view": "^0.1.11", "@react-native-community/netinfo": "^7.1.7", "@react-native-community/picker": "^1.8.1", @@ -40,6 +41,7 @@ "react-native-localize": "^2.1.7", "react-native-safe-area-context": "^3.3.2", "react-native-screens": "^3.10.1", + "react-native-shadow-2": "^6.0.3", "react-native-svg": "^12.2.0", "react-native-svg-transformer": "^1.0.0", "react-native-track-player": "^2.1.2", diff --git a/react-native.config.js b/react-native.config.js index 8f354ef..6cd714b 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,7 +1,7 @@ module.exports = { project: { - ios:{}, - android:{} + ios: {}, + android: {} }, - assets:['./assets/fonts/'], + assets: ['./src/assets/fonts/'], }; diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 487754b..05179bc 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -14,7 +14,7 @@ interface ButtonProps extends PressableProps { } const BaseButton = styled.Pressable` - padding: 16px; + padding: 12px; border-radius: 8px; flex-direction: row; align-items: center; @@ -28,7 +28,8 @@ const BaseButton = styled.Pressable` const ButtonText = styled.Text<{ active?: boolean }>` color: ${THEME_COLOR}; - font-weight: 600; + font-weight: 500; + font-size: 14px; ${props => props.active && css` color: white; diff --git a/src/components/Colors.ts b/src/components/Colors.tsx similarity index 73% rename from src/components/Colors.ts rename to src/components/Colors.tsx index db3973a..64d5c72 100644 --- a/src/components/Colors.ts +++ b/src/components/Colors.tsx @@ -1,7 +1,8 @@ +import { BlurView, BlurViewProperties } from '@react-native-community/blur'; import { THEME_COLOR } from 'CONSTANTS'; -import React from 'react'; +import React, { PropsWithChildren } from 'react'; import { useContext } from 'react'; -import { ColorSchemeName, StyleSheet } from 'react-native'; +import { ColorSchemeName, StyleSheet, useColorScheme } from 'react-native'; /** * Function for generating both the dark and light stylesheets, so that they @@ -11,12 +12,20 @@ function generateStyles(scheme: ColorSchemeName) { return StyleSheet.create({ text: { color: scheme === 'dark' ? '#fff' : '#000', + fontSize: 14, + fontFamily: 'Inter', }, textHalfOpacity: { color: scheme === 'dark' ? '#ffffff88' : '#00000088', + fontSize: 14, + // fontFamily: 'Inter', + }, + textQuarterOpacity: { + color: scheme === 'dark' ? '#ffffff44' : '#00000044', + fontSize: 14, }, view: { - backgroundColor: scheme === 'dark' ? '#111' : '#f6f6f6', + backgroundColor: scheme === 'dark' ? '#111' : '#fff', }, border: { borderColor: scheme === 'dark' ? '#262626' : '#ddd', @@ -25,7 +34,7 @@ function generateStyles(scheme: ColorSchemeName) { backgroundColor: `${THEME_COLOR}${scheme === 'dark' ? '26' : '16'}`, }, imageBackground: { - backgroundColor: scheme === 'dark' ? '#333' : '#ddd', + backgroundColor: scheme === 'dark' ? '#161616' : '#eee', }, modal: { backgroundColor: scheme === 'dark' ? '#22222200' : '#eeeeee00', @@ -34,7 +43,7 @@ function generateStyles(scheme: ColorSchemeName) { backgroundColor: scheme === 'dark' ? '#000' : '#fff', }, button: { - backgroundColor: scheme === 'dark' ? '#161616' : '#e6e6e6', + backgroundColor: scheme === 'dark' ? '#161616' : '#eee', }, input: { backgroundColor: scheme === 'dark' ? '#161616' : '#e6e6e6', @@ -77,4 +86,12 @@ export function DefaultStylesProvider(props: DefaultStylesProviderProps) { const defaultStyles = useDefaultStyles(); return props.children(defaultStyles); +} + +export function ColoredBlurView(props: PropsWithChildren) { + const scheme = useColorScheme(); + + return ( + + ); } \ No newline at end of file diff --git a/src/components/DownloadIcon.tsx b/src/components/DownloadIcon.tsx index a45d67e..5b5c7c8 100644 --- a/src/components/DownloadIcon.tsx +++ b/src/components/DownloadIcon.tsx @@ -30,7 +30,7 @@ const IconOverlay = styled.View` function DownloadIcon({ trackId, size = 16, fill }: DownloadIconProps) { // determine styles const defaultStyles = useDefaultStyles(); - const iconFill = fill || defaultStyles.textHalfOpacity.color; + const iconFill = fill || defaultStyles.textQuarterOpacity.color; // Get download icon from state const entity = useTypedSelector((state) => state.downloads.entities[trackId]); diff --git a/src/components/Typography.ts b/src/components/Typography.ts index 416c2e7..abad679 100644 --- a/src/components/Typography.ts +++ b/src/components/Typography.ts @@ -2,13 +2,14 @@ import styled from 'styled-components/native'; import Text from './Text'; export const Header = styled(Text)` - margin: 24px 0 12px 0; - font-size: 36px; - font-weight: bold; + margin: 0 0 6px 0; + font-size: 24px; + font-weight: 400; `; export const SubHeader = styled(Text)` - font-size: 24px; - margin: 12px 0; - font-weight: 500; + font-size: 14px; + margin: 0 0 6px 0; + font-weight: 400; + opacity: 0.5; `; \ No newline at end of file diff --git a/src/components/WrappableButtonRow.tsx b/src/components/WrappableButtonRow.tsx index d5d17de..3501880 100644 --- a/src/components/WrappableButtonRow.tsx +++ b/src/components/WrappableButtonRow.tsx @@ -5,7 +5,7 @@ export const WrappableButtonRow = styled.View` flex: 0 0 auto; flex-direction: row; flex-wrap: wrap; - margin: 6px -2px; + margin: 24px -2px; `; export const WrappableButton = styled(Button)` diff --git a/src/screens/Music/index.tsx b/src/screens/Music/index.tsx index ec42981..0678f65 100644 --- a/src/screens/Music/index.tsx +++ b/src/screens/Music/index.tsx @@ -9,6 +9,7 @@ import { t } from '@localisation'; import useDefaultStyles from 'components/Colors'; import Playlists from './stacks/Playlists'; import Playlist from './stacks/Playlist'; +import NowPlaying from './overlays/NowPlaying'; const Stack = createStackNavigator(); @@ -16,16 +17,19 @@ function MusicStack() { const defaultStyles = useDefaultStyles(); return ( - - - - - - - + <> + + + + + + + + + ); } diff --git a/src/screens/Music/overlays/NowPlaying/index.tsx b/src/screens/Music/overlays/NowPlaying/index.tsx new file mode 100644 index 0000000..fb8f4c5 --- /dev/null +++ b/src/screens/Music/overlays/NowPlaying/index.tsx @@ -0,0 +1,172 @@ +import React, { useEffect, useRef } from 'react'; +import { ActivityIndicator, Animated, Dimensions, Easing, Pressable, View } from 'react-native'; +import FastImage from 'react-native-fast-image'; +import styled, { css } from 'styled-components/native'; + +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 { ColoredBlurView } from 'components/Colors'; + +const NOW_PLAYING_POPOVER_MARGIN = 6; +const NOW_PLAYING_POPOVER_WIDTH = Dimensions.get('screen').width - 2 * NOW_PLAYING_POPOVER_MARGIN; + +const PopoverPosition = css` + position: absolute; + bottom: ${NOW_PLAYING_POPOVER_MARGIN}px; + left: ${NOW_PLAYING_POPOVER_MARGIN}px; + right: ${NOW_PLAYING_POPOVER_MARGIN}px; + border-radius: 8px; + overflow: visible; +`; + +const Container = styled.View` + ${PopoverPosition}; +`; + +const InnerContainer = styled.Pressable` + padding: 12px; + overflow: hidden; + flex: 1; + flex-direction: row; + align-items: center; +`; + +const ShadowOverlay = styled.View` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +`; + +const Cover = styled(FastImage)` + height: 32px; + width: 32px; + border-radius: 4px; + margin-right: 12px; +`; + +const TrackNameContainer = styled.View` + flex: 1; +`; + +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(); + + switch(state) { + case State.Playing: + return ( + + + + ); + case State.Stopped: + case State.Paused: + return ( + + + + ); + // @ts-expect-error For some reason buffering isn't stated right in the types + case 'buffering': + case State.Buffering: + case State.Connecting: + return ( + + + + ); + default: + return null; + } +} + +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(); + const previousIndex = usePrevious(index); + + const bufferAnimation = useRef(new Animated.Value(0)); + const progressAnimation = useRef(new Animated.Value(0)); + + useEffect(() => { + const hasChangedTrack = previousIndex !== index || duration === 0; + + Animated.timing(bufferAnimation.current, { + toValue: calculateProgressTranslation(buffered, duration), + duration: hasChangedTrack ? 0 : 500, + useNativeDriver: true, + easing: Easing.ease, + }).start(); + Animated.timing(progressAnimation.current, { + toValue: calculateProgressTranslation(position, duration), + duration: hasChangedTrack ? 0 : 500, + useNativeDriver: true, + }).start(); + }, [buffered, duration, position, index, previousIndex]); + + if (!track) { + return null; + } + + return ( + + + + + + + + + + + {track.title} + {track.artist}{track.album ? ` — ${track.album}` : ''} + + + + + + + + + + ); +} + +export default NowPlaying; diff --git a/src/screens/Music/stacks/components/TrackListView.tsx b/src/screens/Music/stacks/components/TrackListView.tsx index a3f5238..edc4d65 100644 --- a/src/screens/Music/stacks/components/TrackListView.tsx +++ b/src/screens/Music/stacks/components/TrackListView.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { Text, ScrollView, Dimensions, RefreshControl, StyleSheet, View } from 'react-native'; +import { ScrollView, Dimensions, RefreshControl, StyleSheet, View, Image } from 'react-native'; import { useGetImage } from 'utility/JellyfinApi'; import styled, { css } from 'styled-components/native'; import { useNavigation } from '@react-navigation/native'; @@ -22,41 +22,43 @@ import Trash from 'assets/icons/trash.svg'; import { useDispatch } from 'react-redux'; import { queueTrackForDownload, removeDownloadedTrack } from 'store/downloads/actions'; import { selectDownloadedTracks } from 'store/downloads/selectors'; +import { Header, SubHeader } from 'components/Typography'; +import Text from 'components/Text'; +import { Shadow } from 'react-native-shadow-2'; const Screen = Dimensions.get('screen'); const styles = StyleSheet.create({ - name: { - fontSize: 36, - fontWeight: 'bold' - }, - artist: { - fontSize: 24, - opacity: 0.5, - marginBottom: 12 - }, index: { - width: 20, - opacity: 0.5, - marginRight: 5 - } + width: 16, + marginRight: 8 + }, + activeText: { + color: THEME_COLOR, + fontWeight: '500', + }, }); const AlbumImage = styled(FastImage)` - border-radius: 10px; - width: ${Screen.width * 0.6}px; - height: ${Screen.width * 0.6}px; - margin: 10px auto; + border-radius: 12px; + height: ${Screen.width - 112}px; + width: ${Screen.width - 112}px; `; -const TrackContainer = styled.View<{isPlaying: boolean}>` - padding: 15px 4px; - border-bottom-width: 1px; +const AlbumImageContainer = styled.View` + margin: 0 12px 24px 12px; + flex: 1; + align-items: center; +`; + +const TrackContainer = styled.View<{ isPlaying: boolean }>` + padding: 12px 4px; flex-direction: row; + border-radius: 6px; ${props => props.isPlaying && css` - margin: 0 -20px; - padding: 15px 24px; + margin: 0 -12px; + padding: 12px 16px; `} `; @@ -119,14 +121,20 @@ const TrackListView: React.FC = ({ return ( } > - - {title} - {artist} + + + + + + +
{title}
+ {artist} @@ -145,28 +153,29 @@ const TrackListView: React.FC = ({ > {listNumberingStyle === 'index' ? i + 1 : tracks[trackId]?.IndexNumber} - + {tracks[trackId]?.Name} - - + + + {Math.round(tracks[trackId]?.RunTimeTicks / 10000000 / 60)} + :{Math.round(tracks[trackId]?.RunTimeTicks / 10000000 % 60).toString().padStart(2, '0')} + + diff --git a/src/utility/formatBytes.ts b/src/utility/formatBytes.ts index f06749f..c354979 100644 --- a/src/utility/formatBytes.ts +++ b/src/utility/formatBytes.ts @@ -5,7 +5,7 @@ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; * Convert a number of bytes to a human-readable string * CREDIT: https://gist.github.com/zentala/1e6f72438796d74531803cc3833c039c */ -export default function formatBytes(bytes: number, decimals: number = 2) { +export default function formatBytes(bytes: number, decimals: number = 1) { if (bytes === 0) { return '0 Bytes'; } diff --git a/src/utility/usePrevious.ts b/src/utility/usePrevious.ts new file mode 100644 index 0000000..0634616 --- /dev/null +++ b/src/utility/usePrevious.ts @@ -0,0 +1,13 @@ +import { useRef, useEffect } from 'react'; + +export default function usePrevious(value: T) { + // The ref object is a generic container whose current property is mutable ... + // ... and can hold any value, similar to an instance property on a class + const ref = useRef(); + // Store current value in ref + useEffect(() => { + ref.current = value; + }, [value]); // Only re-run if value changes + // Return previous value (happens before update in useEffect above) + return ref.current; +} \ No newline at end of file