mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-19 16:52:04 +00:00
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:
parent
f078ae8f19
commit
a54a709c65
13 changed files with 128 additions and 77 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue