feat: library page tabs

This commit is contained in:
Aleksi Lassila
2025-02-15 22:20:44 +02:00
parent c33b1e5b3d
commit 91f1da519f
5 changed files with 139 additions and 103 deletions

View File

@@ -18,10 +18,10 @@
};
</script>
<div class="flex items-center justify-between">
<div class="flex items-center justify-between group">
<slot>
{#if label}
<label class="mr-2">
<label class="mr-4 font-medium text-secondary-200 group-focus-within:text-secondary-50">
{label}
</label>
{/if}

View File

@@ -1,28 +1,32 @@
<script lang="ts">
import type { LibraryItemDto } from '$lib/apis/reiverr/reiverr.openapi';
import type { TmdbMovieFull2, TmdbSeriesFull2 } from '$lib/apis/tmdb/tmdb-api';
import Button from '$lib/components/Button.svelte';
import Carousel from '$lib/components/Carousel/Carousel.svelte';
import Container from '$lib/components/Container.svelte';
import { createModal } from '$lib/components/Modal/modal.store';
import { libraryItemsDataStore } from '$lib/stores/data.store';
import { libraryViewSettings, type LibraryViewSettings } from '$lib/stores/localstorage.store';
import { MixerHorizontal } from 'radix-icons-svelte';
import { onDestroy } from 'svelte';
import { derived, writable } from 'svelte/store';
import TmdbCard from '../../components/Card/TmdbCard.svelte';
import CardGrid from '../../components/CardGrid.svelte';
import DetachedPage from '../../components/DetachedPage/DetachedPage.svelte';
import { scrollIntoView } from '../../selectable';
import OptionsDialog from './OptionsDialog.LibraryPage.svelte';
import { libraryViewSettings, type LibraryViewSettings } from '$lib/stores/localstorage.store';
import type { LibraryItemDto } from '$lib/apis/reiverr/reiverr.openapi';
import type { TmdbMovieFull2, TmdbSeriesFull2 } from '$lib/apis/tmdb/tmdb-api';
import Container from '$lib/components/Container.svelte';
import { derived } from 'svelte/store';
import Carousel from '$lib/components/Carousel/Carousel.svelte';
import TabItem from './TabItem.svelte';
type LibraryItemWithMetadata = LibraryItemDto & { metadata: TmdbSeriesFull2 | TmdbMovieFull2 };
let didMount = false;
let category = writable<'all' | 'series' | 'movies'>('all');
const { isLoading, unsubscribe, ...libraryItems } = libraryItemsDataStore.subscribe();
const sortedLibraryItems = derived(
[libraryItems, libraryViewSettings],
([$libraryItems, $libraryViewSettings]) => sortItems($libraryItems, $libraryViewSettings)
[libraryItems, libraryViewSettings, category],
([items, viewSettings, category]) => sortItems(items, viewSettings, category)
);
const libraryItemsCategorized = derived(
@@ -81,14 +85,20 @@
}
);
let didMount = false;
function sortItems(
items: LibraryItemWithMetadata[] | undefined,
viewSettings: LibraryViewSettings
viewSettings: LibraryViewSettings,
category: 'all' | 'series' | 'movies'
) {
const filtered =
category === 'all'
? items?.slice()
: items?.filter((i) =>
category === 'series' ? i.mediaType === 'Series' : i.mediaType === 'Movie'
);
return (
items?.sort((a, b) => {
filtered?.sort((a, b) => {
const aCreatedAt = a.createdAt;
const bCreatedAt = b.createdAt;
@@ -145,78 +155,84 @@
<DetachedPage class="py-16 space-y-8 min-h-screen flex flex-col" let:hasFocus focusOnMount>
{#if !$isLoading}
<div class="h-full flex-1 flex flex-col">
<div class="px-32 flex items-center justify-between">
<div />
<Container class="px-32 flex items-center justify-between" direction="horizontal">
<Container class="flex space-x-4" direction="horizontal">
<TabItem selected={$category === 'all'} on:select={() => category.set('all')}>All</TabItem
>
<TabItem selected={$category === 'series'} on:select={() => category.set('series')}>
Series
</TabItem>
<TabItem selected={$category === 'movies'} on:select={() => category.set('movies')}>
Movies
</TabItem>
</Container>
<Button icon={MixerHorizontal} on:clickOrSelect={() => createModal(OptionsDialog, {})}>
Options
</Button>
</div>
{#if $libraryItemsCategorized.main.length + $libraryItemsCategorized.upcoming.length + $libraryItemsCategorized.watched.length}
{#if $libraryItemsCategorized.upcoming.length}
<div class="mt-6">
<Carousel
header="Upcoming"
scrollClass="px-32"
on:enter={scrollIntoView({ bottom: 0 })}
>
{#key viewSettingsKey}
{#each $libraryItemsCategorized.upcoming as item}
<TmdbCard
on:enter={scrollIntoView({ horizontal: 128 })}
size="lg"
item={item.metadata}
/>
{/each}
{/key}
</Carousel>
</div>
</Container>
<Container focusOnMount={hasFocus || !didMount} on:mount={() => (didMount = true)}>
{#if $libraryItemsCategorized.main.length + $libraryItemsCategorized.upcoming.length + $libraryItemsCategorized.watched.length}
{#if $libraryItemsCategorized.upcoming.length}
<div class="mt-6">
<Carousel
header="Upcoming"
scrollClass="px-32"
on:enter={scrollIntoView({ bottom: 0 })}
>
{#key viewSettingsKey}
{#each $libraryItemsCategorized.upcoming as item (item.tmdbId)}
<TmdbCard
on:enter={scrollIntoView({ horizontal: 128 })}
size="lg"
item={item.metadata}
/>
{/each}
{/key}
</Carousel>
</div>
{/if}
{#if $libraryItemsCategorized.main.length}
<div class="my-6">
<div class="px-32 mb-6 h3">My Library</div>
<CardGrid class="px-32">
{#key viewSettingsKey}
{#each $libraryItemsCategorized.main as item, index (item.tmdbId)}
<TmdbCard
item={item.metadata}
progress={item.playStates?.[0]?.progress || 0}
on:enter={scrollIntoView(index === 0 ? { top: 128 + 64 } : { vertical: 128 })}
size="dynamic"
navigateWithType
/>
{/each}
{/key}
</CardGrid>
</div>
{/if}
{#if $libraryItemsCategorized.watched.length}
<div class="mt-6 px-32">
<div class="mb-6 h3">Watched</div>
<CardGrid>
{#key viewSettingsKey}
{#each $libraryItemsCategorized.watched as item (item.tmdbId)}
<TmdbCard
item={item.metadata}
progress={item.playStates?.[0]?.progress || 0}
on:enter={scrollIntoView({ vertical: 128 })}
size="dynamic"
navigateWithType
/>
{/each}
{/key}
</CardGrid>
</div>
{/if}
{:else}
<Container focusOnMount class="h-ghost m-auto">
You haven't added anything to your library
</Container>
{/if}
{#if $libraryItemsCategorized.main.length}
<div class="my-6">
<div class="px-32 mb-6 h3">My Library</div>
<CardGrid
class="px-32"
focusOnMount={hasFocus || !didMount}
on:mount={() => (didMount = true)}
>
{#key viewSettingsKey}
{#each $libraryItemsCategorized.main as item, index (item.tmdbId)}
<TmdbCard
item={item.metadata}
progress={item.playStates?.[0]?.progress || 0}
on:enter={scrollIntoView(index === 0 ? { top: 128 + 64 } : { vertical: 128 })}
size="dynamic"
navigateWithType
focusedChild={index === 0}
/>
{/each}
{/key}
</CardGrid>
</div>
{/if}
{#if $libraryItemsCategorized.watched.length}
<div class="mt-6 px-32">
<div class="mb-6 h3">Watched</div>
<CardGrid>
{#key viewSettingsKey}
{#each $libraryItemsCategorized.watched as item (item.tmdbId)}
<TmdbCard
item={item.metadata}
progress={item.playStates?.[0]?.progress || 0}
on:enter={scrollIntoView({ vertical: 128 })}
size="dynamic"
navigateWithType
/>
{/each}
{/key}
</CardGrid>
</div>
{/if}
{:else}
<Container focusOnMount class="h-ghost m-auto">
You haven't added anything to your library
</Container>
{/if}
</Container>
</div>
{/if}
</DetachedPage>

View File

@@ -5,9 +5,9 @@
import { libraryViewSettings } from '$lib/stores/localstorage.store';
const sortByOptions = [
{ label: 'Date Added', value: 'date-added' },
{ label: 'Last Release Date', value: 'last-release-date' },
{ label: 'First Release Date', value: 'first-release-date' },
{ label: 'Date Added', value: 'date-added' },
{ label: 'Title', value: 'title' }
];
@@ -25,7 +25,7 @@
}
</script>
<Dialog let:close on:close class="space-y-8">
<Dialog let:close on:close class="space-y-4">
<h1 class="h3 mb-4 flex items-center space-x-4">
<span>View Options</span>
<!-- <MixerHorizontal size={28} /> -->
@@ -45,7 +45,7 @@
on:select={({ detail: direction }) => updateSortByDirection(direction)}
/>
<div class="space-y-4">
<div class="space-y-2 font-medium">
<Toggle
label="Include upcoming"
checked={!$libraryViewSettings.separateUpcoming}

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import Container from '$lib/components/Container.svelte';
import classNames from 'classnames';
import { createEventDispatcher } from 'svelte';
export let selected = false;
const dispatch = createEventDispatcher<{ select: null }>();
</script>
<Container
class="group cursor-pointer"
let:hasFocus
on:clickOrSelect={() => dispatch('select')}
focusOnClick
>
<span
class={classNames('font-semibold text-2xl group-focus-within:text-primary-500', {
'text-secondary-50': selected && !hasFocus,
'text-secondary-400': !hasFocus && !selected
})}
>
<slot />
</span>
</Container>

View File

@@ -1,28 +1,23 @@
<script lang="ts">
import Container from '$components/Container.svelte';
import Button from '../../components/Button.svelte';
import Toggle from '../../components/Toggle.svelte';
import { localSettings } from '../../stores/localstorage.store';
import classNames from 'classnames';
import Tab from '../../components/Tab/Tab.svelte';
import { useTabs } from '../../components/Tab/Tab';
import SonarrIntegration from '../../components/Integrations/SonarrIntegration.svelte';
import RadarrIntegration from '../../components/Integrations/RadarrIntegration.svelte';
import type { JellyfinUser } from '../../apis/jellyfin/jellyfin-api';
import JellyfinIntegration from '../../components/Integrations/JellyfinIntegration.svelte';
import JellyfinIntegrationUsersDialog from '../../components/Integrations/JellyfinIntegrationUsersDialog.svelte';
import { ArrowRight, Exit, Pencil2, Plus } from 'radix-icons-svelte';
import { reiverrApi } from '../../apis/reiverr/reiverr-api';
import { tmdbApi } from '../../apis/tmdb/tmdb-api';
import SelectField from '../../components/SelectField.svelte';
import { ArrowRight, Exit, Pencil2, Plus, Trash } from 'radix-icons-svelte';
import Button from '../../components/Button.svelte';
import DetachedPage from '../../components/DetachedPage/DetachedPage.svelte';
import EditProfileModal from '../../components/Dialog/CreateOrEditProfileModal.svelte';
import TmdbIntegration from '../../components/Integrations/TmdbIntegration.svelte';
import TmdbIntegrationConnectDialog from '../../components/Integrations/TmdbIntegrationConnectDialog.svelte';
import { createModal } from '../../components/Modal/modal.store';
import DetachedPage from '../../components/DetachedPage/DetachedPage.svelte';
import { user } from '../../stores/user.store';
import { sessions } from '../../stores/session.store';
import EditProfileModal from '../../components/Dialog/CreateOrEditProfileModal.svelte';
import SelectField from '../../components/SelectField.svelte';
import { useTabs } from '../../components/Tab/Tab';
import Tab from '../../components/Tab/Tab.svelte';
import Toggle from '../../components/Toggle.svelte';
import { scrollIntoView } from '../../selectable';
import { reiverrApi } from '../../apis/reiverr/reiverr-api';
import TmdbIntegration from '../../components/Integrations/TmdbIntegration.svelte';
import { localSettings } from '../../stores/localstorage.store';
import { sessions } from '../../stores/session.store';
import { user } from '../../stores/user.store';
import Plugins from './MediaSources.ManagePage.svelte';
enum Tabs {