mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-03-11 22:15:35 +00:00
feat: improve player navigation
This commit is contained in:
parent
b0662b4b1c
commit
12a08f9bb7
7 changed files with 41 additions and 22 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue