diff --git a/src/lib/apis/jellyfin/jellyfinApi.ts b/src/lib/apis/jellyfin/jellyfinApi.ts index d13d327..9e0d302 100644 --- a/src/lib/apis/jellyfin/jellyfinApi.ts +++ b/src/lib/apis/jellyfin/jellyfinApi.ts @@ -173,3 +173,26 @@ export const reportJellyfinPlaybackStopped = ( MediaSourceId: itemId } }); + +export const setJellyfinItemWatched = (jellyfinId: string) => + JellyfinApi.post('/Users/{userId}/PlayedItems/{itemId}', { + params: { + path: { + userId: JELLYFIN_USER_ID, + itemId: jellyfinId + }, + query: { + datePlayed: new Date().toISOString() + } + } + }); + +export const setJellyfinItemUnwatched = (jellyfinId: string) => + JellyfinApi.del('/Users/{userId}/PlayedItems/{itemId}', { + params: { + path: { + userId: JELLYFIN_USER_ID, + itemId: jellyfinId + } + } + }); diff --git a/src/lib/components/Card/Card.svelte b/src/lib/components/Card/Card.svelte index 2e279b9..2f0f122 100644 --- a/src/lib/components/Card/Card.svelte +++ b/src/lib/components/Card/Card.svelte @@ -3,8 +3,13 @@ import { formatMinutesToTime } from '$lib/utils'; import classNames from 'classnames'; import { Clock, Star } from 'radix-icons-svelte'; + import ContextMenu from '../ContextMenu/ContextMenu.svelte'; + import ContextMenuItem from '../ContextMenu/ContextMenuItem.svelte'; + import { setJellyfinItemUnwatched, setJellyfinItemWatched } from '$lib/apis/jellyfin/jellyfinApi'; + import { library } from '$lib/stores/library.store'; export let tmdbId: number; + export let jellyfinId: string | undefined = undefined; export let type: 'movie' | 'series' = 'movie'; export let title: string; export let genres: string[] = []; @@ -17,75 +22,99 @@ export let available = true; export let progress = 0; export let size: 'dynamic' | 'md' | 'lg' = 'md'; - export let randomProgress = false; - if (randomProgress) { - progress = Math.random() > 0.3 ? Math.random() * 100 : 0; + + let watched = false; + $: watched = !available && !!jellyfinId; + + function handleSetWatched() { + if (jellyfinId) { + setJellyfinItemWatched(jellyfinId).finally(() => library.refreshIn(3000)); + } + } + + function handleSetUnwatched() { + if (jellyfinId) { + setJellyfinItemUnwatched(jellyfinId).finally(() => library.refreshIn(3000)); + } } - -
-
0 ? 'padding-bottom: 0.6rem;' : ''} + + + + Mark as watched + + + Mark as unwatched + + + -
-

{title}

-
- {genres.map((genre) => genre.charAt(0).toUpperCase() + genre.slice(1)).join(', ')} -
-
-
- {#if completionTime} -
- Downloaded in {formatMinutesToTime((new Date(completionTime).getTime() - Date.now()) / 1000 / 60)} +
+
0 ? 'padding-bottom: 0.6rem;' : ''} + > +
+

{title}

+
+ {genres.map((genre) => genre.charAt(0).toUpperCase() + genre.slice(1)).join(', ')}
- {:else} - {#if runtimeMinutes} -
- +
+
+ {#if completionTime} +
+ Downloaded in {formatMinutesToTime( + (new Date(completionTime).getTime() - Date.now()) / 1000 / 60 + )} +
+ {:else} + {#if runtimeMinutes} +
+ +
+ {progress + ? formatMinutesToTime(runtimeMinutes - runtimeMinutes * (progress / 100)) + + ' left' + : formatMinutesToTime(runtimeMinutes)} +
+
+ {/if} + {#if seasons}
- {progress - ? formatMinutesToTime(runtimeMinutes - runtimeMinutes * (progress / 100)) + ' left' - : formatMinutesToTime(runtimeMinutes)} + {seasons} Season{seasons > 1 ? 's' : ''} +
+ {/if} + +
+ +
+ {rating ? rating.toFixed(1) : 'N/A'}
{/if} - {#if seasons} -
- {seasons} Season{seasons > 1 ? 's' : ''} -
- {/if} - -
- -
- {rating ? rating.toFixed(1) : 'N/A'} -
-
- {/if} +
-
-
-
- +
+
+ + diff --git a/src/lib/components/ContextMenu/ContextMenu.svelte b/src/lib/components/ContextMenu/ContextMenu.svelte new file mode 100644 index 0000000..7d50e05 --- /dev/null +++ b/src/lib/components/ContextMenu/ContextMenu.svelte @@ -0,0 +1,82 @@ + + + + + {#if $contextMenu === id} + + {/if} + + + + +
+ +
+ +{#if $contextMenu === id} + {#key position} +
+ + {#if heading} +

+ {heading} +

+ {/if} +
+ + +
close()}> + +
+
+ {/key} +{/if} diff --git a/src/lib/components/ContextMenu/ContextMenu.ts b/src/lib/components/ContextMenu/ContextMenu.ts new file mode 100644 index 0000000..e960dcf --- /dev/null +++ b/src/lib/components/ContextMenu/ContextMenu.ts @@ -0,0 +1,17 @@ +import { writable } from 'svelte/store'; + +function createContextMenu() { + const visibleItem = writable(null); + + return { + subscribe: visibleItem.subscribe, + show: (item: Symbol) => { + visibleItem.set(item); + }, + hide: () => { + visibleItem.set(null); + } + }; +} + +export const contextMenu = createContextMenu(); diff --git a/src/lib/components/ContextMenu/ContextMenuItem.svelte b/src/lib/components/ContextMenu/ContextMenuItem.svelte new file mode 100644 index 0000000..99d77bc --- /dev/null +++ b/src/lib/components/ContextMenu/ContextMenuItem.svelte @@ -0,0 +1,17 @@ + + + diff --git a/src/lib/components/EpisodeCard/EpisodeCard.svelte b/src/lib/components/EpisodeCard/EpisodeCard.svelte index a8b5550..2626cb6 100644 --- a/src/lib/components/EpisodeCard/EpisodeCard.svelte +++ b/src/lib/components/EpisodeCard/EpisodeCard.svelte @@ -1,12 +1,15 @@ - - +
+ +
+ {#if progress} +
+ +
+ {/if} + + diff --git a/src/lib/components/PeopleCard/PeopleCard.svelte b/src/lib/components/PeopleCard/PeopleCard.svelte index c6d68c4..7e8449a 100644 --- a/src/lib/components/PeopleCard/PeopleCard.svelte +++ b/src/lib/components/PeopleCard/PeopleCard.svelte @@ -21,7 +21,9 @@ )} href={`/person/${tmdbId}`} > -
+
-
+
- +
+ +
-
+
diff --git a/src/lib/components/TitleShowcase/TitleShowcase.svelte b/src/lib/components/TitleShowcase/TitleShowcase.svelte index b1e1fd5..16d55f5 100644 --- a/src/lib/components/TitleShowcase/TitleShowcase.svelte +++ b/src/lib/components/TitleShowcase/TitleShowcase.svelte @@ -176,13 +176,13 @@ {/if} {#if UIVisible}
{:else if !UIVisible}
diff --git a/src/lib/stores/library.store.ts b/src/lib/stores/library.store.ts index 69dca14..5157259 100644 --- a/src/lib/stores/library.store.ts +++ b/src/lib/stores/library.store.ts @@ -220,6 +220,7 @@ async function getLibrary(): Promise { }; } +let delayedRefreshTimeout: NodeJS.Timeout; function createLibraryStore() { const { update, set, ...library } = writable>(getLibrary()); //TODO promise to undefined @@ -232,6 +233,12 @@ function createLibraryStore() { return { ...library, refresh: async () => getLibrary().then((r) => set(Promise.resolve(r))), + refreshIn: async (ms: number) => { + clearTimeout(delayedRefreshTimeout); + delayedRefreshTimeout = setTimeout(() => { + getLibrary().then((r) => set(Promise.resolve(r))); + }, ms); + }, filterNotInLibrary }; } diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts deleted file mode 100644 index 62f3b4c..0000000 --- a/src/routes/+page.server.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getJellyfinContinueWatching } from '$lib/apis/jellyfin/jellyfinApi'; -import { getTmdbPopularMovies } from '$lib/apis/tmdb/tmdbApi'; -import type { PageServerLoad } from './$types'; - -export const load = (async () => { - const showcases = (await getTmdbPopularMovies()).slice(0, 5); - - const continueWatching = getJellyfinContinueWatching().then(async (items) => { - const itemsFiltered = items?.filter((i) => i.ProviderIds?.Tmdb); - if (!itemsFiltered?.length) return; - - return { - items: itemsFiltered?.map((i) => ({ - tmdbId: i.ProviderIds?.Tmdb, - progress: i.UserData?.PlayedPercentage, - length: (i.RunTimeTicks || 0) / 10_000_000 / 60 - })) - }; - }); - - return { - showcases, - streamed: { - continueWatching - } - }; -}) satisfies PageServerLoad; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3d0f2c9..fd2b04f 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -84,7 +84,8 @@ {/await}
- + +
Continue Watching
{#await continueWatchingProps} {:then props} diff --git a/src/routes/LargePoster.svelte b/src/routes/LargePoster.svelte deleted file mode 100644 index 415c6fb..0000000 --- a/src/routes/LargePoster.svelte +++ /dev/null @@ -1 +0,0 @@ -
Large poster
diff --git a/src/routes/ResourceDetailsControls.svelte b/src/routes/ResourceDetailsControls.svelte deleted file mode 100644 index a3722af..0000000 --- a/src/routes/ResourceDetailsControls.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -
- - - - -
- {#each Array.from({ length }, (_, i) => i) as i} - {#if i === index} - - {:else} - - {/if} - {/each} -
- - - - -
- - - diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte index 4e2bcf1..c61c62a 100644 --- a/src/routes/library/+page.svelte +++ b/src/routes/library/+page.svelte @@ -96,7 +96,8 @@ genres: series.genres || [], backdropUri: item.cardBackdropUrl, rating: series.ratings?.value || series.ratings?.value || item.tmdbRating || 0, - seasons: series.seasons?.length || 0 + seasons: series.seasons?.length || 0, + jellyfinId: item.sonarrSeries?.statistics?.sizeOnDisk ? item.jellyfinId : undefined }; } else if (movie) { props = { @@ -107,7 +108,8 @@ genres: movie.genres || [], backdropUri: item.cardBackdropUrl, rating: movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0, - runtimeMinutes: movie.runtime || 0 + runtimeMinutes: movie.runtime || 0, + jellyfinId: item.radarrMovie?.movieFile ? item.jellyfinId : undefined }; } else { continue; @@ -162,7 +164,7 @@
-
+
diff --git a/src/routes/series/[id]/SeriesPage.svelte b/src/routes/series/[id]/SeriesPage.svelte index 3675927..f329935 100644 --- a/src/routes/series/[id]/SeriesPage.svelte +++ b/src/routes/series/[id]/SeriesPage.svelte @@ -1,5 +1,9 @@