From c99c33a4e564854079b1077e9fc4c8e41b54f19e Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Mon, 2 Oct 2023 20:22:39 -0400 Subject: [PATCH] Bookmarks v1 --- .../src/components/ChannelView/ChannelView.bs | 12 +- .../src/components/ContextMenu/ContextMenu.bs | 83 +++++++++++++- .../components/PlaylistView/PlaylistView.bs | 1 - .../BookmarksScreen/BookmarksScreen.bs | 88 ++++++++++++++- .../BookmarksScreen/BookmarksScreen.xml | 1 + .../Screens/SearchScreen/SearchScreen.bs | 12 +- .../Services/Bookmarks/Bookmarks.bs | 28 +---- .../Services/Bookmarks/Bookmarks.xml | 1 + .../Services/Invidious/InvidiousService.bs | 98 +++++++++++++--- .../src/components/VideoFeed/VideoRowList.bs | 26 ++--- .../src/config/default_home_layout.json5 | 106 ++++++++++++------ .../src/config/invidious_video_api.json5 | 23 ++++ 12 files changed, 372 insertions(+), 107 deletions(-) diff --git a/playlet-lib/src/components/ChannelView/ChannelView.bs b/playlet-lib/src/components/ChannelView/ChannelView.bs index 2c663dbd..35cd7075 100644 --- a/playlet-lib/src/components/ChannelView/ChannelView.bs +++ b/playlet-lib/src/components/ChannelView/ChannelView.bs @@ -133,10 +133,12 @@ end function function CreateChannelFeed(title as string, endpoint as string, ucid as string) as object return { title: title, - apiType: "Invidious", - endpoint: endpoint, - pathParams: { - ucid: ucid - } + items: [{ + apiType: "Invidious", + endpoint: endpoint, + pathParams: { + ucid: ucid + } + }] } end function diff --git a/playlet-lib/src/components/ContextMenu/ContextMenu.bs b/playlet-lib/src/components/ContextMenu/ContextMenu.bs index 9b99a7e1..97f0d4db 100644 --- a/playlet-lib/src/components/ContextMenu/ContextMenu.bs +++ b/playlet-lib/src/components/ContextMenu/ContextMenu.bs @@ -3,8 +3,87 @@ import "pkg:/source/utils/Logging.bs" import "pkg:/source/utils/StringUtils.bs" import "pkg:/source/utils/Types.bs" -' TODO:P0 test animation (fade in) -' TODO:P1 animation (fade out) +' TODO:P0 Context menu +' TODO:P0 Web app support +' TODO:P0 Web apis and open api spec updates +' +' From the main screen: +' When a video is selected: +' - "Play" - DONE +' - "Queue" - DONE +' - "Open Channel" - DONE +' - "Add to "Videos" bookmark" - DONE +' - If video is already in a bookmark, "Remove from Bookmarks" - DONE +' +' When a channel is selected: +' - "Open" - DONE +' - "Add to "Channels" bookmark" - DONE +' - "Add to bookmark" - DONE +' - If channel is already in a bookmark, "Remove from Bookmarks" - DONE +' - PlaylistId starting with "IV" cannot be added to a bookmark - DONE +' +' When a playlist is selected: +' - "Play" - DONE +' - "Queue" - DONE +' - "Open" - DONE +' - "Add to "Playlists" bookmark" - DONE +' - "Add to bookmark" - DONE +' - If playlist is already in a bookmark, "Remove from Bookmarks" - DONE +' When any: +' - "Reload feed" +' +' From Playlist view: +' When a video is selected: +' - "Play" +' - "Queue" +' - "Play video" +' - "Queue video" +' - "Open Channel" +' - "Add to "Playlists" bookmark" +' - "Add to bookmark" +' - "Add to "Videos" bookmark" +' +' From Channel view: +' When a video is selected: +' - "Play" +' - "Queue" +' - "Add to "Channels" bookmark" +' - "Add to - bookmark" feed being latest videos, live, shorts, etc +' - "Add to "Videos" bookmark" +' +' When a playlist is selected: +' - "Play" +' - "Queue" +' - "Open" +' - "Add to "Channels" bookmark" +' - "Add to - bookmark" feed being latest videos, live, shorts, etc +' - "Add to "Playlists" bookmark" +' - "Add to bookmark" +' +' From search view: +' When a video/channel/playlist is selected: +' - Inherit options +' - "Add to "Search - ${q}" bookmark" - this includes the search filters used +' +' From bookmarks view: +' When a video/channel/playlist is selected: +' - Inherit options +' - "Remove item from Bookmarks" +' - "Remove group from Bookmarks" +' - "Reload bookmarks" +' +' TODO:P1 Playlist item seletected from the bookmarks need to play as a playlist, not as a single video +' TODO:P1 Context menu might need to be scrollable (both horizontally and vertically) +' TODO:P1 Context menu might need sections. Example: +' In a Channel view, while selecting a playlist: +' - Channel +' - Add to "Channels" bookmark +' - Other "Channels" related context menu items +' - Playlist +' - Add to "Playlists" bookmark +' - "Play/Queue" +' - Other "Playlists" related context menu items + function Init() m.buttonGroup = m.top.findNode("buttonGroup") m.showAnimation = m.top.findNode("showAnimation") diff --git a/playlet-lib/src/components/PlaylistView/PlaylistView.bs b/playlet-lib/src/components/PlaylistView/PlaylistView.bs index d8c2a29d..23de7d87 100644 --- a/playlet-lib/src/components/PlaylistView/PlaylistView.bs +++ b/playlet-lib/src/components/PlaylistView/PlaylistView.bs @@ -10,7 +10,6 @@ import "pkg:/source/utils/Logging.bs" import "pkg:/source/utils/StringUtils.bs" import "pkg:/source/utils/Types.bs" -' TODO:P0 context menu (play/queue/bookmarks) function Init() m.background = m.top.findNode("background") m.backgroundSmall = m.top.findNode("backgroundSmall") diff --git a/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.bs b/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.bs index 8624cfe0..2d611d62 100644 --- a/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.bs +++ b/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.bs @@ -6,6 +6,9 @@ function Init() m.noBookmarks = m.top.findNode("noBookmarks") m.yesBookmarks = m.top.findNode("yesBookmarks") m.rowList = m.top.FindNode("rowList") + m.isDirty = true + + m.top.ObserveField("visible", FuncName(OnVisibleChange)) end function function OnNodeReady() @@ -15,7 +18,7 @@ function OnNodeReady() m.rowList@.BindNode(invalid) OnBookmarksChange() - m.bookmarks.content.ObserveField("change", FuncName(OnBookmarksChange)) + m.bookmarks.ObserveField("contentChange", FuncName(OnBookmarksChange)) end function function OnFocusChange() as void @@ -30,16 +33,25 @@ function OnFocusChange() as void end if end function -' TODO:P0 handle visiblity: only refresh bookmarks if screen visible. -' Else mark as dirty and refresh when visible -function OnBookmarksChange() +function OnVisibleChange() + if m.top.visible and m.isDirty + m.isDirty = false + OnBookmarksChange() + end if +end function + +function OnBookmarksChange() as void + if not m.top.visible + m.isDirty = true + return + end if hasBookmarks = m.bookmarks.content.getChildCount() > 0 m.noBookmarks.visible = not hasBookmarks m.yesBookmarks.visible = hasBookmarks m.top.focusable = hasBookmarks if hasBookmarks - SetRowListContent() + SetRowListContent(m.bookmarks.content) else if m.rowList.hasFocus() NodeSetFocus(m.navBar, true) @@ -47,8 +59,72 @@ function OnBookmarksChange() end if end function -function SetRowListContent() +function SetRowListContent(bookmarksContent as object) + bookmarks = bookmarksContent.getChildren(-1, 0) + + contentData = [] + for each bookmarkGroup in bookmarks + items = bookmarkGroup.getChildren(-1, 0) + rowData = { + title: bookmarkGroup.title, + items: [] + } + + for each item in items + if item.type = "video" + rowItemData = { + apiType: "Invidious", + endpoint: "video_info", + pathParams: { + id: item.itemId + } + } + rowData.items.push(rowItemData) + else if item.type = "playlist" + rowItemData = { + apiType: "Invidious", + endpoint: "playlist_info", + pathParams: { + plid: item.itemId + } + } + rowData.items.push(rowItemData) + if items.Count() = 1 + rowItemData = { + apiType: "Invidious", + endpoint: "playlist", + pathParams: { + plid: item.itemId + } + } + rowData.items.push(rowItemData) + end if + else if item.type = "channel" + rowItemData = { + apiType: "Invidious", + endpoint: "channel_info", + pathParams: { + ucid: item.itemId + } + } + rowData.items.push(rowItemData) + if items.Count() = 1 + rowItemData = { + apiType: "Invidious", + endpoint: "channel_videos", + pathParams: { + ucid: item.itemId + } + } + rowData.items.push(rowItemData) + end if + end if + end for + + contentData.push(rowData) + end for + m.rowList.contentData = contentData end function function OnkeyEvent(key as string, press as boolean) as boolean diff --git a/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.xml b/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.xml index 956e6391..256b7bed 100644 --- a/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.xml +++ b/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.xml @@ -1,5 +1,6 @@ + diff --git a/playlet-lib/src/components/Screens/SearchScreen/SearchScreen.bs b/playlet-lib/src/components/Screens/SearchScreen/SearchScreen.bs index 1847fd26..eb9dd902 100644 --- a/playlet-lib/src/components/Screens/SearchScreen/SearchScreen.bs +++ b/playlet-lib/src/components/Screens/SearchScreen/SearchScreen.bs @@ -201,8 +201,7 @@ function Search(text as string) m.rowlist.ObserveFieldScoped("someContentReady", FuncName(OnSearchContentReady)) ShowLoadingScreen() - requestData = { - title: `Search - ${text}`, + requestDataItem = { apiType: "Invidious", endpoint: "search", queryParams: { @@ -214,18 +213,21 @@ function Search(text as string) for each filter in filters value = filters[filter] if IsString(value) and value.len() > 0 - requestData.queryParams[filter] = value + requestDataItem.queryParams[filter] = value else if IsArray(value) and value.Count() > 0 ' TODO:P1 this is http client implementation details leaking ' We need a declarative way to set array types - requestData.queryParams[filter] = { + requestDataItem.queryParams[filter] = { value: value, __arrayType: HttpClient.QueryParamArrayType.CommaSeparated } end if end for - m.rowList.contentData = [requestData] + m.rowList.contentData = [{ + title: `Search - ${text}`, + items: [requestDataItem] + }] end function function OnSearchContentReady() as void diff --git a/playlet-lib/src/components/Services/Bookmarks/Bookmarks.bs b/playlet-lib/src/components/Services/Bookmarks/Bookmarks.bs index 5764253a..1ca57bab 100644 --- a/playlet-lib/src/components/Services/Bookmarks/Bookmarks.bs +++ b/playlet-lib/src/components/Services/Bookmarks/Bookmarks.bs @@ -12,7 +12,7 @@ function Init() m.top.content = m.top.findNode("content") m.bookmarksString = "" Load() - m.top.content.ObserveField("change", FuncName(OnChange)) + m.top.ObserveField("contentChange", FuncName(OnContentChange)) end function function Load() as void @@ -94,6 +94,7 @@ function AddBookmark(bookmarkType as string, id as string, groupName as string) }) groupNode.insertChild(node, 0) LogInfo("Added bookmark:", id) + m.top.contentChange = true end function function RemoveBookmark(id as string) as void @@ -110,14 +111,10 @@ function RemoveBookmark(id as string) as void m.top.content.removeChild(group) LogInfo("Removed bookmark group:", group.title) end if + m.top.contentChange = true end function -function OnChange(event as object) as void - change = event.getData() - if change.Operation = "none" - return - end if - +function OnContentChange(event as object) as void Save() end function @@ -149,7 +146,7 @@ end function function GetMenuForPlaylist(playlistNode as object) as object playlistId = playlistNode.playlistId - if StringUtils.IsNullOrEmpty(playlistId) + if StringUtils.IsNullOrEmpty(playlistId) or playlistId.StartsWith("IV") return [] end if @@ -212,18 +209,3 @@ function GetMenuForChannel(channelNode as object) as object end if return menu end function - -' TODO:P0 -' When a video is selected: -' - Add to "Videos" bookmark - DONE -' - Add to bookmarks... - -' When a channel is selected: -' - Add to "Channels" bookmark - DONE -' - Add to bookmark - DONE -' - Add to bookmarks... - -' When a playlist is selected: -' - Add to "Playlists" bookmark - DONE -' - Add to bookmark - DONE -' - Add to bookmarks... diff --git a/playlet-lib/src/components/Services/Bookmarks/Bookmarks.xml b/playlet-lib/src/components/Services/Bookmarks/Bookmarks.xml index 14030525..fe5691c5 100644 --- a/playlet-lib/src/components/Services/Bookmarks/Bookmarks.xml +++ b/playlet-lib/src/components/Services/Bookmarks/Bookmarks.xml @@ -2,6 +2,7 @@ + diff --git a/playlet-lib/src/components/Services/Invidious/InvidiousService.bs b/playlet-lib/src/components/Services/Invidious/InvidiousService.bs index 8c469d3d..4b32586b 100644 --- a/playlet-lib/src/components/Services/Invidious/InvidiousService.bs +++ b/playlet-lib/src/components/Services/Invidious/InvidiousService.bs @@ -49,6 +49,9 @@ namespace Invidious DefaultHandler: m.DefaultHandler, AuthFeedHandler: m.AuthFeedHandler, PlaylistHandler: m.PlaylistHandler, + VideoInfoHandler: m.VideoInfoHandler, + PlaylistInfoHandler: m.PlaylistInfoHandler, + ChannelInfoHandler: m.ChannelInfoHandler, ChannelVideosHandler: m.ChannelVideosHandler, ChannelPlaylistsHandler: m.ChannelPlaylistsHandler, ChannelRelatedChannelsHandler: m.ChannelRelatedChannelsHandler @@ -158,36 +161,70 @@ namespace Invidious function MarkFeedPagination(contentNode as object) as void requestData = contentNode.feed - endpoint = m.endpoints[requestData.endpoint] + ' Only one item feeds will be paginated. + if requestData.items.Count() <> 1 + return + end if + item = requestData.items[0] + + endpoint = m.endpoints[item.endpoint] if endpoint = invalid or StringUtils.IsNullOrEmpty(endpoint.paginationType) return end if - if not requestData.DoesExist("queryParams") - requestData.queryParams = {} + if not item.DoesExist("queryParams") + item.queryParams = {} end if contentNode.paginationType = endpoint.paginationType if endpoint.paginationType = PaginationType.Pages contentNode.page += 1 - requestData.queryParams.page = contentNode.page + item.queryParams.page = contentNode.page else if endpoint.paginationType = PaginationType.Continuation continuation = contentNode.continuation if not StringUtils.IsNullOrEmpty(continuation) - requestData.queryParams.continuation = continuation + item.queryParams.continuation = continuation end if end if + requestData.items[0] = item contentNode.feed = requestData end function function MakeRequest(requestData as object, cancellation = invalid as object) as object - endpoint = m.endpoints[requestData.endpoint] + aggregateResponse = { + success: true, + result: { + items: [] + } + } + + for i = 0 to requestData.items.Count() - 1 + requestDataItem = requestData.items[i] + response = m.MakeRequestSingle(requestDataItem, cancellation) + if not response.success + ' TODO:P1 handle errors better + ' I don't feel confortable aborting the whole feed just because a single item failed + ' This could cause the whole feed to fail if one item (like a video or playlist) got deleted + return response + end if + aggregateResponse.result.items.Append(response.result.items) + + if requestData.items.Count() = 1 and IsString(response.result.continuation) + aggregateResponse.result.continuation = response.result.continuation + end if + end for + + return aggregateResponse + end function + + function MakeRequestSingle(requestDataItem as object, cancellation = invalid as object) as object + endpoint = m.endpoints[requestDataItem.endpoint] if endpoint = invalid return { success: false, - error: `Endpoint ${requestData.endpoint} not found` + error: `Endpoint ${requestDataItem.endpoint} not found` } end if @@ -230,18 +267,18 @@ namespace Invidious end for end if - if requestData.cacheSeconds <> invalid - request.CacheSeconds(requestData.cacheSeconds) + if requestDataItem.cacheSeconds <> invalid + request.CacheSeconds(requestDataItem.cacheSeconds) else if endpoint.cacheSeconds <> invalid request.CacheSeconds(endpoint.cacheSeconds) end if - if requestData.queryParams <> invalid - request.QueryParams(requestData.queryParams) + if requestDataItem.queryParams <> invalid + request.QueryParams(requestDataItem.queryParams) end if - if requestData.pathParams <> invalid - request.PathParams(requestData.pathParams) + if requestDataItem.pathParams <> invalid + request.PathParams(requestDataItem.pathParams) end if request.Cancellation(cancellation) @@ -250,7 +287,7 @@ namespace Invidious responseHandler = endpoint.responseHandler <> invalid ? m.responseHanlders[endpoint.responseHandler] : m.responseHanlders["DefaultHandler"] - result = responseHandler(m, requestData, response) + result = responseHandler(m, requestDataItem, response) if response.IsSuccess() and result <> invalid return { @@ -318,6 +355,39 @@ namespace Invidious return invalid end function + function VideoInfoHandler(m as object, requestData as object, response as object) as object + if response.StatusCode() = 200 + json = response.Json() + json.type = "video" + return { + items: [json] + } + end if + return invalid + end function + + function PlaylistInfoHandler(m as object, requestData as object, response as object) as object + if response.StatusCode() = 200 + json = response.Json() + json.type = "playlist" + return { + items: [json] + } + end if + return invalid + end function + + function ChannelInfoHandler(m as object, requestData as object, response as object) as object + if response.StatusCode() = 200 + json = response.Json() + json.type = "channel" + return { + items: [json] + } + end if + return invalid + end function + function AuthFeedHandler(m as object, requestData as object, response as object) as object m.DeleteExpiredToken(response) diff --git a/playlet-lib/src/components/VideoFeed/VideoRowList.bs b/playlet-lib/src/components/VideoFeed/VideoRowList.bs index 8390f927..7ad14afe 100644 --- a/playlet-lib/src/components/VideoFeed/VideoRowList.bs +++ b/playlet-lib/src/components/VideoFeed/VideoRowList.bs @@ -48,6 +48,11 @@ end function function OnRowItemFocused(event as object) as void index = event.GetData() + if index = invalid or index.count() <> 2 + LogWarn("Invalid index:", index) + return + end if + rowIndex = index[0] rowItemIndex = index[1] @@ -67,6 +72,11 @@ function OnRowItemSelected(index as object) as void return end if + if index = invalid or index.count() <> 2 + LogWarn("Invalid index:", index) + return + end if + rowIndex = index[0] rowItemIndex = index[1] @@ -128,22 +138,6 @@ function OnRowItemLongPressed(index as object) as void end function function OpenContextMenu(rowItem as object) as void - ' TODO:P0 - ' Video menu: - ' - Play - DONE - ' - Queue - DONE - ' - Open channel - DONE - ' - Add to bookmarks... / Remove from bookmarks - ' Playlist menu: - ' - Play - DONE - ' - Queue - DONE - ' - Open playlist - DONE - ' - Open channel - ' - Add to bookmarks... / Remove from bookmarks - ' Channel menu: - ' - Open channel - DONE - ' - Add to bookmarks... / Remove from bookmarks - menu = [] title = "" subtitle = "" diff --git a/playlet-lib/src/config/default_home_layout.json5 b/playlet-lib/src/config/default_home_layout.json5 index 15f99a44..c0540c19 100644 --- a/playlet-lib/src/config/default_home_layout.json5 +++ b/playlet-lib/src/config/default_home_layout.json5 @@ -1,64 +1,100 @@ [ { title: "Subscriptions", - apiType: "Invidious", - endpoint: "auth_feed", + items: [ + { + apiType: "Invidious", + endpoint: "auth_feed", + }, + ], }, { title: "Trending", - apiType: "Invidious", - endpoint: "trending", + items: [ + { + apiType: "Invidious", + endpoint: "trending", + }, + ], }, { title: "Trending - Music", - apiType: "Invidious", - endpoint: "trending", - queryParams: { - type: "Music", - }, + items: [ + { + apiType: "Invidious", + endpoint: "trending", + queryParams: { + type: "Music", + }, + }, + ], }, { title: "Trending - Gaming", - apiType: "Invidious", - endpoint: "trending", - queryParams: { - type: "Gaming", - }, + items: [ + { + apiType: "Invidious", + endpoint: "trending", + queryParams: { + type: "Gaming", + }, + }, + ], }, { title: "Trending - Movies", - apiType: "Invidious", - endpoint: "trending", - queryParams: { - type: "Movies", - }, + items: [ + { + apiType: "Invidious", + endpoint: "trending", + queryParams: { + type: "Movies", + }, + }, + ], }, { title: "Popular", - apiType: "Invidious", - endpoint: "popular", + items: [ + { + apiType: "Invidious", + endpoint: "popular", + }, + ], }, { title: "Funny", - apiType: "Invidious", - endpoint: "search", - queryParams: { - q: "Funny", - sort_by: "upload_date", - }, + items: [ + { + apiType: "Invidious", + endpoint: "search", + queryParams: { + q: "Funny", + sort_by: "upload_date", + }, + }, + ], }, { title: "News", - apiType: "Invidious", - endpoint: "search", - queryParams: { - q: "News", - sort_by: "upload_date", - }, + items: [ + { + apiType: "Invidious", + endpoint: "search", + queryParams: { + q: "News", + sort_by: "upload_date", + }, + }, + ], }, { title: "Playlists", - apiType: "Invidious", - endpoint: "playlists", + items: [ + { + apiType: "Invidious", + endpoint: "playlists", + }, + ], }, ] diff --git a/playlet-lib/src/config/invidious_video_api.json5 b/playlet-lib/src/config/invidious_video_api.json5 index 643f5753..2a005894 100644 --- a/playlet-lib/src/config/invidious_video_api.json5 +++ b/playlet-lib/src/config/invidious_video_api.json5 @@ -139,6 +139,29 @@ responseHandler: "PlaylistHandler", paginationType: "Pages", }, + // video_info, playlist_info and channel_info are used by the + // Bookmarks, so they are cached for longer + { + name: "video_info", + displayName: "Video info", + url: "/api/v1/videos/{id}", + responseHandler: "VideoInfoHandler", + cacheSeconds: 259200, // 3 days + }, + { + name: "playlist_info", + displayName: "Playlist info", + url: "/api/v1/playlists/{plid}", + responseHandler: "PlaylistInfoHandler", + cacheSeconds: 259200, // 3 days + }, + { + name: "channel_info", + displayName: "Channel info", + url: "/api/v1/channels/{ucid}", + responseHandler: "ChannelInfoHandler", + cacheSeconds: 259200, // 3 days + }, { name: "channel_videos", displayName: "Channel videos",