Skip to content

Commit cf43624

Browse files
NoxelFoxelsawyerf
andauthored
Add: Rating button (#107)
* added rating popup * Up: rating button * Del: translation * Format file --------- Co-authored-by: Sawyerf <sawyer.flink@protonmail.ch>
1 parent 63060ef commit cf43624

8 files changed

Lines changed: 187 additions & 49 deletions

File tree

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,68 @@
1-
import React from 'react'
2-
3-
import { useSetUpdateApi } from '~/contexts/updateApi'
4-
import { useConfig } from '~/contexts/config'
5-
import { useTheme } from '~/contexts/theme'
6-
import { getApi, refreshApi } from '~/utils/api'
7-
import IconButton from '~/components/button/IconButton'
8-
import logger from '~/utils/logger'
9-
10-
const FavoritedButton = ({ id, isFavorited = false, style = {}, size = 23 }) => {
11-
const [favorited, setFavorited] = React.useState(isFavorited)
12-
const theme = useTheme()
13-
const config = useConfig()
14-
const setUpdateApi = useSetUpdateApi()
15-
16-
React.useEffect(() => {
17-
setFavorited(isFavorited)
18-
}, [id, isFavorited])
19-
20-
const onPressFavorited = () => {
21-
getApi(config, favorited ? 'unstar' : 'star', { id, albumId: id, artistId: id })
22-
.then(() => {
23-
setFavorited(!favorited)
24-
refreshApi(config, 'getStarred2', null)
25-
.then(() => {
26-
setUpdateApi({ path: 'getStarred2', query: null, uid: 1 })
27-
})
28-
})
29-
.catch((e) => logger.error('FavoritedButton', e.message))
30-
}
31-
32-
return (
33-
<IconButton
34-
style={[{ padding: 20 }, style]}
35-
onPress={onPressFavorited}
36-
size={size}
37-
icon={favorited ? "heart" : "heart-o"}
38-
color={theme.primaryTouch}
39-
/>
40-
)
41-
}
42-
1+
import React from 'react'
2+
3+
import { useSetUpdateApi } from '~/contexts/updateApi'
4+
import { useConfig } from '~/contexts/config'
5+
import { useSongDispatch } from '~/contexts/song'
6+
import { useTheme } from '~/contexts/theme'
7+
import { getApi, refreshApi } from '~/utils/api'
8+
import IconButton from '~/components/button/IconButton'
9+
import RatingPopup from '~/components/popup/RatingPopup'
10+
import logger from '~/utils/logger'
11+
12+
const FavoritedButton = ({ id, isFavorited = false, rating: initialRating = undefined, style = {}, size = 23 }) => {
13+
const [favorited, setFavorited] = React.useState(isFavorited)
14+
const [rating, setRating] = React.useState(initialRating ?? 0)
15+
const [isRatingOpen, setIsRatingOpen] = React.useState(false)
16+
const theme = useTheme()
17+
const config = useConfig()
18+
const songDispatch = useSongDispatch()
19+
const setUpdateApi = useSetUpdateApi()
20+
21+
React.useEffect(() => {
22+
setFavorited(isFavorited)
23+
setRating(initialRating ?? 0)
24+
}, [id, isFavorited, initialRating])
25+
26+
const onPressFavorited = () => {
27+
getApi(config, favorited ? 'unstar' : 'star', { id, albumId: id, artistId: id })
28+
.then(() => {
29+
setFavorited(!favorited)
30+
refreshApi(config, 'getStarred2', null)
31+
.then(() => {
32+
setUpdateApi({ path: 'getStarred2', query: null, uid: 1 })
33+
})
34+
})
35+
.catch((e) => logger.error('FavoritedButton', e.message))
36+
}
37+
38+
const onSaveRating = () => {
39+
getApi(config, 'setRating', { id, rating })
40+
.then(() => {
41+
songDispatch({ type: 'setRating', id, rating })
42+
setIsRatingOpen(false)
43+
})
44+
.catch((e) => logger.error('FavoritedButton', e.message))
45+
}
46+
47+
return (
48+
<>
49+
<IconButton
50+
style={[{ padding: 20 }, style]}
51+
onPress={onPressFavorited}
52+
onLongPress={() => initialRating !== undefined ? setIsRatingOpen(true) : null}
53+
size={size}
54+
icon={favorited ? "heart" : "heart-o"}
55+
color={theme.primaryTouch}
56+
/>
57+
<RatingPopup
58+
visible={isRatingOpen}
59+
rating={rating}
60+
onSelectRating={setRating}
61+
onSave={onSaveRating}
62+
onClose={() => setIsRatingOpen(false)}
63+
/>
64+
</>
65+
)
66+
}
67+
4368
export default FavoritedButton

app/components/button/IconButton.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const IconButton = ({ icon, size = 23, color = undefined, style = {}, styleIcon
1414
delayLongPress={delayLongPress}
1515
onPress={onPress}
1616
onContextMenu={(ev) => {
17+
ev.stopPropagation()
1718
ev.preventDefault()
1819
if (onLongPress) onLongPress()
1920
}}

app/components/item/SongItem.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,15 @@ const SongItem = ({ song, queue, index, isIndex = false, isPlaying = false, setI
9898
</Text>
9999
) : null}
100100
<Cached song={song} />
101-
<FavoritedButton id={song.id} isFavorited={song?.starred} style={{ padding: 5, paddingStart: 10 }} />
102-
</Pressable>
103-
)
104-
}
101+
<FavoritedButton
102+
id={song.id}
103+
isFavorited={song?.starred}
104+
rating={song?.userRating ?? song?.rating ?? 0}
105+
style={{ padding: 5, paddingStart: 10 }}
106+
/>
107+
</Pressable>
108+
)
109+
}
105110

