feat: support back gesture in episode and search modals

feat: immerse all ui when fastforwarding
fix: smaller mobile buttons on video player
fix: only show media track selection if supported
feat: hide settings overlay when activating PiP
feat: close hamburger menu on outside click and navigate
fix: mal and kitsu entry values getting overwritten
fix: no longer exclude ember
fix: some settings scaling on mobile
This commit is contained in:
ThaUnknown 2025-07-14 00:59:17 +02:00
parent f078ae8f19
commit a54a709c65
No known key found for this signature in database
13 changed files with 128 additions and 77 deletions

View file

@ -1,6 +1,6 @@
{
"name": "ui",
"version": "6.4.31",
"version": "6.4.32",
"license": "BUSL-1.1",
"private": true,
"packageManager": "pnpm@9.14.4",

View file

@ -54,10 +54,12 @@
</script>
<script lang='ts'>
import { getContext } from 'svelte'
import ProgressButton from './ui/button/progress-button.svelte'
import { Banner } from './ui/img'
import { goto } from '$app/navigation'
import { beforeNavigate, goto } from '$app/navigation'
import { searchStore } from '$lib'
import { saved } from '$lib/modules/extensions'
import { server } from '$lib/modules/torrent'
@ -147,6 +149,16 @@
$: searchResult && startAnimation(searchResult)
const downloaded = server.downloaded
const stop = getContext<() => void>('stop-progress-bar')
beforeNavigate(({ cancel }) => {
if (open) {
cancel()
close()
stop()
}
})
</script>
<Dialog.Root bind:open onOpenChange={close} portal='#episodeListTarget'>

View file

@ -27,7 +27,6 @@
})
async function runBind (e: MouseEvent | KeyboardEvent, code: KeyCode) {
if ('repeat' in e && e.repeat) return
const kbn = get(binds)
if (cnd(code)) kbn[layout[code] ?? code]?.fn(e)
}

View file

