Show indicator on current playing song. Fixes #128
This commit is contained in:
@@ -2,7 +2,6 @@ import React from 'react'
|
|||||||
import {
|
import {
|
||||||
BulkActionsToolbar,
|
BulkActionsToolbar,
|
||||||
DatagridLoading,
|
DatagridLoading,
|
||||||
FunctionField,
|
|
||||||
ListToolbar,
|
ListToolbar,
|
||||||
TextField,
|
TextField,
|
||||||
useListController,
|
useListController,
|
||||||
@@ -15,9 +14,10 @@ import StarBorderIcon from '@material-ui/icons/StarBorder'
|
|||||||
import { playTracks } from '../audioplayer'
|
import { playTracks } from '../audioplayer'
|
||||||
import {
|
import {
|
||||||
DurationField,
|
DurationField,
|
||||||
SongDetails,
|
|
||||||
SongDatagrid,
|
|
||||||
SongContextMenu,
|
SongContextMenu,
|
||||||
|
SongDatagrid,
|
||||||
|
SongDetails,
|
||||||
|
SongTitleField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import AddToPlaylistDialog from '../dialogs/AddToPlaylistDialog'
|
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 AlbumSongs = (props) => {
|
||||||
const classes = useStyles(props)
|
const classes = useStyles(props)
|
||||||
const classesToolbar = useStylesListToolbar(props)
|
const classesToolbar = useStylesListToolbar(props)
|
||||||
@@ -132,14 +124,11 @@ const AlbumSongs = (props) => {
|
|||||||
sortable={false}
|
sortable={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isDesktop && <TextField source="title" sortable={false} />}
|
<SongTitleField
|
||||||
{!isDesktop && (
|
source="title"
|
||||||
<FunctionField
|
sortable={false}
|
||||||
source="title"
|
showTrackNumbers={!isDesktop}
|
||||||
render={trackName}
|
/>
|
||||||
sortable={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isDesktop && <TextField source="artist" sortable={false} />}
|
{isDesktop && <TextField source="artist" sortable={false} />}
|
||||||
<DurationField source="duration" sortable={false} />
|
<DurationField source="duration" sortable={false} />
|
||||||
<SongContextMenu
|
<SongContextMenu
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useAuthState, useDataProvider, useTranslate } from 'react-admin'
|
|||||||
import ReactJkMusicPlayer from 'react-jinke-music-player'
|
import ReactJkMusicPlayer from 'react-jinke-music-player'
|
||||||
import 'react-jinke-music-player/assets/index.css'
|
import 'react-jinke-music-player/assets/index.css'
|
||||||
import subsonic from '../subsonic'
|
import subsonic from '../subsonic'
|
||||||
import { scrobble, syncQueue } from './queue'
|
import { scrobble, syncQueue, currentPlaying } from './queue'
|
||||||
import themes from '../themes'
|
import themes from '../themes'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
|
||||||
@@ -100,6 +100,9 @@ const Player = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OnAudioProgress = (info) => {
|
const OnAudioProgress = (info) => {
|
||||||
|
if (info.ended) {
|
||||||
|
document.title = 'Navidrome'
|
||||||
|
}
|
||||||
const progress = (info.currentTime / info.duration) * 100
|
const progress = (info.currentTime / info.duration) * 100
|
||||||
if (isNaN(info.duration) || progress < 90) {
|
if (isNaN(info.duration) || progress < 90) {
|
||||||
return
|
return
|
||||||
@@ -112,16 +115,21 @@ const Player = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OnAudioPlay = (info) => {
|
const OnAudioPlay = (info) => {
|
||||||
|
dispatch(currentPlaying(info))
|
||||||
if (info.duration) {
|
if (info.duration) {
|
||||||
document.title = `${info.name} - ${info.singer} - Navidrome`
|
document.title = `${info.name} - ${info.singer} - Navidrome`
|
||||||
dispatch(scrobble(info.trackId, false))
|
dispatch(scrobble(info.trackId, false))
|
||||||
subsonic.scrobble(info.trackId, false)
|
subsonic.scrobble(info.trackId, false)
|
||||||
dataProvider.getOne('keepalive', { id: info.trackId })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onAudioEnded = () => {
|
const onAudioPause = (info) => {
|
||||||
document.title = 'Navidrome'
|
dispatch(currentPlaying(info))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onAudioEnded = (currentPlayId, audioLists, info) => {
|
||||||
|
dispatch(currentPlaying(info))
|
||||||
|
dataProvider.getOne('keepalive', { id: info.trackId })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authenticated && options.audioLists.length > 0) {
|
if (authenticated && options.audioLists.length > 0) {
|
||||||
@@ -131,6 +139,7 @@ const Player = () => {
|
|||||||
onAudioListsChange={OnAudioListsChange}
|
onAudioListsChange={OnAudioListsChange}
|
||||||
onAudioProgress={OnAudioProgress}
|
onAudioProgress={OnAudioProgress}
|
||||||
onAudioPlay={OnAudioPlay}
|
onAudioPlay={OnAudioPlay}
|
||||||
|
onAudioPause={onAudioPause}
|
||||||
onAudioEnded={onAudioEnded}
|
onAudioEnded={onAudioEnded}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const PLAYER_SET_TRACK = 'PLAYER_SET_TRACK'
|
|||||||
const PLAYER_SYNC_QUEUE = 'PLAYER_SYNC_QUEUE'
|
const PLAYER_SYNC_QUEUE = 'PLAYER_SYNC_QUEUE'
|
||||||
const PLAYER_SCROBBLE = 'PLAYER_SCROBBLE'
|
const PLAYER_SCROBBLE = 'PLAYER_SCROBBLE'
|
||||||
const PLAYER_PLAY_TRACKS = 'PLAYER_PLAY_TRACKS'
|
const PLAYER_PLAY_TRACKS = 'PLAYER_PLAY_TRACKS'
|
||||||
|
const PLAYER_CURRENT = 'PLAYER_CURRENT'
|
||||||
|
|
||||||
const mapToAudioLists = (item) => {
|
const mapToAudioLists = (item) => {
|
||||||
// If item comes from a playlist, id is mediaFileId
|
// If item comes from a playlist, id is mediaFileId
|
||||||
@@ -88,13 +89,30 @@ const scrobble = (id, submit) => ({
|
|||||||
submit,
|
submit,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentPlaying = (audioInfo) => ({
|
||||||
|
type: PLAYER_CURRENT,
|
||||||
|
data: audioInfo,
|
||||||
|
})
|
||||||
|
|
||||||
const playQueueReducer = (
|
const playQueueReducer = (
|
||||||
previousState = { queue: [], clear: true, playing: false },
|
previousState = { queue: [], clear: true, playing: false, current: {} },
|
||||||
payload
|
payload
|
||||||
) => {
|
) => {
|
||||||
let queue
|
let queue, current
|
||||||
const { type, data } = payload
|
const { type, data } = payload
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case PLAYER_CURRENT:
|
||||||
|
queue = previousState.queue
|
||||||
|
current = data.ended
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
trackId: data.trackId,
|
||||||
|
paused: data.paused,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...previousState,
|
||||||
|
current,
|
||||||
|
}
|
||||||
case PLAYER_ADD_TRACKS:
|
case PLAYER_ADD_TRACKS:
|
||||||
queue = previousState.queue
|
queue = previousState.queue
|
||||||
Object.keys(data).forEach((id) => {
|
Object.keys(data).forEach((id) => {
|
||||||
@@ -109,10 +127,12 @@ const playQueueReducer = (
|
|||||||
playing: true,
|
playing: true,
|
||||||
}
|
}
|
||||||
case PLAYER_SYNC_QUEUE:
|
case PLAYER_SYNC_QUEUE:
|
||||||
|
current = data.length > 0 ? previousState.current : {}
|
||||||
return {
|
return {
|
||||||
...previousState,
|
...previousState,
|
||||||
queue: data,
|
queue: data,
|
||||||
clear: false,
|
clear: false,
|
||||||
|
current,
|
||||||
}
|
}
|
||||||
case PLAYER_SCROBBLE:
|
case PLAYER_SCROBBLE:
|
||||||
const newQueue = previousState.queue.map((item) => {
|
const newQueue = previousState.queue.map((item) => {
|
||||||
@@ -156,6 +176,7 @@ export {
|
|||||||
playTracks,
|
playTracks,
|
||||||
syncQueue,
|
syncQueue,
|
||||||
scrobble,
|
scrobble,
|
||||||
|
currentPlaying,
|
||||||
shuffleTracks,
|
shuffleTracks,
|
||||||
playQueueReducer,
|
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 List from './List'
|
||||||
import { SongDatagrid, SongDatagridRow } from './SongDatagrid'
|
import { SongDatagrid, SongDatagridRow } from './SongDatagrid'
|
||||||
import SongContextMenu from './SongContextMenu'
|
import SongContextMenu from './SongContextMenu'
|
||||||
|
import SongTitleField from './SongTitleField'
|
||||||
import QuickFilter from './QuickFilter'
|
import QuickFilter from './QuickFilter'
|
||||||
import useAlbumsPerPage from './useAlbumsPerPage'
|
import useAlbumsPerPage from './useAlbumsPerPage'
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export {
|
|||||||
SongDetails,
|
SongDetails,
|
||||||
SongDatagrid,
|
SongDatagrid,
|
||||||
SongDatagridRow,
|
SongDatagridRow,
|
||||||
|
SongTitleField,
|
||||||
DocLink,
|
DocLink,
|
||||||
formatRange,
|
formatRange,
|
||||||
ArtistLinkField,
|
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,
|
SongDetails,
|
||||||
SongContextMenu,
|
SongContextMenu,
|
||||||
SongDatagrid,
|
SongDatagrid,
|
||||||
|
SongTitleField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import AddToPlaylistDialog from '../dialogs/AddToPlaylistDialog'
|
import AddToPlaylistDialog from '../dialogs/AddToPlaylistDialog'
|
||||||
import { AlbumLinkField } from '../song/AlbumLinkField'
|
import { AlbumLinkField } from '../song/AlbumLinkField'
|
||||||
@@ -160,7 +161,7 @@ const PlaylistSongs = (props) => {
|
|||||||
contextAlwaysVisible={!isDesktop}
|
contextAlwaysVisible={!isDesktop}
|
||||||
>
|
>
|
||||||
{isDesktop && <TextField source="id" label={'#'} />}
|
{isDesktop && <TextField source="id" label={'#'} />}
|
||||||
<TextField source="title" />
|
<SongTitleField source="title" showTrackNumbers={false} />
|
||||||
{isDesktop && <AlbumLinkField source="album" />}
|
{isDesktop && <AlbumLinkField source="album" />}
|
||||||
{isDesktop && <TextField source="artist" />}
|
{isDesktop && <TextField source="artist" />}
|
||||||
<DurationField
|
<DurationField
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
SongDatagrid,
|
SongDatagrid,
|
||||||
SongDetails,
|
SongDetails,
|
||||||
QuickFilter,
|
QuickFilter,
|
||||||
|
SongTitleField,
|
||||||
} from '../common'
|
} from '../common'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
import { setTrack } from '../audioplayer'
|
import { setTrack } from '../audioplayer'
|
||||||
@@ -83,7 +84,7 @@ const SongList = (props) => {
|
|||||||
rowClick={handleRowClick}
|
rowClick={handleRowClick}
|
||||||
contextAlwaysVisible={!isDesktop}
|
contextAlwaysVisible={!isDesktop}
|
||||||
>
|
>
|
||||||
<TextField source="title" />
|
<SongTitleField source="title" showTrackNumbers={false} />
|
||||||
{isDesktop && <AlbumLinkField source="album" />}
|
{isDesktop && <AlbumLinkField source="album" />}
|
||||||
<TextField source="artist" />
|
<TextField source="artist" />
|
||||||
{isDesktop && <NumberField source="trackNumber" />}
|
{isDesktop && <NumberField source="trackNumber" />}
|
||||||
|
|||||||
Reference in New Issue
Block a user