feat: progress loader button for autoskip op/ed

This commit is contained in:
ThaUnknown 2025-09-05 18:57:05 +02:00
parent da4d56f44d
commit e92efc3e20
No known key found for this signature in database
5 changed files with 39 additions and 37 deletions

View file

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

View file

@ -184,7 +184,7 @@
<div class='flex items-center relative scale-parent'> <div class='flex items-center relative scale-parent'>
<Input <Input
class='pl-9 bg-background select:bg-accent select:text-accent-foreground shadow-sm no-scale placeholder:opacity-50' class='pl-9 bg-background select:bg-accent select:text-accent-foreground shadow-sm no-scale placeholder:opacity-50'
placeholder='Filter by text, or paste a magnet link or torrent file to specify a torrent manually' placeholder='Filter by text, or paste a magnet link or torrent file here to specify a torrent manually'
bind:value={inputText} /> bind:value={inputText} />
<MagnifyingGlass class='h-4 w-4 shrink-0 opacity-50 absolute left-3 text-muted-foreground z-10 pointer-events-none' /> <MagnifyingGlass class='h-4 w-4 shrink-0 opacity-50 absolute left-3 text-muted-foreground z-10 pointer-events-none' />
</div> </div>

View file

@ -36,8 +36,8 @@
<ButtonPrimitive.Root <ButtonPrimitive.Root
class={cn( class={cn(
buttonVariants({ variant, size, className }), 'relative overflow-hidden',
'relative overflow-hidden' buttonVariants({ variant, size, className })
)} )}
type='button' type='button'
on:click={stopAnimation} on:click={stopAnimation}

View file

