mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-17 19:53:11 +02:00
fix: various youtube trailers related issues
This commit is contained in:
@@ -3,11 +3,14 @@
|
||||
import classNames from 'classnames';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { isFirefox } from '../../utils/browser-detection';
|
||||
import YouTubeBackground from '../YouTubeBackground.svelte';
|
||||
import YouTubeVideo from '../YoutubeVideo.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { localSettings } from '$lib/stores/localstorage.store';
|
||||
|
||||
export let urls: Promise<string[]>;
|
||||
export let items: Promise<{ backdropUrl: string; videoUrl?: string }[]>;
|
||||
export let index: number;
|
||||
export let hasFocus = true;
|
||||
export let heroHasFocus = false;
|
||||
export let hideInterface = false;
|
||||
let visibleIndex = -2;
|
||||
let visibleIndexTimeout: ReturnType<typeof setTimeout>;
|
||||
@@ -37,47 +40,52 @@
|
||||
</script>
|
||||
|
||||
<div class="fixed inset-0" style="-webkit-transform: translate3d(0,0,0);">
|
||||
{#await urls then urlArray}
|
||||
{#if !isFirefox()}
|
||||
{#each urlArray as { trailerUrl, backdropUrl }, i}
|
||||
{#if i === index}
|
||||
{#if trailerUrl}
|
||||
<YouTubeBackground videoId={trailerUrl} backgroundUrl={backdropUrl} />
|
||||
{:else}
|
||||
<div
|
||||
class={classNames('absolute inset-0 bg-center bg-cover', {
|
||||
'opacity-100': visibleIndex === i,
|
||||
'opacity-0': visibleIndex !== i,
|
||||
'scale-110': !hasFocus
|
||||
})}
|
||||
style={`background-image: url('${backdropUrl}'); transition: opacity 500ms, transform 500ms;`}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class={classNames('flex overflow-hidden h-full w-full transition-transform duration-500', { 'scale-110': !hasFocus })}
|
||||
style="perspective: 1px; -webkit-perspective: 1px;">
|
||||
{#each urlArray as { backdropUrl }, i}
|
||||
<div class="w-full h-full flex-shrink-0 relative"
|
||||
style="transform-style: preserve-3d; -webkit-transform-style: preserve-3d; overflow: hidden;"
|
||||
bind:this={htmlElements[i]}>
|
||||
<div
|
||||
{#if true}
|
||||
{#await items then items}
|
||||
{#each items as { videoUrl, backdropUrl }, i}
|
||||
<div
|
||||
class={classNames('absolute inset-0 bg-center bg-cover', {
|
||||
'opacity-100': visibleIndex === i,
|
||||
'opacity-0': visibleIndex !== i,
|
||||
'scale-110': !hasFocus
|
||||
})}
|
||||
style={`background-image: url('${backdropUrl}'); ${
|
||||
!PLATFORM_TV &&
|
||||
'transform: translateZ(-5px) scale(6); -webkit-transform: translateZ(-5px) scale(6);'
|
||||
}`}
|
||||
/>
|
||||
style={`background-image: url('${backdropUrl}'); transition: opacity 500ms, transform 500ms;`}
|
||||
>
|
||||
{#if videoUrl && i === visibleIndex && $localSettings.enableTrailers && $localSettings.autoplayTrailers}
|
||||
<YouTubeVideo videoId={videoUrl} play={heroHasFocus} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
{:else}
|
||||
<div
|
||||
class={classNames('flex overflow-hidden h-full w-full transition-transform duration-500', {
|
||||
'scale-110': !hasFocus
|
||||
})}
|
||||
style="perspective: 1px; -webkit-perspective: 1px;"
|
||||
>
|
||||
{#await items then items}
|
||||
{#each items as { backdropUrl, videoUrl }, i}
|
||||
<div
|
||||
class="w-full h-full flex-shrink-0 basis-auto relative"
|
||||
style="transform-style: preserve-3d; -webkit-transform-style: preserve-3d; overflow: hidden;"
|
||||
bind:this={htmlElements[i]}
|
||||
>
|
||||
<div
|
||||
class="w-full h-full flex-shrink-0 basis-auto bg-center bg-cover absolute inset-0"
|
||||
style={`background-image: url('${backdropUrl}'); ${
|
||||
!PLATFORM_TV &&
|
||||
'transform: translateZ(-5px) scale(6); -webkit-transform: translateZ(-5px) scale(6);'
|
||||
}`}
|
||||
/>
|
||||
<!-- {#if videoUrl && mountVideo}
|
||||
<YouTubeBackground videoId={videoUrl} backgroundUrl={backdropUrl} />
|
||||
{/if} -->
|
||||
</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
<div
|
||||
class={classNames('absolute inset-0 flex flex-col transition-opacity', {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Container from '../Container.svelte';
|
||||
import HeroShowcaseBackground from './HeroBackground.svelte';
|
||||
import HeroBackground from './HeroBackground.svelte';
|
||||
import IconButton from '../FloatingIconButton.svelte';
|
||||
import { ChevronRight } from 'radix-icons-svelte';
|
||||
import PageDots from '../HeroShowcase/PageDots.svelte';
|
||||
@@ -10,13 +10,13 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let urls: Promise<{ trailerUrl: string; backdropUrl: string }[]>;
|
||||
export let items: Promise<{ backdropUrl: string; videoUrl?: string }[]>;
|
||||
export let index = 0;
|
||||
export let hideInterface = false;
|
||||
|
||||
let length = 0;
|
||||
|
||||
$: urls.then((urls) => (length = urls.length));
|
||||
$: items.then((urls) => (length = urls.length));
|
||||
|
||||
function onNext() {
|
||||
if (index === length - 1) {
|
||||
@@ -42,9 +42,9 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
let hasFocusWithin: Readable<boolean>;
|
||||
let heroHasFocusWithin: Readable<boolean>;
|
||||
let focusIndex: Writable<number>;
|
||||
$: backgroundHasFocus = $hasFocusWithin && $focusIndex === 0;
|
||||
$: backgroundHasFocus = $heroHasFocusWithin && $focusIndex === 0;
|
||||
</script>
|
||||
|
||||
<Container
|
||||
@@ -68,10 +68,10 @@
|
||||
dispatch('navigate', detail);
|
||||
}
|
||||
}}
|
||||
bind:hasFocusWithin
|
||||
bind:hasFocusWithin={heroHasFocusWithin}
|
||||
bind:focusIndex
|
||||
>
|
||||
<HeroShowcaseBackground {urls} {index} hasFocus={backgroundHasFocus} {hideInterface} />
|
||||
<HeroBackground {items} {index} hasFocus={backgroundHasFocus} heroHasFocus={$heroHasFocusWithin} {hideInterface} />
|
||||
<div
|
||||
class={classNames('flex flex-1 z-10 transition-opacity', {
|
||||
'opacity-0': hideInterface
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
type: 'movie' | 'tv';
|
||||
posterUri: string;
|
||||
backdropUri: string;
|
||||
trailerUrl?: string;
|
||||
videoUrl?: string;
|
||||
title: string;
|
||||
overview: string;
|
||||
infoProperties: { label: string; href?: string }[];
|
||||
@@ -35,10 +35,12 @@
|
||||
</script>
|
||||
|
||||
<HeroCarousel
|
||||
urls={items.then((items) => items.map((i) => ({
|
||||
backdropUrl: `${TMDB_IMAGES_ORIGINAL}${i.backdropUri}`,
|
||||
trailerUrl: i.trailerUrl || ''
|
||||
})))}
|
||||
items={items.then((items) =>
|
||||
items.map((i) => ({
|
||||
backdropUrl: `${TMDB_IMAGES_ORIGINAL}${i.backdropUri}`,
|
||||
videoUrl: i.videoUrl
|
||||
}))
|
||||
)}
|
||||
bind:index={showcaseIndex}
|
||||
on:enter
|
||||
on:navigate={({ detail }) => {
|
||||
|
||||
@@ -7,14 +7,13 @@
|
||||
|
||||
export let movies: Promise<TmdbMovie2[]>;
|
||||
|
||||
$: items = movies.then(async (movies) => {
|
||||
return Promise.all(
|
||||
$: items = movies
|
||||
.then(async (movies) =>
|
||||
movies.map(async (movie) => {
|
||||
const trailerUrl = movie.id
|
||||
? await tmdbApi.getMovieVideos(movie.id).then((videos) =>
|
||||
videos.find((video) => video.type === 'Trailer' && video.site === 'YouTube')?.key || ''
|
||||
)
|
||||
: '';
|
||||
const movieFull = await tmdbApi.getTmdbMovie(movie.id ?? 0);
|
||||
const videoUrl = movieFull?.videos?.results?.find(
|
||||
(video) => video.type === 'Trailer' && video.site === 'YouTube'
|
||||
)?.key;
|
||||
|
||||
return {
|
||||
id: movie.id ?? 0,
|
||||
@@ -23,7 +22,7 @@
|
||||
backdropUri: movie.backdrop_path ?? '',
|
||||
title: movie.title ?? '',
|
||||
overview: movie.overview ?? '',
|
||||
trailerUrl: trailerUrl ?? '',
|
||||
videoUrl,
|
||||
infoProperties: [
|
||||
...(movie.release_date
|
||||
? [{ label: new Date(movie.release_date).getFullYear().toString() }]
|
||||
@@ -43,8 +42,8 @@
|
||||
]
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
)
|
||||
.then((i) => Promise.all(i));
|
||||
</script>
|
||||
|
||||
<HeroShowcase on:select={({ detail }) => navigate(`/movie/${detail?.id}`)} {items} />
|
||||
|
||||
@@ -10,20 +10,19 @@
|
||||
$: items = series.then(async (series) => {
|
||||
return Promise.all(
|
||||
series.map(async (series) => {
|
||||
const trailerUrl = series.id
|
||||
? await tmdbApi.getSeriesVideos(series.id).then((videos) =>
|
||||
videos.find((video) => video.type === 'Trailer' && video.site === 'YouTube')?.key || ''
|
||||
)
|
||||
: '';
|
||||
const seriesFull = await tmdbApi.getTmdbSeries(series.id ?? 0);
|
||||
const videoUrl = seriesFull?.videos?.results?.find(
|
||||
(video) => video.type === 'Trailer' && video.site === 'YouTube'
|
||||
)?.key;
|
||||
|
||||
return {
|
||||
id: series.id ?? 0,
|
||||
type: 'series' as const,
|
||||
type: 'tv' as const,
|
||||
posterUri: series.poster_path ?? '',
|
||||
backdropUri: series.backdrop_path ?? '',
|
||||
title: series.name ?? '',
|
||||
overview: series.overview ?? '',
|
||||
trailerUrl: trailerUrl ?? '',
|
||||
videoUrl,
|
||||
infoProperties: [
|
||||
...(series.status !== 'Ended'
|
||||
? [{ label: `Since ${new Date(series.first_air_date ?? 0).getFullYear()}` }]
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { PLATFORM_WEB } from '../constants';
|
||||
|
||||
export let videoId: string | null = null;
|
||||
export let backgroundUrl: string;
|
||||
|
||||
let player: any;
|
||||
let showBackgroundImage = true;
|
||||
let showBackgroundImageError = false;
|
||||
let isDestroyed = false;
|
||||
let isPlayerReady = false;
|
||||
let lastRequestedVideoId: string | null = null;
|
||||
const stopBeforeEnd = 12;
|
||||
let checkStopInterval: any = null;
|
||||
let isLoadingPlayer = false;
|
||||
|
||||
function loadYouTubeAPI() {
|
||||
if (!PLATFORM_WEB) return;
|
||||
|
||||
if (!window.YT) {
|
||||
const tag = document.createElement('script');
|
||||
tag.src = 'https://www.youtube.com/iframe_api';
|
||||
document.head.appendChild(tag);
|
||||
window.onYouTubeIframeAPIReady = () => {
|
||||
setupPlayer();
|
||||
};
|
||||
} else {
|
||||
setupPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
function destroyPlayer() {
|
||||
if (player) {
|
||||
try {
|
||||
player.destroy();
|
||||
} catch (e) {
|
||||
console.warn("Error destroying player.", e);
|
||||
}
|
||||
player = null;
|
||||
isPlayerReady = false;
|
||||
clearInterval(checkStopInterval);
|
||||
}
|
||||
}
|
||||
|
||||
function setupPlayer() {
|
||||
if (!PLATFORM_WEB || !window.YT || isDestroyed || !videoId || isLoadingPlayer) return;
|
||||
|
||||
isLoadingPlayer = true;
|
||||
if (videoId === lastRequestedVideoId) {
|
||||
isLoadingPlayer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
lastRequestedVideoId = videoId;
|
||||
destroyPlayer();
|
||||
|
||||
setTimeout(() => {
|
||||
if (!window.YT || isDestroyed) {
|
||||
isLoadingPlayer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
player = new window.YT.Player('youtube-player', {
|
||||
videoId: videoId,
|
||||
playerVars: {
|
||||
autoplay: 1,
|
||||
controls: 0,
|
||||
modestbranding: 1,
|
||||
rel: 0,
|
||||
iv_load_policy: 3,
|
||||
start: 3,
|
||||
fs: 0,
|
||||
disablekb: 1,
|
||||
cc_load_policy: 0,
|
||||
mute: 1
|
||||
},
|
||||
events: {
|
||||
onReady: () => {
|
||||
isLoadingPlayer = false;
|
||||
if (isDestroyed || videoId !== lastRequestedVideoId) {
|
||||
destroyPlayer();
|
||||
return;
|
||||
}
|
||||
isPlayerReady = true;
|
||||
setTimeout(() => player?.playVideo(), 1500);
|
||||
},
|
||||
onStateChange: handlePlayerStateChange,
|
||||
onError: handlePlayerError
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function handlePlayerError(event: any) {
|
||||
if (isDestroyed) return;
|
||||
|
||||
const errorMessages: Record<number, string> = {
|
||||
2: "Invalid video ID.",
|
||||
5: "Playback error.",
|
||||
100: "Video not found.",
|
||||
101: "Embedding restricted by the owner.",
|
||||
150: "Embedding restricted by the owner."
|
||||
};
|
||||
|
||||
console.warn("YouTube Player Error:", errorMessages[event.data] || "Unknown error.");
|
||||
showBackgroundImageError = true;
|
||||
showBackgroundImage = true;
|
||||
destroyPlayer();
|
||||
}
|
||||
|
||||
function handlePlayerStateChange(event: any) {
|
||||
if (isDestroyed || !isPlayerReady) return;
|
||||
|
||||
if (event.data === YT.PlayerState.PLAYING) {
|
||||
setTimeout(() => (showBackgroundImage = false), 1000);
|
||||
|
||||
if (checkStopInterval) {
|
||||
clearInterval(checkStopInterval);
|
||||
}
|
||||
|
||||
checkStopInterval = setInterval(() => {
|
||||
if (!player || showBackgroundImageError || isDestroyed) {
|
||||
clearInterval(checkStopInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
const remainingTime = player.getDuration() - player.getCurrentTime();
|
||||
|
||||
if (remainingTime <= stopBeforeEnd) {
|
||||
try {
|
||||
player.pauseVideo();
|
||||
player.seekTo(0);
|
||||
player.playVideo();
|
||||
} catch (e) {
|
||||
console.warn("Error looping video.", e);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
} else if (event.data === YT.PlayerState.ENDED) {
|
||||
if (!isDestroyed) {
|
||||
try {
|
||||
player.seekTo(0);
|
||||
player.playVideo();
|
||||
} catch (e) {
|
||||
console.warn("Error restarting video.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
isDestroyed = false;
|
||||
loadYouTubeAPI();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
isDestroyed = true;
|
||||
destroyPlayer();
|
||||
});
|
||||
|
||||
$: if (videoId && window.YT && !isDestroyed) {
|
||||
setupPlayer();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div id="youtube-player" class="video-background"></div>
|
||||
|
||||
{#if showBackgroundImage || showBackgroundImageError}
|
||||
<div
|
||||
class="background-image"
|
||||
style={`background-image: url('${backgroundUrl}');`}
|
||||
in:fade={{ duration: 200 }}
|
||||
out:fade={{ duration: 400 }}
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.video-background {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
transform: translate(-50%, -50%) scale(1.6);
|
||||
transition: transform 0.5s ease-in-out;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.video-background {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.video-background {
|
||||
transform: translate(-50%, -50%) scale(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.video-background {
|
||||
transform: translate(-50%, -50%) scale(3);
|
||||
}
|
||||
}
|
||||
|
||||
.background-image {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
222
src/lib/components/YoutubeVideo.svelte
Normal file
222
src/lib/components/YoutubeVideo.svelte
Normal file
@@ -0,0 +1,222 @@
|
||||
<script lang="ts">
|
||||
import { PLATFORM_TV } from '$lib/constants';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const STOP_WHEN_REMAINING = 12;
|
||||
|
||||
export let videoId: string | null = null;
|
||||
export let play = false;
|
||||
export let autoplay = true;
|
||||
export let autoplayDelay = 2000;
|
||||
export let loadTime = PLATFORM_TV ? 2500 : 1000;
|
||||
|
||||
const playerId = `youtube-player-${videoId}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
let didMount = false;
|
||||
let isInitialized = false;
|
||||
let player: YT['Player'];
|
||||
let isPlayerReady = false;
|
||||
let checkStopInterval: ReturnType<typeof setInterval>;
|
||||
let autoplayTimeout: ReturnType<typeof setTimeout>;
|
||||
let loadTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
$: if (isInitialized && player?.playVideo && play) {
|
||||
player.playVideo();
|
||||
} else if (isInitialized && player?.pauseVideo && !play) {
|
||||
player.pauseVideo();
|
||||
}
|
||||
|
||||
$: if (didMount && !isInitialized && play) loadYouTubeAPI();
|
||||
function loadYouTubeAPI() {
|
||||
isInitialized = true;
|
||||
console.log('Loading YouTube API for ' + videoId);
|
||||
if (!window.YT) {
|
||||
const tag = document.createElement('script');
|
||||
tag.src = 'https://www.youtube.com/iframe_api';
|
||||
document.head.appendChild(tag);
|
||||
(window as any).onYouTubeIframeAPIReady = () => {
|
||||
setupPlayer();
|
||||
};
|
||||
} else {
|
||||
setupPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
function destroyPlayer() {
|
||||
console.log('Destroying player');
|
||||
|
||||
clearInterval(checkStopInterval);
|
||||
clearTimeout(autoplayTimeout);
|
||||
clearTimeout(loadTimeout);
|
||||
isPlayerReady = false;
|
||||
|
||||
if (!player) return;
|
||||
|
||||
try {
|
||||
player.destroy();
|
||||
} catch (e) {
|
||||
console.warn('Error destroying player.', e);
|
||||
}
|
||||
}
|
||||
|
||||
function setupPlayer() {
|
||||
if (!window.YT || !videoId) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!window.YT) return;
|
||||
|
||||
player = new window.YT.Player(playerId, {
|
||||
videoId: videoId,
|
||||
playerVars: {
|
||||
autoplay: 0,
|
||||
controls: 0,
|
||||
modestbranding: 1,
|
||||
rel: 0,
|
||||
iv_load_policy: 3,
|
||||
start: 3,
|
||||
fs: 0,
|
||||
disablekb: 1,
|
||||
cc_load_policy: 0,
|
||||
mute: 1
|
||||
},
|
||||
events: {
|
||||
onReady: () => {
|
||||
player?.playVideo();
|
||||
play = true;
|
||||
if (loadTime) {
|
||||
loadTimeout = setTimeout(() => {
|
||||
isPlayerReady = true;
|
||||
console.log('Playing video');
|
||||
}, loadTime);
|
||||
} else {
|
||||
isPlayerReady = true;
|
||||
console.log('Playing video');
|
||||
}
|
||||
},
|
||||
onStateChange: handlePlayerStateChange,
|
||||
onError: handlePlayerError
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function handlePlayerError(event: any) {
|
||||
const errorMessages: Record<number, string> = {
|
||||
2: 'Invalid video ID.',
|
||||
5: 'Playback error.',
|
||||
100: 'Video not found.',
|
||||
101: 'Embedding restricted by the owner.',
|
||||
150: 'Embedding restricted by the owner.'
|
||||
};
|
||||
|
||||
console.error('YouTube Player Error:', errorMessages[event.data] || 'Unknown error.');
|
||||
destroyPlayer();
|
||||
}
|
||||
|
||||
function handlePlayerStateChange(event: any) {
|
||||
if (!isPlayerReady) return;
|
||||
|
||||
if (event.data === window.YT.PlayerState.PLAYING) {
|
||||
// setTimeout(() => (showBackgroundImage = false), 1000);
|
||||
|
||||
clearInterval(checkStopInterval);
|
||||
|
||||
checkStopInterval = setInterval(() => {
|
||||
if (
|
||||
!player
|
||||
// showBackgroundImageError ||
|
||||
) {
|
||||
clearInterval(checkStopInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
const remainingTime = player.getDuration() - player.getCurrentTime();
|
||||
|
||||
if (remainingTime <= STOP_WHEN_REMAINING) {
|
||||
try {
|
||||
player.pauseVideo();
|
||||
player.seekTo(0);
|
||||
player.playVideo();
|
||||
} catch (e) {
|
||||
console.warn('Error looping video.', e);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
} else if (event.data === window.YT.PlayerState.ENDED) {
|
||||
try {
|
||||
player.seekTo(0);
|
||||
player.playVideo();
|
||||
} catch (e) {
|
||||
console.warn('Error restarting video.', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (autoplay) {
|
||||
autoplayTimeout = setTimeout(() => {
|
||||
play = true;
|
||||
didMount = true;
|
||||
}, autoplayDelay);
|
||||
} else {
|
||||
didMount = true;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
destroyPlayer();
|
||||
});
|
||||
$: {
|
||||
const el = document.getElementById(playerId);
|
||||
if (el) el.style.opacity = isPlayerReady && play ? '1' : '0';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div out:fade={{ delay: isPlayerReady && play ? 2000 : 0 }}>
|
||||
<div id={playerId} class="video-background" style="opacity: 0;" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.video-background {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
transform: translate(-50%, -50%) scale(1.6);
|
||||
transition: transform 0.5s ease-in-out, opacity 1s ease-in-out;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.video-background {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.video-background {
|
||||
transform: translate(-50%, -50%) scale(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.video-background {
|
||||
transform: translate(-50%, -50%) scale(3);
|
||||
}
|
||||
}
|
||||
|
||||
.background-image {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -332,6 +332,22 @@
|
||||
localSettings.update((p) => ({ ...p, checkForUpdates: detail }))}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-lg font-medium text-secondary-100 py-2">
|
||||
<label class="mr-2">Enable Trailers</label>
|
||||
<Toggle
|
||||
checked={$localSettings.enableTrailers}
|
||||
on:change={({ detail }) =>
|
||||
localSettings.update((p) => ({ ...p, enableTrailers: detail }))}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-lg font-medium text-secondary-100 py-2">
|
||||
<label class="mr-2">Autoplay Trailers</label>
|
||||
<Toggle
|
||||
checked={$localSettings.autoplayTrailers}
|
||||
on:change={({ detail }) =>
|
||||
localSettings.update((p) => ({ ...p, autoplayTrailers: detail }))}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
|
||||
<Tab {...tab} direction="vertical" tab={Tabs.About}>
|
||||
|
||||
@@ -37,6 +37,23 @@
|
||||
|
||||
$: recommendations = tmdbApi.getMovieRecommendations(tmdbId);
|
||||
|
||||
$: images = $tmdbMovie.then((movie) => {
|
||||
const trailer = movie?.videos?.results?.find(
|
||||
(video) => video.type === 'Trailer' && video.site === 'YouTube'
|
||||
)?.key;
|
||||
|
||||
return (
|
||||
movie?.images.backdrops
|
||||
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
|
||||
?.map((bd, i) => ({
|
||||
backdropUrl: TMDB_IMAGES_ORIGINAL + bd.file_path || '',
|
||||
|
||||
videoUrl: trailer && i === 0 ? trailer : undefined
|
||||
}))
|
||||
.slice(0, 5) || []
|
||||
);
|
||||
});
|
||||
|
||||
let titleProperties: { href?: string; label: string }[] = [];
|
||||
$tmdbMovie.then((movie) => {
|
||||
if (movie?.runtime) {
|
||||
@@ -71,15 +88,7 @@
|
||||
class="h-[calc(100vh-4rem)] flex flex-col py-16 px-32"
|
||||
on:enter={scrollIntoView({ top: 999 })}
|
||||
>
|
||||
<HeroCarousel
|
||||
urls={$tmdbMovie.then(
|
||||
(movie) =>
|
||||
movie?.images.backdrops
|
||||
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
|
||||
?.map((bd) => ({backdropUrl: TMDB_IMAGES_ORIGINAL + bd.file_path || ''}))
|
||||
.slice(0, 5) || []
|
||||
)}
|
||||
>
|
||||
<HeroCarousel items={images}>
|
||||
<Container />
|
||||
<div class="h-full flex-1 flex flex-col justify-end">
|
||||
{#await $tmdbMovie then movie}
|
||||
|
||||
@@ -40,6 +40,22 @@
|
||||
const episodeCards = useRegistrar();
|
||||
let scrollTop: number;
|
||||
|
||||
$: images = $tmdbSeries.then((series) => {
|
||||
const trailer = series?.videos?.results?.find(
|
||||
(video) => video.type === 'Trailer' && video.site === 'YouTube'
|
||||
)?.key;
|
||||
|
||||
return (
|
||||
series?.images.backdrops
|
||||
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
|
||||
?.map((bd, i) => ({
|
||||
backdropUrl: TMDB_IMAGES_ORIGINAL + bd.file_path || '',
|
||||
videoUrl: trailer && i === 0 ? trailer : undefined
|
||||
}))
|
||||
.slice(0, 5) || []
|
||||
);
|
||||
});
|
||||
|
||||
let titleProperties: { href?: string; label: string }[] = [];
|
||||
$tmdbSeries.then((series) => {
|
||||
if (series && series.status !== 'Ended') {
|
||||
@@ -86,15 +102,7 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HeroCarousel
|
||||
urls={$tmdbSeries.then(
|
||||
(series) =>
|
||||
series?.images.backdrops
|
||||
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
|
||||
?.map((bd) => ({backdropUrl: TMDB_IMAGES_ORIGINAL + bd.file_path || ''}))
|
||||
.slice(0, 5) || []
|
||||
)}
|
||||
>
|
||||
<HeroCarousel items={images}>
|
||||
<Container />
|
||||
<div class="h-full flex-1 flex flex-col justify-end">
|
||||
{#await $tmdbSeries then series}
|
||||
|
||||
@@ -44,11 +44,15 @@ export const localSettings = createLocalStorageStore<{
|
||||
useCssTransitions: boolean;
|
||||
checkForUpdates: boolean;
|
||||
skippedVersion: string;
|
||||
enableTrailers: boolean;
|
||||
autoplayTrailers: boolean;
|
||||
}>('settings', {
|
||||
animateScrolling: true,
|
||||
useCssTransitions: true,
|
||||
checkForUpdates: true,
|
||||
skippedVersion: ''
|
||||
skippedVersion: '',
|
||||
enableTrailers: true,
|
||||
autoplayTrailers: true
|
||||
});
|
||||
|
||||
export type LibraryViewSettings = {
|
||||
|
||||
@@ -9,4 +9,59 @@ export type MediaType = 'Movie' | 'Series';
|
||||
|
||||
declare global {
|
||||
const REIVERR_VERSION: string;
|
||||
|
||||
type YTPlayerOptions = {
|
||||
videoId: string;
|
||||
playerVars: Record<string, string | number | boolean>;
|
||||
// playerVars: {
|
||||
// autoplay: 0 | 1;
|
||||
// controls: 0 | 1;
|
||||
// disablekb: 0 | 1;
|
||||
// enablejsapi: 0 | 1;
|
||||
// iv_load_policy: 1 | 3;
|
||||
// loop: 0 | 1;
|
||||
// modestbranding: 0 | 1;
|
||||
// playsinline: 0 | 1;
|
||||
// rel: 0 | 1;
|
||||
// showinfo: 0 | 1;
|
||||
// start: number;
|
||||
// fs: 0 | 1;
|
||||
// cc_load_policy: 0 | 1;
|
||||
// mute: 0 | 1;
|
||||
// };
|
||||
events: {
|
||||
onReady: (event: any) => void;
|
||||
onStateChange: (event: any) => void;
|
||||
onError: (event: any) => void;
|
||||
};
|
||||
};
|
||||
|
||||
type YTPlayer = {
|
||||
new (id: string, options: YTPlayerOptions): YTPlayer;
|
||||
destroy(): void;
|
||||
getDuration(): number;
|
||||
getCurrentTime(): number;
|
||||
pauseVideo(): void;
|
||||
playVideo(): void;
|
||||
stopVideo(): void;
|
||||
seekTo(seconds: number, allowSeekAhead?: boolean): void;
|
||||
};
|
||||
|
||||
// Youtube API
|
||||
interface YT {
|
||||
Player: YTPlayer;
|
||||
PlayerState: {
|
||||
ENDED: number;
|
||||
PLAYING: number;
|
||||
PAUSED: number;
|
||||
BUFFERING: number;
|
||||
CUED: number;
|
||||
};
|
||||
}
|
||||
|
||||
const YT: YT;
|
||||
|
||||
interface Window {
|
||||
YT: YT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@
|
||||
<name>Reiverr</name>
|
||||
<tizen:profile name="tv-samsung"/>
|
||||
<tizen:setting screen-orientation="landscape" context-menu="enable" background-support="disable" encryption="disable" install-location="auto" hwkey-event="enable"/>
|
||||
<tizen:allow-navigation>*</tizen:allow-navigation>
|
||||
<access origin="http://youtube.com" subdomains="true"/>
|
||||
</widget>
|
||||
|
||||
Reference in New Issue
Block a user