mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-05-18 12:11:45 +00:00
feat: scroll wheen in player
feat: track change and volume animations/toasts
This commit is contained in:
parent
34cdb22ae8
commit
58d9854b79
4 changed files with 101 additions and 59 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
71
src/lib/components/ui/player/animations.svelte
Normal file
71
src/lib/components/ui/player/animations.svelte
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue