Skip to content

Commit

Permalink
Play queue notification (#186)
Browse files Browse the repository at this point in the history
* Play queue notification

* Lint fix

* change binding

* Add notification preference

* Lint fix

* changelog

---------

Co-authored-by: github-action linter <[email protected]>
  • Loading branch information
iBicha and github-action linter authored Nov 5, 2023
1 parent 777db66 commit 4233f30
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 61 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Pagination support in web app search (load more button)
- Video links pasted into the web app triggers the dialog to cast the video
- Ability to subscribe/unsubscribe from a channel screen
- A notification when videos are added to the queue (can be disabled from the settings)

### Fixed

Expand Down
1 change: 1 addition & 0 deletions docs/playlet-web-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ components:
type: boolean
sponsorblock.show_notifications:
type: boolean
# TODO:P1 add the rest of properties
PlayQueueObject:
type: object
properties:
Expand Down
4 changes: 3 additions & 1 deletion playlet-lib/src/components/MainScene.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@

<!-- No render nodes -->
<PlayQueue id="PlayQueue"
invidious="bind:../Invidious" />
invidious="bind:../Invidious"
notifications="bind:../Notifications"
preferences="bind:../Preferences" />
<ApplicationInfo id="ApplicationInfo" />
<Preferences id="Preferences" />
<Bookmarks id="Bookmarks" />
Expand Down
4 changes: 3 additions & 1 deletion playlet-lib/src/components/MainScene_bindings.transpiled.brs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ function InitializeBindings()
"appController": "/AppController"
},
"PlayQueue": {
"invidious": "../Invidious"
"invidious": "../Invidious",
"notifications": "../Notifications",
"preferences": "../Preferences"
},
"Invidious": {
"webServer": "../WebServer",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import "pkg:/source/utils/StringUtils.bs"
import "pkg:/source/utils/Types.bs"

' TODO:P2 a lot of shared logic with the SponsorBlock notification, should be refactored into a common notification
function Init()
m.translationAnimation = m.top.findNode("translationAnimation")
m.translationAnimationInterpolator = m.top.findNode("translationAnimationInterpolator")
m.animationTimer = m.top.findNode("animationTimer")

m.top.translation = [1280, 20]
m.translationAnimation.observeField("state", FuncName(OnAnimationState))
m.animationTimer.observeField("fire", FuncName(OnAnimationTimer))
end function

function OnContentSet() as void
content = m.top.content
if content = invalid
return
end if

if not StringUtils.IsNullOrEmpty(content.thumbnail)
m.top.thumbnail = content.thumbnail
else
m.top.thumbnail = "pkg:/images/thumbnail-missing.jpg"
end if

m.top.line1 = content.title
end function

function OnShow()
AnimateIn()
end function

function OnAnimationTimer()
AnimateOut()
end function

function AnimateIn()
m.translationAnimation.unobserveField("state")
m.animationTimer.control = "stop"
m.animationTimer.control = "start"
m.translationAnimation.observeField("state", FuncName(OnAnimationState))

Animate(false)
end function

function AnimateOut()
Animate(true)
end function

function Animate(reverse as boolean) as void
' We are already animating in the requested direction
if m.translationAnimationInterpolator.reverse = reverse and m.translationAnimation.control <> "none"
return
end if

m.translationAnimationInterpolator.reverse = reverse
m.translationAnimation.control = "start"
end function

function OnAnimationState()
if m.translationAnimation.state = "stopped" and m.translationAnimationInterpolator.reverse = true
m.top.getParent().removeChild(m.top)
end if
end function
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<component name="PlayQueueNotification" extends="Group">
<interface>
<field id="content" type="node" onChange="OnContentSet" />
<field id="thumbnail" type="uri" alias="thumbnailPoster.uri" />
<field id="line1" type="string" alias="line1Label.text" />
<field id="show" type="boolean" alwaysNotify="true" onChange="OnShow" />
</interface>
<children>
<Poster
width="500"
height="126"
opacity="0.9"
uri="pkg:/images/white.9.png">

<Poster
id="thumbnailPoster"
loadDisplayMode="scaleToZoom"
width="170"
height="106"
failedBitmapUri="pkg:/images/thumbnail-missing.jpg"
translation="[10,10]">
</Poster>

<LayoutGroup
itemSpacings="[10]"
translation="[190,10]">
<Label
width="300"
font="font:SmallestBoldSystemFont"
horizAlign="center"
color="0x262626ff"
text="Added to queue">
<Font role="font" uri="font:BoldSystemFontFile" size="22" />
</Label>
<Label
id="line1Label"
width="300"
font="font:SmallestSystemFont"
maxLines="3"
color="0x262626ff"
wrap="true" />
</LayoutGroup>
</Poster>
<Animation
id="translationAnimation"
duration="0.3"
optional="true">
<Vector2DFieldInterpolator
id="translationAnimationInterpolator"
key="[0.0, 0.5, 1.0]"
keyValue="[ [1280.0, 20.0], [1020.0, 20.0], [760.0, 20.0] ]"
fieldToInterp="PlayQueueNotification.translation" />
</Animation>
<Timer
id="animationTimer"
duration="3" />
</children>
</component>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace PlayQueue
const NOTIFICATION_NODE_ID = "PlayQueueNotification"

function ShowNotifcation(notifications as object, contentNode as object) as void
notification = notifications.findNode(NOTIFICATION_NODE_ID)
if notification = invalid
notification = notifications.createChild("PlayQueueNotification")
notification.id = NOTIFICATION_NODE_ID
end if
notification.content = contentNode
notification.show = true
end function

function RemoveNotifcation(notifications as object) as void
notification = notifications.findNode(NOTIFICATION_NODE_ID)
if notification <> invalid
notifications.RemoveChild(notification)
end if
end function

function SetVisible(notifications as object, visible as boolean)
notification = notifications.findNode(NOTIFICATION_NODE_ID)
if notification <> invalid
notification.visible = visible
end if
end function

end namespace
5 changes: 5 additions & 0 deletions playlet-lib/src/components/PlayQueue/PlayQueue.bs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import "pkg:/components/Dialog/DialogUtils.bs"
import "pkg:/components/PlaylistView/PlaylistContentTask.bs"
import "pkg:/components/PlayQueue/Notifications/PlayQueueNotificationUtils.bs"
import "pkg:/components/VideoFeed/FeedLoadState.bs"
import "pkg:/components/VideoPlayer/VideoUtils.bs"
import "pkg:/source/asyncTask/asyncTask.bs"
Expand Down Expand Up @@ -30,6 +31,10 @@ end function

function AddToQueue(node as object)
m.queue.push(node)
queueNotifications = m.top.preferences["misc.queue_notifications"]
if queueNotifications
PlayQueue.ShowNotifcation(m.top.notifications, node)
end if
end function

' TODO:P1 better model of the queue (Now playing, etc)
Expand Down
2 changes: 2 additions & 0 deletions playlet-lib/src/components/PlayQueue/PlayQueue.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<field id="index" type="integer" value="-1" />
<field id="playlistIndex" type="integer" value="-1" />
<field id="invidious" type="node" />
<field id="notifications" type="node" />
<field id="preferences" type="node" />
<function name="Play" />
<function name="AddToQueue" />
<function name="GetQueue" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ namespace InvidiousContent
node.liveNow = VideoIsLive(item)
VideoSetPremiereTimestampText(node, item)
SetIfExists(node, "publishedText", item, "publishedText")
if not StringUtils.IsNullOrEmpty(instance)
node.thumbnail = VideoGetThumbnail(item, instance) ?? "pkg:/images/thumbnail-missing.jpg"
end if
VideoSetThumbnail(node, item, instance)
SetIfExists(node, "title", item, "title")
SetIfExists(node, "timestamp", item, "timestamp")
SetIfExists(node, "videoId", item, "videoId")
Expand Down Expand Up @@ -128,10 +126,11 @@ namespace InvidiousContent
end if
end function

function VideoGetThumbnail(videoItem as object, instance as dynamic, quality = "medium" as string) as dynamic
function VideoSetThumbnail(node as object, videoItem as object, instance as dynamic, quality = "medium" as string) as boolean
videoThumbnails = videoItem.videoThumbnails
if videoThumbnails = invalid or videoThumbnails.Count() = 0
return invalid
node.thumbnail = "pkg:/images/thumbnail-missing.jpg"
return false
end if
url = invalid
for each thumbnail in videoThumbnails
Expand All @@ -143,10 +142,14 @@ namespace InvidiousContent
if url = invalid
url = videoThumbnails[0].url
end if
if url.startsWith("/") and not StringUtils.IsNullOrEmpty(instance)
if url.startsWith("/")
if StringUtils.IsNullOrEmpty(instance)
return false
end if
url = instance + url
end if
return url
node.thumbnail = url
return true
end function

function VideoIsLive(videoItem as object) as boolean
Expand Down Expand Up @@ -208,7 +211,9 @@ namespace InvidiousContent
end if
thumbnail = thumbnailUrl
else if playlistItem.videos <> invalid and playlistItem.videos.Count() > 0 and playlistItem.videos[0].index = 0
thumbnail = VideoGetThumbnail(playlistItem.videos[0], instance, quality)
if VideoSetThumbnail(node, playlistItem.videos[0], instance, quality)
return
end if
else if node.getChildCount() > 0
thumbnail = node.getChild(0).thumbnail
end if
Expand Down
14 changes: 14 additions & 0 deletions playlet-lib/src/config/preferences.json5
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,18 @@
},
],
},
{
displayText: "Miscellaneous",
key: "misc",
description: "Misc. preferences",
children: [
{
displayText: "Queue notifications",
key: "misc.queue_notifications",
description: "Show a notification when a video is added to the queue",
type: "boolean",
defaultValue: true,
},
],
},
]
62 changes: 17 additions & 45 deletions playlet-web/src/lib/Api/PlayletApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,73 +43,45 @@ export class PlayletApi {
await fetch(`${PlayletApi.host()}/invidious/logout`);
}

