Add Genres, Parental Ratings, and Years as Movie Filters (#928)

* Add Movie Filters
* Add filter names to translations file.
* Only jump up if content is reloaded
This commit is contained in:
1hitsong 2023-02-25 14:51:36 -05:00 committed by GitHub
parent a33ce8bd57
commit 2eff1401d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 317 additions and 17 deletions

View File

@ -16,6 +16,11 @@ sub init()
m.menus.push(m.top.findNode("sortMenu"))
m.menus.push(m.top.findNode("filterMenu"))
m.filterOptions = m.top.findNode("filterOptions")
m.filterMenu = m.top.findNode("filterMenu")
m.filterMenu.observeField("itemFocused", "onFilterFocusChange")
m.viewNames = []
m.sortNames = []
m.filterNames = []
@ -24,14 +29,46 @@ sub init()
m.fadeAnim = m.top.findNode("fadeAnim")
m.fadeOutAnimOpacity = m.top.findNode("outOpacity")
m.fadeInAnimOpacity = m.top.findNode("inOpacity")
m.showChecklistAnimation = m.top.findNode("showChecklistAnimation")
m.hideChecklistAnimation = m.top.findNode("hideChecklistAnimation")
m.buttons.observeField("focusedIndex", "buttonFocusChanged")
m.favoriteMenu.observeField("buttonSelected", "toggleFavorite")
end sub
sub showChecklist()
if m.filterOptions.opacity = 0
if m.showChecklistAnimation.state = "stopped"
m.showChecklistAnimation.control = "start"
end if
end if
end sub
sub hideChecklist()
if m.filterOptions.opacity = 1
if m.hideChecklistAnimation.state = "stopped"
m.hideChecklistAnimation.control = "start"
end if
end if
end sub
sub onFilterFocusChange()
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
showChecklist()
else
hideChecklist()
end if
m.filterOptions.content = m.filterMenu.content.getChild(m.filterMenu.itemFocused)
if isValid(m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState)
m.filterOptions.checkedState = m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState
else
m.filterOptions.checkedState = []
end if
end sub
sub optionsSet()
' Views Tab
if m.top.options.views <> invalid
viewContent = CreateObject("roSGNode", "ContentNode")
@ -90,8 +127,19 @@ sub optionsSet()
m.selectedFilterIndex = 0
for each filterItem in m.top.options.filter
entry = filterContent.CreateChild("ContentNode")
entry = filterContent.CreateChild("OptionNode")
entry.title = filterItem.Title
entry.name = filterItem.Name
entry.delimiter = filterItem.Delimiter
if isValid(filterItem.options)
for each filterItemOption in filterItem.options
entryOption = entry.CreateChild("ContentNode")
entryOption.title = toString(filterItemOption)
end for
entry.checkedState = filterItem.checkedState
end if
m.filterNames.push(filterItem.Name)
if filterItem.selected <> invalid and filterItem.selected = true
m.selectedFilterIndex = index
@ -193,12 +241,31 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
return true
else if key = "right"
if m.menus[m.selectedItem].isInFocusChain()
' Handle Filter screen
if m.selectedItem = 2
' Selected filter has options, move cursor to it
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() > 0
m.menus[m.selectedItem].setFocus(false)
m.filterOptions.setFocus(true)
return true
end if
end if
end if
else if key = "left"
if m.favoriteMenu.hasFocus()
m.favoriteMenu.setFocus(false)
m.menus[m.selectedItem].visible = true
m.buttons.setFocus(true)
end if
' User wants to escape filter options
if m.filterOptions.isInFocusChain()
m.filterOptions.setFocus(false)
m.menus[m.selectedItem].setFocus(true)
return true
end if
else if key = "OK"
if m.menus[m.selectedItem].isInFocusChain()
' Handle View Screen
@ -229,14 +296,58 @@ function onKeyEvent(key as string, press as boolean) as boolean
end if
end if
end if
' Handle Filter screen
if m.selectedItem = 2
m.selectedFilterIndex = m.menus[2].itemSelected
m.top.filter = m.filterNames[m.selectedFilterIndex]
' If filter has no options, select it
if m.filterMenu.content.getChild(m.filterMenu.itemFocused).getChildCount() = 0
m.menus[2].checkedItem = m.menus[2].itemSelected
m.selectedFilterIndex = m.menus[2].itemSelected
m.top.filter = m.filterNames[m.selectedFilterIndex]
m.top.filterOptions = {}
return true
end if
' Selected filter has options, move cursor to it
m.filterOptions.setFocus(true)
m.menus[m.selectedItem].setFocus(false)
return true
end if
end if
' User pressed OK from inside the filter's options
if m.filterOptions.isInFocusChain()
selectedOptions = []
for i = 0 to m.filterOptions.checkedState.count() - 1
if m.filterOptions.checkedState[i]
selectedValue = toString(m.filterOptions.content.getChild(i).title)
selectedOptions.push(selectedValue)
end if
end for
if selectedOptions.Count() > 0
m.menus[2].checkedItem = m.menus[2].itemFocused
m.selectedFilterIndex = m.menus[2].itemFocused
m.top.filter = m.filterMenu.content.getChild(m.filterMenu.itemFocused).Name
newFilter = {}
newFilter[m.top.filter] = selectedOptions.join(m.filterMenu.content.getChild(m.filterMenu.itemFocused).delimiter)
m.top.filterOptions = newFilter
else
m.menus[2].checkedItem = 0
m.selectedFilterIndex = 0
m.top.filter = m.filterNames[0]
m.top.filterOptions = {}
end if
m.filterMenu.content.getChild(m.filterMenu.itemFocused).checkedState = m.filterOptions.checkedState
return true
end if
return true
else if key = "back" or key = "up"
if key = "back" then hideChecklist()
m.menus[2].visible = true ' Show Filter contents in case hidden by favorite button
if m.menus[m.selectedItem].isInFocusChain()
m.buttons.setFocus(true)
@ -244,6 +355,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
return true
end if
else if key = "options"
hideChecklist()
m.menus[2].visible = true ' Show Filter contents in case hidden by favorite button
m.menus[m.selectedItem].drawFocusFeedback = false
return false

View File

@ -6,15 +6,18 @@
<Poster width="1720" height="880" uri="pkg:/images/dialog.9.png" />
<LayoutGroup horizAlignment="center" translation="[860,50]" itemSpacings="[50]">
<JFButtons id="buttons" />
</LayoutGroup>
<LayoutGroup id="menuOptions" horizAlignment="center" translation="[860,200]" itemSpacings="[50]">
<Group>
<RadiobuttonList id="viewMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
</RadiobuttonList>
<RadiobuttonList id="sortMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="1" numRows="8" drawFocusFeedback="false">
</RadiobuttonList>
<RadiobuttonList id="filterMenu" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
<RadiobuttonList id="filterMenu" checkOnSelect="false" itemspacing="[0,10]" vertFocusAnimationStyle="floatingFocus" opacity="0" drawFocusFeedback="false">
</RadiobuttonList>
</Group>
</LayoutGroup>
<CheckList opacity="0" translation="[900, 200]" id="filterOptions" numRows="8" itemSize="[250, 70]" />
<ButtonGroup translation="[1250,50]">
<Button id="favoriteMenu" iconUri="pkg:/images/icons/favorite.png" focusedIconUri="pkg:/images/icons/favorite.png" focusBitmapUri="" focusFootprintBitmapUri="" text="Favorite" showFocusFootprint="false"></Button>
</ButtonGroup>
@ -25,6 +28,16 @@
<FloatFieldInterpolator id="inOpacity" key="[0.0, 0.5, 1.0]" keyValue="[ 0, 0, 1 ]" fieldToInterp="focus.opacity" />
</Animation>
<Animation id="showChecklistAnimation" duration="0.5" repeat="false">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[0, 1]" fieldToInterp="filterOptions.opacity" />
<Vector2DFieldInterpolator key="[0.0, 1.0]" keyValue="[[860, 200], [560, 200]]" fieldToInterp="menuOptions.translation" />
</Animation>
<Animation id="hideChecklistAnimation" duration="0.5" repeat="false">
<FloatFieldInterpolator key="[0.0, 1.0]" keyValue="[1, 0]" fieldToInterp="filterOptions.opacity" />
<Vector2DFieldInterpolator key="[0.0, 1.0]" keyValue="[[560, 200], [860, 200]]" fieldToInterp="menuOptions.translation" />
</Animation>
</children>
<interface>
<field id="buttons" type="nodearray" />
@ -35,8 +48,10 @@
<field id="sortField" type="string" value="SortName" />
<field id="sortAscending" type="boolean" value="false" />
<field id="filter" type="string" value="All" />
<field id="filterOptions" type="assocarray" value="" />
<field id="favorite" type="string" value="Favorite" />
</interface>
<script type="text/brightscript" uri="ItemGridOptions.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
</component>

View File

@ -83,6 +83,12 @@ sub loadItems()
params.append({ Filters: "IsResumable" })
end if
if isValid(m.top.filterOptions)
if m.top.filterOptions.count() > 0
params.append(m.top.filterOptions)
end if
end if
if m.top.ItemType <> ""
params.append({ IncludeItemTypes: m.top.ItemType })
end if

View File

@ -12,6 +12,7 @@
<field id="nameStartsWith" type="string" value="" />
<field id="recursive" type="boolean" value="true" />
<field id="filter" type="string" value="All" />
<field id="filterOptions" type="assocarray" value="" />
<field id="searchTerm" type="string" value="" />
<field id="studioIds" type="string" value="" />
<field id="genreIds" type="string" value="" />
@ -25,6 +26,7 @@
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
<script type="text/brightscript" uri="pkg:/source/api/baserequest.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/misc.brs" />
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
</component>

View File

@ -67,10 +67,12 @@ sub init()
m.sortAscending = true
m.filter = "All"
m.filterOptions = {}
m.favorite = "Favorite"
m.loadItemsTask = createObject("roSGNode", "LoadItemsTask2")
m.loadLogoTask = createObject("roSGNode", "LoadItemsTask2")
m.getFiltersTask = createObject("roSGNode", "GetFiltersTask")
'set inital counts for overhang before content is loaded.
m.loadItemsTask.totalRecordCount = 0
@ -126,6 +128,7 @@ sub loadInitialItems()
m.sortField = get_user_setting("display." + m.top.parentItem.Id + ".sortField")
m.filter = get_user_setting("display." + m.top.parentItem.Id + ".filter")
m.filterOptions = get_user_setting("display." + m.top.parentItem.Id + ".filterOptions")
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
sortAscendingStr = get_user_setting("display." + m.top.parentItem.Id + ".sortAscending")
@ -136,8 +139,11 @@ sub loadInitialItems()
if not isValid(m.sortField) then m.sortField = "SortName"
if not isValid(m.filter) then m.filter = "All"
if not isValid(m.filterOptions) then m.filterOptions = "{}"
if not isValid(m.view) then m.view = "Movies"
m.filterOptions = ParseJson(m.filterOptions)
if sortAscendingStr = invalid or sortAscendingStr = "true"
m.sortAscending = true
else
@ -165,6 +171,7 @@ sub loadInitialItems()
m.loadItemsTask.sortField = m.sortField
m.loadItemsTask.sortAscending = m.sortAscending
m.loadItemsTask.filter = m.filter
m.loadItemsTask.filterOptions = m.filterOptions
m.loadItemsTask.startIndex = 0
' Load Item Types
@ -216,7 +223,14 @@ sub loadInitialItems()
m.loadItemsTask.observeField("content", "ItemDataLoaded")
m.spinner.visible = true
m.loadItemsTask.control = "RUN"
SetUpOptions()
m.getFiltersTask.observeField("filters", "FilterDataLoaded")
m.getFiltersTask.params = {
userid: get_setting("active_user"),
parentid: m.top.parentItem.Id,
includeitemtypes: "Movie"
}
m.getFiltersTask.control = "RUN"
end sub
' Set Movies view, sort, and filter options
@ -291,12 +305,7 @@ function inStringArray(array, searchValue) as boolean
end function
' Data to display when options button selected
sub SetUpOptions()
options = {}
options.filter = []
options.favorite = []
setMoviesOptions(options)
sub setSelectedOptions(options)
' Set selected view option
for each o in options.views
@ -316,17 +325,76 @@ sub SetUpOptions()
end if
end for
' Set selected filter option
' Set selected filter
for each o in options.filter
if o.Name = m.filter
o.Selected = true
m.options.filter = o.Name
end if
' Select selected filter options
if isValid(o.options) and isValid(m.filterOptions)
if o.options.Count() > 0 and m.filterOptions.Count() > 0
if LCase(o.Name) = LCase(m.filterOptions.keys()[0])
selectedFilterOptions = m.filterOptions[m.filterOptions.keys()[0]].split(o.delimiter)
checkedState = []
for each availableFilterOption in o.options
matchFound = false
for each selectedFilterOption in selectedFilterOptions
if LCase(toString(availableFilterOption).trim()) = LCase(selectedFilterOption.trim())
matchFound = true
end if
end for
checkedState.push(matchFound)
end for
o.checkedState = checkedState
end if
end if
end if
end for
m.options.options = options
end sub
'
' Logo Image Loaded Event Handler
sub FilterDataLoaded(msg)
options = {}
options.filter = []
options.favorite = []
setMoviesOptions(options)
data = msg.GetData()
m.getFiltersTask.unobserveField("filters")
if not isValid(data) then return
' Add Movie filters from the API data
if LCase(m.loadItemsTask.view) = "movies"
if isValid(data.genres)
options.filter.push({ "Title": tr("Genres"), "Name": "Genres", "Options": data.genres, "Delimiter": "|", "CheckedState": [] })
end if
if isValid(data.OfficialRatings)
options.filter.push({ "Title": tr("Parental Ratings"), "Name": "OfficialRatings", "Options": data.OfficialRatings, "Delimiter": "|", "CheckedState": [] })
end if
if isValid(data.Years)
options.filter.push({ "Title": tr("Years"), "Name": "Years", "Options": data.Years, "Delimiter": ",", "CheckedState": [] })
end if
end if
setSelectedOptions(options)
m.options.options = options
end sub
'
' Logo Image Loaded Event Handler
sub LogoImageLoaded(msg)
@ -384,6 +452,10 @@ sub ItemDataLoaded(msg)
m.itemGrid.setFocus(true)
m.genreList.setFocus(false)
if m.data.getChildCount() = 0
m.itemGrid.jumpToItem = 0
end if
for each item in itemData
m.data.appendChild(item)
end for
@ -709,6 +781,16 @@ sub optionsClosed()
set_user_setting("display." + m.top.parentItem.Id + ".filter", m.options.filter)
end if
if not isValid(m.options.filterOptions)
m.filterOptions = {}
end if
if not AssocArrayEqual(m.options.filterOptions, m.filterOptions)
m.filterOptions = m.options.filterOptions
reload = true
set_user_setting("display." + m.top.parentItem.Id + ".filterOptions", FormatJson(m.options.filterOptions))
end if
m.view = get_user_setting("display." + m.top.parentItem.Id + ".landing")
if m.options.view <> m.view
@ -720,6 +802,7 @@ sub optionsClosed()
m.loadItemsTask.NameStartsWith = " "
m.loadItemsTask.searchTerm = ""
m.filter = "All"
m.filterOptions = {}
m.sortField = "SortName"
m.sortAscending = true
@ -727,6 +810,7 @@ sub optionsClosed()
set_user_setting("display." + m.top.parentItem.Id + ".sortField", m.sortField)
set_user_setting("display." + m.top.parentItem.Id + ".sortAscending", "true")
set_user_setting("display." + m.top.parentItem.Id + ".filter", m.filter)
set_user_setting("display." + m.top.parentItem.Id + ".filterOptions", FormatJson(m.filterOptions))
reload = true
end if
@ -845,6 +929,7 @@ function onKeyEvent(key as string, press as boolean) as boolean
m.top.alphaSelected = ""
m.loadItemsTask.filter = "All"
m.filter = "All"
m.filterOptions = {}
m.data = CreateObject("roSGNode", "ContentNode")
m.itemGrid.content = m.data
loadInitialItems()

View File

@ -63,4 +63,5 @@
<script type="text/brightscript" uri="pkg:/source/api/Image.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/deviceCapabilities.brs" />
<script type="text/brightscript" uri="MovieLibraryView.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
</component>

View File

@ -0,0 +1,8 @@
sub init()
m.top.functionName = "getFiltersTask"
end sub
sub getFiltersTask()
m.filters = api_API().items.getFilters(m.top.params)
m.top.filters = m.filters
end sub

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="GetFiltersTask" extends="Task">
<interface>
<field id="params" type="assocarray" />
<field id="filters" type="assocarray" />
</interface>
<script type="text/brightscript" uri="GetFiltersTask.brs" />
<script type="text/brightscript" uri="pkg:/source/utils/config.brs" />
<script type="text/brightscript" uri="pkg:/source/roku_modules/api/api.brs" />
</component>

View File

@ -0,0 +1,2 @@
sub init()
end sub

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<component name="OptionNode" extends="ContentNode">
<interface>
<field id="name" type="string" />
<field id="delimiter" type="string" />
<field id="checkedState" type="array" />
</interface>
<script type="text/brightscript" uri="OptionNode.brs" />
</component>

View File

@ -933,6 +933,16 @@
<translation>Support Direct Play of MPEG-4 content. This may need to be disabled for playback of DIVX encoded video files.</translation>
<extracomment>Settings Menu - Description for option</extracomment>
</message>
<message>
<source>Parental Ratings</source>
<translation>Parental Ratings</translation>
<extracomment>Used in Filter menu</extracomment>
</message>
<message>
<source>Years</source>
<translation>Years</translation>
<extracomment>Used in Filter menu</extracomment>
</message>
<message>
<source>Show What's New Popup</source>
<translation>Show What's New Popup</translation>

View File

@ -282,14 +282,53 @@ function findNodeBySubtype(node, subtype)
return foundNodes
end function
' Search string array for search value. Return if it's found
function inArray(array, searchValue) as boolean
for each item in array
if lcase(item) = lcase(searchValue) then return true
function AssocArrayEqual(Array1 as object, Array2 as object) as boolean
if not isValid(Array1) or not isValid(Array2)
return false
end if
if not Array1.Count() = Array2.Count()
return false
end if
for each key in Array1
if not Array2.DoesExist(key)
return false
end if
if Array1[key] <> Array2[key]
return false
end if
end for
return true
end function
' Search string array for search value. Return if it's found
function inArray(haystack, needle) as boolean
valueToFind = needle
if LCase(type(valueToFind)) <> "rostring" and LCase(type(valueToFind)) <> "string"
valueToFind = str(needle)
end if
valueToFind = lcase(valueToFind)
for each item in haystack
if lcase(item) = valueToFind then return true
end for
return false
end function
function toString(input) as string
if LCase(type(input)) = "rostring" or LCase(type(input)) = "string"
return input
end if
return str(input)
end function
sub startLoadingSpinner()
m.spinner = createObject("roSGNode", "Spinner")
m.spinner.translation = "[900, 450]"