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

Bookmarks web app part 2 #178

Merged
merged 5 commits into from
Oct 24, 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
113 changes: 79 additions & 34 deletions playlet-web/src/lib/Api/InvidiousApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ export class InvidiousApi {
}

public async makeRequest(feedSource: any) {
// TODO:P0 implement localStorage caching
if (!feedSource || !this.instance || !this.endpoints) {
return null;
}
Expand All @@ -95,7 +94,6 @@ export class InvidiousApi {

let url = this.instance + endpoint.url
let queryParams = {}
let headers = {}

if (endpoint.authenticated) {
// Authenticated requests on the web app would be blocked by CORS, so we use the Playlet API as a proxy
Expand Down Expand Up @@ -137,66 +135,66 @@ export class InvidiousApi {
}

url = this.makeUrl(url, queryParams);
const response = await fetch(url, { headers: headers });

let cacheSeconds = undefined
if (feedSource.cacheSeconds !== undefined) {
cacheSeconds = feedSource.cacheSeconds
} else if (endpoint.cacheSeconds !== undefined) {
cacheSeconds = endpoint.cacheSeconds
}

const responseJson = await this.cachedFetch(url, cacheSeconds);

let responseHandler = endpoint.responseHandler !== undefined ? this.responseHandlers[endpoint.responseHandler] : this.responseHandlers["DefaultHandler"];
if (!responseHandler) {
return null;
}
return await responseHandler(feedSource, response);
return await responseHandler(feedSource, responseJson);
}

private async DefaultHandler(feedSource, response) {
const items = await response.json();
return { items };
private async DefaultHandler(feedSource, responseJson) {
return { items: responseJson };
}

private async PlaylistHandler(feedSource, response) {
const json = await response.json();
private async PlaylistHandler(feedSource, responseJson) {
return {
items: json.videos,
items: responseJson.videos,
};
}

private async VideoInfoHandler(feedSource, response) {
const info = await response.json();
info.type = "video";
return { items: [info] };
private async VideoInfoHandler(feedSource, responseJson) {
responseJson.type = "video";
return { items: [responseJson] };
}

private async ChannelInfoHandler(feedSource, response) {
const info = await response.json();
info.type = "channel";
return { items: [info] };
private async ChannelInfoHandler(feedSource, responseJson) {
responseJson.type = "channel";
return { items: [responseJson] };
}

private async PlaylistInfoHandler(feedSource, response) {
const info = await response.json();
info.type = "playlist";
return { items: [info] };
private async PlaylistInfoHandler(feedSource, responseJson) {
responseJson.type = "playlist";
return { items: [responseJson] };
}

private async ChannelVideosHandler(feedSource, response) {
const json = await response.json();
private async ChannelVideosHandler(feedSource, responseJson) {
return {
items: json.videos,
continuation: json.continuation
items: responseJson.videos,
continuation: responseJson.continuation
};
}

private async ChannelPlaylistsHandler(feedSource, response) {
const json = await response.json();
private async ChannelPlaylistsHandler(feedSource, responseJson) {
return {
items: json.playlists,
continuation: json.continuation
items: responseJson.playlists,
continuation: responseJson.continuation
};
}

private async ChannelRelatedChannelsHandler(feedSource, response) {
const json = await response.json();
private async ChannelRelatedChannelsHandler(feedSource, responseJson) {
return {
items: json.relatedChannels,
continuation: json.continuation
items: responseJson.relatedChannels,
continuation: responseJson.continuation
};
}

Expand All @@ -212,4 +210,51 @@ export class InvidiousApi {
encodedUrl.search = mergedParams.toString();
return encodedUrl.toString();
}

private async cachedFetch(url: string, cacheSeconds?: number) {
if (!cacheSeconds) {
const response = await fetch(url);
return await response.json();
}

const cache = this.getCache(url, cacheSeconds);
if (cache) {
return cache;
}

const response = await fetch(url);
const data = await response.json();
this.setCache(url, data);
return data;
}

// TODO:P2 use more appropriate cache storage
private getCache(url: string, cacheSeconds: number) {
const cache = localStorage.getItem(url);
if (!cache) {
return null;
}

try {
const cacheData = JSON.parse(cache);
if (cacheData.timestamp + cacheSeconds * 1000 < Date.now()) {
return null;
}
console.log(`Cache hit for ${url}`);
return cacheData.data;
} catch (error) {
console.error(error);
return null;
}
}

private setCache(url: string, data: any) {
const cacheData = {
__version: 1,
timestamp: Date.now(),
data
};

localStorage.setItem(url, JSON.stringify(cacheData));
}
}
20 changes: 17 additions & 3 deletions playlet-web/src/lib/Screens/BookmarksScreen.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,21 @@
</script>

<div class={visibility ? "" : "hidden"}>
{#each $bookmarksStore as feed}
<VideoListRow {feed} />
{/each}
{#if $bookmarksStore.length === 0}
<div
class="flex flex-col items-center justify-start h-screen w-2/3 mx-auto"
>
<div class="text-2xl font-bold text-gray-500">No Bookmarks</div>
<div class="text-gray-500 text-center">
You currently have no bookmarks.<br />
To add bookmarks, select a video, playlist or channel, and add a bookmark.<br
/>
Please note that Bookmarks is an experimental feature.
</div>
</div>
{:else}
{#each $bookmarksStore as feed}
<VideoListRow {feed} />
{/each}
{/if}
</div>
81 changes: 54 additions & 27 deletions playlet-web/src/lib/Screens/Home/VideoListRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import ChannelCell from "./ChannelCell.svelte";

export let feed: any = undefined;
export let videos = undefined;
export let videos = [];

enum FeedLoadState {
None,
Expand All @@ -22,6 +22,7 @@
let itemWidths = [];

let carouselElement;
let carouselElementIntersecting = false;

let scrollStart = 0;
let scrollEnd = 0;
Expand All @@ -30,8 +31,23 @@
const videoItemWidth = 320 + 16 * 2;
const channelItemWidth = 240 + 16 * 2;

const intersectionObserver = new IntersectionObserver(
function (entries) {
carouselElementIntersecting = entries[0].isIntersecting;
if (carouselElementIntersecting) {
loadRow();
}
},
{ threshold: [0] }
);

$: {
if (carouselElement && itemWidths && itemWidths.length) {
if (
carouselElement &&
carouselElementIntersecting &&
itemWidths &&
itemWidths.length
) {
recalculateVisibileCells();
}
}
Expand All @@ -42,6 +58,12 @@
}
}

$: {
if (carouselElement) {
intersectionObserver.observe(carouselElement);
}
}

let invidiousApi = new InvidiousApi();

playletStateStore.subscribe((value) => {
Expand All @@ -61,6 +83,14 @@
return;
}

if (!carouselElementIntersecting) {
return;
}

if (scrollEnd < videos.length - 3) {
return;
}

if (
feedLoadState === FeedLoadState.Loading ||
feedLoadState === FeedLoadState.Loaded
Expand Down Expand Up @@ -191,29 +221,26 @@
}
</script>

{#if videos}
<div class="text-lg font-semibold m-4">
{feed.title}
</div>
<div
class="carousel carousel-center rounded-box w-full space-x-4"
bind:this={carouselElement}
on:scroll={recalculateVisibileCells}
>
{#each videos as video, i}
<div
class="carousel-item {video.type === 'channel' ? 'w-60' : 'w-80'} p-2"
>
{#if i >= scrollStart && i <= scrollEnd}
{#if video.type === "video"}
<VideoCell {...video} />
{:else if video.type === "playlist"}
<PlaylistCell {...video} />
{:else if video.type === "channel"}
<ChannelCell {...video} />
{/if}
<div class="text-lg font-semibold m-4">
{feed.title}
</div>
<div
class="carousel carousel-center rounded-box w-full space-x-4"
style="min-height: 16rem;"
bind:this={carouselElement}
on:scroll={recalculateVisibileCells}
>
{#each videos as video, i}
<div class="carousel-item {video.type === 'channel' ? 'w-60' : 'w-80'} p-2">
{#if i >= scrollStart && i <= scrollEnd}
{#if video.type === "video"}
<VideoCell {...video} />
{:else if video.type === "playlist"}
<PlaylistCell {...video} />
{:else if video.type === "channel"}
<ChannelCell {...video} />
{/if}
</div>
{/each}
</div>
{/if}
{/if}
</div>
{/each}
</div>