Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize home screen #250

Merged
merged 12 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- A home screen editor allowing to enable/disable feeds, and change their order.

## [0.18.1] - 2024-01-02

### Fixed
Expand Down
39 changes: 37 additions & 2 deletions docs/playlet-web-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ paths:
properties:
index:
type: integer
playlistIndex
playlistIndex:
type: integer
items:
type: array
Expand All @@ -178,7 +178,7 @@ paths:
properties:
index:
type: integer
playlistIndex
playlistIndex:
type: integer
items:
type: array
Expand Down Expand Up @@ -227,6 +227,41 @@ paths:
description: No Content
"400":
description: Bad Request
/api/home-layout:
get:
summary: Get home layout
description: Get the current home layout. This is the layout of the home screen, based on user preferences.
operationId: getHomeLayout
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
title:
type: string
feedSources:
type: array
items:
type: object
properties:
apiType:
type: string
endpoint:
type: string
id:
type: string
queryParams:
type: object
additionalProperties: true
title:
type: string
/api/playlet-lib-urls:
get:
summary: Get Playlet lib URLs
Expand Down
11 changes: 10 additions & 1 deletion playlet-lib/src/components/Screens/HomeScreen/HomeScreen.bs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "HomeScreenUtils.bs"
import "pkg:/components/ContextMenu/ContextMenuUtils.bs"
import "pkg:/components/Navigation/Navigation.bs"
import "pkg:/source/utils/FocusManagement.bs"
Expand All @@ -14,7 +15,15 @@ function OnNodeReady()
SetNavigation(invalid, "left", m.navBar)

m.rowList@.BindNode()
m.rowList.feeds = ParseJson(ReadAsciiFile(m.top.feedFile))

' TODO:P1 home screen should only be refreshed when the user navigates to it
' (similar to bookmarks screen)
m.preferences.observeFieldScoped("misc.home_screen_layout", FuncName(OnHomeLayoutChange))
OnHomeLayoutChange()
end function

function OnHomeLayoutChange() as void
m.rowList.feeds = HomeScreenUtils.GetFeed(m.top.feedFile, m.preferences)
end function

function OnFocusChange() as void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<interface>
<field id="navBar" type="node" bind="/NavBar" />
<field id="invidious" type="node" bind="/Invidious" />
<field id="preferences" type="node" bind="/Preferences" />
<field id="feedFile" type="string" value="libpkg:/config/default_home_layout.yaml" />
</interface>
<children>
Expand Down
28 changes: 28 additions & 0 deletions playlet-lib/src/components/Screens/HomeScreen/HomeScreenUtils.bs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import "pkg:/source/utils/Types.bs"

namespace HomeScreenUtils

function GetFeed(feedFileName as string, preferences as object) as object
feed = ParseJson(ReadAsciiFile(feedFileName))

homeLayout = preferences["misc.home_screen_layout"]
if not IsArray(homeLayout) or homeLayout.Count() = 0
return feed
end if

feedItems = {}
for each item in feed
feedItems[item["id"]] = item
end for

filteredFeed = []
for each item in homeLayout
if item.enabled = true
filteredFeed.push(feedItems[item.id])
end if
end for

return filteredFeed
end function

end namespace
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import "pkg:/components/parts/AutoBind/OnNodeReadyNoOp.bs"
import "pkg:/source/utils/FocusManagement.bs"
import "pkg:/source/utils/Types.bs"

function Init()
m.top.focusable = true
m.top.itemSpacings = [8]

m.button = m.top.findNode("button")
m.button.observeField("buttonSelected", FuncName(OpenHomeScreenEditor))
end function

function OnFocusChange() as void
if not m.top.focus
return
end if

NodeSetFocus(m.button, true)
end function

function BindPreference(preferences as object, key as string)
if m.preferences <> invalid and m.key <> invalid
m.preferences.unobserveFieldScoped(m.key)
end if

m.preferences = preferences
m.key = key

if preferences <> invalid and key <> invalid
preferences.observeFieldScoped(key, FuncName(OnPreferenceChange))
OnPreferenceChange()
end if
end function

function OpenHomeScreenEditor()
editor = CreateObject("roSGNode", "HomeScreenEditor")
m.appController@.PushScreen(editor)
editor@.BindNode()
editor.value = m.top.value
editor.observeField("save", FuncName(OnSaveHomeScreenEditor))
end function

function OnSaveHomeScreenEditor(event as object)
editor = event.GetRoSGNode()
m.top.value = editor.value
end function

function OnPreferenceChange()
m.top.value = m.preferences[m.key]
end function

function OnValueChange() as void
if m.preferences = invalid or m.key = invalid
return
end if

