Initial work on library page

This commit is contained in:
Aleksi Lassila
2023-06-15 02:08:47 +03:00
parent c463bb89e9
commit e41b030d45
15 changed files with 275 additions and 26 deletions

View File

@@ -10,7 +10,7 @@
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Inter:wght@100;200;300;400;500;600;700;800;900&family=Nunito+Sans:wght@200;300;400;500;600;700;800;900;1000&display=swap" rel="stylesheet">
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="bg-zinc-950 min-h-screen text-white">
<body data-sveltekit-preload-data="hover" class="bg-stone-950 min-h-screen text-white">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -8,14 +8,37 @@ export const TmdbApi = axios.create({
}
});
export async function fetchMovieDetails(imdbId: string | number) {
export async function fetchMovieDetails(imdbId: string | number): Promise<TmdbMovieFull> {
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)
videos: await TmdbApi.get<VideosResponse>('/movie/' + imdbId + '/videos').then(
(res) => res.data.results
),
images: await TmdbApi.get<ImagesResponse>('/movie/' + imdbId + '/images').then((res) => {
return {
backdrops: res.data.backdrops,
logos: res.data.logos,
posters: res.data.posters
};
}),
credits: await TmdbApi.get<CreditsResponse>('/movie/' + imdbId + '/credits').then(
(res) => res.data.cast
)
};
}
export interface TmdbMovieFull extends TmdbMovie {
videos: Video[];
images: {
backdrops: Backdrop[];
logos: Logo[];
posters: Poster[];
};
credits: CastMember[];
}
export type MovieDetailsResponse = TmdbMovie;
export interface TmdbMovie {
adult: boolean;
backdrop_path: string;
@@ -119,3 +142,40 @@ export interface Video {
published_at: string;
id: string;
}
export interface ImagesResponse {
backdrops: Backdrop[];
id: number;
logos: Logo[];
posters: Poster[];
}
export interface Backdrop {
aspect_ratio: number;
height: number;
iso_639_1?: string;
file_path: string;
vote_average: number;
vote_count: number;
width: number;
}
export interface Logo {
aspect_ratio: number;
height: number;
iso_639_1: string;
file_path: string;
vote_average: number;
vote_count: number;
width: number;
}
export interface Poster {
aspect_ratio: number;
height: number;
iso_639_1?: string;
file_path: string;
vote_average: number;
vote_count: number;
width: number;
}

12
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,12 @@
import type { Genre } from '$lib/tmdb-api';
export function getRuntime(minutes: number) {
const hours = Math.floor(minutes / 60);
const mins = Math.floor(minutes % 60);
return `${hours > 0 ? hours + 'h ' : ''}${mins}min`;
}
export function formatGenres(genres: Genre[]) {
return genres.map((genre) => genre.name.charAt(0).toUpperCase() + genre.name.slice(1)).join(', ');
}

View File

@@ -1,7 +1,7 @@
<script>
import '../app.css';
import { setClient } from 'svelte-apollo';
import Navbar from './Navbar.svelte';
import Navbar from './components/Navbar.svelte';
</script>
<div class="app">

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import SmallPoster from './SmallPoster.svelte';
import SmallPoster from './components/SmallPoster/SmallPoster.svelte';
import type { PageData } from './$types';
import ResourceDetails from './ResourceDetails/ResourceDetails.svelte';
import ResourceDetails from './components/ResourceDetails/ResourceDetails.svelte';
import ResourceDetailsControls from './ResourceDetailsControls.svelte';
export let data: PageData;

View File

@@ -18,6 +18,6 @@
<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 class="absolute inset-x-0 bottom-6 flex justify-center mx-auto opacity-50">-->
<!-- <ChevronDown size="20" />-->
<!--</div>-->

View File

@@ -66,7 +66,7 @@
remoteResource.backdrop_path +
"')"}
>
<div class="youtube-container absolute h-full scale-[150%]">
<div class="youtube-container absolute h-full scale-[150%] hidden sm:block">
{#if video.key}
<iframe
class={classNames('transition-opacity', {
@@ -86,7 +86,7 @@
</div>
<div
class={classNames(
'bg-gradient-to-b from-[#070501bf] via-20% via-transparent transition-opacity absolute inset-0 z-[1]',
'bg-gradient-to-b from-darken via-20% via-transparent transition-opacity absolute inset-0 z-[1]',
{
'opacity-100': focusTrailer,
'opacity-0': !focusTrailer
@@ -95,12 +95,12 @@
/>
<div
class={classNames(
'h-full w-full px-16 pb-12 pt-32',
'h-full w-full px-16 pb-8 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
'bg-darken': !focusTrailer,
'bg-transparent': focusTrailer
}
)}
>

View File

@@ -0,0 +1,15 @@
<script>
import classNames from 'classnames';
export let value = '';
export let filled = false;
</script>
<div
class={classNames('border rounded p-[0px] px-1 text-[10px] font-medium', {
'text-zinc-200 border-zinc-500': !filled,
'bg-zinc-200 border-zinc-200 text-zinc-900': filled
})}
>
{value}
</div>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import type { TmdbMovieFull } from '$lib/tmdb-api';
import { formatGenres, getRuntime } from '$lib/utils';
import classNames from 'classnames';
export let tmdbMovie: TmdbMovieFull;
export let available = true;
export let progress = 0;
export let progressType: 'watched' | 'downloading' = 'watched';
export let randomProgress = false;
if (randomProgress) {
progress = Math.random() > 0.3 ? Math.random() * 100 : 0;
}
const backdropUrl =
'https://www.themoviedb.org/t/p/original' +
tmdbMovie.images.backdrops.filter((b) => b.iso_639_1 === 'en')[0].file_path;
</script>
<div
style={"background-image: url('" + backdropUrl + "')"}
class="bg-center bg-cover h-40 w-72 rounded overflow-hidden relative drop-shadow-2xl"
>
<div style={'width: ' + progress + '%'} class="h-[2px] bg-zinc-200 bottom-0 absolute z-[1]" />
<div
on:click={() => window.open('/movie/' + tmdbMovie.id, '_self')}
class="h-full w-full opacity-0 hover:opacity-100 transition-opacity flex flex-col justify-between cursor-pointer p-2 px-3 relative z-[1] peer"
style={progress > 0 ? 'padding-bottom: 0.6rem;' : ''}
>
<div>
<h1 class="font-bold tracking-wider">{tmdbMovie.original_title}</h1>
<div class="text-xs text-zinc-300 tracking-wider font-medium">
{formatGenres(tmdbMovie.genres)}
</div>
</div>
<div class="flex justify-between items-end">
{#if progressType === 'watched'}
<div class="text-xs font-medium text-zinc-200">
{progress
? getRuntime(tmdbMovie.runtime - tmdbMovie.runtime * (progress / 100)) + ' left'
: getRuntime(tmdbMovie.runtime)}
</div>
{:else if progressType === 'downloading'}
<div class="text-xs font-medium text-zinc-200">
{Math.floor(progress) + '% Downloaded'}
</div>
{/if}
</div>
</div>
<div
class={classNames('absolute inset-0', {
'bg-darken opacity-0 peer-hover:opacity-100': available,
'bg-[#00000055] peer-hover:bg-darken': !available
})}
/>
</div>

View File

@@ -3,14 +3,15 @@
import { onMount } from 'svelte';
export let tmdbId;
export let progress = 0;
export let randomProgress = false;
if (randomProgress) progress = Math.random() > 0.5 ? Math.round(Math.random() * 100) : 100;
export let type: 'movie' | 'tv' = 'movie';
let bg = '';
let title = 'Loading...';
const progress = Math.random() > 0.5 ? Math.round(Math.random() * 100) : 100;
onMount(() => {
TmdbApi.get('/' + type + '/' + tmdbId)
.then((res) => res.data)
@@ -35,9 +36,7 @@
class="bg-center bg-cover aspect-[2/3] h-72 shadow-2xl m-1.5"
style={"background-image: url('" + bg + "')"}
>
<div
class="w-full h-full bg-gradient-to-b from-[#00000099] via-20% via-transparent hover:bg-[#00000099] transition-all flex"
>
<div class="w-full h-full hover:bg-darken transition-all flex">
<div
class="opacity-0 group-hover:opacity-100 transition-opacity p-2 flex flex-col justify-between flex-1 cursor-pointer"
>

View File

@@ -1,6 +1,75 @@
<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.
<script lang="ts">
import type { PageData } from './$types';
import SmallHorizontalPoster from '../components/SmallHorizontalPoster/SmallHorizontalPoster.svelte';
import type { TmdbMovieFull } from '$lib/tmdb-api';
export let data: PageData;
console.log(data);
<div>Library</div>
const allMovies: Record<string, TmdbMovieFull> = {};
data.tmdbMovies.forEach((m) => (allMovies[m.id] = m));
const tmdbIdToDownloading = {};
(data.downloading as any).forEach((d) => (tmdbIdToDownloading[d.movie.tmdbId] = d));
const tmdbIdToRadarrMovie = {};
(data.radarrMovies as any).forEach((r) => (tmdbIdToRadarrMovie[r.tmdbId] = r));
const downloading = data.tmdbMovies.filter((m) => tmdbIdToDownloading[m.id] !== undefined);
const available = data.tmdbMovies.filter((m) => tmdbIdToDownloading[m.id] === undefined);
const unavailable = data.tmdbMovies.filter(
(m) => !tmdbIdToRadarrMovie[m.id]?.hasFile && !tmdbIdToDownloading[m.id]
);
const watched = [];
const posterGridStyle = 'flex flex-wrap justify-center gap-x-4 gap-y-8';
const headerStyle = 'uppercase tracking-widest font-bold text-center mt-2';
</script>
<div
style="background-image: url('https://www.themoviedb.org/t/p/original/vvjYv7bSWerbsi0LsMjLnTVOX7c.jpg')"
>
<div class="py-24 backdrop-blur-2xl bg-darken px-8 flex flex-col gap-4">
<!-- 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 not available via any source.-->
<!-- <div>Library</div>-->
{#if downloading.length > 0}
<h1 class={headerStyle}>Downloading</h1>
<div class={posterGridStyle}>
{#each downloading as movie (movie.id)}
<SmallHorizontalPoster
progress={(tmdbIdToDownloading[movie.id].sizeleft /
tmdbIdToDownloading[movie.id].size) *
100}
progressType="downloading"
available={false}
tmdbMovie={movie}
/>
{/each}
</div>
{/if}
{#if available.length > 0}
<h1 class={headerStyle}>Available</h1>
<div class={posterGridStyle}>
{#each available as movie (movie.id)}
<SmallHorizontalPoster randomProgress={true} tmdbMovie={movie} />
{/each}
</div>
{/if}
{#if unavailable.length > 0}
<h1 class={headerStyle}>Unavailable</h1>
<div class={posterGridStyle}>
{#each unavailable as movie (movie.id)}
<SmallHorizontalPoster available={false} tmdbMovie={movie} />
{/each}
</div>
{/if}
{#if watched.length > 0}
<h1 class={headerStyle}>Watched</h1>
{/if}
</div>
</div>

View File

@@ -0,0 +1,34 @@
import type { PageLoad } from './$types';
import { radarrApi } from '$lib/servarr-api';
import { fetchMovieDetails } from '$lib/tmdb-api';
export const load = (async () => {
const radarrMovies = await radarrApi
.get('/api/v3/movie', {
params: {}
})
.then((r) => r.data);
let tmdbMovies;
if (radarrMovies) {
tmdbMovies = await Promise.all(
radarrMovies.filter((m) => m.tmdbId).map((m) => fetchMovieDetails(m.tmdbId as any))
);
}
console.log('radarrMovies', radarrMovies);
return {
radarrMovies,
tmdbMovies,
downloading: await radarrApi
.get('/api/v3/queue', {
params: {
query: {
includeMovie: true
}
}
})
.then((r) => r.data?.records)
};
}) satisfies PageLoad;

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types';
import ResourceDetails from '../../ResourceDetails/ResourceDetails.svelte';
import ResourceDetails from '../../components/ResourceDetails/ResourceDetails.svelte';
export let data: PageData;
</script>
<ResourceDetails trailer={false} resource={data.movie} remoteResource={data.remoteMovie} />
<ResourceDetails resource={data.movie} remoteResource={data.remoteMovie} />

View File

@@ -6,6 +6,9 @@ export default {
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Inter', 'system', 'sans-serif']
},
colors: {
darken: '#070501bf'
}
}
},