perf: disable pulse animation when not necessary

chore: improve activity checking code and stores
This commit is contained in:
ThaUnknown 2025-04-10 01:09:42 +02:00
parent 37266b88e8
commit 9d98c00dd6
No known key found for this signature in database
5 changed files with 77 additions and 28 deletions

View file

@ -1,5 +1,6 @@
<script lang='ts'>
import { page } from '$app/stores'
import { activityState, idleState, isPlaying, lockedState } from '$lib/modules/idle'
import { settings } from '$lib/modules/settings'
import { sleep } from '$lib/utils'
@ -7,9 +8,6 @@
export let root: HTMLDivElement
let visibilityState: DocumentVisibilityState
let idleState = 'idle' as 'active' | 'idle'
let lockedState = 'unlocked' as 'unlocked' | 'locked'
let activityState = document.hasFocus() ? 'active' : 'inactive' as 'active' | 'inactive'
let isAnimating = false
let isSpinning = false
@ -30,7 +28,7 @@
}
async function reset () {
if (!isAnimating) return clearTimeout(timeout)
if (!isAnimating) return
root.style.transform = getComputedStyle(root).transform
plate.style.transform = getComputedStyle(plate).transform
isSpinning = isFlying = false
@ -39,32 +37,20 @@
await sleep(790)
isAnimating = isSpinning = isFlying = false
}
// @ts-expect-error non-standard API
const idleDetector = new IdleDetector()
idleDetector.addEventListener('change', () => {
idleState = idleDetector.userState
lockedState = idleDetector.screenState
})
idleDetector.start({
threshold: 60_000
})
function checkIdleState (idleState: 'active' | 'idle', lockedState: 'unlocked' | 'locked', activityState: 'active' | 'inactive', visibilityState: DocumentVisibilityState) {
if ($settings.idleAnimation === 'off') return reset()
// don't waste resources
if (lockedState === 'locked' || visibilityState === 'hidden') return reset()
if (idleState === 'active' && activityState === 'active') return reset()
if ($page.url.pathname === '/app/player/') return reset()
$: active = $lockedState === 'locked' || visibilityState === 'hidden' || ($idleState === 'active' && $activityState === 'active') || $isPlaying
function checkIdleState (active: boolean, idleAnimation: 'fancy' | 'fast' | 'off') {
clearTimeout(timeout)
if (idleAnimation === 'off' || active) return reset()
timeout = setTimeout(start, 20_000)
}
$: checkIdleState(idleState, lockedState, activityState, visibilityState)
$: checkIdleState(active, $settings.idleAnimation)
</script>
<svelte:document bind:visibilityState on:mouseleave={() => { if (!document.hasFocus()) activityState = 'inactive' }} on:mouseenter={() => { activityState = 'active' }} />
<svelte:window on:focus={() => { activityState = 'active' }} on:blur={() => { activityState = 'inactive' }}
on:pointermove={() => { idleState = 'active'; activityState = 'active' }} />
<svelte:document bind:visibilityState />
<div class='preserve-3d absolute w-full h-full overflow-hidden flip backface-hidden backplate bg-black flex-col justify-center pointer-events-none hidden'
bind:this={plate}

View file

@ -7,6 +7,7 @@
import { hideBanner } from '$lib/components/ui/banner'
import { server } from '$lib/modules/torrent'
import { page } from '$app/stores'
import { goto } from '$app/navigation'
onMount(async () => {
await tick()
@ -18,25 +19,29 @@
$: active = resolveFilesPoorly($act)
$: isMiniplayer = $page.route.id !== '/app/player'
function openPlayer () {
goto('/app/player/')
}
</script>
<div class='w-full {isMiniplayer ? 'z-[100] max-w-80 absolute bottom-4 right-4 rounded-lg overflow-clip' : 'h-full'}'>
<div class='w-full {isMiniplayer ? 'z-[49] max-w-80 absolute bottom-4 left-4 md:left-[unset] md:right-4 rounded-lg overflow-clip' : 'h-full'}'>
{#if active}
{#await active}
<div class='w-full flex justify-center items-center bg-black aspect-video'>
<div class='w-full flex justify-center items-center bg-black aspect-video cursor-pointer' on:click={openPlayer}>
<div class='border-[3px] rounded-[50%] w-10 h-10 drop-shadow-lg border-transparent border-t-white animate-spin' />
</div>
{:then mediaInfo}
{#if mediaInfo}
<Mediahandler {mediaInfo} />
{:else}
<div class='w-full flex justify-center items-center bg-black aspect-video'>
<div class='w-full flex justify-center items-center bg-black aspect-video cursor-pointer' on:click={openPlayer}>
<div class='border-[3px] rounded-[50%] w-10 h-10 drop-shadow-lg border-transparent border-t-white animate-spin' />
</div>
{/if}
{/await}
{:else}
<div class='w-full flex justify-center items-center bg-black aspect-video'>
<div class='w-full flex justify-center items-center bg-black aspect-video cursor-pointer' on:click={openPlayer}>
<div class='border-[3px] rounded-[50%] w-10 h-10 drop-shadow-lg border-transparent border-t-white animate-spin' />
</div>
{/if}

View file

@ -31,6 +31,7 @@
import EpisodesList from '$lib/components/EpisodesList.svelte'
import { episodes } from '$lib/modules/anizip'
import { page } from '$app/stores'
import { isPlaying } from '$lib/modules/idle'
export let mediaInfo: MediaInfo
export let files: TorrentFile[]
@ -64,6 +65,8 @@
let paused = true
const cast = false
$: $isPlaying = !paused
$: buffering = readyState < 3
$: immersed = !buffering && !seeking && !paused && !ended && !$pictureInPictureElement

View file

@ -10,12 +10,20 @@
import native from '$lib/modules/native'
import client from '$lib/modules/auth/client'
import * as Avatar from '$lib/components/ui/avatar'
import { lockedState, idleState, activityState } from '$lib/modules/idle'
import { page } from '$app/stores'
const auth = client.hasAuth
$: hasAuth = $auth
let visibilityState: DocumentVisibilityState
$: active = ($lockedState === 'locked' || visibilityState === 'hidden' || ($idleState === 'active' && $activityState === 'active')) && $page.route.id !== '/app/player'
</script>
<svelte:document bind:visibilityState />
<BannerImage class='absolute top-0 left-0 w-14 -z-10 hidden md:block' />
<img src='/logo_cropped.png' alt='logo' class='mb-3 cursor-pointer h-10 object-contain px-1 hidden md:block' loading='lazy' decoding='async' />
<SidebarButton href='/app/home/'>
@ -38,7 +46,7 @@
</SidebarButton>
<Button variant='ghost' on:click={() => native.openURL('https://github.com/sponsors/ThaUnknown/')} class='px-2 w-full relative mt-auto select:!bg-transparent text-[#fa68b6] select:text-[#fa68b6]'>
<Heart size={18} fill='currentColor' class='absolute' />
<Heart size={18} fill='currentColor' class='donate' />
<Heart size={18} fill='currentColor' class={active ? 'donate' : 'hidden'} />
</Button>
<SidebarButton href='/app/settings/'>
<Settings size={18} />

47
src/lib/modules/idle.ts Normal file
View file

@ -0,0 +1,47 @@
import { derived, readable } from 'svelte/store'
import { writable } from 'simple-store-svelte'
import { page } from '$app/stores'
export const activityState = readable<'active' | 'inactive'>(document.hasFocus() ? 'active' : 'inactive', set => {
set(document.hasFocus() ? 'active' : 'inactive')
const ctrl = new AbortController()
window.addEventListener('pointermove', () => set('active'), { signal: ctrl.signal })
window.addEventListener('focus', () => set('active'), { signal: ctrl.signal })
window.addEventListener('blur', () => set('inactive'), { signal: ctrl.signal })
document.addEventListener('mouseenter', () => set('active'), { signal: ctrl.signal })
document.addEventListener('mouseleave', () => {
if (!document.hasFocus()) set('inactive')
}, { signal: ctrl.signal })
return () => ctrl.abort()
})
// @ts-expect-error non-standard API
const idleDetector = new IdleDetector()
idleDetector.start({
threshold: 60_000
})
export const idleState = readable<'active' | 'idle'>(idleDetector.userState, set => {
set(idleDetector.userState)
const ctrl = new AbortController()
idleDetector.addEventListener('change', () => set(idleDetector.userState), { signal: ctrl.signal })
window.addEventListener('pointermove', () => set('active'), { signal: ctrl.signal })
return () => ctrl.abort()
})
export const lockedState = readable<'locked' | 'unlocked'>(idleDetector.screenState, set => {
set(idleDetector.screenState)
const ctrl = new AbortController()
idleDetector.addEventListener('change', () => set(idleDetector.screenState), { signal: ctrl.signal })
return () => ctrl.abort()
})
export const isPlaying = writable(false)