Merge pull request #1622 from 1hitsong/musicTrickBar

Add scrub function to audio trickplay bar
This commit is contained in:
1hitsong 2024-01-31 09:48:28 -05:00 committed by GitHub
commit 7de5e684d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 207 additions and 23 deletions

View File

@ -5,6 +5,9 @@ import "pkg:/source/utils/config.bs"
sub init() sub init()
m.top.optionsAvailable = false m.top.optionsAvailable = false
m.inScrubMode = false
m.lastRecordedPositionTimestamp = 0
m.scrubTimestamp = -1
setupAudioNode() setupAudioNode()
setupAnimationTasks() setupAnimationTasks()
@ -16,6 +19,7 @@ sub init()
m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count() m.playlistTypeCount = m.global.queueManager.callFunc("getQueueUniqueTypes").count()
m.buttonCount = m.buttons.getChildCount() m.buttonCount = m.buttons.getChildCount()
m.seekPosition.translation = [720 - (m.seekPosition.width / 2), m.seekPosition.translation[1]]
m.screenSaverTimeout = 300 m.screenSaverTimeout = 300
@ -32,6 +36,8 @@ sub init()
pageContentChanged() pageContentChanged()
setShuffleIconState() setShuffleIconState()
setLoopButtonImage() setLoopButtonImage()
m.buttons.setFocus(true)
end sub end sub
sub onScreensaverTimeoutLoaded() sub onScreensaverTimeoutLoaded()
@ -117,13 +123,18 @@ sub setupInfoNodes()
m.playPosition = m.top.findNode("playPosition") m.playPosition = m.top.findNode("playPosition")
m.bufferPosition = m.top.findNode("bufferPosition") m.bufferPosition = m.top.findNode("bufferPosition")
m.seekBar = m.top.findNode("seekBar") m.seekBar = m.top.findNode("seekBar")
m.thumb = m.top.findNode("thumb")
m.shuffleIndicator = m.top.findNode("shuffleIndicator") m.shuffleIndicator = m.top.findNode("shuffleIndicator")
m.loopIndicator = m.top.findNode("loopIndicator") m.loopIndicator = m.top.findNode("loopIndicator")
m.positionTimestamp = m.top.findNode("positionTimestamp") m.positionTimestamp = m.top.findNode("positionTimestamp")
m.seekPosition = m.top.findNode("seekPosition")
m.seekTimestamp = m.top.findNode("seekTimestamp")
m.totalLengthTimestamp = m.top.findNode("totalLengthTimestamp") m.totalLengthTimestamp = m.top.findNode("totalLengthTimestamp")
end sub end sub
sub bufferPositionChanged() sub bufferPositionChanged()
if m.inScrubMode then return
if not isValid(m.global.audioPlayer.bufferingStatus) if not isValid(m.global.audioPlayer.bufferingStatus)
bufferPositionBarWidth = m.seekBar.width bufferPositionBarWidth = m.seekBar.width
else else
@ -141,6 +152,8 @@ sub bufferPositionChanged()
end sub end sub
sub audioPositionChanged() sub audioPositionChanged()
stopLoadingSpinner()
if m.global.audioPlayer.position = 0 if m.global.audioPlayer.position = 0
m.playPosition.width = 0 m.playPosition.width = 0
end if end if
@ -159,14 +172,22 @@ sub audioPositionChanged()
playPositionBarWidth = m.seekBar.width playPositionBarWidth = m.seekBar.width
end if end if
if not m.inScrubMode
moveSeekbarThumb(playPositionBarWidth)
' Change the seek position timestamp
m.seekTimestamp.text = secondsToHuman(m.global.audioPlayer.position, false)
end if
' Use animation to make the display smooth ' Use animation to make the display smooth
m.playPositionAnimationWidth.keyValue = [m.playPosition.width, playPositionBarWidth] m.playPositionAnimationWidth.keyValue = [m.playPosition.width, playPositionBarWidth]
m.playPositionAnimation.control = "start" m.playPositionAnimation.control = "start"
' Update displayed position timestamp ' Update displayed position timestamp
if isValid(m.global.audioPlayer.position) if isValid(m.global.audioPlayer.position)
m.lastRecordedPositionTimestamp = m.global.audioPlayer.position
m.positionTimestamp.text = secondsToHuman(m.global.audioPlayer.position, false) m.positionTimestamp.text = secondsToHuman(m.global.audioPlayer.position, false)
else else
m.lastRecordedPositionTimestamp = 0
m.positionTimestamp.text = "0:00" m.positionTimestamp.text = "0:00"
end if end if
@ -217,7 +238,9 @@ sub audioStateChanged()
if m.global.audioPlayer.state = "finished" if m.global.audioPlayer.state = "finished"
' User has enabled single song loop, play current song again ' User has enabled single song loop, play current song again
if m.global.audioPlayer.loopMode = "one" if m.global.audioPlayer.loopMode = "one"
m.scrubTimestamp = -1
playAction() playAction()
exitScrubMode()
return return
end if end if
@ -264,6 +287,11 @@ function previousClicked() as boolean
if m.playlistTypeCount > 1 then return false if m.playlistTypeCount > 1 then return false
if m.global.queueManager.callFunc("getPosition") = 0 then return false if m.global.queueManager.callFunc("getPosition") = 0 then return false
exitScrubMode()
m.lastRecordedPositionTimestamp = 0
m.positionTimestamp.text = "0:00"
if m.global.audioPlayer.state = "playing" if m.global.audioPlayer.state = "playing"
m.global.audioPlayer.control = "stop" m.global.audioPlayer.control = "stop"
end if end if
@ -276,7 +304,6 @@ function previousClicked() as boolean
m.global.queueManager.callFunc("moveBack") m.global.queueManager.callFunc("moveBack")
pageContentChanged() pageContentChanged()
return true return true
end function end function
@ -314,6 +341,11 @@ end sub
function nextClicked() as boolean function nextClicked() as boolean
if m.playlistTypeCount > 1 then return false if m.playlistTypeCount > 1 then return false
exitScrubMode()
m.lastRecordedPositionTimestamp = 0
m.positionTimestamp.text = "0:00"
' Reset loop mode due to manual user interaction ' Reset loop mode due to manual user interaction
if m.global.audioPlayer.loopMode = "one" if m.global.audioPlayer.loopMode = "one"
resetLoopModeToDefault() resetLoopModeToDefault()
@ -379,6 +411,8 @@ sub LoadNextSong()
m.global.audioPlayer.control = "stop" m.global.audioPlayer.control = "stop"
end if end if
exitScrubMode()
' Reset playPosition bar without animation ' Reset playPosition bar without animation
m.playPosition.width = 0 m.playPosition.width = 0
m.global.queueManager.callFunc("moveForward") m.global.queueManager.callFunc("moveForward")
@ -544,6 +578,96 @@ sub setBackdropImage(data)
end if end if
end sub end sub
' setSelectedButtonState: Changes the icon state url for the currently selected button
'
' @param {string} oldState - current state to replace in icon url
' @param {string} newState - state to replace {oldState} with in icon url
sub setSelectedButtonState(oldState as string, newState as string)
selectedButton = m.buttons.getChild(m.top.selectedButtonIndex)
selectedButton.uri = selectedButton.uri.Replace(oldState, newState)
end sub
' processScrubAction: Handles +/- seeking for the audio trickplay bar
'
' @param {integer} seekStep - seconds to move the trickplay position (negative values allowed)
sub processScrubAction(seekStep as integer)
' Prepare starting playStart property value
if m.scrubTimestamp = -1
m.scrubTimestamp = m.lastRecordedPositionTimestamp
end if
' Don't let seek to go past the end of the song
if m.scrubTimestamp + seekStep > m.songDuration - 5
return
end if
if seekStep > 0
' Move seek forward
m.scrubTimestamp += seekStep
else if m.scrubTimestamp >= Abs(seekStep)
' If back seek won't go below 0, move seek back
m.scrubTimestamp += seekStep
else
' Back seek would go below 0, set to 0 directly
m.scrubTimestamp = 0
end if
' Move the seedbar thumb forward
songPercentComplete = m.scrubTimestamp / m.songDuration
playPositionBarWidth = m.seekBar.width * songPercentComplete
moveSeekbarThumb(playPositionBarWidth)
' Change the displayed position timestamp
m.seekTimestamp.text = secondsToHuman(m.scrubTimestamp, false)
end sub
' resetSeekbarThumb: Resets the thumb to the playing position
'
sub resetSeekbarThumb()
m.scrubTimestamp = -1
moveSeekbarThumb(m.playPosition.width)
end sub
' moveSeekbarThumb: Positions the thumb on the seekbar
'
' @param {float} playPositionBarWidth - width of the play position bar
sub moveSeekbarThumb(playPositionBarWidth as float)
' Center the thumb on the play position bar
thumbPostionLeft = playPositionBarWidth - 10
' Don't let thumb go below 0
if thumbPostionLeft < 0 then thumbPostionLeft = 0
' Don't let thumb go past end of seekbar
if thumbPostionLeft > m.seekBar.width - 25
thumbPostionLeft = m.seekBar.width - 25
end if
' Move the thumb
m.thumb.translation = [thumbPostionLeft, m.thumb.translation[1]]
' Move the seek position element so it follows the thumb
m.seekPosition.translation = [720 + thumbPostionLeft - (m.seekPosition.width / 2), m.seekPosition.translation[1]]
end sub
' exitScrubMode: Moves player out of scrub mode state, resets back to standard play mode
'
sub exitScrubMode()
m.buttons.setFocus(true)
m.thumb.setFocus(false)
if m.seekPosition.visible
m.seekPosition.visible = false
end if
resetSeekbarThumb()
m.inScrubMode = false
m.thumb.visible = false
setSelectedButtonState("-default", "-selected")
end sub
' Process key press events ' Process key press events
function onKeyEvent(key as string, press as boolean) as boolean function onKeyEvent(key as string, press as boolean) as boolean
@ -555,9 +679,58 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true return true
end if end if
' Key Event handler when m.thumb is in focus
if m.thumb.hasFocus()
if key = "right"
m.inScrubMode = true
processScrubAction(10)
return true
end if
if key = "left"
m.inScrubMode = true
processScrubAction(-10)
return true
end if
if key = "OK" or key = "play"
if m.inScrubMode
startLoadingSpinner()
m.inScrubMode = false
m.global.audioPlayer.seek = m.scrubTimestamp
return true
end if
return playAction()
end if
end if
if key = "play" if key = "play"
return playAction() return playAction()
else if key = "back" end if
if key = "up"
if not m.thumb.visible
m.thumb.visible = true
setSelectedButtonState("-selected", "-default")
end if
if not m.seekPosition.visible
m.seekPosition.visible = true
end if
m.thumb.setFocus(true)
m.buttons.setFocus(false)
return true
end if
if key = "down"
if m.thumb.visible
exitScrubMode()
end if
return true
end if
if key = "back"
m.global.audioPlayer.control = "stop" m.global.audioPlayer.control = "stop"
m.global.audioPlayer.loopMode = "" m.global.audioPlayer.loopMode = ""
else if key = "rewind" else if key = "rewind"
@ -565,6 +738,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
else if key = "fastforward" else if key = "fastforward"
return nextClicked() return nextClicked()
else if key = "left" else if key = "left"
if m.buttons.hasFocus()
if m.global.queueManager.callFunc("getCount") = 1 then return false if m.global.queueManager.callFunc("getCount") = 1 then return false
if m.top.selectedButtonIndex > 0 if m.top.selectedButtonIndex > 0
@ -572,13 +746,17 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.top.selectedButtonIndex = m.top.selectedButtonIndex - 1 m.top.selectedButtonIndex = m.top.selectedButtonIndex - 1
end if end if
return true return true
end if
else if key = "right" else if key = "right"
if m.buttons.hasFocus()
if m.global.queueManager.callFunc("getCount") = 1 then return false if m.global.queueManager.callFunc("getCount") = 1 then return false
m.previouslySelectedButtonIndex = m.top.selectedButtonIndex m.previouslySelectedButtonIndex = m.top.selectedButtonIndex
if m.top.selectedButtonIndex < m.buttonCount - 1 then m.top.selectedButtonIndex = m.top.selectedButtonIndex + 1 if m.top.selectedButtonIndex < m.buttonCount - 1 then m.top.selectedButtonIndex = m.top.selectedButtonIndex + 1
return true return true
end if
else if key = "OK" else if key = "OK"
if m.buttons.hasFocus()
if m.buttons.getChild(m.top.selectedButtonIndex).id = "play" if m.buttons.getChild(m.top.selectedButtonIndex).id = "play"
return playAction() return playAction()
else if m.buttons.getChild(m.top.selectedButtonIndex).id = "previous" else if m.buttons.getChild(m.top.selectedButtonIndex).id = "previous"
@ -592,6 +770,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if end if
end if end if
end if end if
end if
return false return false
end function end function