@ -25,6 +25,8 @@
import { toast } from 'svelte-sonner' import { toast } from 'svelte-sonner'
import VideoDeband from 'video-deband' import VideoDeband from 'video-deband'
import ProgressButton from '../button/progress-button.svelte'
import Animations, { playAnimation } from './animations.svelte' import Animations, { playAnimation } from './animations.svelte'
import DownloadStats from './downloadstats.svelte' import DownloadStats from './downloadstats.svelte'
import EpisodesModal from './episodesmodal.svelte' import EpisodesModal from './episodesmodal.svelte'
@ -34,7 +36,7 @@
import Seekbar from './seekbar.svelte' import Seekbar from './seekbar.svelte'
import Subs from './subtitles' import Subs from './subtitles'
import Thumbnailer from './thumbnailer' import Thumbnailer from './thumbnailer'
import { getChaptersAniSkip, getChapterTitle, sanitizeChapters, screenshot, type Chapter, type MediaInfo } from './util' import { findChapter, getChaptersAniSkip, getChapterTitle, isChapterSkippable, sanitizeChapters, screenshot, type Chapter, type MediaInfo } from './util'
import Volume from './volume.svelte' import Volume from './volume.svelte'
import type { ResolvedFile } from './resolver' import type { ResolvedFile } from './resolver'
@ -56,7 +58,7 @@
import { settings, SUPPORTS } from '$lib/modules/settings' import { settings, SUPPORTS } from '$lib/modules/settings'
import { w2globby } from '$lib/modules/w2g/lobby' import { w2globby } from '$lib/modules/w2g/lobby'
import { getAnimeProgress, setAnimeProgress } from '$lib/modules/watchProgress' import { getAnimeProgress, setAnimeProgress } from '$lib/modules/watchProgress'
import { toTS, scaleBlurFade } from '$lib/utils' import { toTS, scaleBlurFade, cn } from '$lib/utils'
export let mediaInfo: MediaInfo export let mediaInfo: MediaInfo
export let otherFiles: TorrentFile[] export let otherFiles: TorrentFile[]
@ -316,40 +318,27 @@
let currentSkippable: string | null = null let currentSkippable: string | null = null
function checkSkippableChapters () { function checkSkippableChapters () {
const current = findChapter(currentTime) const current = findChapter(currentTime, chapters)
const wasSkippable = currentSkippable
if (current) { if (current) {
currentSkippable = isChapterSkippable(current) currentSkippable = isChapterSkippable(current)
if ($settings.playerSkip && !wasSkippable) animating = true
} }
} }
$: if (currentSkippable && $settings.playerSkip) skip() function stopAnimation () {
animating = false
const skippableChaptersRx: Array<[string, RegExp]> = [
['Opening', /^op$|opening$|^ncop|^opening /mi],
['Ending', /^ed$|ending$|^nced|^ending /mi],
['Recap', /recap/mi]
]
function isChapterSkippable (chapter: Chapter) {
for (const [name, regex] of skippableChaptersRx) {
if (regex.test(chapter.text)) {
return name
}
}
return null
} }
function findChapter (time: number) { let animating = false
return chapters.find(({ start, end }) => time >= start && time <= end)
}
function skip () { function skip () {
const current = findChapter(currentTime) const current = findChapter(currentTime, chapters)
if (current) { if (current) {
if (!isChapterSkippable(current) && (current.end - current.start) > 100) { if (!isChapterSkippable(current) && (current.end - current.start) > 100) {
currentTime = currentTime + 85 currentTime = currentTime + 85
} else { } else {
const endtime = current.end const endtime = current.end
if ((safeduration - endtime | 0) === 0) return next?.()
currentTime = endtime currentTime = endtime
currentSkippable = null currentSkippable = null
} }
@ -360,7 +349,6 @@
} else { } else {
currentTime = currentTime + 85 currentTime = currentTime + 85
} }
video.currentTime = currentTime
} }
let stats: { let stats: {
@ -742,7 +730,7 @@
<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)} on:wheel={handleWheel}> <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} on:keydown={stopAnimation} on:focusin={stopAnimation} on:pointerenter={stopAnimation} on:pointermove={stopAnimation}>
<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}
@ -795,7 +783,7 @@
</div> </div>
{/if} {/if}
<Options {wrapper} bind:openSubs {video} {seekTo} {selectAudio} {selectVideo} {fullscreen} {chapters} {subtitles} {videoFiles} {selectFile} {pip} bind:playbackRate={$playbackRate} bind:subtitleDelay id='player-options-button-top' <Options {wrapper} bind:openSubs {video} {seekTo} {selectAudio} {selectVideo} {fullscreen} {chapters} {subtitles} {videoFiles} {selectFile} {pip} bind:playbackRate={$playbackRate} bind:subtitleDelay id='player-options-button-top'
class='{($settings.minimalPlayerUI || SUPPORTS.isAndroid) ? 'inline-flex' : 'mobile:inline-flex hidden'} p-3 size-12 absolute z-[1] top-4 left-4 bg-black/20 pointer-events-auto transition-opacity select:opacity-100 delay-150 {immersed && 'opacity-0'}' /> class='{($settings.minimalPlayerUI || SUPPORTS.isAndroid) ? 'inline-flex' : 'mobile:inline-flex hidden'} p-3 size-12 absolute z-[1] top-4 left-4 bg-black/20 pointer-events-auto transition-opacity delay-150 select:opacity-100 {immersed && 'opacity-0'}' />
{#if fastForwarding} {#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> <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} {/if}
@ -827,17 +815,17 @@
{/if} {/if}
<Animations /> <Animations />
</div> </div>
{#if currentSkippable}
<ProgressButton onclick={skip} bind:animating size='default' duration={3000} class={cn('px-7 font-bold absolute bottom-40 right-10 transition-opacity delay-150', immersed && !animating && 'opacity-0')}>
Skip {currentSkippable}
</ProgressButton>
{/if}
<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'>
<div class='flex flex-col gap-2 text-left cursor-pointer'> <div class='flex flex-col gap-2 text-left cursor-pointer'>
<EpisodesModal portal={wrapper} {mediaInfo} /> <EpisodesModal portal={wrapper} {mediaInfo} />
</div> </div>
<div class='flex flex-col gap-2 grow-0 items-end self-end'> <div class='flex flex-col gap-2 grow-0 items-end self-end'>
{#if currentSkippable}
<Button on:click={skip} class='font-bold mb-2'>
Skip {currentSkippable}
</Button>
{/if}
<div class='text-[rgba(217,217,217,0.6)] text-sm leading-none font-light line-clamp-1 capitalize'>{getChapterTitle(seeking ? seekPercent * safeduration / 100 : currentTime, chapters) || ''}</div> <div class='text-[rgba(217,217,217,0.6)] text-sm leading-none font-light line-clamp-1 capitalize'>{getChapterTitle(seeking ? seekPercent * safeduration / 100 : currentTime, chapters) || ''}</div>
<div class='ml-auto self-end text-sm leading-none font-light text-nowrap'>{toTS(seeking ? seekPercent * safeduration / 100 : currentTime)} / {toTS(safeduration)}</div> <div class='ml-auto self-end text-sm leading-none font-light text-nowrap'>{toTS(seeking ? seekPercent * safeduration / 100 : currentTime)} / {toTS(safeduration)}</div>
</div> </div>

View file

@ -194,7 +194,21 @@ export async function screenshot (video: HTMLVideoElement, subtitles?: Subtitles
const blob = await new Promise<Blob>(resolve => canvas.toBlob(b => resolve(b!))) const blob = await new Promise<Blob>(resolve => canvas.toBlob(b => resolve(b!)))
canvas.remove() canvas.remove()
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]) await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })])
toast.success('Screenshot', { toast.success('Screenshot', { description: 'Saved screenshot to clipboard.' })
description: 'Saved screenshot to clipboard.' }
})
const skippableChaptersRx: Array<[string, RegExp]> = [
['Opening', /^op$|opening$|^ncop|^opening /mi],
['Ending', /^ed$|ending$|^nced|^ending /mi],
['Recap', /recap/mi]
]
export function isChapterSkippable ({ text }: Chapter) {
for (const [name, regex] of skippableChaptersRx) {
if (regex.test(text)) return name
}
return null
}
export function findChapter (time: number, chapters: Chapter[]) {
return chapters.find(({ start, end }) => time >= start && time <= end)
} }