components_movies_MovieDetails.bs

import "pkg:/source/roku_modules/log/LogMixin.brs"
import "pkg:/source/utils/config.bs"
import "pkg:/source/utils/misc.bs"
import "pkg:/source/utils/streamSelection.bs"

sub init()
  m.log = log.Logger("MovieDetails")
  m.extrasGrp = m.top.findnode("extrasGrp")
  m.extrasGrid = m.top.findNode("extrasGrid")
  m.top.optionsAvailable = false

  m.options = m.top.findNode("movieOptions")
  m.infoGroup = m.top.findNode("infoGroup")

  ' Set overview width (full width minus margins)
  overview = m.top.findNode("overview")
  overview.width = 1920 - 96 - 96

  m.details = m.top.findNode("details")
  m.tagline = m.top.findNode("tagline")

  m.buttonGrp = m.top.findNode("buttons")
  m.buttonGrp.callFunc("focus")
  m.top.lastFocus = m.buttonGrp

  ' Options button will be created dynamically as needed
  m.optionsButton = invalid

  m.isFirstRun = true
  m.allowButtonUpdates = true ' Allow button updates on init and when returning to screen
  m.top.observeField("itemContent", "itemContentChanged")

  m.directorGenreGroup = m.top.findNode("directorGenreGroup")
  m.infoDividerCount = 0
  m.directorGenreDividerCount = 0
end sub

' OnScreenShown: Callback function when view is presented on screen
'
sub OnScreenShown()
  ' Set backdrop from movie data (only if itemContent is already loaded)
  ' This handles the case when returning to an already-loaded screen
  if isValid(m.top.itemContent)
    if isValid(m.top.itemContent.backdropUrl)
      m.global.sceneManager.callFunc("setBackgroundImage", m.top.itemContent.backdropUrl)
    end if
  end if
  ' If itemContent not loaded, backdrop will be set in itemContentChanged()

  ' set focus to button group
  if m.extrasGrp.opacity = 1
    m.top.lastFocus.setFocus(true)
  else
    m.buttonGrp.setFocus(true)
  end if

  if m.isFirstRun
    m.isFirstRun = false
  else
    ' Trigger data refresh when returning to screen
    m.allowButtonUpdates = true ' Allow button updates for upcoming data refresh
    m.top.refreshMovieDetailsData = not m.top.refreshMovieDetailsData
  end if
end sub

sub trailerAvailableChanged()
  if m.top.trailerAvailable
    ' add trailor button to button group
    trailerButton = CreateObject("roSGNode", "IconButton")
    trailerButton.id = "trailerButton"
    trailerButton.icon = "pkg:/images/icons/playOutline.png"
    trailerButton.text = tr("Play Trailer")
    m.buttonGrp.appendChild(trailerButton)
  else
    ' remove trailor button from button group
    trailerButton = m.top.findNode("trailerButton")
    if isValid(trailerButton)
      m.buttonGrp.removeChild(trailerButton)
    end if
  end if
end sub

