refactor: project structure, absolute paths

This commit is contained in:
Aleksi Lassila
2025-02-02 04:25:38 +02:00
parent ab401cf69e
commit 27467a28b2
81 changed files with 227 additions and 220 deletions

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import type { Readable } from 'svelte/store';
import classNames from 'classnames';
import AnimatedSelection from './AnimateScale.svelte';

View File

@@ -4,7 +4,7 @@
import ProgressBar from '../ProgressBar.svelte';
import LazyImg from '../LazyImg.svelte';
import type { TitleType } from '../../types';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import type { Readable } from 'svelte/store';
import AnimatedSelection from '../AnimateScale.svelte';
import { navigate } from '../StackRouter/StackRouter';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import { onMount } from 'svelte';
import classNames from 'classnames';
import { getCardDimensions } from '../utils';

View File

@@ -2,7 +2,7 @@
import IconButton from '../IconButton.svelte';
import { ChevronLeft, ChevronRight } from 'radix-icons-svelte';
import classNames from 'classnames';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { PLATFORM_TV } from '../../constants';
import type { BackEvent } from '../../selectable';
import { get } from 'svelte/store';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
export let size: 'dynamic' | 'md' | 'lg' = 'md';
export let orientation: 'landscape' | 'portrait' = 'landscape';
</script>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import classNames from 'classnames';
import { onMount } from 'svelte';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
let element: Container;
let scrollX = 0;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import { createEventDispatcher } from 'svelte';
import { Check } from 'radix-icons-svelte';
import classNames from 'classnames';

View File

@@ -0,0 +1,140 @@
<svelte:options accessors />
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { Selectable, type EnterEvent, type NavigateEvent, type KeyEvent } from '../selectable';
import classNames from 'classnames';
/**
* The basis for d-pad navigation. It's a mess, but it works™
*/
const dispatch = createEventDispatcher<{
click: MouseEvent;
select: null;
clickOrSelect: Selectable;
enter: EnterEvent;
mount: Selectable;
navigate: NavigateEvent;
back: KeyEvent;
playPause: KeyEvent;
}>();
export let name: string = '';
export let direction: 'vertical' | 'horizontal' | 'grid' = 'vertical';
export let gridCols: number = 0;
export let focusOnMount = false;
export let trapFocus = false;
export let debugOutline = false;
export let focusOnClick = false;
export let focusedChild = false;
export let disabled = false;
const { registerer, ...rest } = new Selectable(name)
.setDirection(direction === 'grid' ? 'horizontal' : direction)
.setGridColumns(gridCols)
.setTrapFocus(trapFocus)
.setOnFocus((selectable, options) => {
function stopPropagation() {
options.propagate = false;
}
dispatch('enter', { selectable, options, stopPropagation });
})
.setOnNavigate((selectable, options, willLeaveContainer) => {
function preventNavigation() {
options.preventNavigation = true;
}
function stopPropagation() {
options.propagate = false;
}
dispatch('navigate', {
selectable,
options,
willLeaveContainer,
preventNavigation,
stopPropagation,
direction: options.direction
});
})
.setOnSelect(() => {
dispatch('select');
dispatch('clickOrSelect', rest.container);
})
.setOnBack((selectable, options) => {
function stopPropagation() {
options.propagate = false;
}
function bubble() {
options.propagate = true;
}
dispatch('back', { selectable, options, stopPropagation, bubble });
})
.setOnPlayPause((selectable, options) => {
function stopPropagation() {
options.propagate = false;
}
function bubble() {
options.propagate = true;
}
dispatch('playPause', { selectable, options, stopPropagation, bubble });
})
.setAsFocusedChild(focusedChild)
.getStores();
export const selectable = rest.container;
export const hasFocus = rest.hasFocus;
export const hasFocusWithin = rest.hasFocusWithin;
export const focusIndex = rest.focusIndex;
export const activeChild = rest.activeChild;
export let tag = 'div';
$: selectable.setIsDisabled(disabled);
$: selectable.setGridColumns(gridCols);
function handleClick(e: MouseEvent) {
if (focusOnClick) {
selectable.focus();
}
dispatch('click', e);
dispatch('clickOrSelect', rest.container);
}
onMount(() => {
rest.container._mountSelectable(focusOnMount);
dispatch('mount', rest.container);
return () => {
rest.container._unmountContainer();
};
});
</script>
<svelte:element
this={tag}
on:click={handleClick}
on:mousemove
tabindex={disabled ? -1 : 0}
{...$$restProps}
class={classNames($$restProps.class, {
'outline-none': debugOutline === false
})}
use:registerer
>
<slot
hasFocus={$hasFocus}
hasFocusWithin={$hasFocusWithin}
focusIndex={$focusIndex}
{selectable}
/>
</svelte:element>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { type KeyEvent, type NavigateEvent, useRegistrar } from '../../selectable.js';
import { get } from 'svelte/store';
import Sidebar from '../Sidebar/Sidebar.svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import Button from '../Button.svelte';
import { modalStack } from '../Modal/modal.store';
import Dialog from './Dialog.svelte';

View File

@@ -4,7 +4,7 @@
import TextField from '../TextField.svelte';
import Button from '../Button.svelte';
import { ArrowUp, EyeClosed, EyeOpen, Trash, Upload } from 'radix-icons-svelte';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import IconToggle from '../IconToggle.svelte';
import Tab from '../Tab/Tab.svelte';
import { useTabs } from '../Tab/Tab';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Dialog from './Dialog.svelte';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import Button from '../Button.svelte';
import { PLATFORM_WEB } from '../../constants';
import { ExternalLink, InfoCircled } from 'radix-icons-svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import classNames from 'classnames';
import { ArrowDown, Check, TriangleRight } from 'radix-icons-svelte';
import type { Readable } from 'svelte/store';
@@ -9,7 +9,7 @@
export let episodeNumber: number;
export let episodeName: string;
export let backdropUrl: string;
export let handlePlay: (() => void) | undefined = undefined
export let handlePlay: (() => void) | undefined = undefined;
export let isWatched = false;
export let playbackProgress = 0;

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Selectable, type FlowDirection } from '../selectable';
export let name: string = '';
export let direction: FlowDirection = 'vertical';
const { registerer, ...rest } = new Selectable(name).setDirection(direction).getStores();
export const container = rest.container;
export const hasFocus = rest.hasFocus;
export const hasFocusWithin = rest.hasFocusWithin;
onMount(() => {
rest.container._mountSelectable();
});
</script>
<button use:registerer>
<slot />
</button>

