From 99d1e1dc901ab847ac96d0852893d0e04f90cf86 Mon Sep 17 00:00:00 2001 From: Guusvanmeerveld Date: Mon, 28 Mar 2022 20:38:19 +0200 Subject: [PATCH] Started work on #9, many small changes --- .prettierrc.json | 1 + next.config.js | 3 +- src/components/Channel/Inline.tsx | 71 +++++++++++++++++++ src/components/Navbar/index.tsx | 23 ++++-- src/components/Video/Inline.tsx | 94 ++++++++++++++++++++++++ src/components/Video/index.tsx | 114 +++++++++++------------------- src/globals.css | 4 ++ src/interfaces/api/index.ts | 19 +++++ src/interfaces/api/search.ts | 83 ++++++++++++++++++++++ src/interfaces/api/thumbnail.ts | 0 src/interfaces/api/trending.ts | 11 +-- src/interfaces/api/video.ts | 85 +++------------------- src/interfaces/video.ts | 103 +++++++++++++++++++-------- src/next-seo.config.ts | 2 +- src/pages/_app.tsx | 1 + src/pages/results.tsx | 103 +++++++++++++++++++++++++++ src/pages/trending.tsx | 16 ++--- src/pages/watch.tsx | 54 +++++++------- src/utils/conversions.ts | 12 ++-- src/utils/index.ts | 7 +- src/utils/requests.ts | 57 +++++++++++++++ 21 files changed, 629 insertions(+), 234 deletions(-) create mode 100644 src/components/Channel/Inline.tsx create mode 100644 src/components/Video/Inline.tsx create mode 100644 src/globals.css create mode 100644 src/interfaces/api/search.ts create mode 100644 src/interfaces/api/thumbnail.ts create mode 100644 src/pages/results.tsx create mode 100644 src/utils/requests.ts diff --git a/.prettierrc.json b/.prettierrc.json index dfd80d9..6a32fd0 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -9,6 +9,7 @@ "^next.*", "^react$", "^react.*", + "^axios.*", "^@emotion/.*", "^@mui/material/.*", "^@mui/.*", diff --git a/next.config.js b/next.config.js index 0a302cd..c20c4e5 100644 --- a/next.config.js +++ b/next.config.js @@ -12,7 +12,8 @@ module.exports = { env: { NEXT_PUBLIC_GITHUB_URL: packageInfo.repository.url, NEXT_PUBLIC_APP_NAME: process.env.APP_NAME ?? packageInfo.displayName, - NEXT_PUBLIC_DEFAULT_SERVER: process.env.DEFAULT_SERVER ?? "vid.puffyan.us" + NEXT_PUBLIC_DEFAULT_SERVER: + process.env.DEFAULT_SERVER ?? "invidious.privacy.gd" }, basePath: process.env.BASE_PATH ?? "", trailingSlash: !(process.env.CI == "true") diff --git a/src/components/Channel/Inline.tsx b/src/components/Channel/Inline.tsx new file mode 100644 index 0000000..4f5341c --- /dev/null +++ b/src/components/Channel/Inline.tsx @@ -0,0 +1,71 @@ +import Link from "next/link"; + +import { FC } from "react"; + +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; + +import { abbreviateNumber, formatNumber } from "@src/utils/"; + +import { Channel as ChannelModel } from "@interfaces/api/search"; + +const Channel: FC<{ channel: ChannelModel }> = ({ channel }) => { + const theme = useTheme(); + + const thumbnail = channel.authorThumbnails.find( + (thumbnail) => thumbnail.height == 512 + )?.url as string; + + return ( + + + + + + + + {channel.author} + + {abbreviateNumber(channel.subCount)} subscribers •{" "} + {formatNumber(channel.videoCount)} videos + + + + {channel.description} + + + + + + + ); +}; + +export default Channel; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 5bdf2c1..72e5416 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -34,6 +34,8 @@ const Navbar: FC = () => { const [drawerIsOpen, setDrawerState] = useState(false); const router = useRouter(); + const query = router.query["search_query"]; + const toggleDrawer = (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { if ( @@ -141,6 +143,7 @@ const Navbar: FC = () => {
{ - { + if (router.pathname == "/settings") { + e.preventDefault(); + + router.back(); + } + }} > - - + + + + diff --git a/src/components/Video/Inline.tsx b/src/components/Video/Inline.tsx new file mode 100644 index 0000000..8646305 --- /dev/null +++ b/src/components/Video/Inline.tsx @@ -0,0 +1,94 @@ +import Link from "next/link"; + +import { FC } from "react"; + +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +import CircularProgress from "@mui/material/CircularProgress"; +import Grid from "@mui/material/Grid"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; + +import { abbreviateNumber } from "@src/utils/"; + +import VideoModel from "@interfaces/video"; + +import { useAuthorThumbnail } from "@utils/requests"; + +const Video: FC<{ video: VideoModel }> = ({ video }) => { + const theme = useTheme(); + + const { + ref, + isLoading, + thumbnail: authorThumbnail + } = useAuthorThumbnail(video.author.id, 176); + + return ( + + + + {/* eslint-disable-next-line @next/next/no-img-element */} + thumbnail + + + + + + + {video.title} + + + + + {abbreviateNumber(video.views)} Views • Published{" "} + {video.published.text} + + + {video.description.text} + + + + + {isLoading && } + {!isLoading && ( + + )} + + {video.author.name} + + + + + + + + ); +}; + +export default Video; diff --git a/src/components/Video/index.tsx b/src/components/Video/index.tsx index 8cff4dd..2331e6a 100644 --- a/src/components/Video/index.tsx +++ b/src/components/Video/index.tsx @@ -1,12 +1,7 @@ -import axios, { AxiosError } from "axios"; - import Link from "next/link"; import { useRouter } from "next/router"; -import { FC, useEffect } from "react"; - -import { useInView } from "react-intersection-observer/"; -import { useQuery } from "react-query"; +import { FC } from "react"; import Avatar from "@mui/material/Avatar"; import Box from "@mui/material/Box"; @@ -17,94 +12,71 @@ import CardMedia from "@mui/material/CardMedia"; import CircularProgress from "@mui/material/CircularProgress"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; import { abbreviateNumber } from "@src/utils"; -import { Video as VideoModel } from "@interfaces/video"; - -import { useSettings } from "@utils/hooks"; +import VideoModel from "@interfaces/video"; -interface Channel { - authorThumbnails: { url: string; width: number; height: number }[]; -} +import { useAuthorThumbnail } from "@utils/requests"; -const Video: FC = ({ - thumbnail, - title, - id, - author, - views, - published -}) => { - const [settings] = useSettings(); +const Video: FC = (video) => { + const theme = useTheme(); - const { isLoading, error, data, refetch, isFetched } = useQuery< - Channel, - AxiosError - >( - ["channelData", author.id], - () => - axios - .get( - `https://${settings.invidiousServer}/api/v1/channels/${author.id}`, - { - params: { - fields: ["authorThumbnails"].join(",") - } - } - ) - .then((res) => res.data), - { enabled: false } - ); - - const { ref, inView } = useInView({ - threshold: 0 - }); + const { + ref, + isLoading, + thumbnail: authorThumbnail + } = useAuthorThumbnail(video.author.id, 100); const router = useRouter(); - useEffect(() => { - if (inView && !isFetched) refetch(); - }, [inView, isFetched, refetch]); - return ( router.push({ pathname: "/watch", query: { v: id } })} + onClick={() => + router.push({ pathname: "/watch", query: { v: video.id } }) + } > - + - {title} + {video.title} - - - {isLoading && } - {data && ( - thumbnail.width == 100 - )?.url as string - } - /> - )} - - {author.name} - - + + + + {isLoading && } + {!isLoading && ( + + )} + + {video.author.name} + + + - - {abbreviateNumber(views)} Views • Published {published.text} + + {abbreviateNumber(video.views)} Views • Published{" "} + {video.published.text} diff --git a/src/globals.css b/src/globals.css new file mode 100644 index 0000000..e28daa0 --- /dev/null +++ b/src/globals.css @@ -0,0 +1,4 @@ +a { + text-decoration: none; + color: unset; +} diff --git a/src/interfaces/api/index.ts b/src/interfaces/api/index.ts index 348557b..55986a5 100644 --- a/src/interfaces/api/index.ts +++ b/src/interfaces/api/index.ts @@ -1,3 +1,22 @@ export interface Error { error: string; } + +export interface Thumbnail { + url: string; + width: number; + height: number; + quality?: Quality; +} + +export enum Quality { + Default = "default", + End = "end", + High = "high", + Maxres = "maxres", + Maxresdefault = "maxresdefault", + Medium = "medium", + Middle = "middle", + Sddefault = "sddefault", + Start = "start" +} diff --git a/src/interfaces/api/search.ts b/src/interfaces/api/search.ts new file mode 100644 index 0000000..35b76e2 --- /dev/null +++ b/src/interfaces/api/search.ts @@ -0,0 +1,83 @@ +import { Thumbnail } from "@interfaces/api"; +import VideoTrending from "@interfaces/api/trending"; + +interface Results { + type: Type; +} + +export interface ChannelResult extends Results { + type: "channel"; + author: string; + authorId: string; + authorUrl: string; + authorThumbnails: Thumbnail[]; + subCount: number; + videoCount: number; + description: string; + descriptionHtml: string; +} + +export interface VideoResult extends Results { + type: "video"; + title: string; + author: string; + authorId: string; + authorUrl: string; + description: string; + descriptionHtml: string; + videoId: string; + videoThumbnails: Thumbnail[]; + viewCount: number; + published: number; + publishedText: string; + lengthSeconds: number; + liveNow: boolean; + premium: boolean; +} + +export interface PlaylistResult extends Results { + type: "playlist"; + title: string; + playlistId: string; + author: string; + authorId: string; + authorUrl: string; + videoCount: number; + videos: Video[]; +} + +export interface CategoryResult extends Results { + type: "category"; + title: string; + contents: VideoTrending[]; +} + +export interface Content { + type: Type; + title: string; + videoId: string; + author: string; + authorId: string; + authorUrl: string; + videoThumbnails: Thumbnail[]; + description: string; + descriptionHtml: string; + viewCount: number; + published: number; + publishedText: string; + lengthSeconds: number; + liveNow: boolean; + premium: boolean; + isUpcoming: boolean; +} + +export type Type = "category" | "channel" | "playlist" | "video"; + +export interface Video { + title: string; + videoId: string; + lengthSeconds: number; + videoThumbnails: Thumbnail[]; +} + +export default Results; diff --git a/src/interfaces/api/thumbnail.ts b/src/interfaces/api/thumbnail.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/interfaces/api/trending.ts b/src/interfaces/api/trending.ts index b36a2b2..a1b6ddf 100644 --- a/src/interfaces/api/trending.ts +++ b/src/interfaces/api/trending.ts @@ -1,3 +1,5 @@ +import { Thumbnail } from "@interfaces/api"; + interface Trending { type: string; title: string; @@ -5,7 +7,7 @@ interface Trending { author: string; authorId: string; authorUrl: string; - videoThumbnails: VideoThumbnail[]; + videoThumbnails: Thumbnail[]; description: string; descriptionHtml: string; viewCount: number; @@ -17,11 +19,4 @@ interface Trending { isUpcoming: boolean; } -interface VideoThumbnail { - quality: string; - url: string; - width: number; - height: number; -} - export default Trending; diff --git a/src/interfaces/api/video.ts b/src/interfaces/api/video.ts index fac9629..89aeb8d 100644 --- a/src/interfaces/api/video.ts +++ b/src/interfaces/api/video.ts @@ -1,3 +1,11 @@ +import { Thumbnail } from "@interfaces/api"; +import { + AdaptiveFormat, + Caption, + FormatStream, + RecommendedVideo +} from "@interfaces/video"; + export interface Video { type: string; title: string; @@ -37,83 +45,6 @@ export interface Video { recommendedVideos: RecommendedVideo[]; } -export interface AdaptiveFormat { - index: string; - bitrate: string; - init: string; - url: string; - itag: string; - type: string; - clen: string; - lmt: string; - projectionType: ProjectionType; - fps?: number; - container?: Container; - encoding?: string; - resolution?: string; - qualityLabel?: string; -} - -export interface FormatStream { - url: string; - itag: string; - type: string; - quality: string; - fps: number; - container: string; - encoding: string; - resolution: string; - qualityLabel: string; - size: string; -} - -enum Container { - M4A = "m4a", - Mp4 = "mp4", - Webm = "webm" -} - -enum ProjectionType { - Rectangular = "RECTANGULAR" -} - -interface Thumbnail { - url: string; - width: number; - height: number; - quality?: Quality; -} - -enum Quality { - Default = "default", - End = "end", - High = "high", - Maxres = "maxres", - Maxresdefault = "maxresdefault", - Medium = "medium", - Middle = "middle", - Sddefault = "sddefault", - Start = "start" -} - -export interface Caption { - label: string; - language_code: string; - url: string; -} - -export interface RecommendedVideo { - videoId: string; - title: string; - videoThumbnails: Thumbnail[]; - author: string; - authorUrl: string; - authorId: string; - lengthSeconds: number; - viewCountText: string; - viewCount: number; -} - interface Storyboard { url: string; templateUrl: string; diff --git a/src/interfaces/video.ts b/src/interfaces/video.ts index d9f7950..c0ddd12 100644 --- a/src/interfaces/video.ts +++ b/src/interfaces/video.ts @@ -1,11 +1,6 @@ -import { - AdaptiveFormat, - Caption, - FormatStream, - RecommendedVideo -} from "@interfaces/api/video"; - -export interface Video { +import { Thumbnail } from "@interfaces/api"; + +interface Video { thumbnail: string; title: string; description: { @@ -17,6 +12,7 @@ export interface Video { name: string; id: string; url: string; + thumbnail?: string; }; views: number; published: { @@ -26,30 +22,79 @@ export interface Video { length: number; live: boolean; premium: boolean; -} - -export interface FullVideo extends Video { - keywords: string[]; - likes: number; - dislikes: number; - familyFriendly: boolean; - genre: { + keywords?: string[]; + likes?: number; + dislikes?: number; + familyFriendly?: boolean; + genre?: { type: string; url: string; }; - author: { - thumbnail: string; - name: string; - id: string; - url: string; - }; - subscriptions: string; - rating: number; - premiered: Date | undefined; - recommendedVideos: RecommendedVideo[]; - adaptiveFormats: AdaptiveFormat[]; - formatStreams: FormatStream[]; - captions: Caption[]; + subscriptions?: string; + rating?: number; + premiered?: Date; + recommendedVideos?: RecommendedVideo[]; + adaptiveFormats?: AdaptiveFormat[]; + formatStreams?: FormatStream[]; + captions?: Caption[]; +} + +export interface RecommendedVideo { + videoId: string; + title: string; + videoThumbnails: Thumbnail[]; + author: string; + authorUrl: string; + authorId: string; + lengthSeconds: number; + viewCountText: string; + viewCount: number; +} + +export interface Caption { + label: string; + language_code: string; + url: string; +} + +enum ProjectionType { + Rectangular = "RECTANGULAR" +} + +export interface AdaptiveFormat { + index: string; + bitrate: string; + init: string; + url: string; + itag: string; + type: string; + clen: string; + lmt: string; + projectionType: ProjectionType; + fps?: number; + container?: Container; + encoding?: string; + resolution?: string; + qualityLabel?: string; +} + +export interface FormatStream { + url: string; + itag: string; + type: string; + quality: string; + fps: number; + container: string; + encoding: string; + resolution: string; + qualityLabel: string; + size: string; +} + +enum Container { + M4A = "m4a", + Mp4 = "mp4", + Webm = "webm" } export default Video; diff --git a/src/next-seo.config.ts b/src/next-seo.config.ts index 7fa9b25..5e789ec 100644 --- a/src/next-seo.config.ts +++ b/src/next-seo.config.ts @@ -5,7 +5,7 @@ import type { DefaultSeoProps } from "next-seo"; const name = process.env.NEXT_PUBLIC_APP_NAME; const SEO: DefaultSeoProps = { - titleTemplate: `%s | ${name}`, + titleTemplate: `%s - ${name}`, defaultTitle: name, description: packageInfo.description, openGraph: { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4267fcb..48baffd 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -10,6 +10,7 @@ import CssBaseline from "@mui/material/CssBaseline"; import { ThemeProvider } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; +import "@src/globals.css"; import SEO from "@src/next-seo.config"; import createTheme from "@src/theme"; diff --git a/src/pages/results.tsx b/src/pages/results.tsx new file mode 100644 index 0000000..0b419fd --- /dev/null +++ b/src/pages/results.tsx @@ -0,0 +1,103 @@ +import NotFound from "./404"; + +import { NextPage } from "next"; +import { NextSeo } from "next-seo"; +import { useRouter } from "next/router"; + +import { useQuery } from "react-query"; + +import axios from "axios"; + +import Container from "@mui/material/Container"; +import Divider from "@mui/material/Divider"; +import Typography from "@mui/material/Typography"; + +import Result, { + CategoryResult, + ChannelResult, + PlaylistResult, + VideoResult +} from "@interfaces/api/search"; + +import { apiToVideo } from "@utils/conversions"; +import { useSettings } from "@utils/hooks"; + +import Channel from "@components/Channel/Inline"; +import Layout from "@components/Layout"; +import Loading from "@components/Loading"; +import Video from "@components/Video/Inline"; + +const Results: NextPage = () => { + const router = useRouter(); + + const [settings] = useSettings(); + + const query = router.query["search_query"]; + + const { data, isLoading } = useQuery( + ["searchResultsFor", query], + () => + query + ? axios + .get(`https://${settings.invidiousServer}/api/v1/search`, { + params: { + q: query, + type: "all" + } + }) + .then((res) => res.data) + : undefined + ); + + if (!router.isReady || isLoading) + return ( + <> + + + + + + ); + + if (!query) return ; + + const channels = data?.filter((result) => result.type == "channel") as + | ChannelResult[] + | undefined; + + const videos = data?.filter((result) => result.type == "video") as + | VideoResult[] + | undefined; + + // const categories = data?.filter() + + return ( + <> + + + + {channels && channels.length != 0 && ( + <> + Channels + {channels.map((channel, i) => ( + + ))} + + + )} + {videos && videos.length != 0 && ( + <> + Videos + {videos.map((video, i) => ( + + + + ); +}; + +export default Results; diff --git a/src/pages/trending.tsx b/src/pages/trending.tsx index eff3c64..d8617ac 100644 --- a/src/pages/trending.tsx +++ b/src/pages/trending.tsx @@ -1,5 +1,3 @@ -import axios, { AxiosError } from "axios"; - import { NextPage } from "next"; import { NextSeo } from "next-seo"; @@ -7,14 +5,16 @@ import { useState } from "react"; import { useQuery } from "react-query"; +import axios, { AxiosError } from "axios"; + import Box from "@mui/material/Box"; import Chip from "@mui/material/Chip"; import Typography from "@mui/material/Typography"; import { Error } from "@interfaces/api"; -import TrendingModel from "@interfaces/api/trending"; +import VideoTrending from "@interfaces/api/trending"; -import { trendingToVideo } from "@utils/conversions"; +import { apiToVideo } from "@utils/conversions"; import { useSettings } from "@utils/hooks"; import Layout from "@components/Layout"; @@ -26,10 +26,10 @@ const Trending: NextPage = () => { const [settings] = useSettings(); - const { isLoading, error, data, refetch } = useQuery< - TrendingModel[], + const { isLoading, error, data } = useQuery< + VideoTrending[], AxiosError - >("trendingData", (context) => + >("trendingData", () => axios .get(`https://${settings.invidiousServer}/api/v1/trending`, { params: { @@ -84,7 +84,7 @@ const Trending: NextPage = () => { ); })} - + )} diff --git a/src/pages/watch.tsx b/src/pages/watch.tsx index 5fb957b..50a3e65 100644 --- a/src/pages/watch.tsx +++ b/src/pages/watch.tsx @@ -1,5 +1,4 @@ import NotFound from "./404"; -import axios, { AxiosError } from "axios"; import { NextPage } from "next"; import { NextSeo } from "next-seo"; @@ -9,8 +8,10 @@ import { useEffect } from "react"; import { useQuery } from "react-query"; +import axios, { AxiosError } from "axios"; + import { Error } from "@interfaces/api"; -import Video from "@interfaces/api/video"; +import VideoAPI from "@interfaces/api/video"; import { videoToVideo } from "@utils/conversions"; import { useSettings } from "@utils/hooks"; @@ -19,29 +20,35 @@ import Layout from "@components/Layout"; import Loading from "@components/Loading"; const Watch: NextPage = () => { - const router = useRouter(); + const { query, isReady } = useRouter(); - const videoId = router.query["v"]; + const videoId = query["v"]; const [settings] = useSettings(); - const { isLoading, error, data, refetch } = useQuery< - Video, + const { isLoading, error, data } = useQuery< + VideoAPI | null, AxiosError - >( - ["videoData", videoId], - () => - axios - .get(`https://${settings.invidiousServer}/api/v1/videos/${videoId}`, { - params: {} - }) - .then((res) => res.data), - { enabled: false } + >(["videoData", videoId], () => + videoId + ? axios + .get(`https://${settings.invidiousServer}/api/v1/videos/${videoId}`, { + params: {} + }) + .then((res) => res.data) + : null ); - useEffect(() => { - if (videoId) refetch(); - }, [videoId, refetch]); + if (!isReady || isLoading) { + return ( + <> + + + + + + ); + } if (!videoId) { return ; @@ -49,14 +56,9 @@ const Watch: NextPage = () => { return ( <> - - - - {isLoading && } - {} - + + + {} ); }; diff --git a/src/utils/conversions.ts b/src/utils/conversions.ts index 4b89a02..6b66508 100644 --- a/src/utils/conversions.ts +++ b/src/utils/conversions.ts @@ -1,8 +1,10 @@ -import TrendingAPI from "@interfaces/api/trending"; +import { Quality } from "@interfaces/api"; +import { VideoResult } from "@interfaces/api/search"; +import VideoTrending from "@interfaces/api/trending"; import VideoAPI from "@interfaces/api/video"; -import Video, { FullVideo } from "@interfaces/video"; +import Video from "@interfaces/video"; -export const trendingToVideo = (item: TrendingAPI): Video => { +export const apiToVideo = (item: VideoTrending | VideoResult): Video => { return { title: item.title, description: { @@ -24,12 +26,12 @@ export const trendingToVideo = (item: TrendingAPI): Video => { live: item.liveNow, premium: item.premium, thumbnail: item.videoThumbnails.find( - (thumbnail) => thumbnail.quality == "maxresdefault" + (thumbnail) => thumbnail.quality == Quality.Maxresdefault )?.url as string }; }; -export const videoToVideo = (item: VideoAPI): FullVideo => { +export const videoToVideo = (item: VideoAPI): Video => { return { title: item.title, views: item.viewCount, diff --git a/src/utils/index.ts b/src/utils/index.ts index 768376d..f224b9c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -8,11 +8,12 @@ export const abbreviateNumber = (value: number): string => { suffixNum++; } - value = parseInt(value.toPrecision(3)); - - return `${value}${suffixes[suffixNum]}`; + return `${value.toPrecision(4)}${suffixes[suffixNum]}`; }; +export const formatNumber = (number: number) => + number.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, "$1,"); + export const toCamelCase = (string: string): string => string .replace(/(?:^\w|[A-Z]|\b\w)/g, (leftTrim: string, index: number) => diff --git a/src/utils/requests.ts b/src/utils/requests.ts new file mode 100644 index 0000000..26f4bc0 --- /dev/null +++ b/src/utils/requests.ts @@ -0,0 +1,57 @@ +import { useEffect } from "react"; + +import { useInView } from "react-intersection-observer"; +import { useQuery } from "react-query"; + +import axios, { AxiosError } from "axios"; + +import { useSettings } from "@utils/hooks"; + +interface Channel { + authorThumbnails: { url: string; width: number; height: number }[]; +} + +export const useAuthorThumbnail = ( + authorId: string, + quality: 32 | 48 | 76 | 100 | 176 | 512 +): { + isLoading: boolean; + error: AxiosError | null; + ref: (node?: Element) => void; + thumbnail?: string; +} => { + const [settings] = useSettings(); + + const { ref, inView } = useInView({ + threshold: 0 + }); + + const { isLoading, error, data, isFetched, refetch } = useQuery< + Channel, + AxiosError + >( + ["channelData", authorId], + () => + axios + .get( + `https://${settings.invidiousServer}/api/v1/channels/${authorId}`, + { + params: { + fields: ["authorThumbnails"].join(",") + } + } + ) + .then((res) => res.data), + { enabled: false } + ); + + useEffect(() => { + if (!isFetched && inView) refetch(); + }, [refetch, isFetched, inView]); + + const thumbnail = data?.authorThumbnails.find( + (thumbnail) => thumbnail.width == quality + )?.url; + + return { isLoading, error, ref, thumbnail }; +};