Skip to content

Commit

Permalink
add better and fast player (#47)
Browse files Browse the repository at this point in the history
* add better and fast player

* fetch all trending data in action

* load content
  • Loading branch information
iswilljr authored Jan 11, 2025
1 parent 4e657eb commit 7e0a9fb
Show file tree
Hide file tree
Showing 23 changed files with 1,361 additions and 206 deletions.
18 changes: 18 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const disabledTypescriptEslintRules = {
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/promise-function-async': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'react/react-in-jsx-scope': 'off',
}

/** @type {import("eslint").ESLint.ConfigData} */
Expand Down Expand Up @@ -48,6 +49,23 @@ const eslintConfig = {
...disabledTypescriptEslintRules,
},
},
{
files: ['*.{js,jsx,ts,tsx}'],
extends: [
'standard-with-typescript',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:prettier/recommended',
],
settings: {
react: {
version: 'detect',
},
},
rules: {
...disabledTypescriptEslintRules,
},
},
],
}

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
"eslint-config-standard-with-typescript": "43.0.1",
"eslint-plugin-astro": "1.3.1",
"eslint-plugin-prettier": "5.2.1",
"eslint-plugin-react": "7.37.3",
"eslint-plugin-react-hooks": "5.1.0",
"prettier": "3.3.3",
"prettier-plugin-astro": "0.14.1",
"prettier-plugin-tailwindcss": "0.6.8",
Expand Down
920 changes: 915 additions & 5 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const server = {
handler: async () => {
try {
const [trending, moreTrending] = await Promise.all([
getTrending('all', { page: 1 }),
getTrending('all'),
getTrending('all', { page: 2 }),
])
return [...trending.results, ...moreTrending.results]
Expand All @@ -43,11 +43,18 @@ export const server = {
},
}),
trending: defineAction({
input: z.object({ type: z.enum(['all', 'movie', 'tv']) }),
handler: async ({ type }) => {
handler: async () => {
try {
const trending = await getTrending(type)
return trending
const [all, movies, tvShows] = await Promise.all([
getTrending('all'),
getTrending('movie'),
getTrending('tv'),
])
return {
all: all.results,
movies: movies.results,
tvShows: tvShows.results,
}
} catch (e) {
return null
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Auth/NavUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ async function handleLogout() {
window.location.reload()
}

export function NavUser({ isBot = false }) {
export function NavUser({ isBot = false }: { isBot?: boolean }) {
const { user, isLoading, isAuthenticated } = useSession({ isBot })

if (isLoading || !isAuthenticated) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Auth/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function SignInForm({ onSignUp }: { onSignUp: () => void }) {
error && 'mt-2'
)}
>
Don't have an account yet?{' '}
Don't have an account yet?{' '}
<button
onClick={onSignUp}
className='font-semibold text-primary-600 hover:text-primary-500'
Expand Down
6 changes: 3 additions & 3 deletions src/components/Content/Movies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import { swrDefaultOptions } from '@/utils'

export function Movies() {
const { data: trendingMovies } = useSWR(
'movies',
() => actions.trending({ type: 'movie' }),
'trending',
() => actions.trending(),
swrDefaultOptions
)

return (
<MediaGrid
title='Movies'
media='movie'
results={(trendingMovies?.data?.results as any) ?? []}
results={(trendingMovies?.data?.movies as any) ?? []}
icon={<ClapperboardIcon width='18' height='18' stroke='#000' />}
/>
)
Expand Down
6 changes: 3 additions & 3 deletions src/components/Content/TVShows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import { swrDefaultOptions } from '@/utils'

export function TVShows() {
const { data: trendingTvShows } = useSWR(
'tv',
() => actions.trending({ type: 'tv' }),
'trending',
() => actions.trending(),
swrDefaultOptions
)

return (
<MediaGrid
media='tv'
title='TV Shows'
results={(trendingTvShows?.data?.results as any) ?? []}
results={(trendingTvShows?.data?.tvShows as any) ?? []}
icon={<TvIcon width='18' height='18' stroke='#000' />}
/>
)
Expand Down
4 changes: 2 additions & 2 deletions src/components/Content/Trending.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import { swrDefaultOptions } from '@/utils'
export function Trending() {
const { data: trending } = useSWR(
'trending',
() => actions.trending({ type: 'all' }),
() => actions.trending(),
swrDefaultOptions
)

return (
<MediaList
id='trending'
results={(trending?.data?.results as any) ?? []}
results={(trending?.data?.all as any) ?? []}
title="What's Trending Today"
icon={<FlameIcon width='18' height='18' fill='#000' stroke='#000' />}
/>
Expand Down
15 changes: 15 additions & 0 deletions src/components/Home/Content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Trending } from '@/components/Content/Trending'
import { Movies } from '@/components/Content/Movies'
import { TVShows } from '@/components/Content/TVShows'
import { Watching } from '@/components/Content/Watching'

export function Content() {
return (
<div className='-my-[--discover-space] sm:pb-8'>
<Trending />
<Watching />
<Movies />
<TVShows />
</div>
)
}
1 change: 1 addition & 0 deletions src/components/Media/MediaGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function MediaGrid({
<div className='relative z-10 grid w-full grid-cols-[repeat(auto-fill,minmax(150px,1fr))] flex-wrap gap-4'>
{results.map(movie => (
<MediaPoster
key={movie.id}
media={media}
id={movie.id}
rating={movie.vote_average}
Expand Down
1 change: 1 addition & 0 deletions src/components/Media/MediaList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function MediaList({ id, results, title, icon }: MediaListProps) {
{results.map(movie =>
movie.media_type !== 'person' ? (
<MediaCard
key={movie.id}
media={movie.media_type}
id={movie.id}
rating={movie.vote_average}
Expand Down
86 changes: 86 additions & 0 deletions src/components/Player/NextPrevButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { $playerState } from '@/store/player'
import { getSource } from '@/utils/sources'
import { getEpisodeUrl } from '@/utils/url'
import { useStore } from '@nanostores/react'
import { navigate } from 'astro:transitions/client'
import { SkipBackIcon, SkipForwardIcon } from 'lucide-react'

interface NextPrevButtonsProps {
season: number
mediaId: number
mediaTitle: string
firstEpisode: number
lastEpisode: number
totalSeasons: number
initialEpisode: number
}

export function NextPrevButtons({
season,
mediaId,
mediaTitle,
firstEpisode,
lastEpisode,
totalSeasons,
initialEpisode,
}: NextPrevButtonsProps) {
const playerState = useStore($playerState)
const episode = playerState.episode ?? initialEpisode

const isFirstEpisode = episode <= firstEpisode
const isLastEpisode = episode >= lastEpisode
const isLastSeason = season >= totalSeasons

const handleEpisodeClick = (episode: number) => {
$playerState.set({ ...$playerState.get(), episode })
const searchParams = new URL(window.location.href).searchParams
const source = getSource(searchParams.get('source'))
window.history.replaceState(
{},
'',
new URL(
getEpisodeUrl(mediaId, mediaTitle, season, episode, source.id),
window.location.href
).toString()
)
}

const handleNextSeason = () => {
const searchParams = new URL(window.location.href).searchParams
const source = getSource(searchParams.get('source'))
const newSeason = season + 1
void navigate(getEpisodeUrl(mediaId, mediaTitle, newSeason, 1, source.id))
}

return (
<div className='flex w-full items-center justify-end gap-2 rounded-2xl bg-white/10 p-2'>
{!isFirstEpisode && (
<button
onClick={() => handleEpisodeClick(episode - 1)}
className='flex items-center justify-center gap-1 rounded-2xl bg-black/50 px-4 py-2 text-lg text-white'
>
<SkipBackIcon className='size-5 text-white' />
Prev
</button>
)}
{!isLastEpisode && (
<button
onClick={() => handleEpisodeClick(episode + 1)}
className='flex items-center justify-center gap-1 rounded-2xl bg-black/50 px-4 py-2 text-lg text-white'
>
Next
<SkipForwardIcon className='size-5 text-white' />
</button>
)}
{isLastEpisode && !isLastSeason && (
<button
onClick={handleNextSeason}
className='flex items-center justify-center gap-1 rounded-2xl bg-black/50 px-4 py-2 text-lg text-white'
>
Next Season
<SkipForwardIcon className='size-5 text-white' />
</button>
)}
</div>
)
}
94 changes: 94 additions & 0 deletions src/components/Player/SelectEpisode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useEffect } from 'react'
import { PlayIcon } from 'lucide-react'
import { useStore } from '@nanostores/react'
import { $playerState } from '@/store/player'
import { getSeasonOrEpisode } from '@/utils'
import { getEpisodeUrl } from '@/utils/url'
import { getSource, getTvUrl } from '@/utils/sources'
import type { Episode } from 'tmdb-ts'

interface SelectEpisodeProps {
season: number
mediaId: number
mediaTitle: string
episodes: Episode[]
initialEpisode: number
}

export function SelectEpisode({
season,
mediaId,
episodes,
mediaTitle,
initialEpisode,
}: SelectEpisodeProps) {
const playerState = useStore($playerState)
const episode = playerState.episode ?? initialEpisode

const handleEpisodeClick = (episode: number) => {
$playerState.set({ ...$playerState.get(), episode })
const searchParams = new URL(window.location.href).searchParams
const source = getSource(searchParams.get('source'))
window.history.replaceState(
{},
'',
new URL(
getEpisodeUrl(mediaId, mediaTitle, season, episode, source.id),
window.location.href
).toString()
)
}

useEffect(() => {
const playerVideo = document.querySelector('#player-video')
const currentEpisodeNumber = $playerState.get().episode
const episodeIndex = episodes.findIndex(
episodeDetails => episodeDetails.episode_number === currentEpisodeNumber
)

const episodeDetails = episodes[episodeIndex]

if (!playerVideo || !episodeDetails) return

const container = playerVideo.parentElement
const olsSrc = playerVideo.getAttribute('src')
const searchParams = new URL(window.location.href).searchParams

const source = getSource(searchParams.get('source'))
const season = getSeasonOrEpisode(searchParams.get('season'))
const episode = episodeDetails.episode_number

const newSrc = getTvUrl(source.id, mediaId, season, episode)

if (olsSrc === newSrc || !container) return

playerVideo.remove()
playerVideo.setAttribute('src', newSrc)
container?.append(playerVideo)
}, [episodes, mediaId, episode])

return (
<div className='custom-scrollbars overflow-y-auto'>
{episodes.map(episodeDetails => (
<button
key={episodeDetails.id}
aria-current={episodeDetails.episode_number === episode}
onClick={() => handleEpisodeClick(episodeDetails.episode_number)}
className='group relative line-clamp-1 flex h-12 w-full flex-shrink-0 items-center justify-between gap-1 p-4 text-sm even:bg-white/5 hover:bg-white/10 hover:text-white aria-[current=true]:!bg-white/20 aria-[current=true]:!text-primary-400'
>
<p className='flex gap-2 tracking-wide'>
<span className='font-medium'>
{episodeDetails.episode_number}.
</span>
<span className='line-clamp-1 text-start font-normal'>
{episodeDetails.name}
</span>
</p>
<span className='hidden shrink-0 items-center justify-center rounded-full bg-primary-400 p-1 group-aria-[current=true]:flex'>
<PlayIcon width='10' height='10' fill='black' stroke='black' />
</span>
</button>
))}
</div>
)
}
Loading

0 comments on commit 7e0a9fb

Please sign in to comment.