106111
const styles = StyleSheet.create({
107112
song: {
@@ -113,4 +118,4 @@ const styles = StyleSheet.create({
113118
},
114119
})
115120

116-
export default SongItem
121+
export default SongItem

app/components/player/BoxDesktopPlayer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const BoxDesktopPlayer = ({ setFullScreen }) => {
5050
<FavoritedButton
5151
id={song?.songInfo?.id}
5252
isFavorited={stars.some(s => s.id === song.songInfo.id)}
53+
rating={song.songInfo?.userRating ?? song.songInfo?.rating ?? 0}
5354
size={19}
5455
style={{ marginHorizontal: 15, padding: 5 }}
5556
/>

app/components/player/FullScreenHorizontalPlayer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ const FullScreenHorizontalPlayer = ({ setFullScreen }) => {
146146
<FavoritedButton
147147
id={song?.songInfo?.id}
148148
isFavorited={stars.some(s => s.id === song.songInfo.id)}
149+
rating={song.songInfo?.userRating ?? song.songInfo?.rating ?? 0}
149150
size={size.icon.medium}
150151
style={{ padding: 0, paddingBottom: 10, marginStart: 20, width: 'min-content' }}
151152
/>

app/components/player/FullScreenPlayer.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,12 @@ const FullScreenPlayer = ({ setFullScreen }) => {
221221
setFullScreen={setFullScreen}
222222
/>
223223
</View>
224-
<FavoritedButton id={song.songInfo.id} isFavorited={stars.some(s => s.id === song.songInfo.id)} style={{ padding: 20, paddingEnd: 0 }} />
224+
<FavoritedButton
225+
id={song.songInfo.id}
226+
isFavorited={stars.some(s => s.id === song.songInfo.id)}
227+
rating={song.songInfo?.userRating ?? song.songInfo?.rating ?? 0}
228+
style={{ padding: 20, paddingEnd: 0 }}
229+
/>
225230
</View>
226231
<TimeBar />
227232
<View style={{ flexDirection: 'row', width: '100%', marginVertical: 30, alignItems: 'center', justifyContent: 'center', gap: 30 }}>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React from 'react'
2+
import { Modal, Pressable, Text, View } from 'react-native'
3+
import { useTranslation } from 'react-i18next'
4+
5+
import { useTheme } from '~/contexts/theme'
6+
import IconButton from '~/components/button/IconButton'
7+
import mainStyles from '~/styles/main'
8+
import size from '~/styles/size'
9+
10+
const RatingPopup = ({ visible, rating, onSelectRating, onSave, onClose }) => {
11+
const { t } = useTranslation()
12+
const theme = useTheme()
13+
14+
if (!visible) return null
15+
16+
return (
17+
<Modal
18+
transparent={true}
19+
onRequestClose={onClose}
20+
statusBarTranslucent={true}
21+
visible={visible}
22+
>
23+
<Pressable
24+
onPress={onClose}
25+
style={{
26+
flex: 1,
27+
backgroundColor: 'rgba(0,0,0,0.5)',
28+
justifyContent: 'center',
29+
alignItems: 'center',
30+
padding: 20,
31+
}}
32+
>
33+
<View
34+
style={{
35+
width: '100%',
36+
maxWidth: 320,
37+
backgroundColor: theme.secondaryBack,
38+
borderRadius: 15,
39+
padding: 20,
40+
paddingTop: 26,
41+
gap: 15,
42+
}}
43+
>
44+
<View style={{ flexDirection: 'row', justifyContent: 'center', gap: 10 }}>
45+
{[1, 2, 3, 4, 5].map((value) => (
46+
<IconButton
47+
key={value}
48+
size={size.icon.small}
49+
icon={value <= rating ? 'star' : 'star-o'}
50+
color={value <= rating ? theme.primaryTouch : theme.secondaryText}
51+
style={{ padding: 2 }}
52+
onPress={() => onSelectRating(value)}
53+
/>
54+
))}
55+
</View>
56+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', gap: 10 }}>
57+
<Pressable
58+
onPress={() => {
59+
onSelectRating(0)
60+
onSave()
61+
}}
62+
style={({ pressed }) => ([mainStyles.opacity({ pressed }), { padding: 6 }])}
63+
>
64+
<Text style={mainStyles.smallText(theme.secondaryText)}>{t('Clear')}</Text>
65+
</Pressable>
66+
<Pressable
67+
onPress={onSave}
68+
style={({ pressed }) => ([mainStyles.opacity({ pressed }), { padding: 6 }])}
69+
>
70+
<Text style={mainStyles.smallText(theme.primaryTouch)}>{t('Save')}</Text>
71+
</Pressable>
72+
</View>
73+
</View>
74+
</Pressable>
75+
</Modal>
76+
)
77+
}
78+
79+
export default RatingPopup

app/contexts/song/provider.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const convertTrack = (track) => {
5151
covertArt: track.covertArt,
5252
track: track.track,
5353
starred: track.starred,
54+
userRating: track.userRating ?? track.rating ?? 0,
5455
size: track.size,
5556
index: track.index,
5657
mediaType: track.mediaType,
@@ -125,6 +126,26 @@ export const songReducer = (state, action) => {
125126
randomIndex: state.randomIndex?.length ? [...state.randomIndex, newQueue.length - 1] : [],
126127
}, true)
127128
}
129+
case 'setRating': {
130+
if (!state.queue?.length) return state
131+
const newQueue = state.queue.map((track) => {
132+
if (track.id !== action.id) return track
133+
return {
134+
...track,
135+
userRating: action.rating,
136+
rating: action.rating,
137+
}
138+
})
139+
const songInfo = state.songInfo?.id === action.id ? {
140+
...state.songInfo,
141+
userRating: action.rating,
142+
rating: action.rating,
143+
} : state.songInfo
144+
return newSong(state, {
145+
queue: newQueue,
146+
songInfo,
147+
}, true)
148+
}
128149
case 'removeFromQueue': {
129150
if (!state.queue || state.queue.length <= action.index) return state
130151
const newQueue = [...state.queue]
@@ -181,4 +202,4 @@ export const defaultSong = {
181202
actionEndOfSong: 'next',
182203
randomIndex: [],
183204
state: State.Stopped,
184-
}
205+
}

0 commit comments

Comments
 (0)