From 4233f30dec59fcfdc43042c9cbd50e5cf6dd5f4b Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sat, 4 Nov 2023 20:23:02 -0400 Subject: [PATCH] Play queue notification (#186) * Play queue notification * Lint fix * change binding * Add notification preference * Lint fix * changelog --------- Co-authored-by: github-action linter --- CHANGELOG.md | 1 + docs/playlet-web-api.yml | 1 + playlet-lib/src/components/MainScene.xml | 4 +- .../MainScene_bindings.transpiled.brs | 4 +- .../Notifications/PlayQueueNotification.bs | 65 +++++++++++++++++++ .../Notifications/PlayQueueNotification.xml | 58 +++++++++++++++++ .../PlayQueueNotificationUtils.bs | 28 ++++++++ .../src/components/PlayQueue/PlayQueue.bs | 5 ++ .../src/components/PlayQueue/PlayQueue.xml | 2 + .../Invidious/InvidiousToContentNode.bs | 21 +++--- playlet-lib/src/config/preferences.json5 | 14 ++++ playlet-web/src/lib/Api/PlayletApi.ts | 62 +++++------------- .../lib/VideoFeed/PlaylistCastDialog.svelte | 41 +++++++++++- .../src/lib/VideoFeed/VideoCastDialog.svelte | 36 ++++++++-- 14 files changed, 281 insertions(+), 61 deletions(-) create mode 100644 playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotification.bs create mode 100644 playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotification.xml create mode 100644 playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotificationUtils.bs diff --git a/CHANGELOG.md b/CHANGELOG.md index 46340b50..1c1eb92a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/playlet-web-api.yml b/docs/playlet-web-api.yml index 38995db7..78d38dca 100644 --- a/docs/playlet-web-api.yml +++ b/docs/playlet-web-api.yml @@ -538,6 +538,7 @@ components: type: boolean sponsorblock.show_notifications: type: boolean + # TODO:P1 add the rest of properties PlayQueueObject: type: object properties: diff --git a/playlet-lib/src/components/MainScene.xml b/playlet-lib/src/components/MainScene.xml index 3759dd74..e5d4dde4 100644 --- a/playlet-lib/src/components/MainScene.xml +++ b/playlet-lib/src/components/MainScene.xml @@ -47,7 +47,9 @@ + invidious="bind:../Invidious" + notifications="bind:../Notifications" + preferences="bind:../Preferences" /> diff --git a/playlet-lib/src/components/MainScene_bindings.transpiled.brs b/playlet-lib/src/components/MainScene_bindings.transpiled.brs index 5f9c5cf1..50d7fddc 100644 --- a/playlet-lib/src/components/MainScene_bindings.transpiled.brs +++ b/playlet-lib/src/components/MainScene_bindings.transpiled.brs @@ -13,7 +13,9 @@ function InitializeBindings() "appController": "/AppController" }, "PlayQueue": { - "invidious": "../Invidious" + "invidious": "../Invidious", + "notifications": "../Notifications", + "preferences": "../Preferences" }, "Invidious": { "webServer": "../WebServer", diff --git a/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotification.bs b/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotification.bs new file mode 100644 index 00000000..a964990b --- /dev/null +++ b/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotification.bs @@ -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 diff --git a/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotification.xml b/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotification.xml new file mode 100644 index 00000000..f056287b --- /dev/null +++ b/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotification.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotificationUtils.bs b/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotificationUtils.bs new file mode 100644 index 00000000..2927370a --- /dev/null +++ b/playlet-lib/src/components/PlayQueue/Notifications/PlayQueueNotificationUtils.bs @@ -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 diff --git a/playlet-lib/src/components/PlayQueue/PlayQueue.bs b/playlet-lib/src/components/PlayQueue/PlayQueue.bs index f28694a0..245352a0 100644 --- a/playlet-lib/src/components/PlayQueue/PlayQueue.bs +++ b/playlet-lib/src/components/PlayQueue/PlayQueue.bs @@ -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" @@ -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) diff --git a/playlet-lib/src/components/PlayQueue/PlayQueue.xml b/playlet-lib/src/components/PlayQueue/PlayQueue.xml index 2abcfbff..3084ef4f 100644 --- a/playlet-lib/src/components/PlayQueue/PlayQueue.xml +++ b/playlet-lib/src/components/PlayQueue/PlayQueue.xml @@ -4,6 +4,8 @@ + + diff --git a/playlet-lib/src/components/Services/Invidious/InvidiousToContentNode.bs b/playlet-lib/src/components/Services/Invidious/InvidiousToContentNode.bs index 863ab56c..5197b5a3 100644 --- a/playlet-lib/src/components/Services/Invidious/InvidiousToContentNode.bs +++ b/playlet-lib/src/components/Services/Invidious/InvidiousToContentNode.bs @@ -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") @@ -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 @@ -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 @@ -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 diff --git a/playlet-lib/src/config/preferences.json5 b/playlet-lib/src/config/preferences.json5 index 89971a7e..9e06880e 100644 --- a/playlet-lib/src/config/preferences.json5 +++ b/playlet-lib/src/config/preferences.json5 @@ -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, + }, + ], + }, ] diff --git a/playlet-web/src/lib/Api/PlayletApi.ts b/playlet-web/src/lib/Api/PlayletApi.ts index 0fd8c0c2..2686b9c6 100644 --- a/playlet-web/src/lib/Api/PlayletApi.ts +++ b/playlet-web/src/lib/Api/PlayletApi.ts @@ -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(); } diff --git a/playlet-web/src/lib/VideoFeed/PlaylistCastDialog.svelte b/playlet-web/src/lib/VideoFeed/PlaylistCastDialog.svelte index 35bbcf0b..7153a911 100644 --- a/playlet-web/src/lib/VideoFeed/PlaylistCastDialog.svelte +++ b/playlet-web/src/lib/VideoFeed/PlaylistCastDialog.svelte @@ -27,11 +27,11 @@ }); async function playOnTv() { - await PlayletApi.playPlaylist(playlistId, title, videoCount); + await PlayletApi.playPlaylist(getPlaylistInfo()); } async function queueOnTv() { - await PlayletApi.queuePlaylist(playlistId, title, videoCount); + await PlayletApi.queuePlaylist(getPlaylistInfo()); } async function openOnTv() { @@ -42,6 +42,43 @@ let url = `${invidiousInstance}/playlist?list=${playlistId}`; window.open(url); } + + function getPlaylistInfo() { + return { + type: "playlist", + playlistId, + title, + playlistThumbnail: getPlaylistThumbnail(), + videoCount, + videos, + }; + } + + function getPlaylistThumbnail() { + let url = ""; + if (playlistThumbnail) { + url = playlistThumbnail; + if (url.startsWith("/") && invidiousInstance) { + url = invidiousInstance + url; + } + } else if (videos && videos.length) { + const video = videos[0]; + if (video.videoThumbnails) { + const videoThumbnail = + video.videoThumbnails.find( + (thumbnail) => thumbnail.quality === "medium" + ) || video.videoThumbnails[0]; + url = videoThumbnail.url; + } + if (url.startsWith("/") && invidiousInstance) { + url = invidiousInstance + url; + } + } + if (url === "") { + url = `${invidiousInstance}/vi/-----------/mqdefault.jpg`; + } + return url; + } diff --git a/playlet-web/src/lib/VideoFeed/VideoCastDialog.svelte b/playlet-web/src/lib/VideoFeed/VideoCastDialog.svelte index 78c44e50..dce3215c 100644 --- a/playlet-web/src/lib/VideoFeed/VideoCastDialog.svelte +++ b/playlet-web/src/lib/VideoFeed/VideoCastDialog.svelte @@ -35,13 +35,11 @@ }); async function playOnTv() { - const timestamp = videoStartAtChecked ? videoStartAtTimestamp : undefined; - await PlayletApi.playVideo(videoId, timestamp, title, author); + await PlayletApi.playVideo(getVideoInfo()); } async function queueOnTv() { - const timestamp = videoStartAtChecked ? videoStartAtTimestamp : undefined; - await PlayletApi.queueVideo(videoId, timestamp, title, author); + await PlayletApi.queueVideo(getVideoInfo()); } function openInvidious() { @@ -51,6 +49,36 @@ } window.open(url); } + + function getVideoInfo() { + const timestamp = videoStartAtChecked ? videoStartAtTimestamp : undefined; + return { + type: "video", + videoId, + title, + videoThumbnails: getVideoThumbnails(), + author, + lengthSeconds, + liveNow, + viewCount, + timestamp, + }; + } + + function getVideoThumbnails() { + if (!invidiousInstance || !videoThumbnails || !videoThumbnails.length) { + return videoThumbnails; + } + + return videoThumbnails.map((thumbnail) => { + let url = thumbnail.url; + if (url.startsWith("/")) { + url = invidiousInstance + url; + thumbnail.url = url; + } + return thumbnail; + }); + }