feat: improve player navigation

This commit is contained in:
ThaUnknown 2025-05-24 19:46:46 +02:00
parent b0662b4b1c
commit 12a08f9bb7
No known key found for this signature in database
7 changed files with 41 additions and 22 deletions

View file

@ -1,6 +1,6 @@
{
"name": "ui",
"version": "6.3.23",
"version": "6.3.24",
"license": "BUSL-1.1",
"private": true,
"packageManager": "pnpm@9.14.4",

View file

@ -15,7 +15,7 @@
import * as Dialog from '$lib/components/ui/dialog'
import * as Tooltip from '$lib/components/ui/tooltip'
import * as Tree from '$lib/components/ui/tree'
import { dragScroll } from '$lib/modules/navigate'
import { dragScroll, keywrap } from '$lib/modules/navigate'
import { settings } from '$lib/modules/settings'
import { cn, toTS } from '$lib/utils'
@ -65,11 +65,13 @@
}
let fullscreenElement: HTMLElement | null = null
export let id = ''
</script>
<Dialog.Root portal={wrapper} bind:open>
<Dialog.Trigger asChild let:builder>
<Button class={cn('p-3 w-12 h-12', className)} variant='ghost' builders={[builder]}>
<Button class={cn('p-3 w-12 h-12', className)} variant='ghost' builders={[builder]} on:keydown={keywrap(() => { open = !open })} data-left='#player-volume-button, #player-next-button, #player-prev-button, #player-play-pause-button' data-up='#player-seekbar' {id}>
<EllipsisVertical size='24px' class='p-[1px]' />
</Button>
</Dialog.Trigger>
@ -156,7 +158,7 @@
<Tree.Sub>
{#each chapters as { text, start }, i (i)}
<Tree.Item on:click={() => { seekTo(start); open = false }}>
<div class='flex justify-between w-full'>
<div class='flex justify-between w-full pr-2'>
<span>{text || '?'}</span>
<span class='text-muted-foreground'>{toTS(start || 0)}</span>
</div>
@ -192,7 +194,7 @@
</Tree.Item>
<Tree.Item>
<span slot='trigger'>Playlist</span>
<Tree.Sub>
<Tree.Sub class='w-auto max-w-96'>
{#each videoFiles as file, i (i)}
<Tree.Item on:click={() => selectFile(file)}>
<Tooltip.Root>

View file

@ -54,7 +54,7 @@
import { authAggregator } from '$lib/modules/auth'
import { isPlaying } from '$lib/modules/idle'
import native from '$lib/modules/native'
import { click } from '$lib/modules/navigate'
import { click, keywrap } from '$lib/modules/navigate'
import { settings } from '$lib/modules/settings'
import { server } from '$lib/modules/torrent'
import { w2globby } from '$lib/modules/w2g/lobby'
@ -420,7 +420,7 @@
$: if (readyState && !seekIndex) thumbnailer._paintThumbnail(video, playbackIndex)
$: native.setMediaSession(mediaInfo.session, mediaInfo.media.id)
$: native.setPositionState({ duration: safeduration, position: Math.max(0, currentTime), playbackRate })
$: native.setPositionState({ duration: safeduration, position: Math.min(Math.max(0, currentTime), safeduration), playbackRate })
$: native.setPlayBackState(readyState === 0 ? 'none' : paused ? 'paused' : 'playing')
native.setActionHandler('play', playPause)
native.setActionHandler('pause', playPause)
@ -790,7 +790,7 @@
<Seekbar {duration} {currentTime} buffer={buffer / duration * 100} {chapters} bind:seeking bind:seek={seekPercent} on:seeked={finishSeek} on:seeking={startSeek} {thumbnailer} on:keydown={seekBarKey} on:dblclick={fullscreen} />
<div class='justify-between gap-2 {$settings.minimalPlayerUI ? 'hidden' : 'mobile:hidden flex'}'>
<div class='flex text-white gap-2'>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={playPause} id='play-pause-button'>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={playPause} on:keydown={keywrap(playPause)} id='player-play-pause-button' data-up='#player-seekbar'>
{#if paused}
<Play size='24px' fill='currentColor' class='p-0.5' />
{:else}
@ -798,25 +798,25 @@
{/if}
</Button>
{#if prev}
<Button class='p-3 w-12 h-12' variant='ghost' on:click={prev}>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={prev} on:keydown={keywrap(prev)} id='player-prev-button' data-up='#player-seekbar' data-right='#player-next-button, #player-volume-button, #player-options-button'>
<SkipBack size='24px' fill='currentColor' strokeWidth='1' />
</Button>
{/if}
{#if next}
<Button class='p-3 w-12 h-12' variant='ghost' on:click={next}>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={next} on:keydown={keywrap(next)} id='player-next-button' data-up='#player-seekbar' data-right='#player-volume-button, #player-options-button'>
<SkipForward size='24px' fill='currentColor' strokeWidth='1' />
</Button>
{/if}
<Volume bind:volume={$volume} bind:muted />
</div>
<div class='flex gap-2'>
<Options {fullscreen} {wrapper} {seekTo} bind:openSubs {video} {selectAudio} {selectVideo} {chapters} {subtitles} {videoFiles} {selectFile} {pip} bind:playbackRate />
<Options {fullscreen} {wrapper} {seekTo} bind:openSubs {video} {selectAudio} {selectVideo} {chapters} {subtitles} {videoFiles} {selectFile} {pip} bind:playbackRate id='player-options-button' />
{#if subtitles}
<Button class='p-3 w-12 h-12' variant='ghost' on:click={openSubs}>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={openSubs} on:keydown={keywrap(openSubs)} data-up='#player-seekbar'>
<Subtitles size='24px' fill='currentColor' strokeWidth='0' />
</Button>
{/if}
<Button class='p-3 w-12 h-12' variant='ghost' on:click={() => pip.pip()}>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={() => pip.pip()} on:keydown={keywrap(() => pip.pip())} data-up='#player-seekbar'>
{#if pictureInPictureElement}
<PictureInPictureExit size='24px' strokeWidth='2' />
{:else}
@ -824,7 +824,7 @@
{/if}
</Button>
{#if false}
<Button class='p-3 w-12 h-12' variant='ghost'>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={toggleCast} on:keydown={keywrap(toggleCast)} data-up='#player-seekbar'>
{#if cast}
<Cast size='24px' fill='white' strokeWidth='2' />
{:else}
@ -832,7 +832,7 @@
{/if}
</Button>
{/if}
<Button class='p-3 w-12 h-12' variant='ghost' on:click={fullscreen}>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={fullscreen} on:keydown={keywrap(fullscreen)} data-up='#player-seekbar'>
{#if fullscreenElement}
<Minimize size='24px' class='p-0.5' strokeWidth='2.5' />
{:else}

View file

@ -129,7 +129,8 @@
<div class='w-full flex cursor-pointer relative group/seekbar touch-none !transform-none' class:!cursor-grab={seeking}
tabindex='0' role='slider' aria-valuenow='0'
data-down='#play-pause-button'
id='player-seekbar'
data-down='#player-play-pause-button'
data-up='#episode-list-button'
on:dblclick
on:keydown

View file

@ -6,6 +6,7 @@
import VolumeX from 'lucide-svelte/icons/volume-x'
import { Button } from '$lib/components/ui/button'
import { keywrap } from '$lib/modules/navigate'
function clamp (value: number) {
return Math.min(Math.max(value, 0), 1)
@ -42,7 +43,7 @@
</script>
<div class='h-full w-full hidden sm:flex flex-row gap-2 group/volume'>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={mute}>
<Button class='p-3 w-12 h-12' variant='ghost' on:click={mute} on:keydown={keywrap(mute)} id='player-volume-button' data-right='#player-options-button' data-up='#player-seekbar'>
{#if muted}
<VolumeOff size='24px' fill='currentColor' />
{:else if volume === 0}

View file

@ -2,9 +2,18 @@
import { createChildLevel } from './context.ts'
import Menu from './menu.svelte'
import type { HTMLAttributes } from 'svelte/elements'
import { cn } from '$lib/utils'
createChildLevel()
type $$Props = HTMLAttributes<HTMLDivElement>
let className: $$Props['class'] = ''
export { className as class }
</script>
<Menu class='absolute right-[calc(-100%-1.25rem)] top-[calc(-1px-0.25rem)]'>
<Menu class={cn('absolute left-[calc(100%+0.5rem)] top-[calc(-1px-0.25rem)]', className)}>
<slot />
</Menu>

View file

@ -31,6 +31,7 @@ export function clickwrap (cb: (_: MouseEvent) => unknown = noop) {
return (e: MouseEvent) => {
e.stopPropagation()
e.preventDefault()
e.stopImmediatePropagation()
navigator.vibrate(15)
cb(e)
}
@ -38,9 +39,10 @@ export function clickwrap (cb: (_: MouseEvent) => unknown = noop) {
export function keywrap (cb: (_: KeyboardEvent) => unknown = noop) {
return (e: KeyboardEvent) => {
if (e.key === 'Enter' && intputType.value === 'dpad') {
if ((e.key === 'Enter' || e.key === ' ') && intputType.value === 'dpad' && !e.repeat) {
e.stopPropagation()
e.preventDefault()
e.stopImmediatePropagation()
cb(e)
}
}
@ -218,9 +220,13 @@ function navigateDPad (direction = 'up') {
const keyboardFocusable = getFocusableElementPositions()
const currentElement = !document.activeElement || document.activeElement === document.body ? keyboardFocusable[0]! : getElementPosition(document.activeElement as HTMLElement)
// allow overrides via data attributes ex: <div data-up="#id"?>
// this is safe, queryselector accepts undefined
if (focusElement(document.querySelector<HTMLElement>(currentElement.element.dataset[direction]!))) return
// allow overrides via data attributes ex: <div data-up="#id, #id2"?> but order them, as querySelectorAll returns them in order of appearance rather than order of selectors
for (const selector of currentElement.element.dataset[direction]?.split(',') ?? []) {
const element = document.querySelector<HTMLElement>(selector.trim())
if (!element) continue // skip if no element found
if (!element.checkVisibility()) continue // skip elements that are not visible
if (focusElement(element)) return
}
const elementsInDesiredDirection = getElementsInDesiredDirection(keyboardFocusable, currentElement, direction)