@ -99,40 +99,44 @@
</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>
{#if 'audioTracks' in HTMLVideoElement.prototype}
<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>
{/if}
{#if 'videoTracks' in HTMLVideoElement.prototype}
<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>
{/if}
{#if subtitles}
<Tree.Item id='subs'>
<span slot='trigger'>Subtitles</span>
@ -225,7 +229,7 @@
<Tree.Item on:click={fullscreen} active={!!fullscreenElement}>
Fullscreen
</Tree.Item>
<Tree.Item on:click={() => pip.pip()} active={!!$pipElement}>
<Tree.Item on:click={() => { pip.pip(); close() }} active={!!$pipElement}>
Picture in Picture
</Tree.Item>
<Tree.Item on:click={deband} active={$settings.playerDeband}>

View file

@ -24,7 +24,7 @@
import Volume1 from 'lucide-svelte/icons/volume-1'
import Volume2 from 'lucide-svelte/icons/volume-2'
import VolumeX from 'lucide-svelte/icons/volume-x'
import { onDestroy, onMount } from 'svelte'
import { getContext, onDestroy, onMount } from 'svelte'
import { fade } from 'svelte/transition'
import { persisted } from 'svelte-persisted-store'
import { toast } from 'svelte-sonner'
@ -116,12 +116,13 @@
let ended = false
let paused = true
let pointerMoving = false
let fastForwarding = false
const cast = false
$: $isPlaying = !paused
$: buffering = readyState < 3
$: immersed = !buffering && !paused && !ended && !pictureInPictureElement && !pointerMoving
$: immersed = (!buffering && !paused && !ended && !pictureInPictureElement && !pointerMoving) || fastForwarding
$: isMiniplayer = $page.route.id !== '/app/player'
let pointerMoveTimeout = 0
@ -466,10 +467,8 @@
}
function seekBarKey (event: KeyboardEvent) {
// left right up down return preventdefault
if (['ArrowLeft', 'ArrowRight'].includes(event.key)) event.stopPropagation()
if (event.repeat) return
switch (event.key) {
case 'ArrowLeft':
seek(-Number($settings.playerSeek))
@ -673,8 +672,6 @@
$condition = () => !isMiniplayer
let ff = false
function holdToFF (document: HTMLElement, type: 'key' | 'pointer') {
const ctrl = new AbortController()
let timeout = 0
@ -682,22 +679,22 @@
const startFF = () => {
timeout = setTimeout(() => {
paused = false
ff = true
fastForwarding = true
oldPlaybackRate = playbackRate
playbackRate = 2
}, 1000)
}
const endFF = () => {
clearTimeout(timeout)
if (ff) {
ff = false
if (fastForwarding) {
fastForwarding = false
playbackRate = oldPlaybackRate
paused = true
}
}
document.addEventListener(type + 'down' as 'keydown' | 'pointerdown', (event) => {
if (isMiniplayer) return
if ('code' in event && (event.code !== 'Space' || event.repeat)) return
if ('code' in event && (event.code !== 'Space')) return
if ('pointerId' in event) document.setPointerCapture(event.pointerId)
startFF()
}, { signal: ctrl.signal })
@ -736,8 +733,17 @@
setAnimeProgress(mediaInfo.media.id, { episode: mediaInfo.episode, currentTime: video.currentTime, safeduration })
}
const saveProgressLoop = setInterval(saveAnimeProgress, 10000)
onDestroy(() => {
clearInterval(saveProgressLoop)
onDestroy(() => clearInterval(saveProgressLoop))
let episodeListOpen = false
const stopProgressBar = getContext<() => void>('stop-progress-bar')
beforeNavigate(({ cancel }) => {
if (episodeListOpen) {
episodeListOpen = false
cancel()
stopProgressBar()
}
})
</script>
@ -811,23 +817,23 @@
</div>
{/if}
<Options {wrapper} bind:openSubs {video} {seekTo} {selectAudio} {selectVideo} {fullscreen} {chapters} {subtitles} {videoFiles} {selectFile} {pip} bind:playbackRate bind:subtitleDelay
class='{$settings.minimalPlayerUI ? 'inline-flex' : 'mobile:inline-flex hidden'} p-3 w-12 h-12 absolute top-4 left-4 backdrop-blur-lg border-white/15 border bg-black/20 pointer-events-auto transition-opacity select:opacity-100 {immersed && 'opacity-0'}' />
{#if ff}
class='{$settings.minimalPlayerUI ? 'inline-flex' : 'mobile:inline-flex hidden'} p-3 w-12 h-12 absolute top-4 left-4 bg-black/20 pointer-events-auto transition-opacity select:opacity-100 {immersed && 'opacity-0'}' />
{#if fastForwarding}
<div class='absolute top-10 font-bold text-sm animate-[fade-in_.4s_ease] flex items-center leading-none bg-black/60 px-4 py-2 rounded-2xl'>x2 <FastForward class='ml-2' size='12' fill='currentColor' /></div>
{/if}
<div class='mobile:flex hidden gap-4 absolute items-center transition-opacity select:opacity-100' 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' disabled={!prev}>
<SkipBack size='24px' fill='currentColor' strokeWidth='1' />
<div class='mobile:flex hidden gap-10 absolute items-center transition-opacity select:opacity-100' class:opacity-0={immersed || seeking}>
<Button class='p-3 size-10 pointer-events-auto rounded-[50%] bg-black/20' variant='ghost' disabled={!prev}>
<SkipBack fill='currentColor' strokeWidth='1' />
</Button>
<Button class='p-3 w-24 h-24 pointer-events-auto rounded-[50%] backdrop-blur-lg border-white/15 border bg-black/20' variant='ghost' on:click={playPause}>
<Button class='p-2.5 size-12 pointer-events-auto rounded-[50%] bg-black/20' variant='ghost' on:click={playPause}>
{#if paused}
<Play size='42px' fill='currentColor' class='p-0.5' />
<Play fill='currentColor' class='p-0.5' />
{:else}
<Pause size='42px' fill='currentColor' strokeWidth='1' />
<Pause fill='currentColor' strokeWidth='1' />
{/if}
</Button>
<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' disabled={!next}>
<SkipForward size='24px' fill='currentColor' strokeWidth='1' />
<Button class='p-3 size-10 pointer-events-auto rounded-[50%] bg-black/20' variant='ghost' disabled={!next}>
<SkipForward fill='currentColor' strokeWidth='1' />
</Button>
</div>
{#if buffering}
@ -855,7 +861,7 @@
<div class='flex justify-between gap-12 items-end'>
<div class='flex flex-col gap-2 text-left cursor-pointer'>
<a class='text-white text-lg font-normal leading-none line-clamp-1 hover:text-neutral-300 hover:underline' href='/app/anime/{mediaInfo.media.id}'>{mediaInfo.session.title}</a>
<Sheet.Root portal={wrapper}>
<Sheet.Root portal={wrapper} bind:open={episodeListOpen}>
<Sheet.Trigger id='episode-list-button' class='text-[rgba(217,217,217,0.6)] hover:text-neutral-500 text-sm leading-none font-light line-clamp-1 text-left hover:underline'>{mediaInfo.session.description}</Sheet.Trigger>
<Sheet.Content class='w-full sm:w-[550px] p-3 sm:p-6 max-w-full sm:max-w-full h-full overflow-y-scroll flex flex-col pb-0 shrink-0 gap-0 bg-black justify-between'>
{#if mediaInfo.media}

View file

@ -4,20 +4,40 @@
import { Button } from '../button'
import { onNavigate } from '$app/navigation'
import { breakpoints } from '$lib/utils'
let open = false // 152 x 140
onNavigate(() => {
open = false
})
let container: HTMLDivElement | undefined
function outsideclick (node: HTMLDivElement) {
const ctrl = new AbortController()
node.addEventListener('click', e => {
if (!container || container.contains(e.target as Node)) return
open = false
}, { signal: ctrl.signal })
return { destroy: () => ctrl.abort() }
}
</script>
<svelte:window use:outsideclick />
{#if !$breakpoints.md}
<div class='shrink-0 z-50 bg-black absolute left-4 bottom-4 w-14 h-[52px] flex rounded-md items-end justify-end overflow-clip transition-[width,height] group-fullscreen/fullscreen:hidden' class:!w-[152px]={open} class:!h-[140px]={open}>
<div class='shrink-0 z-50 bg-black absolute left-4 bottom-4 w-14 h-[52px] flex rounded-md items-end justify-end overflow-clip transition-[width,height] group-fullscreen/fullscreen:hidden' class:!w-[152px]={open} class:!h-[140px]={open} bind:this={container}>
<div class='p-2 grid grid-cols-3 gap-2 shrink-0'>
<slot />
<Button variant='ghost' class='px-2 w-full relative' on:click={() => { open = !open }}>
{#if open}
<X size={18} fill='currentColor' />
<X size={18} fill='currentColor' class='pointer-events-none' />
{:else}
<Menu size={18} fill='currentColor' />
<Menu size={18} fill='currentColor' class='pointer-events-none' />
{/if}
</Button>
</div>

View file

@ -449,14 +449,15 @@ export default new class KitsuSync {
const kitsuEntry = this.userlist.value[targetMediaId]
const kitsuEntryVariables = {
const kitsuEntryVariables: Partial<KEntry> = {
status: AL_TO_KITSU_STATUS[variables.status!],
progress: variables.progress ?? undefined,
rating: (variables.score ?? 0) < 2 ? undefined : variables.score!.toString(),
reconsumeCount: variables.repeat ?? undefined,
reconsuming: variables.status === 'REPEATING'
}
if (variables.progress) kitsuEntryVariables.progress = variables.progress
if (variables.score) kitsuEntryVariables.rating = (variables.score < 2 ? undefined : variables.score.toString())
if (variables.repeat) kitsuEntryVariables.reconsumeCount = variables.repeat
if (kitsuEntry) {
await this._updateEntry(kitsuEntry.id, kitsuEntryVariables, targetMediaId)
} else {

View file

@ -436,12 +436,13 @@ export default new class MALSync {
const body: MALListUpdate = {
status: AL_TO_MAL_STATUS[variables.status!],
num_watched_episodes: variables.progress ?? 0,
score: variables.score ?? 0,
num_times_rewatched: variables.repeat ?? 0,
is_rewatching: variables.status === 'REPEATING'
}
if (variables.progress) body.num_watched_episodes = variables.progress
if (variables.score) body.score = variables.score
if (variables.repeat) body.num_times_rewatched = variables.repeat
const res = await this._patch<MALStatus>(`${ENDPOINTS.API_ANIME}/${malId}/my_list_status`, body)
if ('error' in res) return

View file

@ -14,8 +14,7 @@ import type { TorrentResult } from 'hayase-extensions'
import { dev } from '$app/environment'
import { options as extensionOptions, saved } from '$lib/modules/extensions'
// TODO: ember exclusions might not be needed anymore as parser was improved
const exclusions = ['DTS', 'TrueHD', '[EMBER]']
const exclusions = ['DTS', 'TrueHD']
const video = document.createElement('video')

View file

@ -4,6 +4,7 @@
import '@fontsource/geist-mono'
import '$lib/modules/navigate'
import { ProgressBar } from '@prgm/sveltekit-progress-bar'
import { setContext } from 'svelte'
import { toast } from 'svelte-sonner'
import Backplate from '$lib/components/Backplate.svelte'
@ -24,6 +25,14 @@
toast.error('Torrent Process Error!', { description: error?.stack ?? error?.message })
console.error(error)
})
const displayThresholdMs = 150
let complete: ((settleTime: number | undefined) => void) | undefined
setContext('stop-progress-bar', () => {
setTimeout(() => {
complete?.(0)
}, displayThresholdMs)
})
</script>
<svelte:head>
@ -31,7 +40,7 @@
</svelte:head>
<div class='w-full h-full flex flex-col backface-hidden bg-black relative overflow-clip md:border-l-2 [border-image:linear-gradient(to_bottom,white_var(--progress),#2dcf58_var(--progress))_1] preserve-3d' bind:this={root} id='root' style:--progress='{100 - updateProgress}%'>
<ProgressBar zIndex={100} />
<ProgressBar zIndex={100} bind:complete {displayThresholdMs} />
<Toaster position='top-right' expand={true} />
<Menubar />

View file

@ -73,7 +73,7 @@
<Switch {id} bind:checked={$settings.playerDeband} />
</SettingCard>
<SettingCard let:id title='Seek Duration' description='Seconds to skip forward or backward when using the seek buttons or keyboard shortcuts. Higher values might negatively impact buffering speeds.'>
<div class='flex items-center relative scale-parent border border-input rounded-md'>
<div class='flex items-center relative scale-parent border border-input rounded-md self-baseline'>
<Input type='number' inputmode='numeric' pattern='[0-9]*.?[0-9]*' min='1' max='50' bind:value={$settings.playerSeek} {id} class='w-32 shrink-0 bg-background pr-12 border-0 no-scale' />
<div class='shrink-0 absolute right-3 z-10 pointer-events-none text-sm leading-5'>sec</div>
</div>

View file

@ -40,7 +40,7 @@
<Switch {id} bind:checked={$settings.torrentStreamedDownload} />
</SettingCard>
<SettingCard let:id title='Transfer Speed Limit' description='Download/Upload speed limit for torrents, higher values increase CPU usage, and values higher than your storage write speeds will quickly fill up RAM.'>
<div class='flex items-center relative scale-parent border border-input rounded-md'>
<div class='flex items-center relative scale-parent border border-input rounded-md self-baseline'>
<Input type='number' inputmode='numeric' pattern='[0-9]*.?[0-9]*' min='1' max='50' step='0.1' bind:value={$settings.torrentSpeed} {id} class='w-32 shrink-0 bg-background pr-12 border-0 no-scale' />
<div class='shrink-0 absolute right-3 z-10 pointer-events-none text-sm leading-5'>Mb/s</div>
</div>

View file

@ -72,7 +72,7 @@
<Switch {id} bind:checked={$settings.torrentStreamedDownload} />
</SettingCard>
<SettingCard class='bg-transparent' let:id title='Transfer Speed Limit' description='Download/Upload speed limit for torrents, higher values increase CPU usage, and values higher than your storage write speeds will quickly fill up RAM.'>
<div class='flex items-center relative scale-parent border border-input rounded-md'>
<div class='flex items-center relative scale-parent border border-input rounded-md self-baseline'>
<Input type='number' inputmode='numeric' pattern='[0-9]*.?[0-9]*' min='1' max='50' step='0.1' bind:value={$settings.torrentSpeed} {id} class='w-32 shrink-0 bg-background pr-12 border-0 no-scale' />
<div class='shrink-0 absolute right-3 z-10 pointer-events-none text-sm leading-5'>Mb/s</div>
</div>