Show indicator on current playing song. Fixes #128
This commit is contained in:
parent
eb4c0f0b84
commit
9d23b191b5
|
@ -2,7 +2,6 @@ import React from 'react'
|
|||
import {
|
||||
BulkActionsToolbar,
|
||||
DatagridLoading,
|
||||
FunctionField,
|
||||
ListToolbar,
|
||||
TextField,
|
||||
useListController,
|
||||
|
@ -15,9 +14,10 @@ import StarBorderIcon from '@material-ui/icons/StarBorder'
|
|||
import { playTracks } from '../audioplayer'
|
||||
import {
|
||||
DurationField,
|
||||
SongDetails,
|
||||
SongDatagrid,
|
||||
SongContextMenu,
|
||||
SongDatagrid,
|
||||
SongDetails,
|
||||
SongTitleField,
|
||||
} from '../common'
|
||||
import AddToPlaylistDialog from '../dialogs/AddToPlaylistDialog'
|
||||
|
||||
|
@ -62,14 +62,6 @@ const useStylesListToolbar = makeStyles({
|
|||
},
|
||||
})
|
||||
|
||||
const trackName = (r) => {
|
||||
const name = r.title
|
||||
if (r.trackNumber) {
|
||||
return r.trackNumber.toString().padStart(2, '0') + ' ' + name
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
const AlbumSongs = (props) => {
|
||||
const classes = useStyles(props)
|
||||
const classesToolbar = useStylesListToolbar(props)
|
||||
|
@ -132,14 +124,11 @@ const AlbumSongs = (props) => {
|
|||
sortable={false}
|
||||
/>
|
||||
)}
|
||||
{isDesktop && <TextField source="title" sortable={false} />}
|
||||
{!isDesktop && (
|
||||
<FunctionField
|
||||
source="title"
|
||||
render={trackName}
|
||||
sortable={false}
|
||||
/>
|
||||
)}
|
||||
<SongTitleField
|
||||
source="title"
|
||||
sortable={false}
|
||||
showTrackNumbers={!isDesktop}
|
||||
/>
|
||||
{isDesktop && <TextField source="artist" sortable={false} />}
|
||||
<DurationField source="duration" sortable={false} />
|
||||
<SongContextMenu
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useAuthState, useDataProvider, useTranslate } from 'react-admin'
|
|||
import ReactJkMusicPlayer from 'react-jinke-music-player'
|
||||
import 'react-jinke-music-player/assets/index.css'
|
||||
import subsonic from '../subsonic'
|
||||
import { scrobble, syncQueue } from './queue'
|
||||
import { scrobble, syncQueue, currentPlaying } from './queue'
|
||||
import themes from '../themes'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
|
||||
|
@ -100,6 +100,9 @@ const Player = () => {
|
|||
}
|
||||
|
||||
const OnAudioProgress = (info) => {
|
||||
if (info.ended) {
|
||||
document.title = 'Navidrome'
|
||||
}
|
||||
const progress = (info.currentTime / info.duration) * 100
|
||||
if (isNaN(info.duration) || progress < 90) {
|
||||
return
|
||||
|
@ -112,16 +115,21 @@ const Player = () => {
|
|||
}
|
||||
|
||||
const OnAudioPlay = (info) => {
|
||||
dispatch(currentPlaying(info))
|
||||
if (info.duration) {
|
||||
document.title = `${info.name} - ${info.singer} - Navidrome`
|
||||
dispatch(scrobble(info.trackId, false))
|
||||
subsonic.scrobble(info.trackId, false)
|
||||
dataProvider.getOne('keepalive', { id: info.trackId })
|
||||
}
|
||||
}
|
||||
|
||||
const onAudioEnded = () => {
|
||||
document.title = 'Navidrome'
|
||||
const onAudioPause = (info) => {
|
||||
dispatch(currentPlaying(info))
|
||||
}
|
||||
|
||||
const onAudioEnded = (currentPlayId, audioLists, info) => {
|
||||
dispatch(currentPlaying(info))
|
||||
dataProvider.getOne('keepalive', { id: info.trackId })
|
||||
}
|
||||
|
||||
if (authenticated && options.audioLists.length > 0) {
|
||||
|
@ -131,6 +139,7 @@ const Player = () => {
|
|||
onAudioListsChange={OnAudioListsChange}
|
||||
onAudioProgress={OnAudioProgress}
|
||||
onAudioPlay={OnAudioPlay}
|
||||
onAudioPause={onAudioPause}
|
||||
onAudioEnded={onAudioEnded}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -6,6 +6,7 @@ const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK'
|
|||
const PLAYER_SYNC_QUEUE = 'PLAYER_SYNC_QUEUE'
|
||||
const PLAYER_SCROBBLE = 'PLAYER_SCROBBLE'
|
||||
const PLAYER_PLAY_TRACKS = 'PLAYER_PLAY_TRACKS'
|
||||
const PLAYER_CURRENT = 'PLAYER_CURRENT'
|
||||
|
||||
const mapToAudioLists = (item) => {
|
||||
// If item comes from a playlist, id is mediaFileId
|
||||
|
@ -88,13 +89,30 @@ const scrobble = (id, submit) => ({
|
|||
submit,
|
||||
})
|
||||
|
||||
const currentPlaying = (audioInfo) => ({
|
||||
type: PLAYER_CURRENT,
|
||||
data: audioInfo,
|
||||
})
|
||||
|
||||
const playQueueReducer = (
|
||||
previousState = { queue: [], clear: true, playing: false },
|
||||
previousState = { queue: [], clear: true, playing: false, current: {} },
|
||||
payload
|
||||
) => {
|
||||
let queue
|
||||
let queue, current
|
||||
const { type, data } = payload
|
||||
switch (type) {
|
||||
case PLAYER_CURRENT:
|
||||
queue = previousState.queue
|
||||
current = data.ended
|
||||
? {}
|
||||
: {
|
||||
trackId: data.trackId,
|
||||
paused: data.paused,
|
||||
}
|
||||
return {
|
||||
...previousState,
|
||||
current,
|
||||
}
|
||||
case PLAYER_ADD_TRACKS:
|
||||
queue = previousState.queue
|
||||
Object.keys(data).forEach((id) => {
|
||||
|
@ -109,10 +127,12 @@ const playQueueReducer = (
|
|||
playing: true,
|
||||
}
|
||||
case PLAYER_SYNC_QUEUE:
|
||||
current = data.length > 0 ? previousState.current : {}
|
||||
return {
|
||||
...previousState,
|
||||
queue: data,
|
||||
clear: false,
|
||||
current,
|
||||
}
|
||||
case PLAYER_SCROBBLE:
|
||||
const newQueue = previousState.queue.map((item) => {
|
||||
|
@ -156,6 +176,7 @@ export {
|
|||
playTracks,
|
||||
syncQueue,
|
||||
scrobble,
|
||||
currentPlaying,
|
||||
shuffleTracks,
|
||||
playQueueReducer,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { FunctionField } from 'react-admin'
|
||||
import get from 'lodash.get'
|
||||
import { useTheme } from '@material-ui/core/styles'
|
||||
import PlayingLight from '../icons/playing-light.gif'
|
||||
import PlayingDark from '../icons/playing-dark.gif'
|
||||
import PausedLight from '../icons/paused-light.png'
|
||||
import PausedDark from '../icons/paused-dark.png'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
icon: {
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
verticalAlign: 'text-top',
|
||||
marginLeft: '-8px',
|
||||
marginTop: '-7px',
|
||||
paddingRight: '3px',
|
||||
},
|
||||
text: {
|
||||
verticalAlign: 'text-top',
|
||||
},
|
||||
})
|
||||
|
||||
const SongTitleField = ({ showTrackNumbers, ...props }) => {
|
||||
const theme = useTheme()
|
||||
const classes = useStyles()
|
||||
const { record } = props
|
||||
const currentTrack = useSelector((state) => get(state, 'queue.current', {}))
|
||||
const currentId = currentTrack.trackId
|
||||
const paused = currentTrack.paused
|
||||
const isCurrent =
|
||||
currentId && (currentId === record.id || currentId === record.mediaFileId)
|
||||
|
||||
const trackName = (r) => {
|
||||
const name = r.title
|
||||
if (r.trackNumber && showTrackNumbers) {
|
||||
return r.trackNumber.toString().padStart(2, '0') + ' ' + name
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
const Icon = () => {
|
||||
let icon
|
||||
if (paused) {
|
||||
icon = theme.palette.type === 'light' ? PausedLight : PausedDark
|
||||
} else {
|
||||
icon = theme.palette.type === 'light' ? PlayingLight : PlayingDark
|
||||
}
|
||||
return (
|
||||
<img
|
||||
src={icon}
|
||||
className={classes.icon}
|
||||
alt={paused ? 'paused' : 'playing'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCurrent && <Icon />}
|
||||
<FunctionField
|
||||
{...props}
|
||||
source="title"
|
||||
render={trackName}
|
||||
className={classes.text}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SongTitleField.propTypes = {
|
||||
record: PropTypes.object,
|
||||
showTrackNumbers: PropTypes.bool,
|
||||
}
|
||||
|
||||
export default SongTitleField
|
|
@ -12,6 +12,7 @@ import DocLink from './DocLink'
|
|||
import List from './List'
|
||||
import { SongDatagrid, SongDatagridRow } from './SongDatagrid'
|
||||
import SongContextMenu from './SongContextMenu'
|
||||
import SongTitleField from './SongTitleField'
|
||||
import QuickFilter from './QuickFilter'
|
||||
import useAlbumsPerPage from './useAlbumsPerPage'
|
||||
|
||||
|
@ -28,6 +29,7 @@ export {
|
|||
SongDetails,
|
||||
SongDatagrid,
|
||||
SongDatagridRow,
|
||||
SongTitleField,
|
||||
DocLink,
|
||||
formatRange,
|
||||
ArtistLinkField,
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 351 B |
Binary file not shown.
After Width: | Height: | Size: 827 B |
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
|
@ -19,6 +19,7 @@ import {
|
|||
SongDetails,
|
||||
SongContextMenu,
|
||||
SongDatagrid,
|
||||
SongTitleField,
|
||||
} from '../common'
|
||||
import AddToPlaylistDialog from '../dialogs/AddToPlaylistDialog'
|
||||
import { AlbumLinkField } from '../song/AlbumLinkField'
|
||||
|
@ -160,7 +161,7 @@ const PlaylistSongs = (props) => {
|
|||
contextAlwaysVisible={!isDesktop}
|
||||
>
|
||||
{isDesktop && <TextField source="id" label={'#'} />}
|
||||
<TextField source="title" />
|
||||
<SongTitleField source="title" showTrackNumbers={false} />
|
||||
{isDesktop && <AlbumLinkField source="album" />}
|
||||
{isDesktop && <TextField source="artist" />}
|
||||
<DurationField
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
SongDatagrid,
|
||||
SongDetails,
|
||||
QuickFilter,
|
||||
SongTitleField,
|
||||
} from '../common'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { setTrack } from '../audioplayer'
|
||||
|
@ -83,7 +84,7 @@ const SongList = (props) => {
|
|||
rowClick={handleRowClick}
|
||||
contextAlwaysVisible={!isDesktop}
|
||||
>
|
||||
<TextField source="title" />
|
||||
<SongTitleField source="title" showTrackNumbers={false} />
|
||||
{isDesktop && <AlbumLinkField source="album" />}
|
||||
<TextField source="artist" />
|
||||
{isDesktop && <NumberField source="trackNumber" />}
|
||||
|
|
Loading…
Reference in New Issue