diff --git a/playlet-lib/src/components/ChannelView/ChannelView.bs b/playlet-lib/src/components/ChannelView/ChannelView.bs index f045e27a..7eaa036a 100644 --- a/playlet-lib/src/components/ChannelView/ChannelView.bs +++ b/playlet-lib/src/components/ChannelView/ChannelView.bs @@ -147,12 +147,12 @@ function CreateChannelFeed(title as string, endpoint as string, ucid as string, return { title: title, feedSources: [{ - id: `inv_${endpoint}_${ucid}`, - title: `${author} - ${title}`, - apiType: "Invidious", - endpoint: endpoint, - pathParams: { - ucid: ucid + "id": `inv_${endpoint}_${ucid}`, + "title": `${author} - ${title}`, + "apiType": "Invidious", + "endpoint": endpoint, + "pathParams": { + "ucid": ucid } }] } diff --git a/playlet-lib/src/components/MainScene.xml b/playlet-lib/src/components/MainScene.xml index 7824da23..b4841509 100644 --- a/playlet-lib/src/components/MainScene.xml +++ b/playlet-lib/src/components/MainScene.xml @@ -63,7 +63,8 @@ applicationInfo="bind:../ApplicationInfo" invidious="bind:../Invidious" preferences="bind:../Preferences" - playQueue="bind:../PlayQueue" /> + playQueue="bind:../PlayQueue" + bookmarks="bind:../Bookmarks" /> \ No newline at end of file diff --git a/playlet-lib/src/components/MainScene_bindings.transpiled.brs b/playlet-lib/src/components/MainScene_bindings.transpiled.brs index e877c8f9..5e161473 100644 --- a/playlet-lib/src/components/MainScene_bindings.transpiled.brs +++ b/playlet-lib/src/components/MainScene_bindings.transpiled.brs @@ -24,7 +24,8 @@ function InitializeBindings() "applicationInfo": "../ApplicationInfo", "invidious": "../Invidious", "preferences": "../Preferences", - "playQueue": "../PlayQueue" + "playQueue": "../PlayQueue", + "bookmarks": "../Bookmarks" } } } diff --git a/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.bs b/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.bs index a6c9112d..0bfa40e0 100644 --- a/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.bs +++ b/playlet-lib/src/components/Screens/BookmarksScreen/BookmarksScreen.bs @@ -73,16 +73,15 @@ function SetRowListContent(bookmarksContent as object) feeds = [] for each bookmarkGroupNode in bookmarkGroupNodes bookmarkNodes = bookmarkGroupNode.getChildren(-1, 0) - feed = { - title: bookmarkGroupNode.title, - feedSources: [] - } - + feedSources = [] for each bookmarkNode in bookmarkNodes - feed.feedSources.push(bookmarkNode.feedSource) + feedSources.push(bookmarkNode.feedSource) end for - feeds.push(feed) + feeds.push({ + "title": bookmarkGroupNode.title, + "feedSources": feedSources + }) end for m.rowList.feeds = feeds diff --git a/playlet-lib/src/components/Screens/SearchScreen/SearchScreen.bs b/playlet-lib/src/components/Screens/SearchScreen/SearchScreen.bs index 26e8007a..3706b83f 100644 --- a/playlet-lib/src/components/Screens/SearchScreen/SearchScreen.bs +++ b/playlet-lib/src/components/Screens/SearchScreen/SearchScreen.bs @@ -204,12 +204,12 @@ function Search(text as string) ShowLoadingScreen() feedSource = { - id: `inv_search_${CryptoUtils.GetMd5(text)}`, - title: `Search - ${text}`, - apiType: "Invidious", - endpoint: "search", - queryParams: { - q: text + "id": `inv_search_${CryptoUtils.GetMd5(text)}`, + "title": `Search - ${text}`, + "apiType": "Invidious", + "endpoint": "search", + "queryParams": { + "q": text } } @@ -229,8 +229,8 @@ function Search(text as string) end for m.rowList.feeds = [{ - title: `Search - ${text}`, - feedSources: [feedSource] + "title": `Search - ${text}`, + "feedSources": [feedSource] }] end function diff --git a/playlet-lib/src/components/Services/Bookmarks/Bookmarks.bs b/playlet-lib/src/components/Services/Bookmarks/Bookmarks.bs index 9195e408..1dd91b17 100644 --- a/playlet-lib/src/components/Services/Bookmarks/Bookmarks.bs +++ b/playlet-lib/src/components/Services/Bookmarks/Bookmarks.bs @@ -53,19 +53,19 @@ function Save() as void bookmarkNodes = bookmarkGroupNode.getChildren(-1, 0) for each bookmarkNode in bookmarkNodes bookmarks.push({ - id: bookmarkNode.id, - feedSource: bookmarkNode.feedSource + "id": bookmarkNode.id, + "feedSource": bookmarkNode.feedSource }) end for groups.push({ - title: bookmarkGroupNode.title, - bookmarks: bookmarks + "title": bookmarkGroupNode.title, + "bookmarks": bookmarks }) end for bookmarksString = FormatJson({ - __version: m.top.__version, - groups: groups + "__version": m.top.__version, + "groups": groups }) if m.bookmarksString = bookmarksString @@ -110,10 +110,10 @@ function AddVideoBookmark(id as string, groupName as string) bookmarkGroupNode = GetOrCreateBookmarkGroup(groupName) feedSource = { - apiType: "Invidious", - endpoint: "video_info", - pathParams: { - id: id + "apiType": "Invidious", + "endpoint": "video_info", + "pathParams": { + "id": id } } AddFeedSourceBookmark(feedSource, id, bookmarkGroupNode) @@ -123,10 +123,10 @@ function AddPlaylistBookmark(id as string, groupName as string) bookmarkGroupNode = GetOrCreateBookmarkGroup(groupName) feedSource = { - apiType: "Invidious", - endpoint: "playlist_info", - pathParams: { - plid: id + "apiType": "Invidious", + "endpoint": "playlist_info", + "pathParams": { + "plid": id } } AddFeedSourceBookmark(feedSource, id, bookmarkGroupNode) @@ -136,21 +136,21 @@ function AddPlaylistBookmarkWithSpread(id as string, groupName as string) bookmarkGroupNode = GetOrCreateBookmarkGroup(groupName) feedSource = { - id: `inv_playlist_${id}`, - title: `${groupName} videos`, - apiType: "Invidious", - endpoint: "playlist", - pathParams: { - plid: id + "id": `inv_playlist_${id}`, + "title": `${groupName} videos`, + "apiType": "Invidious", + "endpoint": "playlist", + "pathParams": { + "plid": id } } AddFeedSourceBookmark(feedSource, `inv_playlist_${id}`, bookmarkGroupNode) feedSource = { - apiType: "Invidious", - endpoint: "playlist_info", - pathParams: { - plid: id + "apiType": "Invidious", + "endpoint": "playlist_info", + "pathParams": { + "plid": id } } AddFeedSourceBookmark(feedSource, id, bookmarkGroupNode) @@ -160,10 +160,10 @@ function AddChannelBookmark(id as string, groupName as string) bookmarkGroupNode = GetOrCreateBookmarkGroup(groupName) feedSource = { - apiType: "Invidious", - endpoint: "channel_info", - pathParams: { - ucid: id + "apiType": "Invidious", + "endpoint": "channel_info", + "pathParams": { + "ucid": id } } AddFeedSourceBookmark(feedSource, id, bookmarkGroupNode) @@ -173,21 +173,21 @@ function AddChannelBookmarkWithSpread(id as string, groupName as string) bookmarkGroupNode = GetOrCreateBookmarkGroup(groupName) feedSource = { - id: `inv_channel_videos_${id}`, - title: `${groupName} videos`, - apiType: "Invidious", - endpoint: "channel_videos", - pathParams: { - ucid: id + "id": `inv_channel_videos_${id}`, + "title": `${groupName} videos`, + "apiType": "Invidious", + "endpoint": "channel_videos", + "pathParams": { + "ucid": id } } AddFeedSourceBookmark(feedSource, `inv_channel_videos_${id}`, bookmarkGroupNode) feedSource = { - apiType: "Invidious", - endpoint: "channel_info", - pathParams: { - ucid: id + "apiType": "Invidious", + "endpoint": "channel_info", + "pathParams": { + "ucid": id } } AddFeedSourceBookmark(feedSource, id, bookmarkGroupNode) diff --git a/playlet-lib/src/components/Services/Invidious/InvidiousService.bs b/playlet-lib/src/components/Services/Invidious/InvidiousService.bs index 269784af..5afe698a 100644 --- a/playlet-lib/src/components/Services/Invidious/InvidiousService.bs +++ b/playlet-lib/src/components/Services/Invidious/InvidiousService.bs @@ -169,7 +169,7 @@ namespace Invidious feedSourceState = feedSource.state if not feedSourceState.DoesExist("queryParams") - feedSourceState.queryParams = {} + feedSourceState["queryParams"] = {} end if feedSourceState.paginationType = endpoint.paginationType diff --git a/playlet-lib/src/components/Web/PlayletWebServer/Middleware/BookmarksRouter.bs b/playlet-lib/src/components/Web/PlayletWebServer/Middleware/BookmarksRouter.bs new file mode 100644 index 00000000..8a3bc8c2 --- /dev/null +++ b/playlet-lib/src/components/Web/PlayletWebServer/Middleware/BookmarksRouter.bs @@ -0,0 +1,65 @@ +import "pkg:/source/utils/RegistryUtils.bs" + +namespace Http + + class BookmarksRouter extends HttpRouter + + function new() + super() + + m.Get("/api/bookmarks", function(context as object) as boolean + response = context.response + + bookmarksString = RegistryUtils.Read(RegistryUtils.BOOKMARKS) + if bookmarksString = invalid + response.Json({ + groups: [] + }) + return true + end if + + response.SetBodyDataString(bookmarksString) + response.ContentType("application/json") + response.http_code = 200 + + return true + end function) + + m.Get("/api/bookmarks/feeds", function(context as object) as boolean + response = context.response + router = context.router + bookmarks = context.server.task.bookmarks + + feeds = router.BookmarksContentToFeeds(bookmarks.content) + + response.Json(feeds) + + return true + end function) + end function + + ' TODO:P1 refactor to share code with Bookmarks screen + function BookmarksContentToFeeds(bookmarksContent as object) as object + bookmarkGroupNodes = bookmarksContent.getChildren(-1, 0) + + feeds = [] + for each bookmarkGroupNode in bookmarkGroupNodes + bookmarkNodes = bookmarkGroupNode.getChildren(-1, 0) + + feedSources = [] + for each bookmarkNode in bookmarkNodes + feedSources.push(bookmarkNode.feedSource) + end for + + feeds.push({ + "title": bookmarkGroupNode.title, + "feedSources": feedSources + }) + end for + + return feeds + end function + + end class + +end namespace diff --git a/playlet-lib/src/components/Web/PlayletWebServer/Middleware/InvidiousRouter.bs b/playlet-lib/src/components/Web/PlayletWebServer/Middleware/InvidiousRouter.bs index 3455cd9b..8a104ce2 100644 --- a/playlet-lib/src/components/Web/PlayletWebServer/Middleware/InvidiousRouter.bs +++ b/playlet-lib/src/components/Web/PlayletWebServer/Middleware/InvidiousRouter.bs @@ -47,14 +47,15 @@ namespace Http ' This is an endpoint allowing the web app to make an authenticated request ' without needing the Invidious token + ' TODO:P2 this should be a POST request m.Get("/invidious/authenticated-request", function(context as object) as boolean request = context.request response = context.response invidiousNode = context.server.task.invidious - requestData = request.query["request-data"] - if requestData = invalid - response.Default(400, "Expected request-data") + feedSource = request.query["feed-source"] + if feedSource = invalid + response.Default(400, "Expected feed-source") return true end if @@ -64,12 +65,10 @@ namespace Http return true end if - requestData = ParseJson(requestData) + feedSource = ParseJson(feedSource) - ' TODO:P0 handle pagination - ' Perhaps the page arg can be added to queryParam of requestData directly invService = new Invidious.InvidiousService(invidiousNode) - invResponse = invService.MakeRequest(requestData) + invResponse = invService.MakeRequest(feedSource, feedSource.state?.queryParams) if not invResponse.success response.Default(500, `Failed to make request: ${invResponse.error}`) return true diff --git a/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.bs b/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.bs index d68173a1..e3173c31 100644 --- a/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.bs +++ b/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.bs @@ -1,3 +1,4 @@ +import "pkg:/components/Web/PlayletWebServer/Middleware/BookmarksRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/DashRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/InvidiousRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/PlayletLibUrlsRouter.bs" @@ -38,6 +39,7 @@ function SetupRoutes(server as object) server.UseRouter(new Http.PreferencesRouter()) server.UseRouter(new Http.InvidiousRouter()) server.UseRouter(new Http.PlayQueueRouter()) + server.UseRouter(new Http.BookmarksRouter()) server.UseRouter(new Http.SearchHistoryRouter()) server.UseRouter(new Http.PlayletLibUrlsRouter()) diff --git a/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.xml b/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.xml index cff46116..c43fb515 100644 --- a/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.xml +++ b/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.xml @@ -4,6 +4,7 @@ + \ No newline at end of file diff --git a/playlet-web/src/App.svelte b/playlet-web/src/App.svelte index d7c83052..60c5390a 100644 --- a/playlet-web/src/App.svelte +++ b/playlet-web/src/App.svelte @@ -4,6 +4,7 @@ import { PlayletApi } from "lib/Api/PlayletApi"; import { appStateStore, + bookmarksStore, homeLayoutFileStore, invidiousVideoApiStore, playletStateStore, @@ -18,6 +19,7 @@ import SettingsScreen from "lib/Screens/SettingsScreen.svelte"; import InfoScreen from "lib/Screens/InfoScreen.svelte"; import LinkDragDrop from "lib/LinkDragDrop.svelte"; + import BookmarksScreen from "lib/Screens/BookmarksScreen.svelte"; onMount(async () => { PlayletApi.getState().then((value) => { @@ -43,6 +45,10 @@ PlayletApi.getSearchHistory().then((value) => { searchHistoryStore.set(value); }); + + PlayletApi.getBookmarkFeeds().then((value) => { + bookmarksStore.set(value); + }); }); let currentScreen: AppState["screen"]; @@ -61,6 +67,8 @@ + + diff --git a/playlet-web/src/assets/star-icon.svg.svelte b/playlet-web/src/assets/star-icon.svg.svelte new file mode 100644 index 00000000..eca7e895 --- /dev/null +++ b/playlet-web/src/assets/star-icon.svg.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/playlet-web/src/lib/Api/InvidiousApi.ts b/playlet-web/src/lib/Api/InvidiousApi.ts index 60944366..7f799c77 100644 --- a/playlet-web/src/lib/Api/InvidiousApi.ts +++ b/playlet-web/src/lib/Api/InvidiousApi.ts @@ -12,6 +12,13 @@ export class InvidiousApi { // Note: handlers for authenticated requests are not needed, since they are handled server side this.responseHandlers = { "DefaultHandler": (requestData, response) => this.DefaultHandler(requestData, response), + "PlaylistHandler": (requestData, response) => this.PlaylistHandler(requestData, response), + "VideoInfoHandler": (requestData, response) => this.VideoInfoHandler(requestData, response), + "PlaylistInfoHandler": (requestData, response) => this.PlaylistInfoHandler(requestData, response), + "ChannelInfoHandler": (requestData, response) => this.ChannelInfoHandler(requestData, response), + "ChannelVideosHandler": (requestData, response) => this.ChannelVideosHandler(requestData, response), + "ChannelPlaylistsHandler": (requestData, response) => this.ChannelPlaylistsHandler(requestData, response), + "ChannelRelatedChannelsHandler": (requestData, response) => this.ChannelRelatedChannelsHandler(requestData, response), } } @@ -44,10 +51,39 @@ export class InvidiousApi { return await response.json(); } - public async makeRequest(feed: any) { - // TODO:P0 handle multiple feed sources - const feedSource = feed.feedSources[0] + public async markFeedSourcePagination(feedSource: any) { + const endpoint = this.endpoints[feedSource.endpoint] + if (!endpoint || !endpoint.paginationType) { + return; + } + + const feedSourceState = feedSource.state + feedSourceState.queryParams = feedSourceState.queryParams || {} + + feedSourceState.paginationType = endpoint.paginationType + + if (feedSourceState.paginationType === "Pages") { + if (!Number.isInteger(feedSourceState.page)) { + feedSourceState.page = 0; + } + feedSourceState.page += 1; + feedSourceState.queryParams.page = feedSourceState.page; + } else if (feedSourceState.paginationType === "Continuation") { + const continuation = feedSourceState.continuation; + if (continuation) { + feedSourceState.queryParams.continuation = continuation; + } else { + delete feedSourceState.queryParams.continuation; + } + } + } + + public canMakeRequest() { + return !!(this.instance && this.endpoints && Object.keys(this.endpoints).length); + } + public async makeRequest(feedSource: any) { + // TODO:P0 implement localStorage caching if (!feedSource || !this.instance || !this.endpoints) { return null; } @@ -90,6 +126,10 @@ export class InvidiousApi { queryParams = { ...queryParams, ...feedSource.queryParams }; } + if (feedSource.state.queryParams !== undefined) { + queryParams = { ...queryParams, ...feedSource.state.queryParams }; + } + if (feedSource.pathParams !== undefined) { for (let param in feedSource.pathParams) { url = url.replace(`{${param}}`, feedSource.pathParams[param]); @@ -111,6 +151,55 @@ export class InvidiousApi { return { items }; } + private async PlaylistHandler(feedSource, response) { + const json = await response.json(); + return { + items: json.videos, + }; + } + + private async VideoInfoHandler(feedSource, response) { + const info = await response.json(); + info.type = "video"; + return { items: [info] }; + } + + private async ChannelInfoHandler(feedSource, response) { + const info = await response.json(); + info.type = "channel"; + return { items: [info] }; + } + + private async PlaylistInfoHandler(feedSource, response) { + const info = await response.json(); + info.type = "playlist"; + return { items: [info] }; + } + + private async ChannelVideosHandler(feedSource, response) { + const json = await response.json(); + return { + items: json.videos, + continuation: json.continuation + }; + } + + private async ChannelPlaylistsHandler(feedSource, response) { + const json = await response.json(); + return { + items: json.playlists, + continuation: json.continuation + }; + } + + private async ChannelRelatedChannelsHandler(feedSource, response) { + const json = await response.json(); + return { + items: json.relatedChannels, + continuation: json.continuation + }; + } + private makeUrl(url: string, params: any) { const encodedUrl = new URL(url); const existingParams = new URLSearchParams(encodedUrl.search); diff --git a/playlet-web/src/lib/Api/PlayletApi.ts b/playlet-web/src/lib/Api/PlayletApi.ts index eebda6c2..cf13db21 100644 --- a/playlet-web/src/lib/Api/PlayletApi.ts +++ b/playlet-web/src/lib/Api/PlayletApi.ts @@ -23,8 +23,8 @@ export class PlayletApi { return await response.json(); } - static async invidiousAuthenticatedRequest(requestData) { - const url = PlayletApi.host() + "/invidious/authenticated-request?request-data=" + encodeURIComponent(JSON.stringify(requestData)); + static async invidiousAuthenticatedRequest(feedSource) { + const url = PlayletApi.host() + "/invidious/authenticated-request?feed-source=" + encodeURIComponent(JSON.stringify(feedSource)); const response = await fetch(url); return await response.json(); } @@ -128,6 +128,11 @@ export class PlayletApi { return await fetch(`${PlayletApi.host()}/api/search-history`, { method: "DELETE" }); } + static async getBookmarkFeeds() { + const response = await fetch(`${PlayletApi.host()}/api/bookmarks/feeds`); + return await response.json(); + } + static async updateInstance(instance) { return await PlayletApi.putJson(`${PlayletApi.host()}/api/preferences`, { "invidious.instance": instance }); } diff --git a/playlet-web/src/lib/BottomNavigation.svelte b/playlet-web/src/lib/BottomNavigation.svelte index 721441d2..3133f14c 100644 --- a/playlet-web/src/lib/BottomNavigation.svelte +++ b/playlet-web/src/lib/BottomNavigation.svelte @@ -3,6 +3,8 @@ import InfoIcon from "../assets/info-icon.svg.svelte"; import SearchIcon from "../assets/search-icon.svg.svelte"; import SettingsIcon from "../assets/settings-icon.svg.svelte"; + import BookmarksIcon from "../assets/star-icon.svg.svelte"; + import { appStateStore } from "lib/Stores"; import type { AppState } from "lib/Types"; @@ -37,6 +39,14 @@ + setScreen("bookmarks")} + class={$appStateStore.screen === "bookmarks" ? "active" : ""} + > + + + + setScreen("settings")} class={$appStateStore.screen === "settings" ? "active" : ""} diff --git a/playlet-web/src/lib/Screens/BookmarksScreen.svelte b/playlet-web/src/lib/Screens/BookmarksScreen.svelte new file mode 100644 index 00000000..e8af7ad8 --- /dev/null +++ b/playlet-web/src/lib/Screens/BookmarksScreen.svelte @@ -0,0 +1,12 @@ + + + + {#each $bookmarksStore as feed} + + {/each} + diff --git a/playlet-web/src/lib/Screens/Home/ScreenHomeRow.svelte b/playlet-web/src/lib/Screens/Home/ScreenHomeRow.svelte deleted file mode 100644 index 26639d64..00000000 --- a/playlet-web/src/lib/Screens/Home/ScreenHomeRow.svelte +++ /dev/null @@ -1,115 +0,0 @@ - - -{#if videos} - - {feed.title} - - - {#each videos as video, i} - - {#if i >= scrollStart && i <= scrollEnd} - {#if video.type === "video"} - - {:else if video.type === "playlist"} - - {:else if video.type === "channel"} - - {/if} - {/if} - - {/each} - -{/if} diff --git a/playlet-web/src/lib/Screens/Home/VideoListRow.svelte b/playlet-web/src/lib/Screens/Home/VideoListRow.svelte new file mode 100644 index 00000000..151f9e57 --- /dev/null +++ b/playlet-web/src/lib/Screens/Home/VideoListRow.svelte @@ -0,0 +1,219 @@ + + +{#if videos} + + {feed.title} + + + {#each videos as video, i} + + {#if i >= scrollStart && i <= scrollEnd} + {#if video.type === "video"} + + {:else if video.type === "playlist"} + + {:else if video.type === "channel"} + + {/if} + {/if} + + {/each} + +{/if} diff --git a/playlet-web/src/lib/Screens/HomeScreen.svelte b/playlet-web/src/lib/Screens/HomeScreen.svelte index 9b00c509..dcb8455f 100644 --- a/playlet-web/src/lib/Screens/HomeScreen.svelte +++ b/playlet-web/src/lib/Screens/HomeScreen.svelte @@ -1,5 +1,5 @@