View File

@@ -1,5 +1,5 @@
<script>
import Container from '../../Container.svelte';
import Container from './Container.svelte';
</script>
<Container>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
</script>
<Container

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import HeroShowcaseBackground from './HeroBackground.svelte';
import IconButton from '../IconButton.svelte';
import { ChevronRight } from 'radix-icons-svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import classNames from 'classnames';
import type { Readable } from 'svelte/store';
import type { ComponentType } from 'svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
import Button from '../Button.svelte';
import { createEventDispatcher, onMount } from 'svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import TextField from '../components/TextField.svelte';
import Button from '../components/Button.svelte';
import { createLocalStorageStore } from '../stores/localstorage.store';

View File

@@ -4,7 +4,7 @@
import { formatSize } from '../../utils';
import { ChevronRight } from 'radix-icons-svelte';
import type { Download } from '../../apis/combined-types';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
export let downloads: Promise<Download[]>;
export let cancelDownload: (downloadId: number) => Promise<any>;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
export let focusOnMount = false;
</script>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../../Container.svelte';
import Container from '../../Container.svelte';
import { formatSize } from '../../../utils';
import Button from '../../Button.svelte';
import FullScreenModal from '../../Modal/FullScreenModal.svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../../Container.svelte';
import Container from '../../Container.svelte';
import { formatSize } from '../../../utils';
import Button from '../../Button.svelte';
import FullScreenModal from '../../Modal/FullScreenModal.svelte';

View File

@@ -4,7 +4,7 @@
import type { Download } from '../../../apis/combined-types';
import type { CancelDownloadFn } from '../MediaManagerModal';
import { scrollIntoView } from '../../../selectable';
import Container from '../../../../Container.svelte';
import Container from '../../Container.svelte';
import classNames from 'classnames';
import { modalStack } from '../../Modal/modal.store';
import MMConfirmDeleteFileDialog from '../Dialogs/MMConfirmDeleteFileDialog.svelte';

View File

@@ -5,7 +5,7 @@
import TableHeaderRow from '../../Table/TableHeaderRow.svelte';
import TableHeaderSortBy from '../../Table/TableHeaderSortBy.svelte';
import TableHeaderCell from '../../Table/TableHeaderCell.svelte';
import Container from '../../../../Container.svelte';
import Container from '../../Container.svelte';
import Button from '../../Button.svelte';
import { Cross1, Trash } from 'radix-icons-svelte';
import { scrollIntoView } from '../../../selectable';

View File

@@ -11,7 +11,7 @@
import { modalStack } from '../Modal/modal.store';
import classNames from 'classnames';
import { fade } from 'svelte/transition';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { capitalize, formatSize } from '../../utils';
import { ArrowRight, Check, Plus } from 'radix-icons-svelte';
import Button from '../Button.svelte';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Dialog from '../Dialog/Dialog.svelte';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import Button from '../Button.svelte';
import { ArrowRight, Check, Plus, Trash } from 'radix-icons-svelte';
import { modalStack } from '../Modal/modal.store';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import classNames from 'classnames';
import MMTitle from './MMTitle.svelte';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import classNames from 'classnames';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { modalStack } from '../Modal/modal.store';
import Modal from '../Modal/Modal.svelte';

View File

@@ -6,7 +6,7 @@
import TableHeaderRow from '../../Table/TableHeaderRow.svelte';
import TableHeaderSortBy from '../../Table/TableHeaderSortBy.svelte';
import type { GrabReleaseFn } from '../MediaManagerModal';
import Container from '../../../../Container.svelte';
import Container from '../../Container.svelte';
import TableHeaderCell from '../../Table/TableHeaderCell.svelte';
import MMTitle from '../MMTitle.svelte';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import classNames from 'classnames';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { modalStack } from './modal.store';
export let modalId: symbol;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { modalStack } from './modal.store';
import classNames from 'classnames';
</script>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import classNames from 'classnames';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import AnimateScale from '../AnimateScale.svelte';
import type { Readable } from 'svelte/store';
import { navigate } from '../StackRouter/StackRouter';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import classNames from 'classnames';
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import type { Readable } from 'svelte/store';
import AnimateScale from './AnimateScale.svelte';
import type { ComponentType } from 'svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import { ArrowRight } from 'radix-icons-svelte';
import classNames from 'classnames';
import { createEventDispatcher } from 'svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import { ArrowRight, Check } from 'radix-icons-svelte';
import classNames from 'classnames';

View File