m.preferences[m.key] = m.top.value
end function
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<component name="EditHomeScreenControl" extends="LayoutGroup" includes="AutoBind,Focus">
<interface>
<field id="displayText" type="string" alias="Button.text" />
<field id="description" type="string" alias="DescriptionLabel.text" />
<field id="value" type="array" onChange="OnValueChange" />
<field id="appController" type="node" bind="/AppController" />
<function name="BindPreference" />
</interface>
<children>
<Button
id="button"
minWidth="300"
showFocusFootprint="true" />
<Label id="DescriptionLabel"
width="450"
color="0xb4b4b4ff"
wrap="true">
<Font role="font" uri="font:SystemFontFile" size="18" />
</Label>
</children>
</component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import "pkg:/components/Navigation/Navigation.bs"
import "pkg:/components/parts/AutoBind/OnNodeReadyNoOp.bs"
import "pkg:/source/utils/MathUtils.bs"
import "pkg:/source/utils/Types.bs"

function Init()
m.homeLayout = {}
homeLayout = ParseJson(ReadAsciiFile("libpkg:/config/default_home_layout.yaml"))
for each item in homeLayout
m.homeLayout[item.id] = item
end for

m.checkList = m.top.findNode("checkList")
m.moveUpButton = m.top.findNode("moveUpButton")
m.moveDownButton = m.top.findNode("moveDownButton")
m.closeButton = m.top.findNode("closeButton")
m.saveButton = m.top.findNode("saveButton")

SetNavigation(m.checkList, "down", m.saveButton)
SetNavigation(m.saveButton, "up", m.checkList)
SetNavigation(m.closeButton, "up", m.checkList)
SetNavigation(m.saveButton, "right", m.closeButton)
SetNavigation(m.closeButton, "left", m.saveButton)
SetNavigation(m.moveUpButton, "left", m.checkList)
SetNavigation(m.moveDownButton, "left", m.checkList)
SetNavigation(m.checkList, "right", m.moveUpButton)
SetNavigation(m.moveUpButton, "down", m.moveDownButton)
SetNavigation(m.moveDownButton, "up", m.moveUpButton)
SetNavigation(m.moveDownButton, "down", m.closeButton)

m.moveUpButton.observeField("buttonSelected", FuncName(OnMoveUpButtonSelected))
m.moveDownButton.observeField("buttonSelected", FuncName(OnMoveDownButtonSelected))
m.saveButton.observeField("buttonSelected", FuncName(OnSaveButtonSelected))
m.closeButton.observeField("buttonSelected", FuncName(Close))

m.checkList.observeField("checkedState", FuncName(OnCheckedStateChange))
end function

function OnFocusChange() as void
if not m.top.focus
return
end if

NodeSetFocus(m.checkList, true)
end function

function OnkeyEvent(key as string, press as boolean) as boolean
if NavigationKeyHandler(key, press).handled
return true
end if

if key = "options" and press
' A pass-through to the app controller, so it can toggle picture-in-picture
return false
end if

if key = "back" and press
Close()
return true
end if

return true
end function

function OnValueChange()
content = m.checkList.content
value = m.top.value

nodes = []
checkedState = []
for each item in value
node = CreateObject("roSGNode", "ContentNode")
node.id = item.id
node.title = m.homeLayout[item.id].title
nodes.push(node)
checkedState.push(item.enabled)
end for

labelCount = content.getChildCount()
if labelCount > 0
content.removeChildrenIndex(labelCount, 0)
end if

content.appendChildren(nodes)
m.checkList.checkedState = checkedState
end function

function OnCheckedStateChange() as void
content = m.checkList.content
if content = invalid or content.getChildCount() = 0
return
end if
checkedState = m.checkList.checkedState
if checkedState = invalid or checkedState.Count() = 0
return
end if

value = []
checkboxes = content.getChildren(-1, 0)

for i = 0 to checkboxes.Count() - 1
checkbox = checkboxes[i]
value.push({
id: checkbox.id
enabled: checkedState[i]
})
end for

m.top.value = value
end function

function OnMoveUpButtonSelected() as void
MoveItem(-1)
end function

function OnMoveDownButtonSelected() as void
MoveItem(1)
end function

function MoveItem(offset as integer) as void
content = m.checkList.content
if content = invalid or content.getChildCount() = 0
return
end if

itemCount = content.getChildCount()
index = m.checkList.itemFocused
newIndex = MathUtils.Max(0, MathUtils.Min(index + offset, itemCount - 1))

if index = newIndex
return
end if

checkedState = m.checkList.checkedState
node = content.getChild(index)
content.insertChild(node, newIndex)

tmp = checkedState[index]
checkedState[index] = checkedState[newIndex]
checkedState[newIndex] = tmp

m.checkList.itemFocused = newIndex
m.checkList.jumpToItem = newIndex
m.checkList.checkedState = checkedState
end function

function OnSaveButtonSelected()
' Save the new layout to m.top.value
OnCheckedStateChange()
m.top.save = true
Close()
end function

function Close()
m.appController@.PopScreen()
end function
Loading