From 9d23b191b5b04d04904ea24b2dbd87f62e15aaff Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 19 Jun 2020 09:12:45 -0400 Subject: [PATCH] Show indicator on current playing song. Fixes #128 --- ui/src/album/AlbumSongs.js | 27 ++++------- ui/src/audioplayer/Player.js | 17 +++++-- ui/src/audioplayer/queue.js | 25 +++++++++- ui/src/common/SongTitleField.js | 79 +++++++++++++++++++++++++++++++ ui/src/common/index.js | 2 + ui/src/icons/paused-dark.png | Bin 0 -> 351 bytes ui/src/icons/paused-light.png | Bin 0 -> 827 bytes ui/src/icons/playing-dark.gif | Bin 0 -> 2349 bytes ui/src/icons/playing-light.gif | Bin 0 -> 2349 bytes ui/src/playlist/PlaylistSongs.js | 3 +- ui/src/song/SongList.js | 3 +- 11 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 ui/src/common/SongTitleField.js create mode 100644 ui/src/icons/paused-dark.png create mode 100644 ui/src/icons/paused-light.png create mode 100644 ui/src/icons/playing-dark.gif create mode 100644 ui/src/icons/playing-light.gif diff --git a/ui/src/album/AlbumSongs.js b/ui/src/album/AlbumSongs.js index 1ff5418ab..7b64a4fe1 100644 --- a/ui/src/album/AlbumSongs.js +++ b/ui/src/album/AlbumSongs.js @@ -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 && } - {!isDesktop && ( - - )} + {isDesktop && } { } 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} /> ) diff --git a/ui/src/audioplayer/queue.js b/ui/src/audioplayer/queue.js index e5f3d74dd..251030987 100644 --- a/ui/src/audioplayer/queue.js +++ b/ui/src/audioplayer/queue.js @@ -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, } diff --git a/ui/src/common/SongTitleField.js b/ui/src/common/SongTitleField.js new file mode 100644 index 000000000..f7e94495f --- /dev/null +++ b/ui/src/common/SongTitleField.js @@ -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 ( + {paused + ) + } + + return ( + <> + {isCurrent && } + + + ) +} + +SongTitleField.propTypes = { + record: PropTypes.object, + showTrackNumbers: PropTypes.bool, +} + +export default SongTitleField diff --git a/ui/src/common/index.js b/ui/src/common/index.js index ae720cc86..0ccf8336e 100644 --- a/ui/src/common/index.js +++ b/ui/src/common/index.js @@ -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, diff --git a/ui/src/icons/paused-dark.png b/ui/src/icons/paused-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..548f9a7f04e398468c8f09c39c5c8f3ffde72228 GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U{v#TaSW-5dwc01=V1d8R>$4^ zU*l)~@=#!vINE)n?PO%O-Q;q)I?o3_TYoS}xHAYOh;-~^W$Ixv-0sj2sPIgT!+`6^ z9VSM%#u?EH2Q&nVxmhGwlkyrF7CW5LrHNvH);Yy>VR_QYfBqf0`#wBx=jXKBOy$jg ze*d+$(B?fLy0qJzVLq3{`zqgkxsB@uJKC8jchP^Q^m>E9q_~3%fdR+h>FVdQ&MBb@ E09zt-=l}o! literal 0 HcmV?d00001 diff --git a/ui/src/icons/paused-light.png b/ui/src/icons/paused-light.png new file mode 100644 index 0000000000000000000000000000000000000000..bf08e24ec612fae8ebbacd85c1dc34813c639075 GIT binary patch literal 827 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$Ysfq{uTGbExU!q>+tIX_n~ zF(p4KRj(qq0H}k3!KT6r$jnVGNmQuF&B-gas<2f8tFQvHLBje<3ScEA*|tg%z5xo( z`9-M;rg|oN21<5Z3JMA~MJZ`kK`w4k?LeNbQbtKhft9{~d3m{Bxv^e;QM$gNrKP35 zfswwEkuFe$ZgFK^Nn(X=Ua>O75STeGsl~}fnFS@8`FRQ;a}$&DOG|8(lt3220mPjp znP~`{@`|C}0(wv%B%^PrXP^%^8>rO=Bx)6uTAZI#3Nk)4FSEqX$Ofz!T^L~-M3-}Z zZb4CMaWPPvogq{UvKYF0|Dw!Pp#MPDz||v*p{ozb$S=tUhILV9vS(gNY7x*fsBV}8 zajSruWup%YE~Mat#0^*!7&&%aHu~_0w&UV=)tU^9`}LkKjv*C{Z!c`*JYc}%5~w~q zm4snd%H7B%%-nCe zL{XwSEs@P7$tBvD2ckmiEa$9R>HKgW=O1`IKkwJ;^?1KON8v_?U?_9I9GC_GI-UOc z^JlpQ!SzCb03i2S#MIE*%0SQD5DMA1RsaA1bqPQkkO#H{z#5Ynl5jg&%qdmY-gsaD zyq6v*6F}JJp6muLzRpIRO}jsb8Wc#o=R;a}g3ve1Pl9c~V8Ga-5~La2saSRf0WIr= zNA#odRVZkjB>-@lLSL9%z%DKT#09{A0U%38VHxF@>z?_t=rG8j`&4v~|Gixn>kcRq z5lOMVX?In+-nhASpRQmkzOQ_Ztjb#RMSW$W@6^>}NR)Ptmou+Lkaz z$Q?7LY@H}i1GnxA?#KQ4{*&0o4GKcvKaV%RC0F1#&b=(Wcv~v?p+o(Yot}zgj$XP; zWUk{=V7ORI{ACOb?H+d6FoVVt zrwMK-vXfvg6!|B<((uDJ3Nzh2dY4uCK)lcpUqh2;ufd#I>OMOZLA(ja$fSg(v{sk( zCzJ#*9YSUo9cH>O-H$()m%52WL0byB#HE?pSDtA>U3s*pYB4zc#t59-zn@bx<&%Fm zc^iMK=6X*)!5tRTjcbOWJ7?2s57Gxq_{`#Hw&2BV>FXNLr7sJNKnWg@Y`Q|_ zp_X?!y%Va`f%AEeUN-9?B-NGf@Iw7pMmNUaQKdktURbq{=4babpz!2dFWUMmdv}md z2+Au%5I%vH`bRriR{L9iudp>r{6NG|PEs{s5;D50GAve`<7bAC%)$V^7F7!BZ}2$0 z)tiO4bg&?nYG-1SPlPl2gtQ)Ht1V2t4u6KW;0r{STPz7~u@GUT&26{DoQ`2rBB?g- zFg>uoxyw5*B)rkoWFx9KO4mVc^5n=ehhJ83JR34dN&9i3`a8F_;q+ZPM@s8_k@4~# zwra1vOI{iA$@A)TJdBjt&WV-}sH9o%uRpqUUrGj);Nl+Q7EqiRSGV_Zlea5#ZqJk@ zJ@vym1F30S-<^ae({g55d3wI05;(@N$W-07?<3}pbs5qY%vV@m6sEr>*iIOodqFuc zHc*qx4E#gxy!ido1;(!H{T>g^GVr?Y7VvoO7TJm_&gOvb&{}yY_=V`URT11q{kD1#`m2^_j-X#Gkje!njaOcf7g)UzY zMpnn}lK1!xUe6~Fj&ni?BesWd{t49dm0kbiC0TWABrT9rYI4!kI*%xtv`M%BRkOO~ zwe$XZFCkiYcLu|Upa$5wsFwVBRvL*b)SJ+63zvJB259 uEh}lhx(1ru?PEA<(Gvto=}@*&B|~g`shaIXM8l8?U&DCG{Ey!VoxcD>{LWYa literal 0 HcmV?d00001 diff --git a/ui/src/icons/playing-light.gif b/ui/src/icons/playing-light.gif new file mode 100644 index 0000000000000000000000000000000000000000..40bb3c428effcaec4d98a94f5c8e6731c590bdb1 GIT binary patch literal 2349 zcmb`J`#;lr9LJ|{Ds^;`=9Wvjji#;8lE--xrLk$aL+z%ivbs+SQ7o@->&l=FTtAuHVcPo~8A)sab z@aRD_z6u3Ruv!BMeFg010Dv3-{5Jsdt{5!6{6gKcKxQWlJnT6g+ZTL)x8;U|ibO<8 ze1FD0m7X^q9=)e37>XY%Un8q>*8TW=uDW_0iPFv%bpuq|GmC_4*n|6Ab|y^_@+M3u zJKmOOfLiy5590p*@JV#@raeMm1h~)R&TqvL_=9~v2QSu^4tnHRKW(R{;(SXl)9p&0 z^Gb=25O)6Klj?YF?fHj70lIQePrfKfI7+u%^oB+^7Q}*Fk;(SzNm4x+vTLa(8k}{1 zygHx9W+2Cf-5eJnyqGXrkjs^DaTw9Vy`+fsc-D-uK%89eTzhBqqnsI_8jp%3M@5>W zV*Rs<#IVQZapjyQ=500&S)$c?Y)fikvuI2)n&75l zJ8{NRad7eoBnaCm%<%B;UsdG+acxC>3r&Hw4&$}bdwMjQcms}+O}m)ZT3t4nR2sr? zjF??^oasIPAki;BeJhEAwi0qn$T)6a>D7X|^mt#@a(LABF}MJ~)XaJ07p89GoocT2 z6%agO5xux(FuHp-qgJzX2$d3qauE)P2`1P7C%$V>Vwws(;9?um#mn(e@@?83Yz!;R^4bJV{Q+cRm;a2}! zRqD{0d}m*qjbM`MT6cc_cK9-SF~QC%g%b6`sspqjyQd*V4)1+w8!u@{kxmLKD1#9J zp;r3F-OLjQTK=rCHA-G2Vkiz&2uxg7moLMT-yFX(0%Vtl@U^Hrz`+LZBRl+=cq>Os z;+@0UnADR|^Z_C5hq-D?Z(m0}LtF9$BF8C~IHy>MNYb{po1!kJ*t9EDn*~fCY+&w! z%-N(jT3Reb^=9chs7<~+S@y`w3bs!}7AfTb?xMcTjy9aWTld(VxOYyZ*_ zBOZBfosLD4vfJ6Q3L%v=>jU-1D-R@Ofk|$j5gs8W$q98DPn!JQ8FTxltvb^e{R|}D z*#^oGn#{`?k>!~Mib|k3!(wxF+kuamyVhk$TM$oSxpABSo?!dd$nOQ^;KWc(9wYQG z`7>e6Cm0TNJ98r{;Da2-$2>F~5*QA_UVgG|C+ zftvP|Eygw`wD6?OHOlnM7;&6!tY#0N9FoKOsHSf)atGeP;K-~|{TUtQDUo3{AJSpx z_L<0H*^WM`;H!^NUGK(^<~A1k(*#k8rqlz24HqefzPEU0;O1vwG%To8^U!kF2|%r66*}8hW|L;{>Y+T*ffr z37BiY#V;*&kwZH4CpxE6W+;)2=wF{qX)>cXM$J|4*~an>i~}$EnL*aI2K5 zRRv*6F|~PY&JK;rr>Q5_H{j|=AY!{#Q(JlJ!QKB#-z!MC9#p#`8pY>%j(jKM-1#5x zTo~;?pN`%WJOFOBzMl+x6Q{N*kn`!A%Kll*3)zTrG(El)L4Ie3^@*Rx6MI&bv_`H1 nr}hRIj$8JHfz!H_ZB)r%+kUE6I}y<^YR1zrZZiMlZ-mZ2g`vz5 literal 0 HcmV?d00001 diff --git a/ui/src/playlist/PlaylistSongs.js b/ui/src/playlist/PlaylistSongs.js index 4a03c35d3..6f4a57ceb 100644 --- a/ui/src/playlist/PlaylistSongs.js +++ b/ui/src/playlist/PlaylistSongs.js @@ -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 && } - + {isDesktop && } {isDesktop && } { rowClick={handleRowClick} contextAlwaysVisible={!isDesktop} > - + {isDesktop && } {isDesktop && }