feat: Added library sorting and pagination, improved image loading and other small adjustments

This commit is contained in:
Aleksi Lassila
2023-08-31 02:45:36 +03:00
parent 203bce45b8
commit ca3911e062
24 changed files with 354 additions and 255 deletions

View File

@@ -55,7 +55,7 @@
)}
on:click={() => {
if (openInModal) {
openTitleModal(tmdbId, type, title);
openTitleModal(tmdbId, type);
} else {
window.location.href = `/${type}/${tmdbId}`;
}

View File

@@ -4,12 +4,17 @@
export let index = 0;
export let size: 'dynamic' | 'md' | 'lg' = 'md';
export let orientation: 'portrait' | 'landscape' = 'landscape';
</script>
<div
class={classNames('rounded overflow-hidden shadow-lg placeholder shrink-0 aspect-video', {
'h-40': size === 'md',
'h-60': size === 'lg',
class={classNames('rounded-xl overflow-hidden shadow-lg placeholder shrink-0', {
'aspect-video': orientation === 'landscape',
'aspect-[2/3]': orientation === 'portrait',
'w-44': size === 'md' && orientation === 'portrait',
'h-44': size === 'md' && orientation === 'landscape',
'w-60': size === 'lg' && orientation === 'portrait',
'h-60': size === 'lg' && orientation === 'landscape',
'w-full': size === 'dynamic'
})}
style={'animation-delay: ' + ((index * 100) % 2000) + 'ms;'}

View File

@@ -6,7 +6,6 @@
export let disabled = false;
export let position: 'absolute' | 'fixed' = 'fixed';
let anchored = position === 'absolute';
export let bottom = false;
export let id = Symbol();
@@ -21,7 +20,10 @@
}
export function handleOpen(event: MouseEvent) {
if (disabled || (anchored && $contextMenu === id)) return; // Clicking button will close menu
if (disabled || (anchored && $contextMenu === id)) {
close();
return;
}
fixedPosition = { x: event.clientX, y: event.clientY };
contextMenu.show(id);
@@ -63,7 +65,15 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div on:contextmenu|preventDefault={handleOpen} on:click={(e) => anchored && e.stopPropagation()}>
<div
on:contextmenu|preventDefault={handleOpen}
on:click={(e) => {
if (anchored) {
e.stopPropagation();
handleOpen(e);
}
}}
>
<slot />
</div>
@@ -75,12 +85,11 @@
? `left: ${
fixedPosition.x - (fixedPosition.x > windowWidth / 2 ? menu?.clientWidth : 0)
}px; top: ${
fixedPosition.y -
(bottom ? (fixedPosition.y > windowHeight / 2 ? menu?.clientHeight : 0) : 0)
fixedPosition.y - (fixedPosition.y > windowHeight / 2 ? menu?.clientHeight : 0)
}px;`
: menu?.getBoundingClientRect()?.left > windowWidth / 2
? `right: 0;${bottom ? 'bottom: 40px;' : ''}`
: `left: 0;${bottom ? 'bottom: 40px;' : ''}`}
? `right: 0;${fixedPosition.y > windowHeight / 2 ? 'bottom: 100%;' : ''}`
: `left: 0;${fixedPosition.y > windowHeight / 2 ? 'bottom: 100%;' : ''}`}
bind:this={menu}
in:fly|global={{ y: 5, duration: 100, delay: anchored ? 0 : 100 }}
out:fly|global={{ y: 5, duration: 100 }}

View File

@@ -1,25 +1,12 @@
<script lang="ts">
import ContextMenu from './ContextMenu.svelte';
import { contextMenu } from '../ContextMenu/ContextMenu';
import Button from '../Button.svelte';
import { DotsVertical } from 'radix-icons-svelte';
export let heading = '';
export let contextMenuId = Symbol();
function handleToggleVisibility() {
if ($contextMenu === contextMenuId) contextMenu.hide();
else contextMenu.show(contextMenuId);
}
</script>
<div class="relative">
<ContextMenu position="absolute" {heading} id={contextMenuId}>
<ContextMenu position="absolute" {heading}>
<slot name="menu" slot="menu" />
<Button slim on:click={handleToggleVisibility}>
<DotsVertical size={24} />
</Button>
<slot />
</ContextMenu>
</div>

View File

@@ -13,7 +13,7 @@
},
$$restProps.class
)}
on:click|stopPropagation
on:click
>
<slot />
</button>

View File