' manageResumeButton: Add or remove Resume button based on playback position
'
sub manageResumeButton()
  resumeButton = m.top.findNode("resumeButton")

  if isValid(m.top.itemContent) and isValid(m.top.itemContent.json) and isValid(m.top.itemContent.json.UserData)
    ' Validate RunTimeTicks before proceeding - required for progress bar calculation
    if not isValid(m.top.itemContent.json.RunTimeTicks) or m.top.itemContent.json.RunTimeTicks <= 0
      ' Invalid runtime, remove resume button if present and exit
      removeResumeButtonWithFocus(resumeButton)
      return
    end if

    if isValid(m.top.itemContent.json.UserData.PlaybackPositionTicks) and m.top.itemContent.json.UserData.PlaybackPositionTicks > 0
      ' Add resume button if not already present
      if not isValid(resumeButton)
        ' Capture current focus state before modifying button group
        currentFocusIndex = m.buttonGrp.buttonFocused
        currentFocusedButton = invalid
        if isValid(currentFocusIndex) and currentFocusIndex >= 0 and currentFocusIndex < m.buttonGrp.getChildCount()
          currentFocusedButton = m.buttonGrp.getChild(currentFocusIndex)
        end if

        resumeButton = CreateObject("roSGNode", "ResumeButton")
        resumeButton.id = "resumeButton"
        resumeButton.icon = "pkg:/images/icons/resume.png"
        resumeButton.text = tr("Resume")
        resumeButton.playbackPositionTicks = m.top.itemContent.json.UserData.PlaybackPositionTicks
        resumeButton.runtimeTicks = m.top.itemContent.json.RunTimeTicks

        ' Insert at index 0 (first position)
        m.buttonGrp.insertChild(resumeButton, 0)

        ' Update focus: move to Resume if previously on Play, otherwise maintain focus on same button
        if isValid(currentFocusedButton) and currentFocusedButton.id = "playButton"
          ' User was on Play button, move them to Resume
          m.buttonGrp.buttonFocused = 0
        else if isValid(currentFocusIndex) and currentFocusIndex >= 0
          ' User was on another button, adjust index to account for insertion at index 0
          m.buttonGrp.buttonFocused = currentFocusIndex + 1
        else
          ' No previous focus or invalid state, default to Resume
          m.buttonGrp.buttonFocused = 0
        end if

        m.buttonGrp.callFunc("focus")
      else
        ' Resume button already exists, update its tick values (data may have refreshed)
        resumeButton.playbackPositionTicks = m.top.itemContent.json.UserData.PlaybackPositionTicks
        resumeButton.runtimeTicks = m.top.itemContent.json.RunTimeTicks
      end if
    else
      ' Remove resume button if present
      removeResumeButtonWithFocus(resumeButton)
    end if
  else
    ' No valid data, remove resume button if present
    removeResumeButtonWithFocus(resumeButton)
  end if
end sub

' removeResumeButtonWithFocus: Helper to remove resume button while preserving focus
' @param {object} resumeButton - The resume button node to remove (if valid)
'
sub removeResumeButtonWithFocus(resumeButton as object)
  if not isValid(resumeButton) then return

  ' Preserve user's focus position when removing Resume button
  currentFocusIndex = m.buttonGrp.buttonFocused

  ' Reset tick values before removing to clean up state
  resumeButton.playbackPositionTicks = 0
  resumeButton.runtimeTicks = 0
  m.buttonGrp.removeChild(resumeButton)

  ' Adjust focus: if user was on Resume (index 0), move to Play (new index 0)
  ' Otherwise, shift focus index down by 1 to maintain same logical button
  if isValid(currentFocusIndex) and currentFocusIndex > 0
    m.buttonGrp.buttonFocused = currentFocusIndex - 1
  else
    m.buttonGrp.buttonFocused = 0
  end if
  m.buttonGrp.callFunc("focus")
end sub

' createInfoLabel: Create a label for the info/director rows
' @param labelId - Unique ID for the label
' @return Configured LabelPrimaryMedium node (bold)
function createInfoLabel(labelId as string) as object
  labelNode = CreateObject("roSGNode", "LabelPrimaryMedium")
  labelNode.id = labelId
  labelNode.vertAlign = "center"
  labelNode.bold = true
  return labelNode
end function

' createDividerNode: Create a bullet divider node for separating info items
' @param dividerId - Unique ID for the divider
' @return Configured divider node
function createDividerNode(dividerId as string) as object
  labelNode = CreateObject("roSGNode", "LabelPrimarySmall")
  labelNode.id = dividerId
  labelNode.horizAlign = "left"
  labelNode.vertAlign = "center"
  labelNode.height = 40
  labelNode.width = 0
  labelNode.text = "•"
  labelNode.bold = true
  return labelNode
end function

' displayInfoNode: Add a node to the info group with dividers
sub displayInfoNode(node as object)
  if not isValid(node) then return
  if m.infoGroup.getChildCount() > 0
    m.infoDividerCount++
    divider = createDividerNode("infoDivider" + m.infoDividerCount.toStr())
    m.infoGroup.appendChild(divider)
  end if
  m.infoGroup.appendChild(node)
end sub

' displayDirectorGenreNode: Add a node to the director/genre group with dividers
sub displayDirectorGenreNode(node as object)
  if not isValid(node) then return
  if m.directorGenreGroup.getChildCount() > 0
    m.directorGenreDividerCount++
    divider = createDividerNode("directorGenreDivider" + m.directorGenreDividerCount.toStr())
    m.directorGenreGroup.appendChild(divider)
  end if
  m.directorGenreGroup.appendChild(node)
end sub

' populateInfoGroup: Dynamically populate info and director/genre groups
sub populateInfoGroup()
  ' Clear existing nodes
  m.infoGroup.removeChildrenIndex(m.infoGroup.getChildCount(), 0)
  m.directorGenreGroup.removeChildrenIndex(m.directorGenreGroup.getChildCount(), 0)
  m.infoDividerCount = 0
  m.directorGenreDividerCount = 0

  itemData = m.top.itemContent.json
  userSettings = m.global.user.settings

  ' print "itemData:", itemData

  ' === ROW 1: Info Group ===
  ' Order: year, officialRating, communityRating, criticRating, runtime, ends-at

  ' 1. Release Year
  if isValid(itemData.productionYear) and itemData.Type <> "Episode"
    yearNode = createInfoLabel("releaseYear")
    yearNode.text = itemData.productionYear.toStr().trim()
    displayInfoNode(yearNode)
  end if

  ' 2. Official Rating (PG, R, etc.)
  if isValid(itemData.officialRating)
    ratingNode = createInfoLabel("officialRating")
    ratingNode.text = itemData.officialRating
    displayInfoNode(ratingNode)
  end if

  ' 3. Community Rating (star + number)
  if userSettings.uiMoviesShowRatings and isValid(itemData.CommunityRating)
    communityRatingNode = CreateObject("roSGNode", "CommunityRating")
    communityRatingNode.id = "communityRating"
    communityRatingNode.rating = itemData.CommunityRating
    displayInfoNode(communityRatingNode)
  end if

  ' 4. Critic Rating (tomato + number)
  if userSettings.uiMoviesShowRatings and isValid(itemData.CriticRating)
    criticRatingNode = CreateObject("roSGNode", "CriticRating")
    criticRatingNode.id = "criticRating"
    criticRatingNode.rating = itemData.CriticRating
    displayInfoNode(criticRatingNode)
  end if

  ' 5. Runtime
  if type(itemData.RunTimeTicks) = "LongInteger"
    runtimeNode = createInfoLabel("runtime")
    runtimeNode.text = stri(getRuntime()).trim() + " " + tr("mins")
    displayInfoNode(runtimeNode)
  end if

  ' 6. Ends At
  if type(itemData.RunTimeTicks) = "LongInteger" and userSettings.uiDesignHideClock <> true
    endsAtNode = createInfoLabel("ends-at")
    endsAtNode.text = tr("Ends at %1").Replace("%1", getEndTime())
    displayInfoNode(endsAtNode)
  end if

  ' 7. Aired (Episodes only)
  if isValid(itemData.PremiereDate) and itemData.Type = "Episode"
    airDate = CreateObject("roDateTime")
    airDate.FromISO8601String(itemData.PremiereDate)
    airedNode = createInfoLabel("aired")
    airedNode.text = tr("Aired") + ": " + airDate.AsDateString("short-month-no-weekday")
    displayInfoNode(airedNode)
  end if

  ' === ROW 2: Director & Genres ===

  ' 1. Director
  directors = []
  if isValid(itemData.people)
    for each person in itemData.people
      if person.type = "Director"
        directors.push(person.name)
      end if
    end for
  end if
  if directors.count() > 0
    directorNode = createInfoLabel("director")
    directorNode.text = tr("Directed by %1").Replace("%1", directors.join(", "))
    displayDirectorGenreNode(directorNode)
  end if

  ' 2. Genres (concatenated with " / ")
  if isValid(itemData.genres) and itemData.genres.count() > 0
    genreNode = createInfoLabel("genre")
    genreNode.text = itemData.genres.join(" / ")
    displayDirectorGenreNode(genreNode)
  else if isValid(itemData.tags) and itemData.tags.count() > 0
    ' Fallback to tags if no genres
    tagNode = createInfoLabel("tag")
    tagNode.text = itemData.tags.join(" / ")
    displayDirectorGenreNode(tagNode)
  end if
end sub

