From f7a63a45d86aec3e0a3a5f78af3268642119a885 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sun, 10 May 2026 18:34:25 +0200 Subject: [PATCH] feat: extract skip segment detection and auto-skip --- static/player/skip/index.ts | 50 ++++++++++++++++++++++++++++++++++ static/player/skip/segments.ts | 44 ++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 static/player/skip/index.ts create mode 100644 static/player/skip/segments.ts diff --git a/static/player/skip/index.ts b/static/player/skip/index.ts new file mode 100644 index 0000000..4a7a234 --- /dev/null +++ b/static/player/skip/index.ts @@ -0,0 +1,50 @@ +import { state } from '../state' +import { displayTimeFromAbsolute, absoluteTimeFromDisplay } from '../timeline' +import { showControls } from '../controls' +import { resolveActiveSegments, renderSegments } from './segments' + +const skipLabel = (type: string): string => type === 'ed' ? 'Skip outro' : 'Skip intro' + +export const updateSkipButton = (currentTime: number): void => { + const btn = state.container.querySelector('[data-skip]') as HTMLButtonElement | null + const displayTime = displayTimeFromAbsolute(currentTime) + + const segment = state.activeSegments.find(s => { + const delay = Math.min(1, Math.max(0.25, (s.end - s.start) * 0.02)) + return displayTime >= s.start + delay && displayTime < s.end + }) + + if (!segment) { + state.activeSkipSegment = null + btn?.classList.add('hidden') + return + } + + const autoSkip = localStorage.getItem('mal:autoskip-enabled') === 'true' + if (autoSkip && displayTime >= segment.start && displayTime < segment.end) { + state.video.currentTime = absoluteTimeFromDisplay(segment.end + 0.01) + return + } + + state.activeSkipSegment = segment + if (btn) { + btn.textContent = skipLabel(segment.type) + btn.title = skipLabel(segment.type) + btn.classList.remove('hidden') + } +} + +export const updateAutoSkipButton = (): void => { + const btn = document.querySelector('[data-autoskip]') as HTMLInputElement | null + btn && (btn.checked = localStorage.getItem('mal:autoskip-enabled') === 'true') +} + +export const setupSkip = (): void => { + document.addEventListener('change', (e) => { + const target = e.target as HTMLElement + if (target.hasAttribute('data-autoskip')) { + localStorage.setItem('mal:autoskip-enabled', (target as HTMLInputElement).checked ? 'true' : 'false') + showControls() + } + }) +} diff --git a/static/player/skip/segments.ts b/static/player/skip/segments.ts new file mode 100644 index 0000000..26a9393 --- /dev/null +++ b/static/player/skip/segments.ts @@ -0,0 +1,44 @@ +import { SkipSegment } from '../types' +import { state } from '../state' + +const MIN_SEGMENT_DURATION = 20 +const MAX_SEGMENT_DURATION = 240 +const MAX_INTRO_START = 180 +const MIN_OUTRO_START_RATIO = 0.5 + +export const resolveActiveSegments = (): void => { + const bounds = state.video.duration + if (bounds <= 0) { state.activeSegments = []; return } + + state.activeSegments = state.parsedSegments + .filter(s => { + const len = s.end - s.start + if (len < MIN_SEGMENT_DURATION || len > MAX_SEGMENT_DURATION) return false + if (s.start < 0 || s.end <= s.start || s.end > bounds + 1) return false + + if (s.type === 'op') { + return s.start <= MAX_INTRO_START && s.start <= bounds * 0.5 + } + if (s.type === 'ed') { + return s.start >= bounds * MIN_OUTRO_START_RATIO + } + return false + }) +} + +export const renderSegments = (): void => { + const track = state.container.querySelector('[data-segments]') as HTMLElement | null + if (!track) return + track.innerHTML = '' + + const bounds = state.video.duration + if (bounds <= 0) return + + state.activeSegments.forEach(s => { + const bar = document.createElement('div') + bar.className = 'absolute top-0 h-full bg-white/80' + bar.style.left = `${(s.start / bounds) * 100}%` + bar.style.width = `${((s.end - s.start) / bounds) * 100}%` + track.appendChild(bar) + }) +}