@@ -1,67 +0,0 @@
<script lang="ts">
import Dialog from '../Dialog/Dialog.svelte';
import {
type EpisodeDownload,
sonarrApi,
type SonarrEpisode
} from '../../apis/sonarr/sonarr-api';
import Button from '../Button.svelte';
import Container from '../../../Container.svelte';
import { formatSize } from '../../utils';
import { Cross1 } from 'radix-icons-svelte';
import { capitalize } from '../../utils.js';
import type { Download } from '../../apis/combined-types';
export let download: Download;
export let title: string;
export let subtitle: string;
export let backgroundUrl: string;
export let onCancel: () => void;
function handleCancelDownload() {
return sonarrApi.cancelDownload(download.id || -1).then(() => onCancel());
}
</script>
<Dialog class="flex flex-col relative">
{#if backgroundUrl}
<div
class="absolute inset-0 bg-cover bg-center h-52"
style="background-image: url({backgroundUrl}); -webkit-mask-image: radial-gradient(at 90% 10%, hsla(0,0%,0%,1) 0px, transparent 70%);"
/>
{/if}
<div class="z-10">
{#if backgroundUrl}
<div class="h-24" />
{/if}
<h1 class="header2">{title}</h1>
<h2 class="header1 mb-4">{subtitle}</h2>
<div
class="grid grid-cols-[1fr_max-content] font-medium mb-16
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-1"
>
<span class="border-b border-secondary-600">Status</span>
<span class="border-b border-secondary-600">{capitalize(download.status || '')}</span>
<span class="border-b border-secondary-600">Progress</span>
<span class="border-b border-secondary-600"
>{formatSize((download?.size || 0) - (download?.sizeleft || 1))} / {formatSize(
download?.size || 0
)}</span
>
<span class="border-b border-secondary-600">Estimated Time Left</span>
<span class="border-b border-secondary-600">{download.timeleft}</span>
<span class="border-b border-secondary-600">Source</span>
<span class="border-b border-secondary-600">{download.indexer}</span>
<span>Quality</span>
<span>{download.quality?.quality?.name}</span>
</div>
<Container class="flex flex-col space-y-4">
<Button type="secondary" confirmDanger action={handleCancelDownload}>
<Cross1 size={19} slot="icon" />
Cancel Downloads
</Button>
</Container>
</div>
</Dialog>

View File

@@ -1,154 +0,0 @@
<script lang="ts">
import type { JellyfinItem } from '../../apis/jellyfin/jellyfin-api';
import EpisodeCard from '../EpisodeCard/EpisodeCard.svelte';
import { useDependantRequest } from '../../stores/data.store';
import { derived, get, type Readable } from 'svelte/store';
import {
tmdbApi,
type TmdbSeasonEpisode,
type TmdbSeason,
type TmdbSeriesFull2
} from '../../apis/tmdb/tmdb-api';
import Carousel from '../Carousel/Carousel.svelte';
import Container from '../../../Container.svelte';
import { registrars, scrollElementIntoView, scrollIntoView, Selectable } from '../../selectable';
import UICarousel from '../Carousel/UICarousel.svelte';
import classNames from 'classnames';
import ScrollHelper from '../ScrollHelper.svelte';
import TmdbEpisodeCard from '../EpisodeCard/TmdbEpisodeCard.svelte';
import { playerState } from '../VideoPlayer/VideoPlayer';
export let id: number;
export let tmdbSeries: Readable<TmdbSeriesFull2 | undefined>;
export let jellyfinEpisodes: Readable<JellyfinItem[] | undefined>;
export let nextJellyfinEpisode: Readable<JellyfinItem | undefined>;
// Exports
export let selectedTmdbEpisode: TmdbSeasonEpisode | undefined;
const containers = new Map<TmdbSeason | TmdbSeasonEpisode, Selectable>();
let scrollTop: number;
const { data: tmdbSeasons, isLoading: isTmdbSeasonsLoading } = useDependantRequest(
(seasons: number) => tmdbApi.getTmdbSeriesSeasons(id, seasons),
tmdbSeries,
(series) => (series?.seasons?.length ? ([series.seasons.length] as const) : undefined)
);
function focusFirstEpisodeOf(season: TmdbSeason) {
let isAlreadySelected = false;
for (const episode of season.episodes || []) {
const selectable = containers.get(episode);
if (selectable && get(selectable.hasFocusWithin)) {
isAlreadySelected = true;
break;
}
}
const episode = season.episodes?.[0];
if (episode && !isAlreadySelected) {
const selectable = containers.get(episode);
if (selectable) selectable.focus({ setFocusedElement: false });
}
}
function focusSeason(season: TmdbSeason) {
const seasonSelectable = containers.get(season);
if (seasonSelectable) seasonSelectable.focus({ setFocusedElement: false });
}
function handleEpisodeMount(event: CustomEvent<Selectable>, tmdbEpisode: TmdbSeasonEpisode) {
containers.set(tmdbEpisode, event.detail);
const selectable = event.detail;
// Handle focus next episode
nextJellyfinEpisode.subscribe(($jellyfinEpisode) => {
const isNextEpisode =
$jellyfinEpisode?.IndexNumber === tmdbEpisode.episode_number &&
$jellyfinEpisode?.ParentIndexNumber === tmdbEpisode.season_number;
if (isNextEpisode) {
selectable.focus({
setFocusedElement: false,
propagate: false
});
const el = selectable.getHtmlElement();
if (el) scrollElementIntoView(el, { left: 64 + 16 });
}
});
}
</script>
<ScrollHelper bind:scrollTop />
{#if $isTmdbSeasonsLoading}
Loading...
{:else if $tmdbSeasons}
<Carousel
scrollClass="px-20"
class={classNames('transition-transform', {
'-translate-y-20': scrollTop < 140
})}
hideControls={scrollTop < 140}
on:enter
{...$$restProps}
>
<svelte:fragment slot="header">
<UICarousel
class={classNames('flex -mx-2 transition-opacity', {
'opacity-0': scrollTop < 140
})}
let:focusIndex
>
{#each $tmdbSeasons as season, i}
<Container
let:hasFocus
on:click={() => focusFirstEpisodeOf(season)}
on:enter={(event) => {
scrollIntoView({ horizontal: 64 })(event);
if (event.detail.options.setFocusedElement) focusFirstEpisodeOf(season);
}}
on:mount={(e) => containers.set(season, e.detail)}
>
<div
class={classNames(
'px-3 py-1 cursor-pointer whitespace-nowrap text-xl tracking-wide font-medium rounded-lg',
'hover:font-semibold hover:tracking-wide hover:text-white',
{
'bg-primary-500 text-black': hasFocus,
//'bg-stone-800/50': hasFocus,
'text-zinc-400': !(focusIndex === i),
'text-white': focusIndex === i && !hasFocus
}
)}
>
Season {season.season_number}
</div>
</Container>
{/each}
</UICarousel>
</svelte:fragment>
{#each $tmdbSeasons as season}
{#each season?.episodes || [] as episode}
{@const jellyfinEpisodeId = $jellyfinEpisodes?.find(
(i) =>
i.IndexNumber === episode.episode_number &&
i.ParentIndexNumber === episode.season_number
)?.Id}
<TmdbEpisodeCard
on:enter={(event) => {
scrollIntoView({ horizontal: 64 + 32 })(event);
focusSeason(season);
selectedTmdbEpisode = episode;
}}
on:mount={(e) => handleEpisodeMount(e, episode)}
{episode}
handlePlay={jellyfinEpisodeId
? () => playerState.streamJellyfinId(jellyfinEpisodeId)
: undefined}
/>
{/each}
{/each}
</Carousel>
{/if}

View File

@@ -1,144 +0,0 @@
<script lang="ts">
import { type Readable } from 'svelte/store';
import {
tmdbApi,
type TmdbEpisode,
type TmdbSeasonEpisode,
type TmdbSeriesFull2
} from '../../apis/tmdb/tmdb-api';
import Container from '../../../Container.svelte';
import { useDependantRequest } from '../../stores/data.store';
import type { JellyfinItem } from '../../apis/jellyfin/jellyfin-api';
import TmdbEpisodeCard from '../EpisodeCard/TmdbEpisodeCard.svelte';
import { scrollIntoView, Selectable } from '../../selectable';
import { playerState } from '../VideoPlayer/VideoPlayer';
import CardGrid from '../CardGrid.svelte';
import UICarousel from '../Carousel/UICarousel.svelte';
import classNames from 'classnames';
import ScrollHelper from '../ScrollHelper.svelte';
import ManageSeasonCard from './ManageSeasonCard.svelte';
import { TMDB_BACKDROP_SMALL } from '../../constants';
import { navigate } from '../StackRouter/StackRouter';
export let id: number;
export let tmdbSeries: Promise<TmdbSeriesFull2 | undefined>;
export let jellyfinEpisodes: Promise<JellyfinItem[]>;
export let currentJellyfinEpisode: Promise<JellyfinItem | undefined>;
export let handleRequestSeason: (season: number) => Promise<any>;
export let tmdbSeasons = tmdbSeries.then((series) =>
tmdbApi.getTmdbSeriesSeasons(id, series?.seasons?.length ?? 1)
);
let awaitedJellyfinEpisodes: JellyfinItem[] = [];
jellyfinEpisodes.then((episodes) => {
awaitedJellyfinEpisodes = episodes;
});
let seasonIndex = 0;
let scrollTop: number;
$: translateUp = scrollTop < 140;
function handleOpenEpisodePage(episode: TmdbSeasonEpisode) {
navigate(`/series/${id}/season/${episode.season_number}/episode/${episode.episode_number}`);
}
function handleMountSeasonButton(s: Selectable, seasonNumber: number) {
currentJellyfinEpisode.then((currentEpisode) => {
if (currentEpisode?.ParentIndexNumber === seasonNumber) {
seasonIndex = currentEpisode?.ParentIndexNumber
? currentEpisode?.ParentIndexNumber - 1
: seasonIndex;
s.focus({ setFocusedElement: false, propagate: false });
}
});
}
function handleMountCard(s: Selectable, episode: TmdbEpisode) {
// currentJellyfinEpisode.then((currentEpisode) => {
// if (
// currentEpisode?.IndexNumber === episode.episode_number &&
// currentEpisode?.ParentIndexNumber === episode.season_number
// ) {
// s.focus({ setFocusedElement: false, propagate: false });
// }
// });
}
</script>
<ScrollHelper bind:scrollTop />
<Container
on:enter
class={classNames('transition-transform mx-32', {
'-translate-y-16': translateUp
})}
>
{#await Promise.all([tmdbSeries, tmdbSeasons]) then [tmdbSeries, tmdbSeasons]}
<UICarousel
class={classNames('flex transition-opacity mb-8', {
'opacity-0': translateUp
})}
on:enter={scrollIntoView({ horizontal: 64 })}
>
{#each tmdbSeasons || [] as season, i}
<Container
on:mount={(e) => handleMountSeasonButton(e.detail, season?.season_number || 0)}
let:hasFocus
on:enter={(event) => {
scrollIntoView({ horizontal: 64 })(event);
seasonIndex = i;
}}
focusOnClick
>
<div
class={classNames(
'font-semibold text-2xl',
'px-3 py-1 cursor-pointer whitespace-nowrap rounded-lg',
'hover:text-white',
{
'bg-primary-500 text-black': hasFocus,
//'bg-stone-800/50': hasFocus,
'text-zinc-400': !(seasonIndex === i),
'text-white': seasonIndex === i && !hasFocus
}
)}
>
Season {season.season_number}
</div>
</Container>
{/each}
</UICarousel>
<CardGrid type="landscape" on:mount>
{#if tmdbSeasons?.[seasonIndex]?.episodes?.length}
{#each tmdbSeasons?.[seasonIndex]?.episodes || [] as episode}
{@const jellyfinEpisode = awaitedJellyfinEpisodes?.find(
(i) =>
i.IndexNumber === episode.episode_number &&
i.ParentIndexNumber === episode.season_number
)}
{@const jellyfinEpisodeId = jellyfinEpisode?.Id}
{#key episode.id}
<TmdbEpisodeCard
{episode}
series={tmdbSeries}
on:mount={(e) => handleMountCard(e.detail, episode)}
on:enter={scrollIntoView({ top: 92, bottom: 128 })}
handlePlay={jellyfinEpisodeId
? () => playerState.streamJellyfinId(jellyfinEpisodeId)
: undefined}
isWatched={jellyfinEpisode?.UserData?.Played || false}
playbackProgress={jellyfinEpisode?.UserData?.PlayedPercentage || 0}
on:clickOrSelect={() => handleOpenEpisodePage(episode)}
/>
{/key}
{/each}
<ManageSeasonCard
backdropUrl={TMDB_BACKDROP_SMALL + tmdbSeries?.backdrop_path}
on:clickOrSelect={() => handleRequestSeason(seasonIndex + 1)}
on:enter={scrollIntoView({ top: 92, bottom: 128 })}
/>
{/if}
</CardGrid>
{/await}
</Container>

View File

@@ -1,53 +0,0 @@
<script lang="ts">
import Dialog from '../Dialog/Dialog.svelte';
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
import Button from '../Button.svelte';
import Container from '../../../Container.svelte';
import { formatSize } from '../../utils';
import { Trash } from 'radix-icons-svelte';
import type { FileResource } from '../../apis/combined-types';
export let file: FileResource;
export let title = '';
export let subtitle = '';
export let backgroundUrl: string;
export let onDelete: () => void;
function handleDeleteFile() {
return sonarrApi.deleteSonarrEpisode(file.id || -1).then(() => onDelete());
}
</script>
<Dialog class="flex flex-col relative">
{#if backgroundUrl}
<div
class="absolute inset-0 bg-cover bg-center h-52"
style="background-image: url({backgroundUrl}); -webkit-mask-image: radial-gradient(at 90% 10%, hsla(0,0%,0%,1) 0px, transparent 70%);"
/>
{/if}
<div class="z-10">
{#if backgroundUrl}
<div class="h-24" />
{/if}
<h1 class="header2">{title}</h1>
<h2 class="header1 mb-4">{subtitle}</h2>
<div
class="grid grid-cols-[1fr_max-content] font-medium mb-16
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-1"
>
<span class="border-b border-secondary-600">Runtime</span>
<span class="border-b border-secondary-600">{file.mediaInfo?.runTime}</span>
<span class="border-b border-secondary-600">Size on Disk</span>
<span class="border-b border-secondary-600">{formatSize(file.size || 0)}</span>
<span>Quality</span>
<span>{file.quality?.quality?.name}</span>
</div>
<Container class="flex flex-col space-y-4">
<Button type="secondary" confirmDanger action={handleDeleteFile}>
<Trash size={19} slot="icon" />
Delete File
</Button>
</Container>
</div>
</Dialog>

View File

@@ -1,37 +0,0 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import type { Readable } from 'svelte/store';
import AnimateScale from '../AnimateScale.svelte';
import classNames from 'classnames';
import { Plus } from 'radix-icons-svelte';
import { getCardDimensions } from '../../utils';
import IconOverlay from '../IconOverlay.svelte';
export let backdropUrl: string;
let hasFocus: Readable<boolean>;
let dimensions = getCardDimensions(window.innerWidth, 'landscape');
</script>
<svelte:window
on:resize={(e) => (dimensions = getCardDimensions(e.currentTarget.innerWidth, 'landscape'))}
/>
<AnimateScale hasFocus={$hasFocus}>
<Container
class={classNames(
'flex flex-col shrink-0',
'overflow-hidden rounded-2xl cursor-pointer group relative selectable transition-opacity'
)}
style={`width: ${dimensions.width}px; height: ${dimensions.height}px`}
on:clickOrSelect
on:enter
bind:hasFocus
>
<div
class="bg-cover bg-center absolute inset-0"
style={`background-image: url('${backdropUrl}')`}
/>
<IconOverlay icon={Plus} />
</Container>
</AnimateScale>

View File

@@ -1,547 +0,0 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import HeroCarousel from '../HeroCarousel/HeroCarousel.svelte';
import DetachedPage from '../DetachedPage/DetachedPage.svelte';
import { useRequest } from '../../stores/data.store';
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants';
import classNames from 'classnames';
import {
Bookmark,
Cross1,
DotFilled,
ExternalLink,
Minus,
Play,
Plus,
Trash
} from 'radix-icons-svelte';
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
import {
type EpisodeDownload,
type EpisodeFileResource,
sonarrApi
} from '../../apis/sonarr/sonarr-api';
import Button from '../Button.svelte';
import { playerState } from '../VideoPlayer/VideoPlayer';
import { createModal, modalStack } from '../Modal/modal.store';
import { get, writable } from 'svelte/store';
import { scrollIntoView, useRegistrar } from '../../selectable';
import ScrollHelper from '../ScrollHelper.svelte';
import Carousel from '../Carousel/Carousel.svelte';
import TmdbPersonCard from '../PersonCard/TmdbPersonCard.svelte';
import TmdbCard from '../Card/TmdbCard.svelte';
import EpisodeGrid from './EpisodeGrid.svelte';
import { formatSize } from '../../utils';
import FileDetailsDialog from './FileDetailsDialog.svelte';
import SonarrMediaManagerModal from '../MediaManagerModal/SonarrMediaManagerModal.svelte';
import MMAddToSonarrDialog from '../MediaManagerModal/MMAddToSonarrDialog.svelte';
import ConfirmDialog from '../Dialog/ConfirmDialog.svelte';
import DownloadDetailsDialog from './DownloadDetailsDialog.svelte';
import { reiverrApiNew, sources, user } from '../../stores/user.store';
import type { VideoStreamCandidateDto } from '../../apis/reiverr/reiverr.openapi';
import type { MediaSource } from '../../apis/reiverr/reiverr.openapi';
import SelectDialog from '../Dialog/SelectDialog.svelte';
import { useUserData } from '../../stores/library.store';
import { handleOpenStreamSelector } from '../../pages/MoviePage/MoviePage.shared';
export let id: string;
const tmdbId = Number(id);
const showUserData = reiverrApiNew.users
.getShowUserData($user?.id as string, id)
.then((r) => r.data);
const { inLibrary, progress, handleAddToLibrary, handleRemoveFromLibrary } = useUserData(
'Series',
id,
showUserData
);
//
const availableForStreaming = writable(false);
const streams = getStreams();
streams.forEach((p) =>
p.streams.then((s) => availableForStreaming.update((p) => p || s.length > 0))
);
const { promise: tmdbSeries, data: tmdbSeriesData } = useRequest(tmdbApi.getTmdbSeries, tmdbId);
let tmdbSeasons = $tmdbSeries.then((series) =>
tmdbApi.getTmdbSeriesSeasons(tmdbId, series?.seasons?.length ?? 1)
);
let sonarrItem = sonarrApi.getSeriesByTmdbId(tmdbId);
const { promise: recommendations } = useRequest(tmdbApi.getSeriesRecommendations, tmdbId);
$: sonarrDownloads = getDownloads(sonarrItem);
$: sonarrFiles = getFiles(sonarrItem);
$: sonarrSeasonNumbers = Promise.all([sonarrFiles, sonarrDownloads]).then(
([files, downloads]) => [
...new Set(files.map((item) => item.seasonNumber || -1)),
...new Set(downloads.map((item) => item.seasonNumber || -1))
]
);
$: sonarrEpisodes = Promise.all([sonarrItem, sonarrSeasonNumbers])
.then(([item, seasons]) =>
Promise.all(seasons.map((s) => sonarrApi.getEpisodes(item?.id || -1, s)))
)
.then((items) => items.flat());
const jellyfinSeries = getJellyfinSeries(id);
const jellyfinEpisodes = jellyfinSeries.then(
(s) => (s && jellyfinApi.getJellyfinEpisodes(s.Id)) || []
);
const nextJellyfinEpisode = jellyfinEpisodes.then((items) =>
items.find((i) => i.UserData?.Played === false)
);
const episodeCards = useRegistrar();
let scrollTop: number;
// let hideInterface = false;
// modalStack.top.subscribe((modal) => {
// hideInterface = !!modal;
// });
function getStreams() {
const out: { source: MediaSource; streams: Promise<VideoStreamCandidateDto[]> }[] = [];
for (const source of get(sources)) {
out.push({
source: source.source,
streams: showUserData.then((userData) => {
const { season, episode } = userData.playState ?? {};
return reiverrApiNew.sources
.getEpisodeStreams(source.source.id, id, season ?? 1, episode ?? 1)
.then((r) => r.data?.streams ?? []);
})
});
}
return out;
}
function getJellyfinSeries(id: string) {
return jellyfinApi.getLibraryItemFromTmdbId(id);
}
const onGrabRelease = () => setTimeout(() => (sonarrDownloads = getDownloads(sonarrItem)), 8000);
function handleAddedToSonarr() {
sonarrItem = sonarrApi.getSeriesByTmdbId(tmdbId);
sonarrItem.then(
(sonarrItem) =>
sonarrItem &&
createModal(SonarrMediaManagerModal, {
season: 1,
sonarrItem,
onGrabRelease
})
);
}
async function handleRequestSeason(season: number) {
return sonarrItem.then((sonarrItem) => {
const tmdbSeries = get(tmdbSeriesData);
if (sonarrItem) {
createModal(SonarrMediaManagerModal, {
season,
sonarrItem,
onGrabRelease
});
} else if (tmdbSeries) {
createModal(MMAddToSonarrDialog, {
title: tmdbSeries.name || '',
tmdbId: tmdbSeries.id || -1,
backdropUri: tmdbSeries.backdrop_path || '',
onComplete: handleAddedToSonarr
});
} else {
console.error('No series found');
}
});
}
async function getFiles(item: typeof sonarrItem) {
return item.then((item) => (item ? sonarrApi.getFilesBySeriesId(item?.id || -1) : []));
}
async function getDownloads(item: typeof sonarrItem) {
return item.then((item) => (item ? sonarrApi.getDownloadsBySeriesId(item?.id || -1) : []));
}
function createConfirmDeleteSeasonDialog(files: EpisodeFileResource[]) {
createModal(ConfirmDialog, {
header: 'Delete Season Files?',
body: `Are you sure you want to delete all ${files.length} file(s) from season ${files[0]?.seasonNumber}?`,
confirm: () =>
sonarrApi
.deleteSonarrEpisodes(files.map((f) => f.id || -1))
.then(() => (sonarrFiles = getFiles(sonarrItem)))
});
}
function createConfirmCancelDownloadsDialog(downloads: EpisodeDownload[]) {
createModal(ConfirmDialog, {
header: 'Cancel Season Downloads?',
body: `Are you sure you want to cancel all ${downloads.length} download(s) from season ${downloads[0]?.seasonNumber}?`,
confirm: () =>
sonarrApi
.cancelDownloads(downloads.map((f) => f.id || -1))
.then(() => (sonarrDownloads = getDownloads(sonarrItem)))
});
}
async function handlePlay() {
const awaitedStreams = await Promise.all(
streams.map(async (p) => ({ ...p, streams: await p.streams }))
).then((d) => d.filter((p) => p.streams.length > 0));
if (awaitedStreams.length > 1) {
modalStack.create(SelectDialog, {
title: 'Select Media Source',
subtitle: 'Select the media source you want to use',
options: awaitedStreams.map((p) => p.source.id),
handleSelectOption: (sourceId) => {
const s = awaitedStreams.find((p) => p.source.id === sourceId);
const key = s?.streams[0]?.key;
showUserData.then((userData) =>
playerState.streamEpisode(
id,
userData.playState?.season ?? 1,
userData.playState?.episode ?? 1,
userData,
sourceId,
key
)
);
}
});
} else if (awaitedStreams.length === 1) {
const asd = awaitedStreams.find((p) => p.streams.length > 0);
const sourceId = asd?.source.id;
const key = asd?.streams[0]?.key;
showUserData.then((userData) =>
playerState.streamEpisode(
id,
userData.playState?.season ?? 1,
userData.playState?.episode ?? 1,
userData,
sourceId,
key
)
);
}
}
</script>
<DetachedPage let:handleGoBack let:registrar>
<ScrollHelper bind:scrollTop />
<div class="relative">
<Container
class="h-[calc(100vh-4rem)] flex flex-col py-16 px-32"
on:enter={scrollIntoView({ top: 0 })}
on:navigate={({ detail }) => {
if (detail.direction === 'down' && detail.willLeaveContainer) {
$episodeCards?.focus();
detail.preventNavigation();
}
}}
>
<HeroCarousel
urls={$tmdbSeries.then(
(series) =>
series?.images.backdrops
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
?.map((i) => TMDB_IMAGES_ORIGINAL + i.file_path)
.slice(0, 5) || []
)}
>
<Container />
<div class="h-full flex-1 flex flex-col justify-end">
{#await $tmdbSeries then series}
{#if series}
<div
class={classNames(
'text-left font-medium tracking-wider text-stone-200 hover:text-amber-200 mt-2',
{
'text-4xl sm:text-5xl 2xl:text-6xl': series.name?.length || 0 < 15,
'text-3xl sm:text-4xl 2xl:text-5xl': series.name?.length || 0 >= 15
}
)}
>
{series.name}
</div>
<div
class="flex items-center gap-1 uppercase text-zinc-300 font-semibold tracking-wider mt-2 text-lg"
>
<p class="flex-shrink-0">
{#if series.status !== 'Ended'}
Since {new Date(series.first_air_date || Date.now())?.getFullYear()}
{:else}
Ended {new Date(series.last_air_date || Date.now())?.getFullYear()}
{/if}
</p>
<!-- <DotFilled />
<p class="flex-shrink-0">{movie.runtime}</p> -->
<DotFilled />
<p class="flex-shrink-0">
<a href={'https://www.themoviedb.org/movie/' + series.id}
>{series.vote_average} TMDB</a
>
</p>
</div>
<div
class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-4xl mt-4 text-lg"
>
{series.overview}
</div>
{/if}
{/await}
{#await nextJellyfinEpisode then nextJellyfinEpisode}
<Container
direction="horizontal"
class="flex mt-8"
focusOnMount
on:back={handleGoBack}
on:mount={registrar}
>
<Button
class="mr-4"
action={handlePlay}
secondaryAction={() =>
Promise.all([$tmdbSeries, showUserData]).then(([tmdbSeries, userData]) => {
tmdbSeries && handleOpenStreamSelector(tmdbSeries, userData);
})}
disabled={!$availableForStreaming}
>
Play
<Play size={19} slot="icon" />
</Button>
{#if !$inLibrary}
<Button class="mr-4" action={handleAddToLibrary} icon={Bookmark}>
Add to Library
</Button>
{:else}
<Button class="mr-4" action={handleRemoveFromLibrary} icon={Minus}>
Remove from Library
</Button>
{/if}
<!-- {#if nextJellyfinEpisode}
<Button
class="mr-4"
on:clickOrSelect={() =>
nextJellyfinEpisode?.Id && playerState.streamJellyfinId(nextJellyfinEpisode.Id)}
>
Play Season {nextJellyfinEpisode?.ParentIndexNumber} Episode
{nextJellyfinEpisode?.IndexNumber}
<Play size={19} slot="icon" />
</Button> -->
<!-- {:else} -->
<Button class="mr-4" action={() => handleRequestSeason(1)}>
Request
<Plus size={19} slot="icon" />
</Button>
<!-- {/if} -->
{#if PLATFORM_WEB}
<Button class="mr-4">
Open In TMDB
<ExternalLink size={19} slot="icon-after" />
</Button>
<Button class="mr-4">
Open In Jellyfin
<ExternalLink size={19} slot="icon-after" />
</Button>
{/if}
</Container>
{/await}
</div>
</HeroCarousel>
</Container>
<div
class={classNames('transition-opacity', {
// 'opacity-0': hideInterface
})}
>
<EpisodeGrid
on:enter={scrollIntoView({ top: -32, bottom: 128 })}
on:mount={episodeCards.registrar}
id={Number(id)}
tmdbSeries={$tmdbSeries}
{tmdbSeasons}
{jellyfinEpisodes}
currentJellyfinEpisode={nextJellyfinEpisode}
{handleRequestSeason}
/>
<Container on:enter={scrollIntoView({ top: 0 })} class="pt-8">
{#await $tmdbSeries then series}
<Carousel scrollClass="px-32" class="mb-8">
<div slot="header">Show Cast</div>
{#each series?.aggregate_credits?.cast?.slice(0, 15) || [] as credit}
<TmdbPersonCard on:enter={scrollIntoView({ horizontal: 128 })} tmdbCredit={credit} />
{/each}
</Carousel>
{/await}
{#await $recommendations then recommendations}
<Carousel scrollClass="px-32" class="mb-8">
<div slot="header">Recommendations</div>
{#each recommendations || [] as recommendation}
<TmdbCard item={recommendation} on:enter={scrollIntoView({ horizontal: 128 })} />
{/each}
</Carousel>
{/await}
</Container>
{#await $tmdbSeries then series}
<Container class="flex-1 bg-secondary-950 pt-8 px-32" on:enter={scrollIntoView({ top: 0 })}>
<h1 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">More Information</h1>
<div class="text-zinc-300 font-medium text-lg flex flex-wrap">
<div class="flex-1">
<div class="mb-8">
<h2 class="uppercase text-sm font-semibold text-zinc-500 mb-0.5">Created By</h2>
{#each series?.created_by || [] as creator}
<div>{creator.name}</div>
{/each}
</div>
<div class="mb-8">
<h2 class="uppercase text-sm font-semibold text-zinc-500 mb-0.5">Network</h2>
<div>{series?.networks?.[0]?.name}</div>
</div>
</div>
<div class="flex-1">
<div class="mb-8">
<h2 class="uppercase text-sm font-semibold text-zinc-500 mb-0.5">Language</h2>
<div>{series?.spoken_languages?.[0]?.name}</div>
</div>
<div class="mb-8">
<h2 class="uppercase text-sm font-semibold text-zinc-500 mb-0.5">Last Air Date</h2>
<div>{series?.last_air_date}</div>
</div>
</div>
</div>
</Container>
{/await}
{#await Promise.all( [sonarrSeasonNumbers, sonarrFiles, sonarrEpisodes, sonarrDownloads] ) then [seasons, files, episodes, downloads]}
{#if files?.length}
<Container
class="flex-1 bg-secondary-950 pt-8 pb-16 px-32 flex flex-col"
on:enter={scrollIntoView({ top: 32 })}
>
<!-- <h1 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">Local Files</h1>-->
<div class="space-y-16">
{#each seasons as season}
{@const seasonEpisodes = episodes.filter((e) => e.seasonNumber === season)}
{@const seasonFiles = files.filter((f) => f.seasonNumber === season)}
{@const seasonDownloads = downloads.filter((d) => d.seasonNumber === season)}
<div>
<div class="flex justify-between">
<h2 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">
Season {season} Files
</h2>
</div>
<Container direction="grid" gridCols={2} class="grid grid-cols-2 gap-8">
{#each seasonEpisodes as episode}
{@const file = seasonFiles.find((f) => f.id === episode.episodeFileId)}
{@const download = seasonDownloads.find((d) => d.episodeId === episode.id)}
<Container
class={classNames(
'flex space-x-8 items-center text-zinc-300 font-medium relative overflow-hidden',
'px-8 py-4 border-2 border-transparent rounded-xl',
{
'bg-secondary-800 focus-within:bg-primary-700 focus-within:border-primary-500': true,
'hover:bg-primary-700 hover:border-primary-500 cursor-pointer': true
// 'bg-primary-700 focus-within:border-primary-500': selected,
// 'bg-secondary-800 focus-within:border-zinc-300': !selected
}
)}
on:clickOrSelect={() => {
if (file)
modalStack.create(FileDetailsDialog, {
file,
title: episode?.title || '',
subtitle: `Season ${episode?.seasonNumber} Episode ${episode?.episodeNumber}`,
backgroundUrl: episode?.images?.[0]?.remoteUrl || '',
onDelete: () => (sonarrFiles = getFiles(sonarrItem))
});
else if (download)
modalStack.create(DownloadDetailsDialog, {
download,
title: episode?.title || '',
subtitle: `Season ${episode?.seasonNumber} Episode ${episode?.episodeNumber}`,
backgroundUrl: episode?.images?.[0]?.remoteUrl || '',
onCancel: () => (sonarrDownloads = getDownloads(sonarrItem))
});
}}
on:enter={scrollIntoView({ vertical: 128 })}
focusOnClick
>
{#if download}
<div
class="absolute inset-0 bg-secondary-50/10"
style={`width: ${
(((download.size || 0) - (download.sizeleft || 0)) /
(download.size || 1)) *
100
}%`}
/>
{/if}
<div class="flex-1">
<h1 class="text-lg">
{episode?.episodeNumber}. {episode?.title}
</h1>
</div>
{#if file}
<div>
{file?.mediaInfo?.runTime}
</div>
<div>
{formatSize(file?.size || 0)}
</div>
<div>
{file?.quality?.quality?.name}
</div>
{:else if download}
<div>
{formatSize((download?.size || 0) - (download?.sizeleft || 1))} / {formatSize(
download?.size || 0
)}
</div>
<div>
{download?.quality?.quality?.name}
</div>
{/if}
</Container>
{/each}
</Container>
<Container direction="horizontal" class="flex mt-8">
{#if seasonFiles?.length}
<Button on:clickOrSelect={() => createConfirmDeleteSeasonDialog(seasonFiles)}>
<Trash size={19} slot="icon" />
Delete Season Files
</Button>
{/if}
{#if seasonDownloads?.length}
<Button
on:clickOrSelect={() => createConfirmCancelDownloadsDialog(seasonDownloads)}
>
<Cross1 size={19} slot="icon" />
Cancel Season Downloads
</Button>
{/if}
</Container>
</div>
{/each}
</div>
</Container>
{/if}
{/await}
</div>
</div>
</DetachedPage>

View File

@@ -10,7 +10,7 @@
} from 'radix-icons-svelte';
import classNames from 'classnames';
import { get, type Readable, writable, type Writable } from 'svelte/store';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { registrars, Selectable } from '../../selectable';
import { stackRouter, navigate } from '../StackRouter/StackRouter';
import { onMount } from 'svelte';

View File

@@ -1,19 +1,19 @@
import { derived, get, writable } from 'svelte/store';
import { type ComponentType } from 'svelte';
import SeriesHomePage from '../../pages/SeriesHomePage.svelte';
import SeriesPage from '../SeriesPage/SeriesPage.svelte';
import EpisodePage from '../../pages/EpisodePage.svelte';
import EpisodePage from '../../pages/TitlePages/EpisodePage.svelte';
import { modalStack } from '../Modal/modal.store';
import MoviesHomePage from '../../pages/MoviesHomePage.svelte';
import MoviePage from '../../pages/MoviePage/MoviePage.svelte';
import LibraryPage from '../../pages/LibraryPage.svelte';
import SearchPage from '../../pages/SearchPage.svelte';
import PageNotFound from '../../pages/PageNotFound.svelte';
import ManagePage from '../../pages/ManagePage/ManagePage.svelte';
import PersonPage from '../../pages/PersonPage.svelte';
import UsersPage from '../../pages/UsersPage.svelte';
import UiComponents from '../../pages/UiComponents.svelte';
import StreamSelectorPage from '../../pages/StreamSelectorPage/StreamSelectorPage.svelte';
import StreamSelectorPage from '../../pages/TitlePages/StreamSelectorModal.svelte';
import SeriesPage from '../../pages/TitlePages/SeriesPage/SeriesPage.svelte';
import MoviePage from '../../pages/TitlePages/MoviePage/MoviePage.svelte';
import UiComponents from '../../pages/UIComponents.svelte';
interface Page {
id: symbol;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import classNames from 'classnames';
import type { NavigateEvent, Selectable } from '../../selectable';
import type { Writable } from 'svelte/store';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import type { NavigateEvent } from '../../selectable';
function handleNavigate({ detail }: CustomEvent<NavigateEvent>) {}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
export let rows: number;
</script>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import classNames from 'classnames';
export let disabled = false;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
</script>
<Container

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import classNames from 'classnames';
import { ChevronDown, ChevronUp } from 'radix-icons-svelte';
import type { Readable } from 'svelte/store';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import classNames from 'classnames';
import type { Readable } from 'svelte/store';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import type { FormEventHandler, HTMLInputTypeAttribute } from 'svelte/elements';
import { type ComponentType, createEventDispatcher } from 'svelte';
import { PLATFORM_TV } from '../constants';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../Container.svelte';
import Container from './Container.svelte';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import classNames from 'classnames';
</script>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import classNames from 'classnames';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import VideoPlayer from './VideoPlayer.svelte';
import type { PlaybackInfo, Subtitles, SubtitleInfo, AudioTrack } from './VideoPlayer';
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import { createEventDispatcher } from 'svelte';
import classNames from 'classnames';
import type { NavigateEvent } from '../../selectable';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import VideoElement from './VideoElement.svelte';
import type { PlaybackInfo, SubtitleInfo, Subtitles } from './VideoPlayer';
import classNames from 'classnames';

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import classNames from 'classnames';
import Container from '../../../Container.svelte';
import Container from '../Container.svelte';
import VideoPlayer from './VideoPlayer.svelte';
import type { PlaybackInfo, Subtitles, SubtitleInfo, AudioTrack } from './VideoPlayer';
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api';
@@ -58,7 +58,7 @@
$: videoStreamP && asd();
const asd = () =>
const asd = () =>
videoStreamP.then((stream) => {
// async function loadPlaybackInfo(
// options: { audioStreamIndex?: number; bitrate?: number; playbackPosition?: number } = {}