sub itemContentChanged()
  ' Updates video metadata
  item = m.top.itemContent

  ' Set backdrop from movie data
  if isValid(item) and isValid(item.backdropUrl) and item.backdropUrl <> ""
    m.global.sceneManager.callFunc("setBackgroundImage", item.backdropUrl)
  else
    m.global.sceneManager.callFunc("setBackgroundImage", "")
  end if

  if isValid(item) and isValid(item.json)
    userSettings = m.global.user.settings
    itemData = item.json
    m.top.id = itemData.id

    ' Set default video source if user hasn't selected one yet
    if m.top.selectedVideoStreamId = "" and isValid(itemData.MediaSources)
      m.top.selectedVideoStreamId = itemData.MediaSources[0].id
    end if

    SetDefaultAudioTrack(itemData)

    ' Populate info rows dynamically
    populateInfoGroup()

    ' Handle all "As Is" fields
    m.top.overhangTitle = itemData.name
    setFieldText("overview", itemData.overview)

    if userSettings.uiDetailsHideTagline = false
      if itemData.taglines.count() > 0
        setFieldText("tagline", itemData.taglines[0])
      end if
    else
      m.details.removeChild(m.tagline)
    end if

    updateFavoriteButton()
    updateWatchedButton()
    SetUpVideoOptions(itemData.mediaSources)
    SetUpAudioOptions(itemData.mediaStreams)
    updateOptionsButtonVisibility()

    ' Only manage resume button when explicitly allowed (initial load or returning to screen)
    ' Prevents button changes during playback transition
    if m.allowButtonUpdates
      manageResumeButton()
      m.allowButtonUpdates = false ' Disable until next OnScreenShown
    end if
  end if

  m.buttonGrp.visible = true
  stopLoadingSpinner()
end sub

sub SetUpVideoOptions(streams)
  videos = []
  codecDetailsSet = false

  for i = 0 to streams.Count() - 1
    if streams[i].VideoType = "VideoFile"
      codec = ""
      if isValid(streams[i].mediaStreams) and streams[i].mediaStreams.Count() > 0

        ' find the first (default) video track to get the codec for the details screen
        if codecDetailsSet = false
          for index = 0 to streams[i].mediaStreams.Count() - 1
            if streams[i].mediaStreams[index].Type = "Video"
              setFieldText("video_codec", tr("Video") + ": " + streams[i].mediaStreams[index].displayTitle)
              codecDetailsSet = true
              exit for
            end if
          end for
        end if

        ' Find first video stream - MediaStreams[0] might be subtitle/audio
        videoStream = getFirstVideoStream(streams[i].mediaStreams)
        if isValid(videoStream) and isValid(videoStream.displayTitle)
          codec = videoStream.displayTitle
        else
          codec = tr("N/A")
        end if
      end if

      ' Create options for user to switch between video tracks
      videos.push({
        "Title": streams[i].Name,
        "Description": tr("Video"),
        "Selected": m.top.selectedVideoStreamId = streams[i].id,
        "StreamID": streams[i].id,
        "video_codec": codec
      })
    end if
  end for

  if streams.count() > 1
    m.top.findnode("video_codec_count").text = "+" + stri(streams.Count() - 1).trim()
  end if

  options = {}
  options.videos = videos
  m.options.options = options
end sub

sub SetUpAudioOptions(streams)
  tracks = []

  for i = 0 to streams.Count() - 1
    if streams[i].Type = "Audio"
      tracks.push({ "Title": streams[i].displayTitle, "Description": streams[i].Title, "Selected": m.top.selectedAudioStreamIndex = i, "StreamIndex": i })
    end if
  end for

  if tracks.count() > 1
    m.top.findnode("audio_codec_count").text = "+" + stri(tracks.Count() - 1).trim()
  end if

  options = {}
  if isValid(m.options.options.videos)
    options.videos = m.options.options.videos
  end if
  options.audios = tracks
  m.options.options = options
end sub

sub SetDefaultAudioTrack(itemData)
  ' Extract MediaStreams from itemData
  mediaStreams = invalid
  if isValid(itemData.mediaStreams)
    mediaStreams = itemData.mediaStreams
  else if isValid(itemData.MediaSources) and isValid(itemData.MediaSources[0]) and isValid(itemData.MediaSources[0].MediaStreams)
    mediaStreams = itemData.MediaSources[0].MediaStreams
  end if

  if not isValid(mediaStreams) then return

  ' Get user settings for audio selection
  localUser = m.global.user

  ' Resolve playDefaultAudioTrack setting (JellyRock override or web client)
  playDefault = resolvePlayDefaultAudioTrack(localUser.settings, localUser.config)

  ' Use new comprehensive audio selection logic
  selectedIndex = findBestAudioStreamIndex(mediaStreams, playDefault, localUser.config.audioLanguagePreference)

  ' Find the audio stream with the selected index to get its displayTitle
  defaultAudioStream = invalid
  for each stream in mediaStreams
    if LCase(stream.Type) = "audio" and isValid(stream.index) and stream.index = selectedIndex
      defaultAudioStream = stream
      exit for
    end if
  end for

  ' Set the selected audio stream index
  m.top.selectedAudioStreamIndex = selectedIndex

  ' Update UI with audio codec information
  if isValid(defaultAudioStream) and isValid(defaultAudioStream.displayTitle)
    setFieldText("audio_codec", tr("Audio") + ": " + defaultAudioStream.displayTitle)
  end if
