mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-22 00:35:12 +02:00
feat: Added library sorting and pagination, improved image loading and other small adjustments
This commit is contained in:
@@ -55,7 +55,7 @@
|
||||
)}
|
||||
on:click={() => {
|
||||
if (openInModal) {
|
||||
openTitleModal(tmdbId, type, title);
|
||||
openTitleModal(tmdbId, type);
|
||||
} else {
|
||||
window.location.href = `/${type}/${tmdbId}`;
|
||||
}
|
||||
|
||||
@@ -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;'}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
$$restProps.class
|
||||
)}
|
||||
on:click|stopPropagation
|
||||
on:click
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user