Completed channel view

This commit is contained in:
WardPearce
2024-03-31 12:51:55 +13:00
parent c845610ec5
commit 17a79acbda
5 changed files with 152 additions and 21 deletions
-1
View File
@@ -20,7 +20,6 @@ Modern material design for Invidious
- Audio only mode.
## Todo
- Complete channel view.
- Playlist support.
- PWA support.
+17 -1
View File
@@ -1,6 +1,6 @@
import { get } from 'svelte/store';
import { auth, returnYTDislikesInstance } from '../../store';
import type { Channel, ChannelPage, Comments, Playlist, PlaylistPage, ReturnYTDislikes, SearchSuggestion, Subscription, Video, VideoPlay } from './model';
import type { Channel, ChannelContentPlaylists, ChannelContentVideos, ChannelPage, Comments, Playlist, PlaylistPage, ReturnYTDislikes, SearchSuggestion, Subscription, Video, VideoPlay } from './model';
export function buildPath(path: string): string {
return `${import.meta.env.VITE_DEFAULT_INVIDIOUS_INSTANCE}/api/v1/${path}`;
@@ -54,6 +54,22 @@ export async function getChannel(channelId: string): Promise<ChannelPage> {
return await resp.json();
}
export async function getChannelContent(
channelId: string,
parameters: {
type?: 'videos' | 'playlists' | 'streams' | 'shorts';
continuation?: string;
}): Promise<ChannelContentVideos | ChannelContentPlaylists> {
if (typeof parameters.type === 'undefined') parameters.type = 'videos';
const url = new URL(buildPath(`channels/${channelId}/${parameters.type}`));
if (typeof parameters.continuation !== 'undefined') url.searchParams.set('continuation', parameters.continuation);
const resp = await fetch(url.toString());
return await resp.json();
}
export async function getSearchSuggestions(search: string): Promise<SearchSuggestion> {
const path = new URL(buildPath("search/suggestions"));
path.search = new URLSearchParams({ q: search }).toString();
+10
View File
@@ -185,6 +185,16 @@ export interface PlaylistPageVideo extends PlaylistVideo {
viewCount: number;
}
export interface ChannelContentVideos {
videos: Video[];
continuation: string;
}
export interface ChannelContentPlaylists {
playlists: PlaylistPage[];
continuation: string;
}
export interface PlaylistPage extends Playlist {
description: string;
descriptionHtml: string;
@@ -13,7 +13,11 @@
onMount(() => {
img = new Image();
img.src = playlist.videos[0].videoThumbnails[4].url;
if (playlist.videos.length > 0) {
img.src = playlist.videos[0].videoThumbnails[4].url;
} else {
img.src = playlist.playlistThumbnail;
}
img.onload = () => {
loaded = true;
@@ -1,11 +1,71 @@
<script lang="ts">
import { cleanNumber } from '$lib/misc';
import { amSubscribed, deleteUnsubscribe, getChannelContent, postSubscribe } from '$lib/Api';
import type { ChannelContentPlaylists, ChannelContentVideos } from '$lib/Api/model';
import PageLoading from '$lib/PageLoading.svelte';
import PlaylistThumbnail from '$lib/PlaylistThumbnail.svelte';
import Thumbnail from '$lib/Thumbnail.svelte';
import { activePage } from '../../../store.js';
import { cleanNumber } from '$lib/misc';
import { onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store';
import { activePage, auth } from '../../../store';
export let data;
activePage.set(null);
let isSubscribed = false;
let tab: 'videos' | 'playlists' | 'streams' | 'shorts' = 'videos';
let displayContent: ChannelContentPlaylists | ChannelContentVideos | undefined = undefined;
async function handleScroll() {
if (typeof displayContent === 'undefined') return;
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 5) {
const newContent = await getChannelContent(data.channel.authorId, {
type: tab,
continuation: displayContent.continuation
});
if ('videos' in newContent && 'videos' in displayContent) {
displayContent.videos = [...displayContent.videos, ...newContent.videos];
} else if ('playlists' in displayContent && 'playlists' in newContent) {
displayContent.playlists = [...displayContent.playlists, ...newContent.playlists];
}
displayContent.continuation = newContent.continuation;
}
}
async function changeTab(newTab: 'videos' | 'playlists' | 'streams' | 'shorts') {
tab = newTab;
displayContent = undefined;
displayContent = await getChannelContent(data.channel.authorId, { type: tab });
}
onMount(async () => {
displayContent = await getChannelContent(data.channel.authorId, { type: 'videos' });
if (get(auth)) {
isSubscribed = await amSubscribed(data.channel.authorId);
}
window.addEventListener('scroll', handleScroll);
});
onDestroy(() => {
window.removeEventListener('scroll', handleScroll);
});
async function toggleSubscribed() {
if (isSubscribed) {
await deleteUnsubscribe(data.channel.authorId);
} else {
await postSubscribe(data.channel.authorId);
}
isSubscribed = !isSubscribed;
}
</script>
<div class="padding">
@@ -24,28 +84,70 @@
<p>{cleanNumber(data.channel.subCount)} subscribers</p>
<p style="width: 60vw;">{data.channel.description}</p>
</div>
<button class="inverse-surface large">Subscribe</button>
<button
on:click={toggleSubscribed}
class:inverse-surface={!isSubscribed}
class:border={isSubscribed}
>
{#if !isSubscribed}
Subscribe
{:else}
Unsubscribe
{/if}
</button>
</div>
<div class="tabs left-align">
{#each data.channel.tabs as tab}
<a href={`?tab=${tab}`}>
<span>{tab}</span>
</a>
{/each}
</div>
<div class="divider"></div>
<div class="grid padding">
{#each data.channel.latestVideos as video}
<div class="s12 m6 l2">
<Thumbnail {video} />
</div>
{/each}
<a class:active={tab === 'videos'} on:click={() => changeTab('videos')} href={`#video`}>
<i>movie</i>
<span>Videos</span>
</a>
<a class:active={tab === 'shorts'} on:click={() => changeTab('shorts')} href={`#short`}>
<i>smartphone</i>
<span>Shorts</span>
</a>
<a class:active={tab === 'streams'} on:click={() => changeTab('streams')} href={`#stream`}>
<i>stream</i>
<span>Streams</span>
</a>
<a
class:active={tab === 'playlists'}
on:click={() => changeTab('playlists')}
href={`#playlist`}
>
<i>playlist_add_check</i>
<span>Playlists</span>
</a>
</div>
</div>
{#if displayContent}
<div class="page right active">
<div class="space"></div>
<div class="grid">
{#if 'videos' in displayContent}
{#each displayContent.videos as video}
<div class="s12 m6 l2">
<article class="no-padding" style="height: 100%;">
<Thumbnail {video} />
</article>
</div>
{/each}
{:else}
{#each displayContent.playlists as playlist}
<div class="s12 m6 l2">
<article class="no-padding" style="height: 100%;">
<PlaylistThumbnail {playlist} />
</article>
</div>
{/each}
{/if}
</div>
</div>
{:else}
<PageLoading />
{/if}
<style>
.description {
display: flex;