Skip to content

Commit e072167

Browse files
committed
feat: add track info page
1 parent 7107945 commit e072167

9 files changed

Lines changed: 220 additions & 40 deletions

File tree

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import type { SharedValue } from 'react-native-reanimated'
2+
import type { ArtworkGradientColors } from '@/hooks/useArtworkColors'
23
import { createContext, use } from 'react'
34

45
interface ContextType {
56
percent: SharedValue<number>
67
thresholdPercent: SharedValue<number>
8+
artworkColors: ArtworkGradientColors
79
}
810

911
export const Context = createContext({} as ContextType)
1012

11-
export function usePlayerAnimation() {
13+
export function usePlayerContext() {
1214
const value = use(Context)
1315
if (!value) {
14-
throw new Error('usePlayerAnimation must be used within a PlayerProvider')
16+
throw new Error('usePlayerContext must be used within a PlayerProvider')
1517
}
1618
return value
1719
}

apps/mobile/modules/player/FullPlayer.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { StyleSheet, View } from 'react-native'
22
import PagerView from 'react-native-pager-view'
33
import Animated, { Extrapolation, interpolate, useAnimatedStyle } from 'react-native-reanimated'
4-
import { usePlayerAnimation } from './Context'
4+
import { usePlayerContext } from './Context'
55
import FullPlayerArtwork from './FullPlayerArtwork'
66
import FullPlayerControl from './FullPlayerControl'
77
import FullPlayerHeader from './FullPlayerHeader'
88
import LyricsView from './LyricsView'
9+
import TrackInfo from './TrackInfo'
910

1011
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
1112

1213
export default function FullPlayer() {
13-
const { thresholdPercent } = usePlayerAnimation()
14+
const { thresholdPercent } = usePlayerContext()
1415

1516
const animatedStyle = useAnimatedStyle(() => ({
1617
opacity: interpolate(
@@ -25,7 +26,11 @@ export default function FullPlayer() {
2526
<Animated.View style={[styles.container, animatedStyle]}>
2627
<FullPlayerHeader />
2728

28-
<AnimatedPagerView style={styles.pagerContainer} initialPage={0} orientation="horizontal">
29+
<AnimatedPagerView style={styles.pagerContainer} initialPage={1} orientation="horizontal">
30+
<View style={styles.trackInfoPage}>
31+
<TrackInfo />
32+
</View>
33+
2934
<View style={styles.artworkPage}>
3035
<FullPlayerArtwork />
3136
<View style={styles.artworkPageLyrics}>
@@ -50,6 +55,9 @@ const styles = StyleSheet.create({
5055
pagerContainer: {
5156
flex: 1,
5257
},
58+
trackInfoPage: {
59+
flex: 1,
60+
},
5361
artworkPage: {
5462
flex: 1,
5563
},

apps/mobile/modules/player/MiniPlayer.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { IconButton, Text, useTheme } from 'react-native-paper'
55
import Animated, { Extrapolation, interpolate, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'
66
import { ScrollingText } from '@/components/ui/ScrollingText'
77
import { MINI_HEIGHT } from '@/constants/Player'
8-
import { usePlayerAnimation } from './Context'
8+
import { usePlayerContext } from './Context'
99

1010
export default function MiniPlayer({ onPress }: { onPress: () => void }) {
1111
const { colors } = useTheme()
12-
const { thresholdPercent } = usePlayerAnimation()
12+
const { thresholdPercent } = usePlayerContext()
1313
const isPlaying = usePlayerStore.use.isPlaying()
1414
const displayTrack = useDisplayTrack()
1515

apps/mobile/modules/player/PlayerBackground.tsx

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,16 @@ import { LinearGradient } from 'expo-linear-gradient'
22
import { View } from 'react-native'
33
import { useTheme } from 'react-native-paper'
44
import Animated, { Extrapolation, interpolate, useAnimatedStyle } from 'react-native-reanimated'
5-
import { useArtworkColors } from '@/hooks/useArtworkColors'
6-
import { usePlayerAnimation } from './Context'
5+
import { usePlayerContext } from './Context'
76

8-
interface PlayerBackgroundProps {
9-
artworkUrl?: string
10-
}
11-
12-
export default function PlayerBackground({ artworkUrl }: PlayerBackgroundProps) {
13-
const { dominant, vibrant, muted } = useArtworkColors(artworkUrl)
7+
export default function PlayerBackground() {
148
const { colors } = useTheme()
15-
const { thresholdPercent } = usePlayerAnimation()
9+
const { thresholdPercent, artworkColors } = usePlayerContext()
1610

1711
// 确保颜色值有效
18-
const safeVibrant = vibrant || colors.primary
19-
const safeDominant = dominant || colors.secondary
20-
const safeMuted = muted || colors.outline
12+
const safeVibrant = artworkColors.vibrant || colors.primary
13+
const safeDominant = artworkColors.dominant || colors.secondary
14+
const safeMuted = artworkColors.muted || colors.outline
2115

2216
const baseStyle = {
2317
flex: 1,
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { formatFileSize, formatTime } from '@flow/core'
2+
import { useDisplayTrack } from '@flow/player'
3+
import { Image, StyleSheet, View } from 'react-native'
4+
import { Text } from 'react-native-paper'
5+
import Animated from 'react-native-reanimated'
6+
import { usePlayerContext } from './Context'
7+
8+
export default function TrackInfo() {
9+
const { artworkColors } = usePlayerContext()
10+
const displayTrack = useDisplayTrack()
11+
12+
return (
13+
<Animated.ScrollView
14+
style={styles.rootContainer}
15+
showsVerticalScrollIndicator={false}
16+
contentContainerStyle={styles.container}
17+
>
18+
<View style={[styles.group, { backgroundColor: `${artworkColors.background}20` }]}>
19+
<Text style={styles.header}>音频信息</Text>
20+
<View style={styles.audioSingleRow}>
21+
<View style={styles.audioCompactCard}>
22+
<Text style={styles.audioCompactLabel}>格式</Text>
23+
<Text style={styles.audioCompactValue}>{displayTrack?.format?.toUpperCase() || '--'}</Text>
24+
</View>
25+
<View style={styles.audioCompactCard}>
26+
<Text style={styles.audioCompactLabel}>声道</Text>
27+
<Text style={styles.audioCompactValue}>{displayTrack?.channels || '--'}</Text>
28+
</View>
29+
<View style={styles.audioCompactCard}>
30+
<Text style={styles.audioCompactLabel}>比特率</Text>
31+
<Text style={styles.audioCompactValue}>
32+
{displayTrack?.bitrate ? `${displayTrack.bitrate}kbps` : '--'}
33+
</Text>
34+
</View>
35+
<View style={styles.audioCompactCard}>
36+
<Text style={styles.audioCompactLabel}>采样率</Text>
37+
<Text style={styles.audioCompactValue}>
38+
{displayTrack?.sampleRate ? `${displayTrack.sampleRate}hz` : '--'}
39+
</Text>
40+
</View>
41+
</View>
42+
</View>
43+
44+
<View style={[styles.group, { backgroundColor: `${artworkColors.background}20` }]}>
45+
<Text style={styles.header}>专辑</Text>
46+
<View style={styles.content}>
47+
<Image
48+
source={{ uri: displayTrack?.artwork }}
49+
style={styles.albumArtwork}
50+
/>
51+
<Text>{displayTrack?.album}</Text>
52+
</View>
53+
</View>
54+
55+
<View style={[styles.group, { backgroundColor: `${artworkColors.background}20` }]}>
56+
<Text style={styles.header}>艺术家</Text>
57+
<View style={styles.content}>
58+
<Text>{displayTrack?.artist}</Text>
59+
</View>
60+
</View>
61+
62+
<View style={[styles.group, { backgroundColor: `${artworkColors.background}20` }]}>
63+
<Text style={styles.header}>文件信息</Text>
64+
65+
<View style={styles.fileInfoItem}>
66+
<Text style={styles.label}>文件路径</Text>
67+
<Text style={styles.pathText} numberOfLines={2} ellipsizeMode="middle">
68+
{displayTrack?.url}
69+
</Text>
70+
</View>
71+
<View style={styles.fileInfoItem}>
72+
<Text style={styles.label}>文件大小</Text>
73+
<Text style={styles.value}>{formatFileSize(displayTrack?.fileSize ?? 0)}</Text>
74+
</View>
75+
<View style={styles.fileInfoItem}>
76+
<Text style={styles.label}>创建时间</Text>
77+
<Text style={styles.value}>{formatTime(displayTrack?.createdAt)}</Text>
78+
</View>
79+
<View style={[styles.fileInfoItem, { marginBottom: 0 }]}>
80+
<Text style={styles.label}>修改时间</Text>
81+
<Text style={styles.value}>{formatTime(displayTrack?.modifiedAt)}</Text>
82+
</View>
83+
</View>
84+
</Animated.ScrollView>
85+
)
86+
}
87+
88+
const styles = StyleSheet.create({
89+
rootContainer: {
90+
flex: 1,
91+
marginVertical: 16,
92+
},
93+
container: {
94+
paddingHorizontal: 28,
95+
flex: 1,
96+
justifyContent: 'center',
97+
gap: 12,
98+
},
99+
group: {
100+
padding: 12,
101+
borderRadius: 12,
102+
overflow: 'hidden',
103+
},
104+
header: {
105+
marginBottom: 10,
106+
fontSize: 14,
107+
fontWeight: '600',
108+
letterSpacing: 0.2,
109+
},
110+
content: {
111+
flexDirection: 'row',
112+
alignItems: 'center',
113+
gap: 8,
114+
},
115+
audioSingleRow: {
116+
flexDirection: 'row',
117+
gap: 4,
118+
},
119+
audioCompactCard: {
120+
flex: 1,
121+
backgroundColor: 'rgba(255, 255, 255, 0.05)',
122+
paddingVertical: 5,
123+
paddingHorizontal: 4,
124+
borderRadius: 4,
125+
alignItems: 'center',
126+
minHeight: 36,
127+
justifyContent: 'center',
128+
},
129+
audioCompactLabel: {
130+
fontSize: 9,
131+
fontWeight: '500',
132+
opacity: 0.6,
133+
marginBottom: 1,
134+
textAlign: 'center',
135+
textTransform: 'uppercase',
136+
letterSpacing: 0.2,
137+
},
138+
audioCompactValue: {
139+
fontSize: 11,
140+
fontWeight: '600',
141+
textAlign: 'center',
142+
lineHeight: 14,
143+
},
144+
fileInfoItem: {
145+
marginBottom: 8,
146+
},
147+
label: {
148+
fontSize: 11,
149+
fontWeight: '500',
150+
opacity: 0.7,
151+
marginBottom: 2,
152+
},
153+
value: {
154+
fontSize: 13,
155+
lineHeight: 18,
156+
},
157+
pathText: {
158+
fontSize: 13,
159+
lineHeight: 15,
160+
},
161+
albumArtwork: {
162+
width: 38,
163+
height: 38,
164+
borderRadius: 4,
165+
},
166+
})

apps/mobile/modules/player/index.tsx

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useDisplayTrack } from '@flow/player'
2-
import { useMemo, useRef, useState } from 'react'
2+
import { useMemo, useState } from 'react'
33
import { Dimensions, StyleSheet } from 'react-native'
44
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
55
import PagerView from 'react-native-pager-view'
@@ -15,12 +15,12 @@ import Animated, {
1515
withTiming,
1616
} from 'react-native-reanimated'
1717
import { MINI_HEIGHT } from '@/constants/Player'
18+
import { useArtworkColors } from '@/hooks/useArtworkColors'
1819
import { useBackHandler } from '@/hooks/useBackHandler'
1920
import { Context } from './Context'
2021
import FullPlayer from './FullPlayer'
2122
import MiniPlayer from './MiniPlayer'
2223
import PlayerBackground from './PlayerBackground'
23-
// import QueueList from './QueueList'
2424

2525
const { height: SCREEN_HEIGHT } = Dimensions.get('window')
2626
const MIN_VELOCITY = 500 // Velocity Threshold
@@ -33,8 +33,6 @@ export function Player() {
3333
const SNAP_FULL = 0
3434

3535
const [isFull, setIsFull] = useState(false)
36-
const [page, setPage] = useState(0)
37-
const pagerRef = useRef<PagerView>(null)
3836

3937
// Init: show mini player
4038
const translateY = useSharedValue(SNAP_MINI)
@@ -105,8 +103,6 @@ export function Player() {
105103
transform: [{ translateY: translateY.value }],
106104
}))
107105

108-
const contextValue = useMemo(() => ({ percent, thresholdPercent }), [percent, thresholdPercent])
109-
110106
useAnimatedReaction(
111107
() => thresholdPercent.value,
112108
(value) => {
@@ -118,38 +114,38 @@ export function Player() {
118114
[isFull],
119115
)
120116

121-
useBackHandler(isFull || page === 1, () => {
122-
if (page === 1) {
123-
pagerRef.current?.setPage(0)
124-
}
125-
else if (isFull) {
126-
animateToPosition('MINI')
127-
}
117+
useBackHandler(isFull, () => {
118+
animateToPosition('MINI')
128119
})
129120

130121
const displayTrack = useDisplayTrack()
131122

123+
const artworkColors = useArtworkColors(displayTrack?.artwork ?? '')
124+
125+
const contextValue = useMemo(
126+
() => ({
127+
percent,
128+
thresholdPercent,
129+
artworkColors,
130+
}),
131+
[percent, thresholdPercent, artworkColors],
132+
)
133+
132134
return (
133135
<Context value={contextValue}>
134136
<AnimatedPagerView
135-
ref={pagerRef}
136137
initialPage={0}
137138
style={[styles.container, animatedStyle]}
138139
orientation="vertical"
139-
onPageSelected={event => setPage(event.nativeEvent.position)}
140140
overScrollMode="never"
141141
>
142142
<GestureDetector gesture={pan}>
143143
<Animated.View style={{ flex: 1 }}>
144-
<PlayerBackground artworkUrl={displayTrack?.artwork ?? ''} />
144+
<PlayerBackground />
145145
<MiniPlayer onPress={() => animateToPosition('FULL')} />
146146
<FullPlayer />
147147
</Animated.View>
148148
</GestureDetector>
149-
150-
{/* <AnimatedPagerView style={{ flex: 1, backgroundColor: 'red', paddingVertical: 50 }}>
151-
<QueueList />
152-
</AnimatedPagerView> */}
153149
</AnimatedPagerView>
154150
</Context>
155151
)

packages/core/src/utils/file.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function formatFileSize(size: number) {
2+
if (size < 1024) {
3+
return `${size} B`
4+
}
5+
if (size < 1024 * 1024) {
6+
return `${(size / 1024).toFixed(2)} KB`
7+
}
8+
return `${(size / 1024 / 1024).toFixed(2)} MB`
9+
}

packages/core/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './debugger'
2+
export * from './file'
23
export * from './logger'
34
export * from './merge'
45
export * from './time'

packages/core/src/utils/time.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ export function formatDuration(duration: number) {
2121
* ss - seconds (00-59)
2222
* SSS - milliseconds (000-999)
2323
*/
24-
export function formatTime(date: Date | null, format = 'YYYY-MM-DD HH:mm:ss'): string {
24+
export function formatTime(date: Date | number | undefined, format = 'YYYY-MM-DD HH:mm:ss'): string {
2525
if (!date)
2626
return ''
2727

28+
if (typeof date === 'number') {
29+
date = new Date(date)
30+
}
31+
2832
const map: Record<string, string> = {
2933
YYYY: String(date.getFullYear()),
3034
MM: String(date.getMonth() + 1).padStart(2, '0'),

0 commit comments

Comments
 (0)