static async playVideo(videoId, timestamp, title, author) {
if (!videoId) {
static async playVideo(args) {
if (!args.videoId) {
return;
}
const args = { videoId };
if (timestamp !== undefined) {
if (typeof timestamp === "string") {
timestamp = parseInt(timestamp);

if (args.timestamp !== undefined) {
if (typeof args.timestamp === "string") {
args.timestamp = parseInt(args.timestamp);
}
args["timestamp"] = timestamp;
}
if (title !== undefined) {
args["title"] = title;
}
if (author !== undefined) {
args["author"] = author;
}

await PlayletApi.postJson(`${PlayletApi.host()}/api/queue/play`, args);
}

static async playPlaylist(playlistId, title, videoCount) {
if (!playlistId) {
static async playPlaylist(args) {
if (!args.playlistId) {
return;
}
const args = { playlistId };
if (title !== undefined) {
args["title"] = title;
}
if (videoCount !== undefined) {
args["author"] = videoCount;
}

await PlayletApi.postJson(`${PlayletApi.host()}/api/queue/play`, args);
}

static async queueVideo(videoId, timestamp, title, author) {
if (!videoId) {
static async queueVideo(args) {
if (!args.videoId) {
return;
}
const args = { videoId };
if (timestamp !== undefined) {
if (typeof timestamp === "string") {
timestamp = parseInt(timestamp);

if (args.timestamp !== undefined) {
if (typeof args.timestamp === "string") {
args.timestamp = parseInt(args.timestamp);
}
args["timestamp"] = timestamp;
}
if (title !== undefined) {
args["title"] = title;
}
if (author !== undefined) {
args["author"] = author;
}
const response = await PlayletApi.postJson(`${PlayletApi.host()}/api/queue`, args);
return await response.json();
}

static async queuePlaylist(playlistId, title, videoCount) {
if (!playlistId) {
static async queuePlaylist(args) {
if (!args.playlistId) {
return;
}
const args = { playlistId };
if (title !== undefined) {
args["title"] = title;
}
if (videoCount !== undefined) {
args["author"] = videoCount;
}
const response = await PlayletApi.postJson(`${PlayletApi.host()}/api/queue`, args);
return await response.json();
}
Expand Down
Loading

0 comments on commit 4233f30

Please sign in to comment.