Video background done

This commit is contained in:
Aleksi Lassila
2023-06-14 14:42:07 +03:00
parent 948da70753
commit 8345708288
57 changed files with 10793 additions and 2737 deletions

45
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@apollo/client": "^3.7.15",
"axios": "^1.4.0",
"graphql": "^16.6.0",
"openapi-fetch": "^0.2.1",
"radix-icons-svelte": "^1.2.1",
"svelte-apollo": "^0.5.0"
},
@@ -34,6 +35,7 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.26.0",
"graphql-codegen-svelte-apollo": "^1.1.0",
"openapi-typescript": "^6.2.7",
"postcss": "^8.4.24",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
@@ -3265,6 +3267,15 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -6311,6 +6322,40 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-fetch": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.2.1.tgz",
"integrity": "sha512-XrpiPz8fPUdYtgcXSU4u8jJ/At67PbFsj6XRZZbt6lMsrosKplWY7dUgnkstYaroschNz/NfYSPIiq0sOaY0nw=="
},
"node_modules/openapi-typescript": {
"version": "6.2.7",
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-6.2.7.tgz",
"integrity": "sha512-SEdqtFLWmbc2CckzZVSF4/cpPgWlWZp02P0wVCLV/7mCE+7qKukIoiVCLfWJIVeklWuGLZkA/PdJ6OwWaqJ6Ig==",
"dev": true,
"dependencies": {
"ansi-colors": "^4.1.3",
"fast-glob": "^3.2.12",
"js-yaml": "^4.1.0",
"supports-color": "^9.3.1",
"undici": "^5.22.1",
"yargs-parser": "^21.1.1"
},
"bin": {
"openapi-typescript": "bin/cli.js"
}
},
"node_modules/openapi-typescript/node_modules/supports-color": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.3.1.tgz",
"integrity": "sha512-knBY82pjmnIzK3NifMo3RxEIRD9E0kIzV4BKcyTZ9+9kWgLMxd4PrsTSMoFQUabgRBbF8KOLRDCyKgNV+iK44Q==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/optimism": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.2.tgz",

View File

@@ -32,6 +32,7 @@
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.26.0",
"graphql-codegen-svelte-apollo": "^1.1.0",
"openapi-typescript": "^6.2.7",
"postcss": "^8.4.24",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.8.1",
@@ -48,6 +49,7 @@
"@apollo/client": "^3.7.15",
"axios": "^1.4.0",
"graphql": "^16.6.0",
"openapi-fetch": "^0.2.1",
"radix-icons-svelte": "^1.2.1",
"svelte-apollo": "^0.5.0"
}

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Reiverr</title>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />

View File

@@ -1,12 +0,0 @@
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client/core';
const client = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:4000/graphql'
}),
cache: new InMemoryCache({
addTypename: false
})
});
export default client;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
mutation clearCache {
result: clearRedisCache {
success
message
}
}

View File

@@ -1,14 +0,0 @@
mutation downloadOwnTorrent(
$mediaId: Int!
$mediaType: FileType!
$torrent: String!
) {
downloadOwnTorrent(
mediaId: $mediaId
mediaType: $mediaType
torrent: $torrent
) {
success
message
}
}

View File

@@ -1,20 +0,0 @@
mutation startScanLibrary {
result: startScanLibraryJob {
success
message
}
}
mutation startFindNewEpisodes {
result: startFindNewEpisodesJob {
success
message
}
}
mutation startDownloadMissing {
result: startDownloadMissingJob {
success
message
}
}

View File

@@ -1,31 +0,0 @@
mutation downloadMovie($movieId: Int!, $jackettResult: JackettInput!) {
result: downloadMovie(movieId: $movieId, jackettResult: $jackettResult) {
success
message
}
}
mutation downloadTVEpisode($episodeId: Int!, $jackettResult: JackettInput!) {
result: downloadTVEpisode(
episodeId: $episodeId
jackettResult: $jackettResult
) {
success
message
}
}
mutation downloadSeason(
$tvShowTMDBId: Int!
$seasonNumber: Int!
$jackettResult: JackettInput!
) {
result: downloadSeason(
tvShowTMDBId: $tvShowTMDBId
seasonNumber: $seasonNumber
jackettResult: $jackettResult
) {
success
message
}
}

View File

@@ -1,6 +0,0 @@
mutation removeMovie($tmdbId: Int!) {
result: removeMovie(tmdbId: $tmdbId) {
success
message
}
}

View File

@@ -1,6 +0,0 @@
mutation removeTVShow($tmdbId: Int!) {
result: removeTVShow(tmdbId: $tmdbId) {
success
message
}
}

View File

@@ -1,9 +0,0 @@
mutation resetLibrary($deleteFiles: Boolean!, $resetSettings: Boolean!) {
result: resetLibrary(
deleteFiles: $deleteFiles
resetSettings: $resetSettings
) {
success
message
}
}

View File

@@ -1,6 +0,0 @@
mutation saveQuality($qualities: [QualityInput!]!) {
result: saveQualityParams(qualities: $qualities) {
success
message
}
}

View File

@@ -1,6 +0,0 @@
mutation saveTags($tags: [TagInput!]!) {
result: saveTags(tags: $tags) {
success
message
}
}

View File

@@ -1,5 +0,0 @@
mutation trackMovie($title: String!, $tmdbId: Int!) {
movie: trackMovie(title: $title, tmdbId: $tmdbId) {
id
}
}

View File

@@ -1,5 +0,0 @@
mutation trackTVShow($tmdbId: Int!, $seasonNumbers: [Int!]!) {
tvShow: trackTVShow(tmdbId: $tmdbId, seasonNumbers: $seasonNumbers) {
id
}
}

View File

@@ -1,6 +0,0 @@
mutation updateParams($params: [UpdateParamsInput!]!) {
result: updateParams(params: $params) {
success
message
}
}

View File

@@ -1,22 +0,0 @@
query getCalendar {
calendar: getCalendar {
movies {
id
title
state
releaseDate
}
tvEpisodes {
id
tvShow {
id
title
}
episodeNumber
seasonNumber
state
releaseDate
}
}
}

View File

@@ -1,31 +0,0 @@
query getDiscover(
$entertainment: Entertainment
$originLanguage: String
$primaryReleaseYear: String
$score: Float
$genres: [Float!]
$page: Float
) {
TMDBResults: discover(
entertainment: $entertainment
originLanguage: $originLanguage
primaryReleaseYear: $primaryReleaseYear
score: $score
genres: $genres
page: $page
) {
page
totalResults
totalPages
results {
id
tmdbId
title
posterPath
overview
runtime
voteAverage
releaseDate
}
}
}

View File

@@ -1,18 +0,0 @@
query getDownloading {
searching: getSearchingMedias {
id
title
resourceId
resourceType
}
downloading: getDownloadingMedias {
id
title
tag
quality
torrent
resourceId
resourceType
}
}

View File

@@ -1,13 +0,0 @@
query getGenres {
genres: getGenres {
movieGenres {
id,
name
}
tvShowGenres {
id,
name
}
}
}

View File

@@ -1,6 +0,0 @@
query getLanguages {
languages: getLanguages {
code,
language
}
}

View File

@@ -1,16 +0,0 @@
query getLibraryMovies {
movies: getMovies {
id
tmdbId
title
originalTitle
state
posterPath
overview
runtime
voteAverage
releaseDate
createdAt
updatedAt
}
}

View File

@@ -1,15 +0,0 @@
query getLibraryTVShows {
tvShows: getTVShows {
id
tmdbId
title
originalTitle
posterPath
runtime
overview
voteAverage
releaseDate
createdAt
updatedAt
}
}

View File

@@ -1,26 +0,0 @@
fragment MissingTVEpisodes on EnrichedTVEpisode {
id
seasonNumber
episodeNumber
releaseDate
tvShow {
id
title
}
}
fragment MissingMovies on EnrichedMovie {
id
title
releaseDate
}
query getMissing {
tvEpisodes: getMissingTVEpisodes {
...MissingTVEpisodes
}
movies: getMissingMovies {
...MissingMovies
}
}

View File

@@ -1,8 +0,0 @@
query getMovieFileDetails($tmdbId: Int!) {
details: getMovieFileDetails(tmdbId: $tmdbId) {
id
libraryPath
libraryFileSize
torrentFileName
}
}

View File

@@ -1,11 +0,0 @@
query getParams {
params: getParams {
region
language
tmdb_api_key
jackett_api_key
max_movie_download_size
max_tvshow_episode_download_size
organize_library_strategy
}
}

View File

@@ -1,25 +0,0 @@
query getPopular {
results: getPopular {
movies {
id
tmdbId
title
releaseDate
posterPath
overview
runtime
voteAverage
}
tvShows {
id
tmdbId
title
releaseDate
posterPath
overview
runtime
voteAverage
}
}
}

View File

@@ -1,11 +0,0 @@
query getQuality($type: Entertainment!) {
qualities: getQualityParams(type: $type) {
id
name
match
score
updatedAt
createdAt
type
}
}

View File

@@ -1,23 +0,0 @@
query getRecommended {
tvShows: getRecommendedTVShows {
id
tmdbId
title
releaseDate
posterPath
overview
runtime
voteAverage
}
movies: getRecommendedMovies {
id
tmdbId
title
releaseDate
posterPath
overview
runtime
voteAverage
}
}

View File

@@ -1,9 +0,0 @@
query getTags {
tags: getTags {
id
name
score
createdAt
updatedAt
}
}

View File

@@ -1,14 +0,0 @@
query getTorrentStatus($torrents: [GetTorrentStatusInput!]!) {
torrents: getTorrentStatus(torrents: $torrents) {
id
resourceId
resourceType
percentDone
rateDownload
rateUpload
uploadRatio
uploadedEver
totalSize
status
}
}

View File

@@ -1,22 +0,0 @@
query getTVSeasonDetails($tvShowTMDBId: Int!, $seasonNumber: Int!) {
episodes: getTVSeasonDetails(
tvShowTMDBId: $tvShowTMDBId
seasonNumber: $seasonNumber
) {
id
episodeNumber
seasonNumber
state
updatedAt
voteAverage
releaseDate
createdAt
tvShow {
id
title
tmdbId
updatedAt
createdAt
}
}
}

View File

@@ -1,12 +0,0 @@
query getTVShowSeasons($tvShowTMDBId: Int!) {
seasons: getTVShowSeasons(tvShowTMDBId: $tvShowTMDBId) {
id
name
seasonNumber
episodeCount
overview
posterPath
airDate
inLibrary
}
}

View File

@@ -1,13 +0,0 @@
query omdbSearch(
$title: String!
) {
result: omdbSearch(
title: $title
) {
ratings {
IMDB
rottenTomatoes
metaCritic
}
}
}

View File

@@ -1,18 +0,0 @@
query searchTorrent($query: String!) {
results: searchJackett(query: $query) {
id
title
quality
qualityScore
seeders
peers
link
downloadLink
tag
tagScore
normalizedTitle
normalizedTitleParts
size
publishDate
}
}

View File

@@ -1,25 +0,0 @@
query search($query: String!) {
results: search(query: $query) {
movies {
id
tmdbId
title
releaseDate
posterPath
overview
runtime
voteAverage
}
tvShows {
id
tmdbId
title
releaseDate
posterPath
overview
runtime
voteAverage
}
}
}

5289
src/lib/radarr.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

18
src/lib/servarr-api.ts Normal file
View File

@@ -0,0 +1,18 @@
import createClient from 'openapi-fetch';
import type { paths as radarrPaths } from '$lib/radarr';
import type { paths as sonarrPaths } from '$lib/sonarr';
import { PUBLIC_RADARR_API_KEY, PUBLIC_SONARR_API_KEY } from '$env/static/public';
export const radarrApi = createClient<radarrPaths>({
baseUrl: 'http://radarr.home',
headers: {
'X-Api-Key': PUBLIC_RADARR_API_KEY
}
});
export const sonarrApi = createClient<sonarrPaths>({
baseUrl: 'http://sonarr.home',
headers: {
'X-Api-Key': PUBLIC_SONARR_API_KEY
}
});

5087
src/lib/sonarr.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,3 +7,11 @@ export const TmdbApi = axios.create({
Authorization: `Bearer ${PUBLIC_TMDB_API_KEY}`
}
});
export async function fetchMovieDetails(imdbId: string | number) {
return {
...(await TmdbApi.get('/movie/' + imdbId).then((res) => res.data)),
videos: await TmdbApi.get('/movie/' + imdbId + '/videos').then((res) => res.data.results),
credits: await TmdbApi.get('/movie/' + imdbId + '/credits').then((res) => res.data.cast)
};
}

6
src/lib/types.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { components as radarrComponents } from '$lib/radarr';
import type { components as sonarrComponents } from '$lib/sonarr';
export type MovieResource = radarrComponents['schemas']['MovieResource'];
export type SeriesResource = sonarrComponents['schemas']['SeriesResource'];

View File

@@ -1,10 +1,7 @@
<script>
import '../app.css';
import { setClient } from 'svelte-apollo';
import client from '$lib/apollo-client';
import Navbar from './Navbar.svelte';
setClient(client);
</script>
<div class="app">

View File

@@ -1,156 +1,32 @@
<script lang="ts">
import { getPopular } from '$lib/graphql';
import { ChevronDown, ChevronLeft, ChevronRight, Dot, DotFilled } from 'radix-icons-svelte';
import SmallPoster from './SmallPoster.svelte';
import type { PageData } from './$types';
import ResourceDetails from './ResourceDetails/ResourceDetails.svelte';
import ResourceDetailsControls from './ResourceDetailsControls.svelte';
const popular = getPopular({});
// let bgUrl = '';
//
// $: {
// const popularMovie = $popular.data?.results?.movies[2];
//
// if (popularMovie) {
// TmdbApi.get('/movie/' + popularMovie.tmdbId + '/images')
// .then((res) => {
// console.log('GOt response');
// bgUrl = 'https://www.themoviedb.org/t/p/original/' + res.data.backdrops[0].file_path;
// })
// .catch(console.error);
// }
// }
const backgroundUrl = 'https://www.themoviedb.org/t/p/original/5AcP07WJl1VZbnloLZrMVgYjR2s.jpg';
$: if ($popular.data.results) console.log($popular.data.results);
export let data: PageData;
let movies = data.showcases;
let movie = movies[0];
</script>
<div
class="h-screen w-screen bg-center bg-cover"
style={"background-image: url('" + backgroundUrl + "')"}
>
<div
class="bg-[#070501bf] h-full w-full flex px-16 pb-12 pt-32 grid grid-cols-[1fr_max-content] grid-rows-[1fr_min-content] gap-x-16 gap-y-8 relative"
>
<div class="flex flex-col justify-self-start min-w-0">
<!-- <h2 class="tracking-wider font-display font-light text-amber-200">Popular Now</h2>-->
<!-- <h1 class="uppercase text-8xl font-bold font-display">Top Gun: Maveric</h1>-->
<!-- <h2 class="text-zinc-300 text-sm self-end">-->
<!-- <span class="font-bold uppercase tracking-wider">September</span> 2022-->
<!-- </h2>-->
<div class="relative">
<h2 class="text-zinc-300 text-sm self-end">
<span class="font-bold uppercase tracking-wider">September</span> 2022
</h2>
<h2
class="tracking-wider font-display font-extrabold text-amber-300 absolute opacity-10 text-8xl -ml-6 mt-8"
>
Popular Now
</h2>
<h1 class="uppercase text-8xl font-bold font-display z-[1] relative">Top Gun: Maveric</h1>
</div>
<ResourceDetails remoteResource={movie}>
<ResourceDetailsControls slot="page-controls" />
</ResourceDetails>
<div class="mt-auto max-w-3xl flex flex-col gap-4">
<div class="text-xl font-semibold tracking-wider">Feel the need... The need for speed.</div>
<div
class="tracking-wider text-zinc-200 font-light leading-6 pl-4 border-l-2 border-zinc-300"
>
After more than thirty years of service as one of the Navys top aviators, and dodging the
advancement in rank that would ground him, Pete “Maverick” Mitchell finds himself training
a detachment of TOP GUN graduates for a specialized mission the likes of which no living
pilot has ever seen.
</div>
</div>
<div class="flex gap-6 mt-10">
<div class="flex gap-1">
<button
class="bg-white border-2 border-white hover:bg-amber-400 hover:border-amber-400 transition-colors text-zinc-900 px-8 py-3.5 uppercase tracking-widest font-extrabold cursor-pointer text-xs"
>Stream</button
>
<div
class="hidden items-center justify-center border-2 border-white w-10 cursor-pointer hover:bg-white hover:text-zinc-900 transition-colors"
>
<ChevronDown size="20" />
</div>
</div>
<button
class="border-2 border-white cursor-pointer transition-colors px-8 py-3.5 uppercase tracking-widest font-semibold text-xs hover:bg-white hover:text-black"
>Watch Trailer</button
>
</div>
</div>
<div class="flex flex-col gap-6">
<h3 class="text-xs tracking-wide uppercase">Details</h3>
<div class="flex flex-col gap-1">
<div class="tracking-widest font-extralight text-sm">Action, Drama</div>
<div class="tracking-widest font-extralight text-sm">2h 13min</div>
<div class="tracking-widest font-extralight text-sm">Currently <b>Streaming</b></div>
<div class="tracking-widest font-extralight text-sm"><b>83%</b> IMDB</div>
<div class="flex mt-4">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="text-white w-4"
><g
><path d="M0 0h24v24H0z" fill="none" /><path
d="M11.29 3.814l2.02 5.707.395 1.116.007-4.81.01-4.818h4.27L18 11.871c.003 5.98-.003 10.89-.015 10.9-.012.009-.209 0-.436-.027-.989-.118-2.29-.236-3.34-.282a14.57 14.57 0 0 1-.636-.038c-.003-.004-.273-.762-.776-2.184v-.004l-2.144-6.061-.34-.954-.008 4.586c-.006 4.365-.01 4.61-.057 4.61-.163 0-1.57.09-2.04.136-.308.027-.926.09-1.37.145-.446.051-.816.085-.823.078C6.006 22.77 6 17.867 6 11.883V1.002h.005V1h4.288l.028.08c.007.016.065.176.157.437l.641 1.778.173.496-.001.023z"
fill-rule="evenodd"
fill="currentColor"
/></g
></svg
>
</div>
</div>
<h3 class="text-xs tracking-wide uppercase">Starring</h3>
<div class="flex flex-col gap-1">
<div class="tracking-widest font-extralight text-sm">Tom Cruise</div>
<div class="tracking-widest font-extralight text-sm">Miles Teller</div>
<div class="tracking-widest font-extralight text-sm">Jennifer Connelly</div>
<div class="tracking-widest font-extralight text-sm">Jon Hamm</div>
<div class="tracking-widest font-extralight text-sm">Glen Powell</div>
</div>
</div>
<div class="relative z-[1] flex gap-8 justify-between items-center col-span-2">
<div class="p-4 cursor-pointer text-zinc-200 hover:text-zinc-100">
<ChevronLeft size="24" />
</div>
<div class="flex gap-2">
<DotFilled size="15" />
<DotFilled size="15" class="opacity-20" />
<DotFilled size="15" class="opacity-20" />
<DotFilled size="15" class="opacity-20" />
<DotFilled size="15" class="opacity-20" />
</div>
<div class="p-4 cursor-pointer text-zinc-200 hover:text-zinc-100">
<ChevronRight size="24" />
</div>
</div>
<div class="absolute inset-x-0 bottom-6 flex justify-center mx-auto opacity-50">
<ChevronDown size="20" />
</div>
</div>
</div>
{#if $popular.loading}
{#if !data?.showcases?.length}
<div>Loading</div>
{:else if $popular.error}
<div>Error: {$popular.error.message}</div>
{:else}
<div class="" style={"background-image: url('" + backgroundUrl + "')"}>
<div class="p-8 flex flex-col gap-6 backdrop-blur-xl bg-[#000000ee]">
<div
class="bg-cover bg-center"
style={"background-image: url('https://www.themoviedb.org/t/p/original/" +
movie.backdrop_path +
"')"}
>
<div class="p-8 flex flex-col gap-6 backdrop-blur-xl bg-[#000000dd]">
<h1 class="uppercase tracking-widest font-bold">Continue Watching</h1>
<!-- <h1-->
<!-- class="text-black bg-white px-6 py-3 inline-block text-xs uppercase tracking-widest font-bold self-start"-->
<!-- >-->
<!-- Continue Watching-->
<!-- </h1>-->
<!-- <h1-->
<!-- class="text-white px-6 py-3 inline-block text-xs uppercase tracking-widest font-bold self-start border-2 border-white"-->
<!-- >-->
<!-- Continue Watching-->
<!-- </h1>-->
<div class="flex gap-4 overflow-x-scroll">
{#each $popular.data.results.movies.splice(0, 5) as item (item.id)}
<SmallPoster tmdbId={item.tmdbId} />
{/each}
{#each $popular.data.results.tvShows.splice(0, 2) as item (item.id)}
<SmallPoster type="tv" tmdbId={item.tmdbId} />
{#each data.showcases.splice(0, 5) as item (item.id)}
<SmallPoster tmdbId={item.id} />
{/each}
</div>
</div>

12
src/routes/+page.ts Normal file
View File

@@ -0,0 +1,12 @@
import type { PageLoad } from './$types';
import { radarrApi } from '$lib/servarr-api';
import { fetchMovieDetails, TmdbApi } from '$lib/tmdb-api';
export const load = (async () => {
const movies = await TmdbApi.get('/movie/popular').then((res) => res.data.results.slice(0, 5));
console.log('movies', movies);
const showcases = await Promise.all(movies.map((m: any) => fetchMovieDetails(m.id)));
console.log('showcases: ', showcases);
return { showcases };
}) satisfies PageLoad;

View File

@@ -1,11 +1,16 @@
<script>
import { MagnifyingGlass, Person } from 'radix-icons-svelte';
import classNames from 'classnames';
import { page } from '$app/stores';
let y = 0;
let transparent = true;
let baseStyle = '';
function getLinkStyle(path) {
return $page.url.pathname === path ? 'text-amber-200' : 'hover:text-zinc-50 cursor-pointer';
}
$: {
transparent = y === 0;
baseStyle = classNames(
@@ -29,11 +34,11 @@
<div
class="flex items-center justify-center gap-8 font-normal text-sm tracking-wider text-zinc-200"
>
<div class="cursor-pointer text-amber-200">Home</div>
<div class="hover:text-zinc-50 cursor-pointer">Discover</div>
<div class="hover:text-zinc-50 cursor-pointer">Library</div>
<div class="hover:text-zinc-50 cursor-pointer">Sources</div>
<div class="hover:text-zinc-50 cursor-pointer">Settings</div>
<a href="/" class={$page && getLinkStyle('/')}>Home</a>
<a href="/discover" class={$page && getLinkStyle('/discover')}>Discover</a>
<a href="/library" class={$page && getLinkStyle('/library')}>Library</a>
<a href="/sources" class={$page && getLinkStyle('/sources')}>Sources</a>
<a href="/settings" class={$page && getLinkStyle('/settings')}>Settings</a>
</div>
<div class="flex gap-2">
<div class="p-2 cursor-pointer text-zinc-200 hover:text-zinc-50">

View File

@@ -0,0 +1,206 @@
<script lang="ts">
import type { MovieResource } from '$lib/types';
import { ChevronDown, Clock } from 'radix-icons-svelte';
import { onMount } from 'svelte';
import classNames from 'classnames';
import { fade } from 'svelte/transition';
export let resource: MovieResource;
export let trailer = true;
export let remoteResource: any;
let video: any = remoteResource.videos.filter(
(v) => v.site === 'YouTube' && v.type === 'Trailer'
)?.[0];
const monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
const releaseDate = new Date(remoteResource.release_date);
let showTrailer = false;
let focusTrailer = false;
let trailerStartTime = 0;
function openTrailer() {
window
.open(
'https://www.youtube.com/watch?v=' +
video.key +
'&autoplay=1&t=' +
(trailerStartTime === 0 ? 0 : Math.floor((Date.now() - trailerStartTime) / 1000)),
'_blank'
)
.focus();
}
let opacityStyle;
const transitionStyle = 'transition: opacity 0.3s ease-in-out;';
$: opacityStyle = (focusTrailer ? 'opacity: 0;' : 'opacity: 100;') + transitionStyle;
onMount(() => {
setTimeout(() => {
showTrailer = trailer;
trailerStartTime = Date.now();
}, 2500);
});
</script>
<div
class="h-screen w-screen bg-center bg-cover relative overflow-hidden"
style={"background-image: url('https://www.themoviedb.org/t/p/original" +
remoteResource.backdrop_path +
"')"}
>
<div class="youtube-container absolute h-full scale-[150%]">
{#if video.key}
<iframe
class={classNames('transition-opacity', {
'opacity-100': showTrailer,
'opacity-0': !showTrailer
})}
transition:fade
src={'https://www.youtube.com/embed/' +
video.key +
'?autoplay=1&mute=1&loop=1&color=white&controls=0&modestbranding=1&playsinline=1&rel=0&enablejsapi=1'}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
{/if}
</div>
<div
class={classNames(
'bg-gradient-to-b from-[#070501bf] via-20% via-transparent transition-opacity absolute inset-0 z-[1]',
{
'opacity-100': focusTrailer,
'opacity-0': !focusTrailer
}
)}
/>
<div
class={classNames(
'h-full w-full px-16 pb-12 pt-32',
'grid grid-cols-[1fr_max-content] grid-rows-[1fr_min-content] gap-x-16 gap-y-8 relative z-[2]',
'transition-colors',
{
'bg-[#070501bf]': !focusTrailer,
'bg-[#00000000]': focusTrailer
}
)}
>
<div class="flex flex-col justify-self-start min-w-0 row-span-full">
<div class="relative" style={opacityStyle}>
<h2 class="text-zinc-300 text-sm self-end">
<span class="font-bold uppercase tracking-wider"
>{monthNames[releaseDate.getMonth()]}</span
>
{releaseDate.getFullYear()}
</h2>
<h2
class="tracking-wider font-display font-extrabold text-amber-300 absolute opacity-10 text-8xl -ml-6 mt-8"
>
<slot name="reason">Popular Now</slot>
</h2>
<h1 class="uppercase text-8xl font-bold font-display z-[1] relative">
{remoteResource.original_title}
</h1>
</div>
<div class="mt-auto max-w-3xl flex flex-col gap-4" style={opacityStyle}>
<div class="text-xl font-semibold tracking-wider">{remoteResource.tagline}</div>
<div
class="tracking-wider text-zinc-200 font-light leading-6 pl-4 border-l-2 border-zinc-300"
>
{remoteResource.overview}
</div>
</div>
<div class="flex gap-6 mt-10">
<div class="flex gap-1">
<button
class="bg-white border-2 border-white hover:bg-amber-400 hover:border-amber-400 transition-colors text-zinc-900 px-8 py-3.5 uppercase tracking-widest font-extrabold cursor-pointer text-xs"
style={opacityStyle}>Stream</button
>
<div
class="hidden items-center justify-center border-2 border-white w-10 cursor-pointer hover:bg-white hover:text-zinc-900 transition-colors"
>
<ChevronDown size="20" />
</div>
</div>
<button
on:mouseover={() => (focusTrailer = true)}
on:mouseleave={() => (focusTrailer = false)}
on:click={openTrailer}
class="border-2 border-white cursor-pointer transition-colors px-8 py-3.5 uppercase tracking-widest font-semibold text-xs hover:bg-white hover:text-black opacity-100"
>Watch Trailer</button
>
</div>
</div>
<div class="flex flex-col gap-6 max-w-[14rem] row-span-full" style={opacityStyle}>
<h3 class="text-xs tracking-wide uppercase">Details</h3>
<div class="flex flex-col gap-1">
<div class="tracking-widest font-extralight text-sm">
{remoteResource.genres
.map((g) => g.name.charAt(0).toUpperCase() + g.name.slice(1))
.join(', ')}
</div>
<div class="flex gap-1.5 items-center">
<Clock size="14" />
<div class="tracking-widest font-extralight text-sm">
{Math.floor(remoteResource.runtime / 60)}h {remoteResource.runtime % 60}m
</div>
</div>
<div class="tracking-widest font-extralight text-sm">Currently <b>Streaming</b></div>
<div class="tracking-widest font-extralight text-sm">
<b>{remoteResource.vote_average}</b> TMDB
</div>
<div class="flex mt-4">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="text-white w-4"
><g
><path d="M0 0h24v24H0z" fill="none" /><path
d="M11.29 3.814l2.02 5.707.395 1.116.007-4.81.01-4.818h4.27L18 11.871c.003 5.98-.003 10.89-.015 10.9-.012.009-.209 0-.436-.027-.989-.118-2.29-.236-3.34-.282a14.57 14.57 0 0 1-.636-.038c-.003-.004-.273-.762-.776-2.184v-.004l-2.144-6.061-.34-.954-.008 4.586c-.006 4.365-.01 4.61-.057 4.61-.163 0-1.57.09-2.04.136-.308.027-.926.09-1.37.145-.446.051-.816.085-.823.078C6.006 22.77 6 17.867 6 11.883V1.002h.005V1h4.288l.028.08c.007.016.065.176.157.437l.641 1.778.173.496-.001.023z"
fill-rule="evenodd"
fill="currentColor"
/></g
></svg
>
</div>
</div>
<h3 class="text-xs tracking-wide uppercase">Starring</h3>
<div class="flex flex-col gap-1">
{#each remoteResource.credits.slice(0, 5) as a}
<div class="tracking-widest font-extralight text-sm">{a.name}</div>
{/each}
</div>
</div>
<slot name="page-controls" />
</div>
</div>
<style>
.youtube-container {
overflow: hidden;
width: 100%;
aspect-ratio: 16/9;
pointer-events: none;
}
.youtube-container iframe {
width: 300%;
height: 100%;
margin-left: -100%;
}
</style>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { ChevronDown, ChevronLeft, ChevronRight, DotFilled } from 'radix-icons-svelte';
</script>
<div class="relative z-[1] flex gap-8 justify-between items-center col-span-2">
<div class="p-4 cursor-pointer text-zinc-200 hover:text-zinc-100">
<ChevronLeft size="24" />
</div>
<div class="flex gap-2">
<DotFilled size="15" />
<DotFilled size="15" class="opacity-20" />
<DotFilled size="15" class="opacity-20" />
<DotFilled size="15" class="opacity-20" />
<DotFilled size="15" class="opacity-20" />
</div>
<div class="p-4 cursor-pointer text-zinc-200 hover:text-zinc-100">
<ChevronRight size="24" />
</div>
</div>
<div class="absolute inset-x-0 bottom-6 flex justify-center mx-auto opacity-50">
<ChevronDown size="20" />
</div>

View File

@@ -60,9 +60,10 @@
class="bg-white border-2 border-white hover:bg-amber-400 hover:border-amber-400 transition-colors text-zinc-900 px-8 py-2.5 uppercase tracking-widest font-extrabold cursor-pointer text-xs"
>Stream</button
>
<button
class="border-2 border-white cursor-pointer transition-colors px-8 py-2.5 uppercase tracking-widest font-semibold text-xs hover:bg-amber-400 hover:text-black"
>Details</button
<a
href={'/' + type + '/' + tmdbId}
class="border-2 border-white cursor-pointer transition-colors px-8 py-2.5 uppercase tracking-widest font-semibold text-xs hover:bg-amber-400 hover:text-black text-center"
>Details</a
>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="pt-24">
Does not contain any of the titles in library.
<div>Discover</div>
<div>For You</div>
<div>Popular Movies</div>
<div>Popular TV Shows</div>
<div>Networks</div>
<div>Categories</div>
</div>

View File

@@ -0,0 +1,6 @@
<div class="pt-24">
Contains all the titles available locally, the ones already watched previously (greyed out at the
bottom), and the ones that are in some sort of watchlist and available via any source.
<div>Library</div>
</div>

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import type { PageData } from './$types';
import ResourceDetails from '../../ResourceDetails/ResourceDetails.svelte';
import ResourceDetailsControls from '../../ResourceDetailsControls.svelte';
export let data: PageData;
console.log('data', data);
</script>
<ResourceDetails resource={data.movie} remoteResource={data.remoteMovie} />

View File

@@ -0,0 +1,18 @@
import type { PageLoad } from './$types';
import { radarrApi } from '$lib/servarr-api';
import { fetchMovieDetails, TmdbApi } from '$lib/tmdb-api';
export const load = (async ({ params }) => {
return {
movie: await radarrApi
.get('/api/v3/movie', {
params: {
query: {
tmdbId: Number(params.id)
}
}
})
.then((res) => res.data?.[0]),
remoteMovie: fetchMovieDetails(params.id)
};
}) satisfies PageLoad;

View File

@@ -0,0 +1 @@
<div>Settings</div>

View File

@@ -0,0 +1,6 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<div>TV Show of {data.tmdbId}</div>

View File

@@ -0,0 +1,3 @@
import type { PageLoad } from './$types';
export const load = (({ params }) => ({ tmdbId: params.id })) satisfies PageLoad;

View File

@@ -0,0 +1,11 @@
<div class="pt-24">
<div>Sources</div>
<div>Streaming services</div>
<div>(P2P network?)</div>
<div>Configure Sonarr</div>
<div>Configure Radarr</div>
<div>(Configure Plex)</div>
<div>(Configure Jellyfinn)</div>
<div>(Configure IMDB)</div>
<div>(Configure TMDB)</div>
</div>