mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-21 14:21:57 +00:00
feat: progress loader button for autoskip op/ed
This commit is contained in:
parent
da4d56f44d
commit
e92efc3e20
5 changed files with 39 additions and 37 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue