feat: scroll wheen in player

feat: track change and volume animations/toasts
This commit is contained in:
ThaUnknown 2025-09-03 17:03:51 +02:00
parent 34cdb22ae8
commit 58d9854b79
No known key found for this signature in database
4 changed files with 101 additions and 59 deletions

View file

@ -110,6 +110,7 @@ It is meant to feel look, work and perform like a premium streaming service, but
* Support for most popular BEP's. * Support for most popular BEP's.
* Persist torrents, cache progress, and rescan instantly. * Persist torrents, cache progress, and rescan instantly.
* View detailed torrent and peer info. * View detailed torrent and peer info.
* Batch downloads.
<p align="center"> <p align="center">
<img src='https://raw.githubusercontent.com/hayase-app/website/main/static/modal.webp' width='400px'></img> <img src='https://raw.githubusercontent.com/hayase-app/website/main/static/modal.webp' width='400px'></img>

View file

@ -1,6 +1,6 @@
{ {
"name": "ui", "name": "ui",
"version": "6.4.118", "version": "6.4.119",
"license": "BUSL-1.1", "license": "BUSL-1.1",
"private": true, "private": true,
"packageManager": "pnpm@9.15.5", "packageManager": "pnpm@9.15.5",

View file

@ -0,0 +1,71 @@
<script lang='ts' context='module'>
import FastForward from 'lucide-svelte/icons/fast-forward'
import Pause from 'lucide-svelte/icons/pause'
import Rewind from 'lucide-svelte/icons/rewind'
import Volume1 from 'lucide-svelte/icons/volume-1'
import Volume2 from 'lucide-svelte/icons/volume-2'
import { writable } from 'simple-store-svelte'
import Play from '$lib/components/icons/Play.svelte'
import { settings } from '$lib/modules/settings'
type AnimationType = 'play' | 'pause' | 'seekforw' | 'seekback' | 'volumeup' | 'volumedown' | (string & {})
export function playAnimation (type: AnimationType) {
animations.value = [...animations.value, { type, id: crypto.randomUUID() }]
}
function endAnimation (id: string) {
const animationList = animations.value
const index = animationList.findIndex(animation => animation.id === id)
if (index !== -1) animationList.splice(index, 1)
animations.value = animationList
}
interface Animation {
type: AnimationType
id: string
}
const animations = writable<Animation[]>([])
</script>
{#if !$settings.minimalPlayerUI}
{#each $animations as { type, id } (id)}
<div class='absolute animate-pulse-once' on:animationend={() => endAnimation(id)}>
{#if type === 'play'}
<Play size='64px' fill='white' />
{:else if type === 'pause'}
<Pause size='64px' fill='white' />
{:else if type === 'seekforw'}
<FastForward size='64px' fill='white' />
{:else if type === 'seekback'}
<Rewind size='64px' fill='white' />
{:else if type === 'volumeup'}
<Volume2 size='64px' fill='white' />
{:else if type === 'volumedown'}
<Volume1 size='64px' fill='white' />
{:else}
<div class='text-4xl font-bold text-white'>{type}</div>
{/if}
</div>
{/each}
{/if}
<style>
.animate-pulse-once {
animation: pulse-once .4s linear;
}
@keyframes pulse-once {
0% {
opacity: 1;
scale: 1;
}
100% {
opacity: 0;
scale: 1.2;
}
}
</style>

View file

@ -28,6 +28,7 @@
import { toast } from 'svelte-sonner' import { toast } from 'svelte-sonner'
import VideoDeband from 'video-deband' import VideoDeband from 'video-deband'
import Animations, { playAnimation } from './animations.svelte'
import { condition, loadWithDefaults } from './keybinds.svelte' import { condition, loadWithDefaults } from './keybinds.svelte'
import Options from './options.svelte' import Options from './options.svelte'
import PictureInPicture from './pip' import PictureInPicture from './pip'
@ -178,10 +179,15 @@
}) })
} }
} }
function changeVolume (delta: number) {
playAnimation(delta > 0 ? 'volumeup' : 'volumedown')
$volume = Math.min(1, Math.max(0, $volume + delta))
}
function selectAudio (id: string) { function selectAudio (id: string) {
if (id) { if (id) {
for (const track of video.audioTracks ?? []) { for (const track of video.audioTracks ?? []) {
track.enabled = track.id === id track.enabled = track.id === id
playAnimation(track.label)
} }
seek(-0.2) // stupid fix because video freezes up when chaging tracks seek(-0.2) // stupid fix because video freezes up when chaging tracks
} }
@ -190,6 +196,7 @@
if (id) { if (id) {
for (const track of video.videoTracks ?? []) { for (const track of video.videoTracks ?? []) {
track.selected = track.id === id track.selected = track.id === id
playAnimation(track.label)
} }
} }
} }
@ -200,8 +207,8 @@
playAnimation(time > 0 ? 'seekforw' : 'seekback') playAnimation(time > 0 ? 'seekforw' : 'seekback')
} }
function seekTo (time: number) { function seekTo (time: number) {
playAnimation(time > currentTime ? 'seekforw' : 'seekback')
video.currentTime = currentTime = time video.currentTime = currentTime = time
playAnimation(time > currentTime ? 'seekforw' : 'seekback')
} }
let wasPaused = false let wasPaused = false
function startSeek () { function startSeek () {
@ -235,25 +242,6 @@
}) })
} }
// animations
function playAnimation (type: 'play' | 'pause' | 'seekforw' | 'seekback') {
animations.push({ type, id: crypto.randomUUID() })
// eslint-disable-next-line no-self-assign
animations = animations
}
function endAnimation (id: string) {
const index = animations.findIndex(animation => animation.id === id)
if (index !== -1) animations.splice(index, 1)
// eslint-disable-next-line no-self-assign
animations = animations
}
interface Animation {
type: 'play' | 'pause' | 'seekforw' | 'seekback'
id: string
}
let animations: Animation[] = []
let chapters: Chapter[] = [] let chapters: Chapter[] = []
const chaptersPromise = native.chapters(mediaInfo.file.hash, mediaInfo.file.id) const chaptersPromise = native.chapters(mediaInfo.file.hash, mediaInfo.file.id)
async function loadChapters (pr: typeof chaptersPromise, safeduration: number) { async function loadChapters (pr: typeof chaptersPromise, safeduration: number) {
@ -364,8 +352,8 @@
$: if (currentSkippable && $settings.playerSkip) skip() $: if (currentSkippable && $settings.playerSkip) skip()
const skippableChaptersRx: Array<[string, RegExp]> = [ const skippableChaptersRx: Array<[string, RegExp]> = [
['Opening', /^op$|opening$|^ncop/mi], ['Opening', /^op$|opening$|^ncop|^opening /mi],
['Ending', /^ed$|ending$|^nced/mi], ['Ending', /^ed$|ending$|^nced|^ending /mi],
['Recap', /recap/mi] ['Recap', /recap/mi]
] ]
function isChapterSkippable (chapter: Chapter) { function isChapterSkippable (chapter: Chapter) {
@ -478,9 +466,11 @@
function cycleSubtitles () { function cycleSubtitles () {
if (!subtitles) return if (!subtitles) return
const entries = Object.entries(subtitles._tracks.value) const entries = Object.entries(subtitles._tracks.value)
const index = entries.findIndex(([index]) => index === subtitles!.current.value) if (!entries.length) return
const nextIndex = (index + 1) const index = entries.findIndex(([index]) => index === subtitles!.current.value) + 1
subtitles.selectCaptions((index + 1) >= entries.length ? -1 : entries[nextIndex]![0]) const [id, info] = entries[index] ?? [-1, { meta: { name: 'Off', language: 'Eng' } }]
playAnimation(info.meta.name ?? info.meta.language ?? 'Eng')
subtitles.selectCaptions(id)
} }
function seekBarKey (event: KeyboardEvent) { function seekBarKey (event: KeyboardEvent) {
@ -632,7 +622,7 @@
e.preventDefault() e.preventDefault()
e.stopImmediatePropagation() e.stopImmediatePropagation()
e.stopPropagation() e.stopPropagation()
$volume = Math.min(1, $volume + 0.05) changeVolume(0.05)
}, },
id: 'volume_up', id: 'volume_up',
icon: Volume2, icon: Volume2,
@ -645,7 +635,7 @@
e.preventDefault() e.preventDefault()
e.stopImmediatePropagation() e.stopImmediatePropagation()
e.stopPropagation() e.stopPropagation()
$volume = Math.max(0, $volume - 0.05) changeVolume(-0.05)
}, },
id: 'volume_down', id: 'volume_down',
icon: Volume1, icon: Volume1,
@ -779,11 +769,20 @@
stopProgressBar() stopProgressBar()
} }
}) })
function handleWheel ({ shiftKey, deltaY }: WheelEvent) {
const sign = Math.sign(deltaY)
if (shiftKey) {
seek(Number($settings.playerSeek) * sign * -1)
} else {
changeVolume(-0.05 * sign)
}
}
</script> </script>
<svelte:document bind:fullscreenElement bind:visibilityState use:holdToFF={'key'} /> <svelte:document bind:fullscreenElement bind:visibilityState use:holdToFF={'key'} />
<div class='w-full h-full relative content-center bg-black overflow-clip text-left touch-none' class:fitWidth class:seeking class:pip={pictureInPictureElement} bind:this={wrapper} on:navigate={() => resetMove(2000)}> <div class='w-full h-full relative content-center bg-black overflow-clip text-left touch-none' class:fitWidth class:seeking class:pip={pictureInPictureElement} bind:this={wrapper} on:navigate={() => resetMove(2000)} on:wheel={handleWheel}>
<video class='w-full h-full touch-none' preload='metadata' class:cursor-none={immersed} class:cursor-pointer={isMiniplayer} class:object-cover={fitWidth} class:opacity-0={$settings.playerDeband || seeking || pictureInPictureElement} class:absolute={$settings.playerDeband} class:top-0={$settings.playerDeband} <video class='w-full h-full touch-none' preload='metadata' class:cursor-none={immersed} class:cursor-pointer={isMiniplayer} class:object-cover={fitWidth} class:opacity-0={$settings.playerDeband || seeking || pictureInPictureElement} class:absolute={$settings.playerDeband} class:top-0={$settings.playerDeband}
use:createSubtitles use:createSubtitles
use:createDeband={$settings.playerDeband} use:createDeband={$settings.playerDeband}
@ -881,21 +880,7 @@
<div class='border-[3px] rounded-[50%] w-10 h-10 drop-shadow-lg border-transparent border-t-white animate-spin' /> <div class='border-[3px] rounded-[50%] w-10 h-10 drop-shadow-lg border-transparent border-t-white animate-spin' />
</div> </div>
{/if} {/if}
{#if !$settings.minimalPlayerUI} <Animations />
{#each animations as { type, id } (id)}
<div class='absolute animate-pulse-once' on:animationend={() => endAnimation(id)}>
{#if type === 'play'}
<Play size='64px' fill='white' />
{:else if type === 'pause'}
<Pause size='64px' fill='white' />
{:else if type === 'seekforw'}
<FastForward size='64px' fill='white' />
{:else if type === 'seekback'}
<Rewind size='64px' fill='white' />
{/if}
</div>
{/each}
{/if}
</div> </div>
<div class='absolute w-full bottom-0 flex flex-col gradient px-6 py-3 transition-opacity delay-150 select:opacity-100' class:opacity-0={immersed}> <div class='absolute w-full bottom-0 flex flex-col gradient px-6 py-3 transition-opacity delay-150 select:opacity-100' class:opacity-0={immersed}>
<div class='flex justify-between gap-12 items-end'> <div class='flex justify-between gap-12 items-end'>
@ -1029,19 +1014,4 @@
.gradient-to-bottom { .gradient-to-bottom {
background: linear-gradient(to bottom, oklab(0 0 0 / 0.85) 0%, oklab(0 0 0 / 0.7) 35%, oklab(0 0 0 / 0) 100%); background: linear-gradient(to bottom, oklab(0 0 0 / 0.85) 0%, oklab(0 0 0 / 0.7) 35%, oklab(0 0 0 / 0) 100%);
} }
.animate-pulse-once {
animation: pulse-once .4s linear;
}
@keyframes pulse-once {
0% {
opacity: 1;
scale: 1;
}
100% {
opacity: 0;
scale: 1.2;
}
}
</style> </style>