end sub

sub setFieldText(field, value)
  node = m.top.findNode(field)
  if not isValid(node) or not isValid(value) then return

  ' Handle non strings... Which _shouldn't_ happen, but hey
  if type(value) = "roInt" or type(value) = "Integer"
    value = str(value).trim()
  else if type(value) = "roFloat" or type(value) = "Float"
    value = str(value).trim()
  else if type(value) <> "roString" and type(value) <> "String"
    value = ""
  else
    value = value.trim()
  end if

  node.text = value
end sub

function getRuntime() as integer

  itemData = m.top.itemContent.json

  ' A tick is .1ms, so 1/10,000,000 for ticks to seconds,
  ' then 1/60 for seconds to minutess... 1/600,000,000
  return round(itemData.RunTimeTicks / 600000000.0)
end function

function getEndTime() as string
  itemData = m.top.itemContent.json

  date = CreateObject("roDateTime")
  duration_s = int(itemData.RunTimeTicks / 10000000.0)
  date.fromSeconds(date.asSeconds() + duration_s)
  date.toLocalTime()

  return formatTime(date)
end function

sub updateFavoriteButton()
  fave = m.top.itemContent.favorite
  favoriteButton = m.top.findNode("favoriteButton")
  if isValid(favoriteButton)
    if isValid(fave) and fave
      favoriteButton.selected = true
    else
      favoriteButton.selected = false
    end if
  end if
end sub

sub updateWatchedButton()
  watched = m.top.itemContent.watched
  watchedButton = m.top.findNode("watchedButton")
  if isValid(watchedButton)
    if watched
      watchedButton.selected = true
    else
      watchedButton.selected = false
    end if
  end if
end sub

' updateOptionsButtonVisibility: Create/remove options button based on available tracks
' Creates button if there are multiple video or audio tracks to choose from
' Removes button if there is 1 or fewer video tracks AND 1 or fewer audio tracks
'
sub updateOptionsButtonVisibility()
  videoCount = 0
  audioCount = 0

  ' Count video tracks
  if isValid(m.options.options) and isValid(m.options.options.videos)
    videoCount = m.options.options.videos.count()
  end if

  ' Count audio tracks
  if isValid(m.options.options) and isValid(m.options.options.audios)
    audioCount = m.options.options.audios.count()
  end if

  ' Determine if options button should be shown
  shouldShowOptions = videoCount > 1 or audioCount > 1

  ' Check current state
  buttonExists = isValid(m.optionsButton)

  if shouldShowOptions and not buttonExists
    ' Create and add the options button (always right after play button)
    ' Capture current focus state before modifying button group
    currentFocusIndex = m.buttonGrp.buttonFocused

    m.optionsButton = CreateObject("roSGNode", "IconButton")
    m.optionsButton.id = "optionsButton"
    m.optionsButton.icon = "pkg:/images/icons/settings.png"
    m.optionsButton.text = tr("Options")

    playButtonIndex = getButtonIndex("playButton")
    if playButtonIndex >= 0
      insertIndex = playButtonIndex + 1
      m.buttonGrp.insertChild(m.optionsButton, insertIndex)

      ' Adjust focus index if user was on a button after the insertion point
      if isValid(currentFocusIndex) and currentFocusIndex >= insertIndex
        m.buttonGrp.buttonFocused = currentFocusIndex + 1
      end if
    else
      ' Play button not found; append options button to end and log warning
      m.log.warn("playButton not found in button group; appending optionsButton to end")
      m.buttonGrp.appendChild(m.optionsButton)
    end if
  else if not shouldShowOptions and buttonExists
    ' Capture current focus state before removing button
    currentFocusIndex = m.buttonGrp.buttonFocused
    optionsButtonIndex = getButtonIndex("optionsButton")

    ' Remove the button and clear reference
    m.buttonGrp.removeChild(m.optionsButton)
    m.optionsButton = invalid

    ' Adjust focus index if user was on a button after the removed button
    if isValid(currentFocusIndex) and isValid(optionsButtonIndex) and currentFocusIndex > optionsButtonIndex
      m.buttonGrp.buttonFocused = currentFocusIndex - 1
    end if
  end if