View File

@ -4,8 +4,9 @@
<Poster id="backdrop" opacity=".5" loadDisplayMode="scaleToZoom" width="1920" height="1200" blendColor="#3f3f3f" /> <Poster id="backdrop" opacity=".5" loadDisplayMode="scaleToZoom" width="1920" height="1200" blendColor="#3f3f3f" />
<Poster id="shuffleIndicator" width="64" height="64" uri="pkg:/images/icons/shuffleIndicator-off.png" translation="[1150,775]" opacity="0" /> <Poster id="shuffleIndicator" width="64" height="64" uri="pkg:/images/icons/shuffleIndicator-off.png" translation="[1150,775]" opacity="0" />
<Poster id="loopIndicator" width="64" height="64" uri="pkg:/images/icons/loopIndicator-off.png" translation="[700,775]" opacity="0" /> <Poster id="loopIndicator" width="64" height="64" uri="pkg:/images/icons/loopIndicator-off.png" translation="[700,775]" opacity="0" />
<Label id="positionTimestamp" width="100" height="25" horizAlign="right" font="font:SmallestSystemFont" translation="[590,825]" color="#999999" text="0:00" /> <Label id="positionTimestamp" width="100" height="25" horizAlign="right" font="font:SmallestSystemFont" translation="[590,838]" color="#999999" text="0:00" />
<Label id="totalLengthTimestamp" width="100" height="25" horizAlign="left" font="font:SmallestSystemFont" translation="[1230,825]" color="#999999" /> <Label id="totalLengthTimestamp" width="100" height="25" horizAlign="left" font="font:SmallestSystemFont" translation="[1230,838]" color="#999999" />
<LayoutGroup id="toplevel" layoutDirection="vert" horizAlignment="center" translation="[960,175]" itemSpacings="[40]"> <LayoutGroup id="toplevel" layoutDirection="vert" horizAlignment="center" translation="[960,175]" itemSpacings="[40]">
<LayoutGroup id="main_group" layoutDirection="vert" horizAlignment="center" itemSpacings="[15]"> <LayoutGroup id="main_group" layoutDirection="vert" horizAlignment="center" itemSpacings="[15]">
<Poster id="albumCover" width="500" height="500" /> <Poster id="albumCover" width="500" height="500" />
@ -16,6 +17,7 @@
<Rectangle id="seekBar" color="0x00000099" width="500" height="10"> <Rectangle id="seekBar" color="0x00000099" width="500" height="10">
<Rectangle id="bufferPosition" color="0xFFFFFF44" height="10"></Rectangle> <Rectangle id="bufferPosition" color="0xFFFFFF44" height="10"></Rectangle>
<Rectangle id="playPosition" color="#00a4dcFF" height="10"></Rectangle> <Rectangle id="playPosition" color="#00a4dcFF" height="10"></Rectangle>
<Poster id="thumb" width="25" height="25" uri="pkg:/images/icons/circle.png" visible="false" translation="[0, -10]" />
</Rectangle> </Rectangle>
<LayoutGroup id="buttons" layoutDirection="horiz" horizAlignment="center" itemSpacings="[45]"> <LayoutGroup id="buttons" layoutDirection="horiz" horizAlignment="center" itemSpacings="[45]">
<Poster id="loop" width="64" height="64" uri="pkg:/images/icons/loop-default.png" opacity="0" /> <Poster id="loop" width="64" height="64" uri="pkg:/images/icons/loop-default.png" opacity="0" />
@ -37,6 +39,9 @@
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, 1.0]" fieldToInterp="loop.opacity" /> <FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0.0, 1.0]" fieldToInterp="loop.opacity" />
</Animation> </Animation>
</LayoutGroup> </LayoutGroup>
<Rectangle id="seekPosition" visible="false" color="0x00000090" height="40" width="110" translation="[720, 790]">
<Label text="0:00" id="seekTimestamp" width="110" height="40" horizAlign="center" vertAlign="center" font="font:SmallestSystemFont" />
</Rectangle>
<Rectangle id="screenSaverBackground" width="1920" height="1080" color="#000000" visible="false" /> <Rectangle id="screenSaverBackground" width="1920" height="1080" color="#000000" visible="false" />
<Poster id="screenSaverAlbumCover" width="500" height="500" translation="[960,575]" opacity="0" /> <Poster id="screenSaverAlbumCover" width="500" height="500" translation="[960,575]" opacity="0" />
<Poster id="PosterOne" width="389" height="104" translation="[960,540]" opacity="0" /> <Poster id="PosterOne" width="389" height="104" translation="[960,540]" opacity="0" />

BIN
images/icons/circle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB