mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-17 21:53:12 +02:00
feat: library page tabs
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
25
src/lib/pages/LibraryPage/TabItem.svelte
Normal file
25
src/lib/pages/LibraryPage/TabItem.svelte
Normal 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>
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user