end sub

' getButtonIndex: Helper to find button index in button group
' @param {string} buttonId - The id of the button to find
' @returns {integer} - The index of the button, or -1 if not found
'
function getButtonIndex(buttonId as string) as integer
  for i = 0 to m.buttonGrp.getChildCount() - 1
    child = m.buttonGrp.getChild(i)
    if isValid(child) and child.id = buttonId
      return i
    end if
  end for
  return -1
end function

function round(f as float) as integer
  ' BrightScript only has a "floor" round
  ' This compares floor to floor + 1 to find which is closer
  m = int(f)
  n = m + 1
  x = abs(f - m)
  y = abs(f - n)
  if y > x
    return m
  else
    return n
  end if
end function

'Check if options updated and any reloading required
sub audioOptionsClosed()
  if m.options.audioStreamIndex <> m.top.selectedAudioStreamIndex
    m.top.selectedAudioStreamIndex = m.options.audioStreamIndex
    setFieldText("audio_codec", tr("Audio") + ": " + m.top.itemContent.json.mediaStreams[m.top.selectedAudioStreamIndex].displayTitle)
  end if

  m.buttonGrp.callFunc("focus")
end sub

' Check if options were updated and if any reloding is needed...
sub videoOptionsClosed()
  if m.options.videoStreamId <> m.top.selectedVideoStreamId
    m.top.selectedVideoStreamId = m.options.videoStreamId
    setFieldText("video_codec", tr("Video") + ": " + m.options.video_codec)
    ' Because the video stream has changed (i.e. the actual video)... we need to reload the audio stream choices for that video
    m.top.unobservefield("itemContent")
    itemData = m.top.itemContent.json
    for each mediaSource in itemData.mediaSources
      if mediaSource.id = m.top.selectedVideoStreamId
        itemData.mediaStreams = []
        for i = 0 to mediaSource.mediaStreams.Count() - 1
          itemData.mediaStreams.push(mediaSource.mediaStreams[i])
        end for
        SetDefaultAudioTrack(itemData)
        SetUpAudioOptions(itemData.mediaStreams)
        exit for
      end if
    end for
    m.top.itemContent.json = itemData
    m.top.observeField("itemContent", "itemContentChanged")
  end if

  m.buttonGrp.callFunc("focus")
end sub

function onKeyEvent(key as string, press as boolean) as boolean
  ' Due to the way the button pressed event works, need to catch the release for the button as the press is being sent
  ' directly to the main loop.  Will get this sorted in the layout update for Movie Details
  if key = "OK" and isValid(m.optionsButton) and m.optionsButton.isInFocusChain()
    m.options.visible = true
    m.options.setFocus(true)
  end if

  if key = "down" and m.buttonGrp.isInFocusChain()
    m.top.lastFocus = m.extrasGrid
    m.extrasGrid.setFocus(true)
    m.top.findNode("VertSlider").reverse = false
    m.top.findNode("pplAnime").control = "start"
    return true
  end if

  if key = "up" and m.top.findNode("extrasGrid").isInFocusChain()
    if m.extrasGrid.itemFocused = 0
      m.top.lastFocus = m.buttonGrp
      m.top.findNode("VertSlider").reverse = true
      m.top.findNode("pplAnime").control = "start"
      m.buttonGrp.callFunc("focus")
      return true
    end if
  end if

  if not press then return false

  if key = "back"
    if m.options.visible = true
      m.options.visible = false
      videoOptionsClosed()
      audioOptionsClosed()
      return true
    end if
  else if key = "play" and m.extrasGrid.hasFocus()
    print "Play was pressed from the movie details extras slider"
    if isValid(m.extrasGrid.focusedItem)
      m.top.quickPlayNode = m.extrasGrid.focusedItem
      return true
    end if
  end if

  return false
end function