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

Play queue notification #186

Merged
merged 7 commits into from
Nov 5, 2023
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
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