@@ -11,18 +11,18 @@
}
</script>
<img
{src}
{alt}
class={classNames(
'transition-opacity',
{
'opacity-0': !loaded,
'opacity-100': loaded
},
$$restProps.class
)}
style="object-fit: cover; width: 100%; height: 100%;"
loading="lazy"
on:load={handleLoad}
/>
<div
class={classNames('transition-opacity duration-300', {
'opacity-0': !loaded,
'opacity-100': loaded
})}
>
<img
{src}
{alt}
class={classNames($$restProps.class)}
style="object-fit: cover; width: 100%; height: 100%;"
loading="lazy"
on:load={handleLoad}
/>
</div>

View File

@@ -5,6 +5,7 @@
import ProgressBar from '../ProgressBar.svelte';
import { playerState } from '../VideoPlayer/VideoPlayer';
import LazyImg from '../LazyImg.svelte';
import { Star } from 'radix-icons-svelte';
export let tmdbId: number | undefined = undefined;
export let tvdbId: number | undefined = undefined;
@@ -14,18 +15,24 @@
export let title = '';
export let subtitle = '';
export let rating: number | undefined = undefined;
export let progress = 0;
export let size: 'dynamic' | 'md' = 'md';
export let size: 'dynamic' | 'md' | 'lg' = 'md';
export let orientation: 'portrait' | 'landscape' = 'landscape';
</script>
<a
href={tmdbId || tvdbId ? `/${type}/${tmdbId || tvdbId}` : '#'}
class={classNames(
'relative flex shadow-lg rounded-xl aspect-[2/3] w-44 selectable group hover:text-inherit flex-shrink-0 overflow-hidden',
'relative flex shadow-lg rounded-xl selectable group hover:text-inherit flex-shrink-0 overflow-hidden',
{
'w-44': size === 'md',
'aspect-video': orientation === 'landscape',
'aspect-[2/3]': orientation === 'portrait',
'w-44': size === 'md' && orientation === 'portrait',
'h-44': size === 'md' && orientation === 'landscape',
'w-60': size === 'lg' && orientation === 'portrait',
'h-60': size === 'lg' && orientation === 'landscape',
'w-full': size === 'dynamic'
}
)}
@@ -33,7 +40,7 @@
<LazyImg src={backdropUrl} class="absolute inset-0 group-hover:scale-105 transition-transform" />
<div
class={classNames(
'flex-1 flex flex-col justify-between bg-darken opacity-0 group-hover:opacity-100 transition-opacity z-[1]',
'flex-1 flex flex-col justify-between bg-black bg-opacity-60 opacity-0 group-hover:opacity-100 transition-opacity z-[1]',
{
'py-2 px-3': true
}
@@ -42,7 +49,7 @@
<div class="flex justify-self-start justify-between">
<slot name="top-left">
<div>
<h1 class="font-semibold line-clamp-2">{title}</h1>
<h1 class="text-zinc-100 font-bold line-clamp-2 text-lg">{title}</h1>
<h2 class="text-zinc-300 text-sm font-medium line-clamp-2">{subtitle}</h2>
</div>
</slot>
@@ -52,7 +59,13 @@
</div>
<div class="flex justify-self-end justify-between">
<slot name="bottom-left">
<div />
<div>
{#if rating}
<h2 class="flex items-center gap-1.5 text-sm text-zinc-300 font-medium">
<Star />{rating.toFixed(1)}
</h2>
{/if}
</div>
</slot>
<slot name="bottom-right">
<div />
@@ -75,7 +88,7 @@
{/if}
{#if progress}
<div
class="absolute bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 group-hover:opacity-0 transition-opacity bg-gradient-to-t ease-in-out z-[1]"
class="absolute bottom-2 lg:bottom-3 inset-x-2 lg:inset-x-3 bg-gradient-to-t ease-in-out z-[1]"
>
<ProgressBar {progress} />
</div>

View File

@@ -1,7 +1,16 @@
<script lang="ts">
import { onMount } from 'svelte';
export let progress = 0;
let mounted = false;
onMount(() => {
mounted = true;
});
</script>
<div class="h-1 bg-zinc-200 bg-opacity-20 rounded-full overflow-hidden">
<div style={'width: ' + progress + '%'} class="h-full bg-zinc-200 bg-opacity-80" />
<div
style={'max-width: ' + (mounted ? progress : 0) + '%'}
class="h-full bg-zinc-200 bg-opacity-80 transition-[max-width] delay-200 duration-500"
/>
</div>

View File

@@ -3,6 +3,8 @@
import type { RadarrMovie } from '$lib/apis/radarr/radarrApi';
import type { SonarrSeries } from '$lib/apis/sonarr/sonarrApi';
import type { TitleType } from '$lib/types';
import { DotsVertical } from 'radix-icons-svelte';
import Button from '../Button.svelte';
import ContextMenuButton from '../ContextMenu/ContextMenuButton.svelte';
import LibraryItemContextItems from '../ContextMenu/LibraryItemContextItems.svelte';
@@ -20,4 +22,7 @@
<svelte:fragment slot="menu">
<LibraryItemContextItems {jellyfinItem} {sonarrSeries} {radarrMovie} {type} {tmdbId} />
</svelte:fragment>
<Button slim>
<DotsVertical size={24} />
</Button>
</ContextMenuButton>

View File

@@ -6,7 +6,6 @@
import { modalStack } from '../../stores/modal.store';
export let tmdbId: number;
export let title: string = '';
export let type: TitleType;
export let modalId: symbol;
@@ -26,7 +25,7 @@
{#if type === 'movie'}
<MoviePage {tmdbId} isModal={true} {handleCloseModal} />
{:else}
<SeriesPage {tmdbId} {title} isModal={true} {handleCloseModal} />
<SeriesPage {tmdbId} isModal={true} {handleCloseModal} />
{/if}
</div>
</div>

View File

@@ -123,7 +123,7 @@
out:fade|global={{ duration: ANIMATION_DURATION }}
>
<div class="flex gap-4 items-center">
<Button size="lg" type="primary" on:click={() => openTitleModal(tmdbId, type, title)}>
<Button size="lg" type="primary" on:click={() => openTitleModal(tmdbId, type)}>
<span>{$_('titleShowcase.details')}</span><ChevronRight size={20} />
</Button>
{#if trailerId}

View File

@@ -34,6 +34,7 @@
import Slider from './Slider.svelte';
import { playerState } from './VideoPlayer';
import { linear } from 'svelte/easing';
import ContextMenuButton from '../ContextMenu/ContextMenuButton.svelte';
export let modalId: symbol;
@@ -366,8 +367,8 @@
'cursor-none': !uiVisible
}
)}
in:fade|global={{ duration: 500, easing: linear }}
out:fade|global={{ duration: 300, easing: linear }}
in:fade|global={{ duration: 300, easing: linear }}
out:fade|global={{ duration: 200, easing: linear }}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
@@ -375,8 +376,6 @@
bind:this={videoWrapper}
on:mousemove={() => handleUserInteraction(false)}
on:touchend|preventDefault={() => handleUserInteraction(true)}
on:dblclick|preventDefault={() => (fullscreen = !fullscreen)}
on:click={() => (paused = !paused)}
in:fade|global={{ duration: 500, delay: 1200, easing: linear }}
>
<!-- svelte-ignore a11y-media-has-caption -->
@@ -398,6 +397,8 @@
bind:muted={mute}
class="sm:w-full sm:h-full"
playsinline={true}
on:dblclick|preventDefault={() => (fullscreen = !fullscreen)}
on:click={() => (paused = !paused)}
/>
{#if uiVisible}
@@ -436,29 +437,22 @@
</IconButton>
<div class="flex items-center space-x-3">
<div class="relative">
<ContextMenu
heading="Quality"
position="absolute"
bottom={true}
id={qualityContextMenuId}
>
<svelte:fragment slot="menu">
{#each getQualities(resolution) as quality}
<SelectableContextMenuItem
selected={quality.maxBitrate === currentBitrate}
on:click={() => handleSelectQuality(quality.maxBitrate)}
>
{quality.name}
</SelectableContextMenuItem>
{/each}
</svelte:fragment>
<IconButton on:click={handleQualityToggleVisibility}>
<Gear size={20} />
</IconButton>
</ContextMenu>
</div>
<ContextMenuButton heading="Quality">
<svelte:fragment slot="menu">
{#each getQualities(resolution) as quality}
<SelectableContextMenuItem
selected={quality.maxBitrate === currentBitrate}
on:click={() => handleSelectQuality(quality.maxBitrate)}
>
{quality.name}
</SelectableContextMenuItem>
{/each}
</svelte:fragment>
<IconButton>
<Gear size={20} />
</IconButton>
</ContextMenuButton>
<IconButton
on:click={() => {
mute = !mute;