mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-20 15:55:14 +02:00
refactor: project structure, absolute paths
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
140
src/lib/components/Container.svelte
Normal file
140
src/lib/components/Container.svelte
Normal 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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
20
src/lib/components/FocusableButton.svelte
Normal file
20
src/lib/components/FocusableButton.svelte
Normal 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>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import Container from '../../Container.svelte';
|
||||
import Container from './Container.svelte';
|
||||
</script>
|
||||
|
||||
<Container>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../../Container.svelte';
|
||||
import Container from '../Container.svelte';
|
||||
</script>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../../Container.svelte';
|
||||
import Container from '../Container.svelte';
|
||||
|
||||
export let focusOnMount = false;
|
||||
</script>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>) {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../../Container.svelte';
|
||||
import Container from '../Container.svelte';
|
||||
|
||||
export let rows: number;
|
||||
</script>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../../Container.svelte';
|
||||
import Container from '../Container.svelte';
|
||||
</script>
|
||||
|
||||
<Container
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../Container.svelte';
|
||||
import Container from './Container.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../../Container.svelte';
|
||||
import Container from '../Container.svelte';
|
||||
import classNames from 'classnames';
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 } = {}
|
||||
|
||||
Reference in New Issue
Block a user