feat: Episode page
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -33,7 +33,7 @@
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte": "^3.59.2",
|
||||
"svelte-check": "^3.6.2",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"svelte-navigator": "^3.2.2",
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"radix-icons-svelte": "^1.2.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte": "^3.59.2",
|
||||
"svelte-check": "^3.6.2",
|
||||
"svelte-i18n": "^4.0.0",
|
||||
"svelte-navigator": "^3.2.2",
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Api } from '../api.interface';
|
||||
import { appState } from '../../stores/app-state.store';
|
||||
import type { DeviceProfile } from './playback-profiles';
|
||||
import axios from 'axios';
|
||||
import { log } from '../../utils';
|
||||
|
||||
export type JellyfinItem = components['schemas']['BaseItemDto'];
|
||||
|
||||
@@ -82,7 +83,13 @@ export class JellyfinApi implements Api<paths> {
|
||||
hasTmdbId: true,
|
||||
recursive: true,
|
||||
includeItemTypes: ['Movie', 'Series'],
|
||||
fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated']
|
||||
fields: [
|
||||
'ProviderIds',
|
||||
'Genres',
|
||||
'DateLastMediaAdded',
|
||||
'DateCreated',
|
||||
'MediaSources'
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -265,6 +272,39 @@ export class JellyfinApi implements Api<paths> {
|
||||
// }
|
||||
// }).then((r) => r.data?.Items || []);
|
||||
|
||||
episodesCache: JellyfinItem[] = [];
|
||||
getEpisode = async (
|
||||
seriesId: string,
|
||||
season: number,
|
||||
episode: number,
|
||||
refreshCache = false
|
||||
): Promise<JellyfinItem | undefined> =>
|
||||
this.getClient()
|
||||
.GET('/Users/{userId}/Items', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId()
|
||||
},
|
||||
query: {
|
||||
// @ts-ignore
|
||||
seriesId,
|
||||
parentIndexNumber: season,
|
||||
indexNumber: episode,
|
||||
recursive: true,
|
||||
includeItemTypes: ['Episode'],
|
||||
fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated', 'MediaSources']
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) =>
|
||||
r.data?.Items?.find(
|
||||
(i) =>
|
||||
i?.ParentIndexNumber === season &&
|
||||
i?.IndexNumber === episode &&
|
||||
i?.SeriesId === seriesId
|
||||
)
|
||||
);
|
||||
|
||||
getJellyfinEpisodes = async (parentId = '') =>
|
||||
this.getClient()
|
||||
?.GET('/Users/{userId}/Items', {
|
||||
@@ -415,28 +455,33 @@ export class JellyfinApi implements Api<paths> {
|
||||
}
|
||||
});
|
||||
|
||||
setJellyfinItemWatched = async (jellyfinId: string) =>
|
||||
this.getClient()?.POST('/Users/{userId}/PlayedItems/{itemId}', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId(),
|
||||
itemId: jellyfinId
|
||||
},
|
||||
query: {
|
||||
datePlayed: new Date().toISOString()
|
||||
markAsWatched = async (jellyfinId: string) =>
|
||||
this.getClient()
|
||||
?.POST('/Users/{userId}/PlayedItems/{itemId}', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId(),
|
||||
itemId: jellyfinId
|
||||
},
|
||||
query: {
|
||||
datePlayed: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
setJellyfinItemUnwatched = async (jellyfinId: string) =>
|
||||
this.getClient()?.DELETE('/Users/{userId}/PlayedItems/{itemId}', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId(),
|
||||
itemId: jellyfinId
|
||||
.then((res) => res.response.status === 200);
|
||||
|
||||
markAsUnwatched = async (jellyfinId: string) =>
|
||||
this.getClient()
|
||||
?.DELETE('/Users/{userId}/PlayedItems/{itemId}', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId(),
|
||||
itemId: jellyfinId
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.then((res) => res.response.status === 200);
|
||||
|
||||
getJellyfinHealth = async (
|
||||
baseUrl: string | undefined = undefined,
|
||||
|
||||
@@ -15,12 +15,14 @@ export type TmdbSeries2 =
|
||||
operations['tv-series-details']['responses']['200']['content']['application/json'];
|
||||
export type TmdbSeason =
|
||||
operations['tv-season-details']['responses']['200']['content']['application/json'];
|
||||
export type TmdbEpisode = NonNullable<TmdbSeason['episodes']>[0];
|
||||
export type TmdbSeasonEpisode = NonNullable<TmdbSeason['episodes']>[0];
|
||||
export type TmdbPerson =
|
||||
operations['person-details']['responses']['200']['content']['application/json'];
|
||||
export type TmdbCredit =
|
||||
| NonNullable<TmdbSeriesFull2['aggregate_credits']['cast']>[0]
|
||||
| NonNullable<TmdbMovieFull2['credits']['cast']>[0];
|
||||
export type TmdbEpisode =
|
||||
operations['tv-episode-details']['responses']['200']['content']['application/json'];
|
||||
|
||||
export interface TmdbPersonFull extends TmdbPerson {
|
||||
images: operations['person-images']['responses']['200']['content']['application/json'];
|
||||
@@ -176,6 +178,26 @@ export class TmdbApi implements Api<paths> {
|
||||
}
|
||||
}).then((res) => res.data?.results || []);
|
||||
|
||||
getEpisode = (
|
||||
seriesId: number,
|
||||
season: number,
|
||||
episode: number
|
||||
): Promise<TmdbEpisode | undefined> =>
|
||||
this.getClient()
|
||||
.GET('/3/tv/{series_id}/season/{season_number}/episode/{episode_number}', {
|
||||
params: {
|
||||
path: {
|
||||
series_id: seriesId,
|
||||
season_number: season,
|
||||
episode_number: episode
|
||||
},
|
||||
query: {
|
||||
append_to_response: 'credits,external_ids,images'
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.data);
|
||||
|
||||
// OTHER
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
export let inactive: boolean = false;
|
||||
export let focusOnMount: boolean = false;
|
||||
export let style: 'primary' | 'secondary' = 'primary';
|
||||
|
||||
let hasFocus: Readable<boolean>;
|
||||
</script>
|
||||
@@ -14,12 +15,9 @@
|
||||
<Container
|
||||
bind:hasFocus
|
||||
class={classNames(
|
||||
'px-6 py-2 rounded-lg font-medium tracking-wide flex items-center',
|
||||
'px-6 py-2 rounded-lg font-medium tracking-wide flex items-center selectable',
|
||||
{
|
||||
// 'bg-primary-500 text-secondary-700': $hasFocus,
|
||||
// 'bg-secondary-700': !$hasFocus,
|
||||
// 'hover:bg-primary-500 hover:text-secondary-700': true,
|
||||
'bg-secondary-700 selectable': true,
|
||||
'bg-secondary-700': style === 'primary',
|
||||
'cursor-pointer': !inactive,
|
||||
'cursor-not-allowed pointer-events-none opacity-40': inactive
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../../Container.svelte';
|
||||
import classNames from 'classnames';
|
||||
import { Check, CheckCircled, TriangleRight } from 'radix-icons-svelte';
|
||||
import { ArrowDown, Check, TriangleRight } from 'radix-icons-svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import AnimateScale from '../AnimateScale.svelte';
|
||||
|
||||
@@ -22,10 +22,7 @@
|
||||
class={classNames(
|
||||
'w-full h-64',
|
||||
'flex flex-col shrink-0',
|
||||
'overflow-hidden rounded-2xl cursor-pointer group relative px-4 py-3 selectable transition-opacity',
|
||||
{
|
||||
'opacity-75': !isOnDeck && !$hasFocus
|
||||
}
|
||||
'overflow-hidden rounded-2xl cursor-pointer group relative px-4 py-3 selectable transition-opacity'
|
||||
)}
|
||||
on:clickOrSelect
|
||||
on:enter
|
||||
@@ -55,24 +52,28 @@
|
||||
</div>
|
||||
<!-- Background Image -->
|
||||
<div
|
||||
class="absolute inset-0 bg-center bg-cover"
|
||||
class={classNames('absolute inset-0 bg-center bg-cover', {
|
||||
// 'opacity-75': !isOnDeck && !$hasFocus
|
||||
})}
|
||||
style={`background-image: url('${backdropUrl}')`}
|
||||
/>
|
||||
<!-- Background Overlay / Tint -->
|
||||
<div
|
||||
class={classNames('absolute inset-0', {
|
||||
'bg-gradient-to-t from-secondary-900/75 from-10% to-40% to-transparent': true
|
||||
'bg-gradient-to-t from-secondary-900/75 from-10% to-40% ': true,
|
||||
'to-secondary-900/25': !isOnDeck && !$hasFocus
|
||||
|
||||
// isOnDeck || $hasFocus,
|
||||
// 'bg-gradient-to-t from-secondary-900/75 from-10% to-40% to-secondary-900/25':
|
||||
// !isOnDeck && !$hasFocus
|
||||
})}
|
||||
/>
|
||||
{#if handlePlay}
|
||||
<div
|
||||
class={classNames(
|
||||
'group-hover:opacity-100 absolute inset-0 z-20 flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 absolute inset-0 z-20 flex items-center justify-center'
|
||||
)}
|
||||
>
|
||||
{#if handlePlay}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class={classNames(
|
||||
@@ -84,7 +85,17 @@
|
||||
>
|
||||
<TriangleRight size={32} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if !isOnDeck}
|
||||
<div
|
||||
class={classNames(
|
||||
'rounded-full p-4 cursor-pointer',
|
||||
'bg-zinc-900/90 text-zinc-200',
|
||||
'hover:bg-primary-500 hover:text-secondary-800' // group-focus-within:text-secondary-800 group-focus-within:bg-primary-500
|
||||
)}
|
||||
>
|
||||
<ArrowDown size={19} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Container>
|
||||
</AnimateScale>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import EpisodeCard from './EpisodeCard.svelte';
|
||||
import type { TmdbEpisode } from '../../apis/tmdb/tmdb-api';
|
||||
import type { TmdbSeasonEpisode } from '../../apis/tmdb/tmdb-api';
|
||||
import { TMDB_BACKDROP_SMALL } from '../../constants';
|
||||
|
||||
export let episode: TmdbEpisode;
|
||||
export let episode: TmdbSeasonEpisode;
|
||||
export let handlePlay: (() => void) | undefined;
|
||||
export let isWatched = false;
|
||||
export let playbackProgress = 0;
|
||||
|
||||
0
src/lib/components/HeroInfo/HeroInfoSubtitle.svelte
Normal file
0
src/lib/components/HeroInfo/HeroInfoSubtitle.svelte
Normal file
0
src/lib/components/HeroInfo/HeroInfoTags.svelte
Normal file
0
src/lib/components/HeroInfo/HeroInfoTags.svelte
Normal file
17
src/lib/components/HeroInfo/HeroInfoTitle.svelte
Normal file
17
src/lib/components/HeroInfo/HeroInfoTitle.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import classNames from 'classnames';
|
||||
|
||||
export let title: string | undefined;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={classNames(
|
||||
'text-left font-medium tracking-wider text-zinc-200 hover:text-amber-200 mt-1',
|
||||
{
|
||||
'text-4xl sm:text-5xl lg:text-6xl': (title?.length || 0) < 15,
|
||||
'text-3xl sm:text-4xl lg:text-5xl': (title?.length || 0) >= 15
|
||||
}
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
@@ -5,7 +5,7 @@
|
||||
import { derived, get, type Readable } from 'svelte/store';
|
||||
import {
|
||||
tmdbApi,
|
||||
type TmdbEpisode,
|
||||
type TmdbSeasonEpisode,
|
||||
type TmdbSeason,
|
||||
type TmdbSeriesFull2
|
||||
} from '../../apis/tmdb/tmdb-api';
|
||||
@@ -24,9 +24,9 @@
|
||||
export let nextJellyfinEpisode: Readable<JellyfinItem | undefined>;
|
||||
|
||||
// Exports
|
||||
export let selectedTmdbEpisode: TmdbEpisode | undefined;
|
||||
export let selectedTmdbEpisode: TmdbSeasonEpisode | undefined;
|
||||
|
||||
const containers = new Map<TmdbSeason | TmdbEpisode, Selectable>();
|
||||
const containers = new Map<TmdbSeason | TmdbSeasonEpisode, Selectable>();
|
||||
let scrollTop: number;
|
||||
|
||||
const { data: tmdbSeasons, isLoading: isTmdbSeasonsLoading } = useDependantRequest(
|
||||
@@ -58,7 +58,7 @@
|
||||
if (seasonSelectable) seasonSelectable.focus({ setFocusedElement: false });
|
||||
}
|
||||
|
||||
function handleEpisodeMount(event: CustomEvent<Selectable>, tmdbEpisode: TmdbEpisode) {
|
||||
function handleEpisodeMount(event: CustomEvent<Selectable>, tmdbEpisode: TmdbSeasonEpisode) {
|
||||
containers.set(tmdbEpisode, event.detail);
|
||||
const selectable = event.detail;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { type Readable } from 'svelte/store';
|
||||
import { tmdbApi, type TmdbEpisode, type TmdbSeriesFull2 } from '../../apis/tmdb/tmdb-api';
|
||||
import { tmdbApi, 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';
|
||||
@@ -44,7 +44,7 @@
|
||||
episodeCard.subscribe((e) => e?.focus({ setFocusedElement: false, propagate: false }));
|
||||
});
|
||||
|
||||
function handleOpenEpisodePage(episode: TmdbEpisode) {
|
||||
function handleOpenEpisodePage(episode: TmdbSeasonEpisode) {
|
||||
navigate(`season/${episode.season_number}/episode/${episode.episode_number}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import HeroCarousel from '../HeroCarousel/HeroCarousel.svelte';
|
||||
import DetachedPage from '../DetachedPage/DetachedPage.svelte';
|
||||
import { useActionRequest, useDependantRequest, useRequest } from '../../stores/data.store';
|
||||
import { tmdbApi, type TmdbEpisode } from '../../apis/tmdb/tmdb-api';
|
||||
import { tmdbApi, type TmdbSeasonEpisode } from '../../apis/tmdb/tmdb-api';
|
||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../../constants';
|
||||
import classNames from 'classnames';
|
||||
import { DotFilled, Download, ExternalLink, File, Play, Plus } from 'radix-icons-svelte';
|
||||
@@ -48,7 +48,7 @@
|
||||
sonarrApi.addSeriesToSonarr
|
||||
);
|
||||
|
||||
let selectedTmdbEpisode: TmdbEpisode | undefined;
|
||||
let selectedTmdbEpisode: TmdbSeasonEpisode | undefined;
|
||||
const episodeCards = useRegistrar();
|
||||
|
||||
let scrollTop: number;
|
||||
@@ -267,4 +267,4 @@
|
||||
</div>
|
||||
</DetachedPage>
|
||||
|
||||
<Route path="/season/:season/episode/:episode/*" component={EpisodePage} />
|
||||
<Route path="/season/:season/episode/:episode/*" component={EpisodePage} {id} />
|
||||
|
||||
@@ -1,10 +1,127 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../Container.svelte';
|
||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||
import { useActionRequest, useDependantRequest, useRequest } from '../stores/data.store';
|
||||
import { TMDB_IMAGES_ORIGINAL } from '../constants';
|
||||
import classNames from 'classnames';
|
||||
import { Check, DotFilled, Download, Play, Trash } from 'radix-icons-svelte';
|
||||
import HeroInfoTitle from '../components/HeroInfo/HeroInfoTitle.svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||
import { playerState } from '../components/VideoPlayer/VideoPlayer';
|
||||
import { formatSize } from '../utils';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let id: string; // Series ID
|
||||
export let season: string;
|
||||
export let episode: string;
|
||||
|
||||
let isWatched = false;
|
||||
|
||||
const tmdbEpisode = tmdbApi.getEpisode(Number(id), Number(season), Number(episode));
|
||||
const jellyfinSeries = jellyfinApi.getLibraryItemFromTmdbId(id);
|
||||
let jellyfinEpisode = jellyfinSeries.then((series) =>
|
||||
jellyfinApi.getEpisode(series?.Id || '', Number(season), Number(episode))
|
||||
);
|
||||
|
||||
const { send: toggleMarkAs, isFetching: markAsLoading } = useActionRequest(async () => {
|
||||
const episode = await jellyfinEpisode;
|
||||
|
||||
const jellyfinId = episode?.Id;
|
||||
if (!jellyfinId) return;
|
||||
|
||||
if (isWatched) {
|
||||
return jellyfinApi.markAsUnwatched(jellyfinId).then((ok) => (isWatched = !ok));
|
||||
} else {
|
||||
return jellyfinApi.markAsWatched(jellyfinId).then((ok) => (isWatched = ok));
|
||||
}
|
||||
});
|
||||
|
||||
jellyfinEpisode.then((e) => {
|
||||
isWatched = e?.UserData?.Played || false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<DetachedPage>
|
||||
Episode Page for {season}x{episode}
|
||||
<DetachedPage let:handleGoBack let:registrar>
|
||||
{#await tmdbEpisode then episode}
|
||||
<div
|
||||
class="bg-center bg-cover absolute inset-x-0 h-screen -z-10"
|
||||
style={`background-image: url('${TMDB_IMAGES_ORIGINAL + episode?.still_path}')`}
|
||||
/>
|
||||
<div class="absolute inset-0 flex flex-col -z-10">
|
||||
<div class="h-screen bg-gradient-to-t from-secondary-500 to-transparent" />
|
||||
<div class="flex-1 bg-secondary-500" />
|
||||
</div>
|
||||
|
||||
<!-- <HeroCarousel /> -->
|
||||
|
||||
<Container
|
||||
on:navigate={handleGoBack}
|
||||
on:mount={registrar}
|
||||
focusOnMount
|
||||
class="h-screen flex flex-col justify-end mx-20 py-16"
|
||||
>
|
||||
<div class="mt-2 text-zinc-200 font-medium text-lg tracking-wider">
|
||||
Season {episode?.season_number} Episode {episode?.episode_number}
|
||||
</div>
|
||||
<HeroInfoTitle title={episode?.name} />
|
||||
<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">
|
||||
<a href={'https://www.themoviedb.org/movie/' + episode?.id}>
|
||||
{episode?.vote_average} TMDB
|
||||
</a>
|
||||
</p>
|
||||
<DotFilled />
|
||||
<p class="flex-shrink-0">{episode?.runtime} Minutes</p>
|
||||
|
||||
{#await jellyfinEpisode then episode}
|
||||
{#if episode?.MediaSources?.[0]?.Size}
|
||||
<DotFilled />
|
||||
<p class="flex-shrink-0">{formatSize(episode?.MediaSources?.[0]?.Size)}</p>
|
||||
{/if}
|
||||
{#if episode?.MediaSources?.[0]?.MediaStreams?.[0]?.DisplayTitle}
|
||||
<DotFilled />
|
||||
<p class="flex-shrink-0">
|
||||
{episode?.MediaSources?.[0]?.MediaStreams?.[0]?.DisplayTitle}
|
||||
</p>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
<div class="text-stone-300 font-medium line-clamp-3 opacity-75 max-w-4xl mt-4">
|
||||
{episode?.overview}
|
||||
</div>
|
||||
<Container direction="horizontal" class="flex mt-8">
|
||||
{#await jellyfinEpisode then episode}
|
||||
<Button
|
||||
class="mr-4"
|
||||
on:clickOrSelect={() => episode?.Id && playerState.streamJellyfinId(episode.Id)}
|
||||
>
|
||||
Play
|
||||
<Play size={19} slot="icon" />
|
||||
</Button>
|
||||
<Button class="mr-4" inactive={$markAsLoading} on:clickOrSelect={toggleMarkAs}>
|
||||
{#if isWatched}
|
||||
Mark as Unwatched
|
||||
{:else}
|
||||
Mark as Watched
|
||||
{/if}
|
||||
<Check slot="icon" size={19} />
|
||||
</Button>
|
||||
{/await}
|
||||
<Button class="mr-4">Request <Download slot="icon" size={19} /></Button>
|
||||
<Button class="mr-4">Delete Files <Trash slot="icon" size={19} /></Button>
|
||||
</Container>
|
||||
</Container>
|
||||
{/await}
|
||||
</DetachedPage>
|
||||
|
||||
@@ -702,7 +702,7 @@ export function handleKeyboardNavigation(event: KeyboardEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
Selectable.focusedObject.subscribe(console.debug);
|
||||
Selectable.focusedObject.subscribe((e) => console.debug('Focused object', e));
|
||||
|
||||
type Offsets = Partial<
|
||||
Record<
|
||||
|
||||
@@ -328,3 +328,39 @@ export const useActionRequest = <P extends (...args: A) => Promise<any>, A exten
|
||||
send
|
||||
};
|
||||
};
|
||||
|
||||
export const useActionRequest2 = <P extends (...args: A) => Promise<any>, A extends any[]>(
|
||||
fn: P
|
||||
) => {
|
||||
const request = writable<ReturnType<P>>(undefined);
|
||||
const data = writable<Awaited<ReturnType<P>> | undefined>(undefined);
|
||||
const isFetching = writable(false);
|
||||
|
||||
function send(...args: Parameters<P>): ReturnType<P> {
|
||||
isFetching.set(true);
|
||||
// @ts-ignore
|
||||
const p: ReturnType<P> = fn(...args)
|
||||
.then((res) => {
|
||||
data.set(res);
|
||||
return res;
|
||||
})
|
||||
.finally(() => {
|
||||
isFetching.set(false);
|
||||
});
|
||||
|
||||
request.set(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
return {
|
||||
promise: request,
|
||||
data: {
|
||||
subscribe: data.subscribe
|
||||
},
|
||||
|
||||
isFetching: {
|
||||
subscribe: isFetching.subscribe
|
||||
},
|
||||
send
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user