mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-18 19:53:18 +02:00
feat: Show playback time and other improvements to the video player
This commit is contained in:
@@ -22,6 +22,17 @@
|
||||
export let modalId: symbol;
|
||||
export let hidden: boolean = false;
|
||||
|
||||
const itemP = jellyfinApi.getLibraryItem(id);
|
||||
|
||||
let title: string = '';
|
||||
let subtitle: string = '';
|
||||
itemP.then((item) => {
|
||||
title = item?.Name || '';
|
||||
subtitle = `${item?.SeriesName || ''} S${item?.ParentIndexNumber || ''}E${
|
||||
item?.IndexNumber || ''
|
||||
}`;
|
||||
});
|
||||
|
||||
let video: HTMLVideoElement;
|
||||
let paused: boolean;
|
||||
let progressTime: number;
|
||||
@@ -46,11 +57,9 @@
|
||||
};
|
||||
|
||||
async function loadPlaybackInfo(
|
||||
id: string,
|
||||
options: { audioStreamIndex?: number; bitrate?: number; playbackPosition?: number } = {}
|
||||
) {
|
||||
const item = await jellyfinApi.getLibraryItem(id);
|
||||
|
||||
const item = await itemP;
|
||||
const mediaLanguagesStore = createLocalStorageStore<MediaLanguageStore>(
|
||||
'media-tracks-' + (item?.SeriesName || id),
|
||||
{}
|
||||
@@ -154,7 +163,7 @@
|
||||
language: s.Language || ''
|
||||
})) || [],
|
||||
selectAudioTrack: (index: number) =>
|
||||
loadPlaybackInfo(id, {
|
||||
loadPlaybackInfo({
|
||||
...options,
|
||||
audioStreamIndex: index,
|
||||
playbackPosition: progressTime * 10_000_000
|
||||
@@ -179,7 +188,7 @@
|
||||
}, 10_000);
|
||||
}
|
||||
|
||||
loadPlaybackInfo(id);
|
||||
loadPlaybackInfo();
|
||||
|
||||
onDestroy(() => {
|
||||
if (reportProgressInterval) clearInterval(reportProgressInterval);
|
||||
@@ -197,6 +206,8 @@
|
||||
<VideoPlayer
|
||||
{playbackInfo}
|
||||
modalHidden={$modalStackTop?.id !== modalId}
|
||||
{title}
|
||||
{subtitle}
|
||||
bind:paused
|
||||
bind:progressTime
|
||||
bind:video
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import Container from '../../../Container.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import classNames from 'classnames';
|
||||
import type { NavigateEvent } from '../../selectable';
|
||||
import { formatMinutesToTime, formatSecondsToTime } from '../../utils';
|
||||
|
||||
export let totalTime: number;
|
||||
export let progressTime: number;
|
||||
@@ -13,6 +15,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
jumpTo: number;
|
||||
playPause: void;
|
||||
}>();
|
||||
|
||||
let pausedBeforeSeeking = paused;
|
||||
@@ -31,49 +34,71 @@
|
||||
|
||||
dispatch('jumpTo', progressTime);
|
||||
}
|
||||
|
||||
function handleNavigateEvent({ detail }: CustomEvent<NavigateEvent>) {
|
||||
if (detail.direction === 'left') {
|
||||
dispatch('jumpTo', progressTime - 10);
|
||||
detail.preventNavigation();
|
||||
} else if (detail.direction === 'right') {
|
||||
dispatch('jumpTo', progressTime + 30);
|
||||
detail.preventNavigation();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container class="w-full h-4 relative overflow-hidden -mx-1 group" let:hasFocus focusOnMount>
|
||||
<div class="absolute inset-y-1 inset-x-2 rounded-full bg-zinc-300/50" />
|
||||
<div class="w-full flex flex-col">
|
||||
<Container
|
||||
class="h-4 relative overflow-hidden group"
|
||||
let:hasFocus
|
||||
focusOnMount
|
||||
on:navigate={handleNavigateEvent}
|
||||
on:select={() => dispatch('playPause')}
|
||||
>
|
||||
<div class="absolute inset-y-1 inset-x-2 rounded-full bg-zinc-300/50" />
|
||||
|
||||
<!-- Secondary progress -->
|
||||
<div
|
||||
class="absolute inset-y-1 inset-x-2 rounded-full bg-zinc-300/50 transition-transform"
|
||||
style={`left: 0.5rem; right: calc(${(1 - bufferedTime / totalTime) * 100}% - 0.5rem + ${
|
||||
bufferedTime / totalTime
|
||||
}rem);`}
|
||||
/>
|
||||
<!-- Secondary progress -->
|
||||
<div
|
||||
class="absolute inset-y-1 inset-x-2 rounded-full bg-zinc-300/50 transition-transform"
|
||||
style={`left: 0.5rem; right: calc(${(1 - bufferedTime / totalTime) * 100}% - 0.5rem + ${
|
||||
bufferedTime / totalTime
|
||||
}rem);`}
|
||||
/>
|
||||
|
||||
<!-- Primary progress -->
|
||||
<div
|
||||
class="absolute inset-y-1 inset-x-2 rounded-full bg-secondary-100 transition-transform"
|
||||
style={`left: 0.5rem; right: calc(${(1 - progressTime / totalTime) * 100}% - 0.5rem + ${
|
||||
progressTime / totalTime
|
||||
}rem);`}
|
||||
/>
|
||||
<!-- Primary progress -->
|
||||
<div
|
||||
class="absolute inset-y-1 inset-x-2 rounded-full bg-secondary-100 transition-transform"
|
||||
style={`left: 0.5rem; right: calc(${(1 - progressTime / totalTime) * 100}% - 0.5rem + ${
|
||||
progressTime / totalTime
|
||||
}rem);`}
|
||||
/>
|
||||
|
||||
<div
|
||||
class={classNames(
|
||||
'absolute inset-y-0 w-4 h-4 bg-primary-500 rounded-full drop-shadow-2xl transition-opacity',
|
||||
{
|
||||
'opacity-0 group-hover:opacity-100': !hasFocus
|
||||
}
|
||||
)}
|
||||
style={`left: calc(${(progressTime / totalTime) * 100}% - ${progressTime / totalTime}rem);
|
||||
<div
|
||||
class={classNames(
|
||||
'absolute inset-y-0 w-4 h-4 bg-primary-500 rounded-full drop-shadow-2xl transition-opacity',
|
||||
{
|
||||
'opacity-0 group-hover:opacity-100': !hasFocus
|
||||
}
|
||||
)}
|
||||
style={`left: calc(${(progressTime / totalTime) * 100}% - ${progressTime / totalTime}rem);
|
||||
box-shadow: 0 0 0.25rem 2px #00000033;
|
||||
`}
|
||||
/>
|
||||
/>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
class="w-full absolute cursor-pointer h-4 inset-y-0 opacity-0"
|
||||
min={0}
|
||||
max={totalTime}
|
||||
{step}
|
||||
bind:value={progressTime}
|
||||
on:mousedown={handleStartSeeking}
|
||||
on:mouseup={handleStopSeeking}
|
||||
on:touchstart={handleStartSeeking}
|
||||
on:touchend={handleStopSeeking}
|
||||
/>
|
||||
</Container>
|
||||
<input
|
||||
type="range"
|
||||
class="w-full absolute cursor-pointer h-4 inset-y-0 opacity-0"
|
||||
min={0}
|
||||
max={totalTime}
|
||||
{step}
|
||||
bind:value={progressTime}
|
||||
on:mousedown={handleStartSeeking}
|
||||
on:mouseup={handleStopSeeking}
|
||||
on:touchstart={handleStartSeeking}
|
||||
on:touchend={handleStopSeeking}
|
||||
/>
|
||||
</Container>
|
||||
<div class="flex justify-between px-2 pt-4">
|
||||
<span>{formatSecondsToTime(progressTime)}</span>
|
||||
<span>-{formatSecondsToTime(totalTime - progressTime)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
export let playbackInfo: PlaybackInfo | undefined;
|
||||
export let subtitleInfo: SubtitleInfo | undefined;
|
||||
export let title: string;
|
||||
export let subtitle: string;
|
||||
|
||||
export let modalHidden = false;
|
||||
|
||||
@@ -121,12 +123,22 @@
|
||||
bind:video
|
||||
bind:buffering
|
||||
/>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div
|
||||
class={classNames(
|
||||
'absolute inset-0 bg-gradient-to-b from-transparent from-60% to-secondary-950/75 transition-opacity pointer-events-none',
|
||||
{
|
||||
'opacity-0': !showInterface
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<Container
|
||||
class={classNames('absolute inset-x-12 top-8 transition-opacity', {
|
||||
'opacity-0': !showInterface
|
||||
})}
|
||||
>
|
||||
Title
|
||||
<!-- Title-->
|
||||
</Container>
|
||||
{#if buffering || !videoDidLoad}
|
||||
<div class="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
||||
@@ -148,9 +160,12 @@
|
||||
handleHideInterface();
|
||||
}
|
||||
}}
|
||||
class="flex justify-between p-2"
|
||||
class="flex justify-between px-2 py-4 items-end"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<div class="text-secondary-300 font-medium text-wider text-lg">{subtitle}</div>
|
||||
<h1 class="header4">{title}</h1>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<IconButton
|
||||
on:clickOrSelect={() => {
|
||||
@@ -183,6 +198,10 @@
|
||||
on:jumpTo={(e) => {
|
||||
video.currentTime = e.detail;
|
||||
}}
|
||||
on:playPause={() => {
|
||||
if (paused) video.play();
|
||||
else video.pause();
|
||||
}}
|
||||
bind:totalTime
|
||||
bind:progressTime
|
||||
bind:bufferedTime
|
||||
|
||||
Reference in New Issue
Block a user