mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-19 21:02:06 +00:00
wip: player keybinds
This commit is contained in:
parent
445afd1320
commit
86cfff1ec7
5 changed files with 411 additions and 70 deletions
|
|
@ -60,6 +60,7 @@
|
|||
"lucide-svelte": "^0.452.0",
|
||||
"p2pt": "^1.5.1",
|
||||
"simple-store-svelte": "^1.0.6",
|
||||
"svelte-keybinds": "^1.0.9",
|
||||
"svelte-persisted-store": "^0.12.0",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
|
|
|
|||
|
|
@ -74,6 +74,9 @@ importers:
|
|||
simple-store-svelte:
|
||||
specifier: ^1.0.6
|
||||
version: 1.0.6
|
||||
svelte-keybinds:
|
||||
specifier: ^1.0.9
|
||||
version: 1.0.9
|
||||
svelte-persisted-store:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0(svelte@4.2.19)
|
||||
|
|
@ -125,7 +128,7 @@ importers:
|
|||
version: 0.0.18(svelte@4.2.19)
|
||||
eslint-config-standard-universal:
|
||||
specifier: github:thaunknown/eslint-config-standard-universal
|
||||
version: https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/4e269161dc3d4285eb96781a29c13c9c66ded4d3(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(jiti@1.21.6)
|
||||
version: https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/c0cd0946f376fa99433109da8553c7c2013f2934(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(jiti@1.21.6)
|
||||
globals:
|
||||
specifier: ^15.11.0
|
||||
version: 15.11.0
|
||||
|
|
@ -1173,8 +1176,8 @@ packages:
|
|||
peerDependencies:
|
||||
eslint: '>=6.0.0'
|
||||
|
||||
eslint-config-standard-universal@https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/4e269161dc3d4285eb96781a29c13c9c66ded4d3:
|
||||
resolution: {tarball: https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/4e269161dc3d4285eb96781a29c13c9c66ded4d3}
|
||||
eslint-config-standard-universal@https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/c0cd0946f376fa99433109da8553c7c2013f2934:
|
||||
resolution: {tarball: https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/c0cd0946f376fa99433109da8553c7c2013f2934}
|
||||
version: 1.0.4
|
||||
|
||||
eslint-import-resolver-node@0.3.9:
|
||||
|
|
@ -2198,6 +2201,9 @@ packages:
|
|||
peerDependencies:
|
||||
svelte: ^3.19.0 || ^4.0.0
|
||||
|
||||
svelte-keybinds@1.0.9:
|
||||
resolution: {integrity: sha512-bQt9azkXX4SgMJpJzYWQB6D0hj45+Ro2+2Awr4YNtjmuRuKdio+Rxuhky5JJyBBfyRQ7YT63nSR3whH4FACv1A==}
|
||||
|
||||
svelte-persisted-store@0.12.0:
|
||||
resolution: {integrity: sha512-BdBQr2SGSJ+rDWH8/aEV5GthBJDapVP0GP3fuUCA7TjYG5ctcB+O9Mj9ZC0+Jo1oJMfZUd1y9H68NFRR5MyIJA==}
|
||||
engines: {node: '>=0.14'}
|
||||
|
|
@ -2224,8 +2230,8 @@ packages:
|
|||
resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
svelte@5.22.6:
|
||||
resolution: {integrity: sha512-dxHyh3USJyayafSt5I5QD7KuoCM5ZGdIOtLQiKHEro7tymdh0jMcNkiSBVHW+LOA2jEqZEHhyfwN6/pCjx0Fug==}
|
||||
svelte@5.23.1:
|
||||
resolution: {integrity: sha512-DUu3e5tQDO+PtKffjqJ548YfeKtw2Rqc9/+nlP26DZ0AopWTJNylkNnTOP/wcgIt1JSnovyISxEZ/lDR1OhbOw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tabbable@6.2.0:
|
||||
|
|
@ -3558,7 +3564,7 @@ snapshots:
|
|||
eslint: 9.17.0(jiti@1.21.6)
|
||||
semver: 7.6.3
|
||||
|
||||
eslint-config-standard-universal@https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/4e269161dc3d4285eb96781a29c13c9c66ded4d3(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(jiti@1.21.6):
|
||||
eslint-config-standard-universal@https://codeload.github.com/thaunknown/eslint-config-standard-universal/tar.gz/c0cd0946f376fa99433109da8553c7c2013f2934(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(jiti@1.21.6):
|
||||
dependencies:
|
||||
'@stylistic/eslint-plugin': 4.2.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
|
||||
eslint: 9.17.0(jiti@1.21.6)
|
||||
|
|
@ -3566,9 +3572,9 @@ snapshots:
|
|||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2))(eslint@9.17.0(jiti@1.21.6))
|
||||
eslint-plugin-n: 17.15.0(eslint@9.17.0(jiti@1.21.6))
|
||||
eslint-plugin-promise: 7.2.1(eslint@9.17.0(jiti@1.21.6))
|
||||
eslint-plugin-svelte: 3.1.0(eslint@9.17.0(jiti@1.21.6))(svelte@5.22.6)
|
||||
eslint-plugin-svelte: 3.1.0(eslint@9.17.0(jiti@1.21.6))(svelte@5.23.1)
|
||||
globals: 16.0.0
|
||||
svelte: 5.22.6
|
||||
svelte: 5.23.1
|
||||
typescript: 5.7.2
|
||||
typescript-eslint: 8.18.0(eslint@9.17.0(jiti@1.21.6))(typescript@5.7.2)
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -3650,7 +3656,7 @@ snapshots:
|
|||
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.6))
|
||||
eslint: 9.17.0(jiti@1.21.6)
|
||||
|
||||
eslint-plugin-svelte@3.1.0(eslint@9.17.0(jiti@1.21.6))(svelte@5.22.6):
|
||||
eslint-plugin-svelte@3.1.0(eslint@9.17.0(jiti@1.21.6))(svelte@5.23.1):
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.6))
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
|
@ -3662,9 +3668,9 @@ snapshots:
|
|||
postcss-load-config: 3.1.4(postcss@8.4.49)
|
||||
postcss-safe-parser: 7.0.1(postcss@8.4.49)
|
||||
semver: 7.6.3
|
||||
svelte-eslint-parser: 1.0.1(svelte@5.22.6)
|
||||
svelte-eslint-parser: 1.0.1(svelte@5.23.1)
|
||||
optionalDependencies:
|
||||
svelte: 5.22.6
|
||||
svelte: 5.23.1
|
||||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
|
|
@ -4634,7 +4640,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- picomatch
|
||||
|
||||
svelte-eslint-parser@1.0.1(svelte@5.22.6):
|
||||
svelte-eslint-parser@1.0.1(svelte@5.23.1):
|
||||
dependencies:
|
||||
eslint-scope: 8.2.0
|
||||
eslint-visitor-keys: 4.2.0
|
||||
|
|
@ -4643,12 +4649,14 @@ snapshots:
|
|||
postcss-scss: 4.0.9(postcss@8.4.49)
|
||||
postcss-selector-parser: 7.1.0
|
||||
optionalDependencies:
|
||||
svelte: 5.22.6
|
||||
svelte: 5.23.1
|
||||
|
||||
svelte-hmr@0.16.0(svelte@4.2.19):
|
||||
dependencies:
|
||||
svelte: 4.2.19
|
||||
|
||||
svelte-keybinds@1.0.9: {}
|
||||
|
||||
svelte-persisted-store@0.12.0(svelte@4.2.19):
|
||||
dependencies:
|
||||
svelte: 4.2.19
|
||||
|
|
@ -4685,7 +4693,7 @@ snapshots:
|
|||
magic-string: 0.30.12
|
||||
periscopic: 3.1.0
|
||||
|
||||
svelte@5.22.6:
|
||||
svelte@5.23.1:
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
|
|
|||
|
|
@ -193,6 +193,10 @@ details:active,
|
|||
filter: invert(1) grayscale(1) contrast(100)
|
||||
}
|
||||
|
||||
.svelte-keybinds {
|
||||
background: black !important;
|
||||
}
|
||||
|
||||
/* Backplate related things */
|
||||
|
||||
body {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
import * as Tree from '$lib/components/ui/tree'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { EllipsisVertical } from 'lucide-svelte'
|
||||
import { normalizeTracks } from './util'
|
||||
import { normalizeTracks, type Chapter } from './util'
|
||||
import type { Writable } from 'simple-store-svelte'
|
||||
import { tick } from 'svelte'
|
||||
import type { HTMLAttributes } from 'svelte/elements'
|
||||
import { cn } from '$lib/utils'
|
||||
import { cn, toTS } from '$lib/utils'
|
||||
import Keybinds from 'svelte-keybinds'
|
||||
|
||||
export let wrapper: HTMLDivElement
|
||||
|
||||
|
|
@ -15,6 +16,10 @@
|
|||
|
||||
export let selectAudio: (id: string) => void
|
||||
export let selectVideo: (id: string) => void
|
||||
export let fullscreen: () => void
|
||||
export let chapters: Chapter[]
|
||||
export let seekTo: (time: number) => void
|
||||
export let playbackRate: number
|
||||
|
||||
let open = false
|
||||
|
||||
|
|
@ -28,6 +33,15 @@
|
|||
|
||||
let className: HTMLAttributes<HTMLDivElement>['class'] = ''
|
||||
export { className as class }
|
||||
|
||||
export let showKeybinds = false
|
||||
function close () {
|
||||
if (showKeybinds) {
|
||||
showKeybinds = false
|
||||
} else {
|
||||
open = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root portal={wrapper} bind:open>
|
||||
|
|
@ -36,55 +50,117 @@
|
|||
<EllipsisVertical size='24px' class='p-[1px]' />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class='absolute bg-transparent border-none p-0 shadow-none h-full w-full overflow-hidden'>
|
||||
<div on:pointerdown|self={() => { open = false }} class='h-full flex w-full justify-center items-center overflow-y-scroll'>
|
||||
<Tree.Root bind:state={treeState}>
|
||||
<Tree.Item>
|
||||
<span slot='trigger'>Audio</span>
|
||||
<Tree.Sub>
|
||||
{#each Object.entries(normalizeTracks(video.audioTracks ?? [])) as [lang, tracks] (lang)}
|
||||
<Dialog.Content class='absolute bg-transparent border-none p-0 shadow-none size-full overflow-hidden'>
|
||||
<div on:pointerdown|self={close} class='size-full flex justify-center items-center flex-col overflow-y-scroll text-[6px] lg:text-xs'>
|
||||
{#if showKeybinds}
|
||||
<div class='bg-black py-3 px-4 rounded-md text-sm lg:text-lg font-bold mb-4'>
|
||||
Drag and drop binds to change them
|
||||
</div>
|
||||
<Keybinds let:prop={item} autosave={true} clickable={true}>
|
||||
{#if item?.type}
|
||||
<div class='size-full flex justify-center p-1.5 lg:p-3' title={item.desc}>
|
||||
{#if item.icon}
|
||||
<svelte:component this={item.icon} size='2rem' class='h-full' fill={item.id === 'play_arrow' ? 'currentColor' : 'none'} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class='size-full content-center text-center lg:text-lg' title={item?.desc}>{item?.id ?? ''}</div>
|
||||
{/if}
|
||||
</Keybinds>
|
||||
{:else}
|
||||
<Tree.Root bind:state={treeState}>
|
||||
<Tree.Item>
|
||||
<span slot='trigger'>Audio</span>
|
||||
<Tree.Sub>
|
||||
{#each Object.entries(normalizeTracks(video.audioTracks ?? [])) as [lang, tracks] (lang)}
|
||||
<Tree.Item>
|
||||
<span slot='trigger' class='capitalize'>{lang}</span>
|
||||
<Tree.Sub>
|
||||
{#each tracks as track (track.id)}
|
||||
<Tree.Item active={track.enabled} on:click={() => { selectAudio(track.id); open = false }}>
|
||||
<span>{track.label}</span>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
<Tree.Item>
|
||||
<span slot='trigger'>Video</span>
|
||||
<Tree.Sub>
|
||||
{#each Object.entries(normalizeTracks(video.videoTracks ?? [])) as [lang, tracks] (lang)}
|
||||
<Tree.Item>
|
||||
<span slot='trigger' class='capitalize'>{lang}</span>
|
||||
<Tree.Sub>
|
||||
{#each tracks as track (track.id)}
|
||||
<Tree.Item active={track.enabled} on:click={() => { selectVideo(track.id); open = false }}>
|
||||
<span>{track.label}</span>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
<Tree.Item id='subs'>
|
||||
<span slot='trigger'>Subtitles</span>
|
||||
<Tree.Sub>
|
||||
<Tree.Item>
|
||||
<span slot='trigger' class='capitalize'>{lang}</span>
|
||||
<Tree.Sub>
|
||||
{#each tracks as track (track.id)}
|
||||
<Tree.Item active={track.enabled} on:click={() => { selectAudio(track.id); open = false }}>
|
||||
<span>{track.label}</span>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
<span>Consulting</span>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
<Tree.Item>
|
||||
<span slot='trigger'>Video</span>
|
||||
<Tree.Sub>
|
||||
{#each Object.entries(normalizeTracks(video.videoTracks ?? [])) as [lang, tracks] (lang)}
|
||||
<Tree.Item>
|
||||
<span slot='trigger' class='capitalize'>{lang}</span>
|
||||
<Tree.Sub>
|
||||
{#each tracks as track (track.id)}
|
||||
<Tree.Item active={track.enabled} on:click={() => { selectVideo(track.id); open = false }}>
|
||||
<span>{track.label}</span>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
<span>Support</span>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
<Tree.Item id='subs'>
|
||||
<span slot='trigger'>Subtitles</span>
|
||||
<Tree.Sub>
|
||||
<Tree.Item>
|
||||
<span>Consulting</span>
|
||||
</Tree.Item>
|
||||
<Tree.Item>
|
||||
<span>Support</span>
|
||||
</Tree.Item>
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
</Tree.Root>
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
<Tree.Item>
|
||||
<span slot='trigger'>Chapters</span>
|
||||
<Tree.Sub>
|
||||
{#each chapters as { text, start }, i (i)}
|
||||
<Tree.Item on:click={() => { seekTo(start); open = false }}>
|
||||
<div class='flex justify-between w-full'>
|
||||
<span>{text || '?'}</span>
|
||||
<span class='text-muted-foreground'>{toTS(start || 0)}</span>
|
||||
</div>
|
||||
</Tree.Item>
|
||||
{/each}
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
<Tree.Item>
|
||||
<span slot='trigger'>Playback Rate</span>
|
||||
<Tree.Sub>
|
||||
<Tree.Item on:click={() => { playbackRate = 0.5; open = false }}>
|
||||
<span>0.5x</span>
|
||||
</Tree.Item>
|
||||
<Tree.Item on:click={() => { playbackRate = 0.75; open = false }}>
|
||||
<span>0.75x</span>
|
||||
</Tree.Item>
|
||||
<Tree.Item on:click={() => { playbackRate = 1; open = false }}>
|
||||
<span>1x</span>
|
||||
</Tree.Item>
|
||||
<Tree.Item on:click={() => { playbackRate = 1.25; open = false }}>
|
||||
<span>1.25x</span>
|
||||
</Tree.Item>
|
||||
<Tree.Item on:click={() => { playbackRate = 1.5; open = false }}>
|
||||
<span>1.5x</span>
|
||||
</Tree.Item>
|
||||
<Tree.Item on:click={() => { playbackRate = 1.75; open = false }}>
|
||||
<span>1.75x</span>
|
||||
</Tree.Item>
|
||||
<Tree.Item on:click={() => { playbackRate = 2; open = false }}>
|
||||
<span>2x</span>
|
||||
</Tree.Item>
|
||||
</Tree.Sub>
|
||||
</Tree.Item>
|
||||
<Tree.Item on:click={() => (showKeybinds = !showKeybinds)}>
|
||||
Keybinds
|
||||
</Tree.Item>
|
||||
<Tree.Item on:click={fullscreen}>
|
||||
Fullscreen
|
||||
</Tree.Item>
|
||||
</Tree.Root>
|
||||
{/if}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@
|
|||
import { Button } from '$lib/components/ui/button'
|
||||
import { settings } from '$lib/modules/settings'
|
||||
import { bindPiP, toTS } from '$lib/utils'
|
||||
import { Cast, FastForward, Maximize, Minimize, Pause, Rewind, SkipBack, SkipForward } from 'lucide-svelte'
|
||||
import { Cast, FastForward, Maximize, Minimize, Pause, Rewind, SkipBack, SkipForward, Captions, Contrast, List, PictureInPicture2, Proportions, RefreshCcw, RotateCcw, RotateCw, ScreenShare, Volume1, Volume2, VolumeX } from 'lucide-svelte'
|
||||
import { writable, type Writable } from 'simple-store-svelte'
|
||||
import { persisted } from 'svelte-persisted-store'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import Seekbar from './seekbar.svelte'
|
||||
import type { SvelteMediaTimeRange } from 'svelte/elements'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { autoPiP, getChapterTitle, sanitizeChapters, type MediaInfo } from './util'
|
||||
import { autoPiP, getChapterTitle, sanitizeChapters, type Chapter, type MediaInfo } from './util'
|
||||
import Thumbnailer from './thumbnailer'
|
||||
import { onMount } from 'svelte'
|
||||
import native from '$lib/modules/native'
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
import EpisodesList from '$lib/components/EpisodesList.svelte'
|
||||
import { episodes } from '$lib/modules/anizip'
|
||||
import Volume from './volume.svelte'
|
||||
import { loadWithDefaults } from 'svelte-keybinds'
|
||||
|
||||
export let mediaInfo: MediaInfo
|
||||
// bindings
|
||||
|
|
@ -70,6 +71,10 @@
|
|||
return enable ? video.requestPictureInPicture() : document.exitPictureInPicture()
|
||||
}
|
||||
|
||||
function toggleCast () {
|
||||
// TODO
|
||||
}
|
||||
|
||||
$: fullscreenElement ? screen.orientation.lock('landscape') : screen.orientation.unlock()
|
||||
|
||||
function checkAudio () {
|
||||
|
|
@ -127,6 +132,10 @@
|
|||
if (!wasPaused) video.play()
|
||||
}
|
||||
|
||||
function screenshot () {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// animations
|
||||
|
||||
function playAnimation (type: 'play' | 'pause' | 'seekforw' | 'seekback') {
|
||||
|
|
@ -157,11 +166,97 @@
|
|||
// other
|
||||
|
||||
$: chapters = sanitizeChapters([
|
||||
{ start: 5, end: 15, text: 'Chapter 0' },
|
||||
{ start: 5, end: 15, text: 'OP' },
|
||||
{ start: 1.0 * 60, end: 1.2 * 60, text: 'Chapter 1' },
|
||||
{ start: 1.4 * 60, end: 88, text: 'Chapter 2 ' }
|
||||
], safeduration)
|
||||
|
||||
let currentSkippable: string | null = null
|
||||
function checkSkippableChapters () {
|
||||
const current = findChapter(currentTime)
|
||||
if (current) {
|
||||
currentSkippable = isChapterSkippable(current)
|
||||
}
|
||||
}
|
||||
const skippableChaptersRx: Array<[string, RegExp]> = [
|
||||
['Opening', /^op$|opening$|^ncop/mi],
|
||||
['Ending', /^ed$|ending$|^nced/mi],
|
||||
['Recap', /recap/mi]
|
||||
]
|
||||
function isChapterSkippable (chapter: Chapter) {
|
||||
for (const [name, regex] of skippableChaptersRx) {
|
||||
if (regex.test(chapter.text)) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function findChapter (time: number) {
|
||||
return chapters.find(({ start, end }) => time >= start && time <= end)
|
||||
}
|
||||
|
||||
function skip () {
|
||||
const current = findChapter(currentTime)
|
||||
if (current) {
|
||||
if (!isChapterSkippable(current) && (current.end - current.start) > 100) {
|
||||
currentTime = currentTime + 85
|
||||
} else {
|
||||
const endtime = current.end
|
||||
if ((safeduration - endtime | 0) === 0) return next()
|
||||
currentTime = endtime
|
||||
currentSkippable = null
|
||||
}
|
||||
} else if (currentTime < 10) {
|
||||
currentTime = 90
|
||||
} else if (safeduration - currentTime < 90) {
|
||||
currentTime = safeduration
|
||||
} else {
|
||||
currentTime = currentTime + 85
|
||||
}
|
||||
video.currentTime = currentTime
|
||||
}
|
||||
|
||||
let stats: any | null = null
|
||||
let requestCallback: number | null = null
|
||||
function toggleStats () {
|
||||
if (requestCallback) {
|
||||
stats = null
|
||||
video.cancelVideoFrameCallback(requestCallback)
|
||||
requestCallback = null
|
||||
} else {
|
||||
requestCallback = video.requestVideoFrameCallback((a, b) => {
|
||||
stats = {}
|
||||
handleStats(a, b, b)
|
||||
})
|
||||
}
|
||||
}
|
||||
async function handleStats (now: number, metadata: VideoFrameCallbackMetadata, lastmeta: VideoFrameCallbackMetadata) {
|
||||
if (stats) {
|
||||
const msbf = (metadata.mediaTime - lastmeta.mediaTime) / (metadata.presentedFrames - lastmeta.presentedFrames)
|
||||
const fps = (1 / msbf).toFixed(3)
|
||||
stats = {
|
||||
fps,
|
||||
presented: metadata.presentedFrames,
|
||||
dropped: video.getVideoPlaybackQuality().droppedVideoFrames,
|
||||
processing: metadata.processingDuration + ' ms',
|
||||
viewport: video.clientWidth + 'x' + video.clientHeight,
|
||||
resolution: videoWidth + 'x' + videoHeight,
|
||||
buffer: getBufferHealth(metadata.mediaTime) + ' s',
|
||||
speed: video.playbackRate || 1
|
||||
}
|
||||
setTimeout(() => video.requestVideoFrameCallback((n, m) => handleStats(n, m, metadata)), 200)
|
||||
}
|
||||
}
|
||||
function getBufferHealth (time: number) {
|
||||
for (let index = video.buffered.length; index--;) {
|
||||
if (time < video.buffered.end(index) && time >= video.buffered.start(index)) {
|
||||
return (video.buffered.end(index) - time) | 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
$: seekIndex = Math.max(0, Math.floor(seekPercent * safeduration / 100 / thumbnailer.interval))
|
||||
|
||||
$: native.setMediaSession(mediaInfo.session)
|
||||
|
|
@ -184,22 +279,173 @@
|
|||
if (['ArrowLeft', 'ArrowRight'].includes(event.key)) event.stopPropagation()
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
seek(-5)
|
||||
seek(-$settings.playerSeek)
|
||||
break
|
||||
case 'ArrowRight':
|
||||
seek(5)
|
||||
seek($settings.playerSeek)
|
||||
break
|
||||
case 'Enter':
|
||||
playPause()
|
||||
break
|
||||
}
|
||||
}
|
||||
let fitWidth = false
|
||||
loadWithDefaults({
|
||||
KeyX: {
|
||||
fn: () => screenshot(),
|
||||
id: 'screenshot_monitor',
|
||||
icon: ScreenShare,
|
||||
type: 'icon',
|
||||
desc: 'Save Screenshot to Clipboard'
|
||||
},
|
||||
KeyI: {
|
||||
fn: () => toggleStats(),
|
||||
icon: List,
|
||||
id: 'list',
|
||||
type: 'icon',
|
||||
desc: 'Toggle Stats'
|
||||
},
|
||||
Space: {
|
||||
fn: () => playPause(),
|
||||
id: 'play_arrow',
|
||||
icon: Play,
|
||||
type: 'icon',
|
||||
desc: 'Play/Pause'
|
||||
},
|
||||
KeyN: {
|
||||
fn: () => next(),
|
||||
id: 'skip_next',
|
||||
icon: SkipForward,
|
||||
type: 'icon',
|
||||
desc: 'Next Episode'
|
||||
},
|
||||
KeyB: {
|
||||
fn: () => prev(),
|
||||
id: 'skip_previous',
|
||||
icon: SkipBack,
|
||||
type: 'icon',
|
||||
desc: 'Previous Episode'
|
||||
},
|
||||
KeyA: {
|
||||
fn: () => {
|
||||
$settings.playerDeband = !$settings.playerDeband
|
||||
},
|
||||
id: 'deblur',
|
||||
icon: Contrast,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Video Debanding'
|
||||
},
|
||||
KeyM: {
|
||||
fn: () => (muted = !muted),
|
||||
id: 'volume_off',
|
||||
icon: VolumeX,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Mute'
|
||||
},
|
||||
KeyP: {
|
||||
fn: () => pip(),
|
||||
id: 'picture_in_picture',
|
||||
icon: PictureInPicture2,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Picture in Picture'
|
||||
},
|
||||
KeyF: {
|
||||
fn: () => fullscreen(),
|
||||
id: 'fullscreen',
|
||||
icon: Maximize,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Fullscreen'
|
||||
},
|
||||
KeyS: {
|
||||
fn: () => skip(),
|
||||
id: '+90',
|
||||
desc: 'Skip Intro/90s'
|
||||
},
|
||||
KeyW: {
|
||||
fn: () => { fitWidth = !fitWidth },
|
||||
id: 'fit_width',
|
||||
icon: Proportions,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Video Cover'
|
||||
},
|
||||
KeyD: {
|
||||
fn: () => toggleCast(),
|
||||
id: 'cast',
|
||||
icon: Cast,
|
||||
type: 'icon',
|
||||
desc: 'Toggle Cast [broken]'
|
||||
},
|
||||
KeyC: {
|
||||
fn: () => cycleSubtitles(),
|
||||
id: 'subtitles',
|
||||
icon: Captions,
|
||||
type: 'icon',
|
||||
desc: 'Cycle Subtitles'
|
||||
},
|
||||
ArrowLeft: {
|
||||
fn: () => {
|
||||
seek(-$settings.playerSeek)
|
||||
},
|
||||
id: 'fast_rewind',
|
||||
icon: Rewind,
|
||||
type: 'icon',
|
||||
desc: 'Rewind'
|
||||
},
|
||||
ArrowRight: {
|
||||
fn: () => {
|
||||
seek($settings.playerSeek)
|
||||
},
|
||||
id: 'fast_forward',
|
||||
icon: FastForward,
|
||||
type: 'icon',
|
||||
desc: 'Seek'
|
||||
},
|
||||
ArrowUp: {
|
||||
fn: () => {
|
||||
$volume = Math.min(1, $volume + 0.05)
|
||||
},
|
||||
id: 'volume_up',
|
||||
icon: Volume2,
|
||||
type: 'icon',
|
||||
desc: 'Volume Up'
|
||||
},
|
||||
ArrowDown: {
|
||||
fn: () => {
|
||||
$volume = Math.max(0, $volume - 0.05)
|
||||
},
|
||||
id: 'volume_down',
|
||||
icon: Volume1,
|
||||
type: 'icon',
|
||||
desc: 'Volume Down'
|
||||
},
|
||||
BracketLeft: {
|
||||
fn: () => { playbackRate = video.defaultPlaybackRate -= 0.1 },
|
||||
id: 'history',
|
||||
icon: RotateCcw,
|
||||
type: 'icon',
|
||||
desc: 'Decrease Playback Rate'
|
||||
},
|
||||
BracketRight: {
|
||||
fn: () => { playbackRate = video.defaultPlaybackRate += 0.1 },
|
||||
id: 'update',
|
||||
icon: RotateCw,
|
||||
type: 'icon',
|
||||
desc: 'Increase Playback Rate'
|
||||
},
|
||||
Backslash: {
|
||||
fn: () => { playbackRate = video.defaultPlaybackRate = 1 },
|
||||
icon: RefreshCcw,
|
||||
id: 'schedule',
|
||||
type: 'icon',
|
||||
desc: 'Reset Playback Rate'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:document bind:fullscreenElement use:bindPiP={pictureInPictureElement} />
|
||||
|
||||
<div style:aspect-ratio='{videoWidth} / {videoHeight}' class='max-w-full max-h-full min-w-[clamp(0%,700px,100%)] relative content-center fullscreen:bg-black fullscreen:rounded-none rounded-xl overflow-clip text-left' bind:this={wrapper}>
|
||||
<video class='w-full max-h-full grow bg-black' preload='auto' class:cursor-none={immersed}
|
||||
<video class='w-full max-h-full grow bg-black' preload='auto' class:cursor-none={immersed} class:object-cover={fitWidth}
|
||||
src={mediaInfo.url}
|
||||
bind:videoHeight
|
||||
bind:videoWidth
|
||||
|
|
@ -216,6 +462,7 @@
|
|||
on:click={playPause}
|
||||
on:dblclick={fullscreen}
|
||||
on:loadeddata={checkAudio}
|
||||
on:timeupdate={checkSkippableChapters}
|
||||
use:autoPiP={pip}
|
||||
/>
|
||||
<div class='absolute w-full h-full flex items-center justify-center top-0 pointer-events-none'>
|
||||
|
|
@ -224,7 +471,7 @@
|
|||
<img {src} alt='thumbnail' class='w-full h-full bg-black absolute top-0 right-0' loading='lazy' decoding='async' />
|
||||
{/await}
|
||||
{/if}
|
||||
<Options {wrapper} bind:openSubs {video} {selectAudio} {selectVideo} class='mobile:inline-flex hidden p-3 w-12 h-12 absolute top-10 right-10 backdrop-blur-lg border-white/15 border bg-black/20 pointer-events-auto select:opacity-100 cursor-default {immersed && 'opacity-0'}' />
|
||||
<Options {wrapper} bind:openSubs {video} {seekTo} {selectAudio} {selectVideo} {fullscreen} {chapters} bind:playbackRate class='mobile:inline-flex hidden p-3 w-12 h-12 absolute top-10 right-10 backdrop-blur-lg border-white/15 border bg-black/20 pointer-events-auto select:opacity-100 cursor-default {immersed && 'opacity-0'}' />
|
||||
<div class='mobile:flex hidden gap-4 absolute items-center select:opacity-100 cursor-default' class:opacity-0={immersed}>
|
||||
<Button class='p-3 w-16 h-16 pointer-events-auto rounded-[50%] backdrop-blur-lg border-white/15 border bg-black/20' variant='ghost'>
|
||||
<SkipBack size='24px' fill='currentColor' strokeWidth='1' />
|
||||
|
|
@ -260,7 +507,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
<div class='absolute w-full bottom-0 flex flex-col gradient px-10 py-4 transition-opacity select:opacity-100 cursor-default' class:opacity-0={immersed}>
|
||||
<div class='flex justify-between gap-12'>
|
||||
<div class='flex justify-between gap-12 items-end'>
|
||||
<div class='flex flex-col gap-2 text-left cursor-pointer'>
|
||||
<div class='text-white text-lg font-normal leading-none line-clamp-1 hover:text-neutral-300' use:click={() => goto(`/app/anime/${mediaInfo.media.id}`)}>{mediaInfo.session.title}</div>
|
||||
<Sheet.Root portal={wrapper}>
|
||||
|
|
@ -273,6 +520,11 @@
|
|||
</Sheet.Root>
|
||||
</div>
|
||||
<div class='flex flex-col gap-2 grow-0 items-end self-end'>
|
||||
{#if currentSkippable}
|
||||
<Button on:click={skip} class='font-bold mb-2'>
|
||||
Skip {currentSkippable}
|
||||
</Button>
|
||||
{/if}
|
||||
<div class='text-[rgba(217,217,217,0.6)] text-sm leading-none font-light line-clamp-1'>{getChapterTitle(seeking ? seekPercent * safeduration / 100 : currentTime, chapters) || ''}</div>
|
||||
<div class='ml-auto self-end text-sm leading-none font-light text-nowrap'>{toTS(seeking ? seekPercent * safeduration / 100 : currentTime)} / {toTS(safeduration)}</div>
|
||||
</div>
|
||||
|
|
@ -296,7 +548,7 @@
|
|||
<Volume bind:volume={$volume} bind:muted />
|
||||
</div>
|
||||
<div class='flex gap-2'>
|
||||
<Options {wrapper} bind:openSubs {video} {selectAudio} {selectVideo} />
|
||||
<Options {fullscreen} {wrapper} {seekTo} bind:openSubs {video} {selectAudio} {selectVideo} {chapters} bind:playbackRate />
|
||||
<Button class='p-3 w-12 h-12' variant='ghost' on:click={openSubs}>
|
||||
<Subtitles size='24px' fill='currentColor' strokeWidth='0' />
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue