mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-17 18:11:20 -08:00
refactor(chapter): PKG-CHAPTER-03 split runtime into typed lifecycle and view modules
This commit is contained in:
1 parent
a0286c4e95
commit
48660c4e12
23 files changed
+1207
-497
No files matched your search
@@ -0,0 +1,36 @@
|
||||
import type { Chapter } from './types'
|
||||
|
||||
export function normalizeChapters(chapters: Chapter[] = [], duration: number): Chapter[] {
|
||||
if (!Array.isArray(chapters) || !chapters.length || !Number.isFinite(duration) || duration <= 0)
|
||||
return []
|
||||
|
||||
// Sorting, Infinity replacement and gap insertion intentionally mutate caller data.
|
||||
chapters.sort((a, b) => a.start - b.start)
|
||||
for (const [index, chapter] of chapters.entries()) {
|
||||
if (!chapter || typeof chapter.start !== 'number' || typeof chapter.end !== 'number' || typeof chapter.title !== 'string')
|
||||
throw new TypeError('Illegal chapter data type')
|
||||
|
||||
if (chapter.end === Infinity)
|
||||
chapter.end = duration
|
||||
|
||||
const next = chapters[index + 1]
|
||||
if (!Number.isFinite(chapter.start) || !Number.isFinite(chapter.end)
|
||||
|| chapter.start < 0 || chapter.end > duration || chapter.start >= chapter.end
|
||||
|| (next && chapter.end > next.start)) { throw new Error('Illegal chapter time point') }
|
||||
}
|
||||
|
||||
const first = chapters[0]
|
||||
const last = chapters[chapters.length - 1]
|
||||
if (first && first.start > 0)
|
||||
chapters.unshift({ start: 0, end: first.start, title: '' })
|
||||
if (last && last.end < duration)
|
||||
chapters.push({ start: last.end, end: duration, title: '' })
|
||||
|
||||
for (let index = 0; index < chapters.length - 1; index++) {
|
||||
const current = chapters[index]
|
||||
const next = chapters[index + 1]
|
||||
if (current && next && current.end !== next.start)
|
||||
chapters.splice(index + 1, 0, { start: current.end, end: next.start, title: '' })
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import style from './style.less?inline'
|
||||
|
||||
export default function artplayerPluginChapter(option = {}) {
|
||||
return (art) => {
|
||||
const { $player } = art.template
|
||||
const { setStyle, append, clamp, query, addClass, removeClass } = art.constructor.utils
|
||||
|
||||
const html = `
|
||||
<div class="art-chapter">
|
||||
<div class="art-chapter-inner">
|
||||
<div class="art-progress-hover"></div>
|
||||
<div class="art-progress-loaded"></div>
|
||||
<div class="art-progress-played"></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
let $chapters = []
|
||||
|
||||
const $inner = art.query('.art-control-progress-inner')
|
||||
const $control = append($inner, '<div class="art-chapters"></div>')
|
||||
const $title = append($inner, '<div class="art-chapter-title"></div>')
|
||||
|
||||
function showTitle({ $chapter, width }) {
|
||||
const title = $chapter.dataset.title.trim()
|
||||
if (title) {
|
||||
$title.textContent = title
|
||||
const titleWidth = $title.clientWidth
|
||||
if (width <= titleWidth / 2) {
|
||||
setStyle($title, 'left', 0)
|
||||
}
|
||||
else if (width > $inner.clientWidth - titleWidth / 2) {
|
||||
setStyle($title, 'left', `${$inner.clientWidth - titleWidth}px`)
|
||||
}
|
||||
else {
|
||||
setStyle($title, 'left', `${width - titleWidth / 2}px`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function update(chapters = []) {
|
||||
$chapters = []
|
||||
$control.textContent = ''
|
||||
removeClass($player, 'artplayer-plugin-chapter')
|
||||
|
||||
if (!Array.isArray(chapters))
|
||||
return
|
||||
if (!chapters.length)
|
||||
return
|
||||
if (!art.duration)
|
||||
return
|
||||
|
||||
chapters = chapters.sort((a, b) => a.start - b.start)
|
||||
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const chapter = chapters[i]
|
||||
const nextChapter = chapters[i + 1]
|
||||
|
||||
if (chapter.end === Infinity) {
|
||||
chapter.end = art.duration
|
||||
}
|
||||
|
||||
if (
|
||||
typeof chapter.start !== 'number'
|
||||
|| typeof chapter.end !== 'number'
|
||||
|| typeof chapter.title !== 'string'
|
||||
) {
|
||||
throw new TypeError('Illegal chapter data type')
|
||||
}
|
||||
|
||||
if (chapter.start < 0 || chapter.end > art.duration || chapter.start >= chapter.end) {
|
||||
throw new Error('Illegal chapter time point')
|
||||
}
|
||||
|
||||
if (nextChapter && chapter.end > nextChapter.start) {
|
||||
throw new Error('Illegal chapter time point')
|
||||
}
|
||||
}
|
||||
|
||||
if (chapters[0].start > 0) {
|
||||
chapters.unshift({ start: 0, end: chapters[0].start, title: '' })
|
||||
}
|
||||
|
||||
if (chapters[chapters.length - 1].end < art.duration) {
|
||||
chapters.push({ start: chapters[chapters.length - 1].end, end: art.duration, title: '' })
|
||||
}
|
||||
|
||||
for (let i = 0; i < chapters.length - 1; i++) {
|
||||
if (chapters[i].end !== chapters[i + 1].start) {
|
||||
chapters.splice(i + 1, 0, {
|
||||
start: chapters[i].end,
|
||||
end: chapters[i + 1].start,
|
||||
title: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$chapters = chapters.map((chapter) => {
|
||||
const $chapter = append($control, html)
|
||||
const start = clamp(chapter.start, 0, art.duration)
|
||||
const end = clamp(chapter.end, 0, art.duration)
|
||||
const duration = end - start
|
||||
const percentage = duration / art.duration
|
||||
$chapter.dataset.start = start
|
||||
$chapter.dataset.end = end
|
||||
$chapter.dataset.duration = duration
|
||||
$chapter.dataset.title = chapter.title.trim()
|
||||
$chapter.style.width = `${percentage * 100}%`
|
||||
|
||||
return {
|
||||
$chapter,
|
||||
$hover: query('.art-progress-hover', $chapter),
|
||||
$loaded: query('.art-progress-loaded', $chapter),
|
||||
$played: query('.art-progress-played', $chapter),
|
||||
}
|
||||
})
|
||||
|
||||
addClass($player, 'artplayer-plugin-chapter')
|
||||
art.emit('setBar', 'loaded', art.loaded || 0)
|
||||
}
|
||||
|
||||
art.on('setBar', (type, percentage) => {
|
||||
if (!$chapters.length)
|
||||
return
|
||||
|
||||
for (let i = 0; i < $chapters.length; i++) {
|
||||
const { $chapter, $loaded, $played, $hover } = $chapters[i]
|
||||
|
||||
const $target = {
|
||||
hover: $hover,
|
||||
loaded: $loaded,
|
||||
played: $played,
|
||||
}[type]
|
||||
|
||||
if (!$target)
|
||||
return
|
||||
|
||||
const width = $control.clientWidth * percentage
|
||||
const currentTime = art.duration * percentage
|
||||
const duration = Number.parseFloat($chapter.dataset.duration)
|
||||
const start = Number.parseFloat($chapter.dataset.start)
|
||||
const end = Number.parseFloat($chapter.dataset.end)
|
||||
|
||||
if (currentTime < start) {
|
||||
setStyle($target, 'width', 0)
|
||||
}
|
||||
|
||||
if (currentTime > end) {
|
||||
setStyle($target, 'width', '100%')
|
||||
}
|
||||
|
||||
if (currentTime >= start && currentTime <= end) {
|
||||
const percentage = (currentTime - start) / duration
|
||||
setStyle($target, 'width', `${percentage * 100}%`)
|
||||
|
||||
if (type === 'hover' && width > 0) {
|
||||
showTitle({ $chapter, width })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
art.once('video:loadedmetadata', () => update(option.chapters))
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginChapter',
|
||||
update: ({ chapters }) => update(chapters),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'artplayer-plugin-chapter'
|
||||
let $style = document.getElementById(id)
|
||||
if (!$style) {
|
||||
$style = document.createElement('style')
|
||||
$style.id = id
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.head.appendChild($style)
|
||||
})
|
||||
}
|
||||
else {
|
||||
(document.head || document.documentElement).appendChild($style)
|
||||
}
|
||||
}
|
||||
$style.textContent = style
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type Artplayer from 'artplayer'
|
||||
import type { Bar, Option, Result } from './types'
|
||||
import { normalizeChapters } from './chapters'
|
||||
import { createProgress } from './progress'
|
||||
import { installStyle } from './stylesheet'
|
||||
|
||||
export default function artplayerPluginChapter(option: Option = {}) {
|
||||
return (art: Artplayer): Result => {
|
||||
const player = art.template.$player
|
||||
const inner = art.query<HTMLElement>('.art-control-progress-inner')
|
||||
if (!inner)
|
||||
throw new Error('Missing ArtPlayer progress container')
|
||||
const className = 'artplayer-plugin-chapter'
|
||||
let progress: ReturnType<typeof createProgress> | undefined = createProgress(inner)
|
||||
|
||||
function update(chapters?: Option['chapters']) {
|
||||
if (!progress || art.isDestroy)
|
||||
return
|
||||
progress.clear()
|
||||
player.classList.remove(className)
|
||||
const normalized = normalizeChapters(chapters, art.duration)
|
||||
if (!normalized.length)
|
||||
return
|
||||
progress.render(normalized, art.duration)
|
||||
player.classList.add(className)
|
||||
art.emit('setBar', 'loaded', art.loaded || 0)
|
||||
}
|
||||
|
||||
function setBar(type: Bar, percentage: number) {
|
||||
if (!art.isDestroy)
|
||||
progress?.setBar(type, percentage, art.duration)
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
update(option.chapters)
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
art.off('setBar', setBar)
|
||||
art.off('video:loadedmetadata', initialize)
|
||||
art.off('destroy', destroy)
|
||||
progress?.destroy()
|
||||
progress = undefined
|
||||
player.classList.remove(className)
|
||||
}
|
||||
|
||||
art.on('setBar', setBar)
|
||||
art.once('video:loadedmetadata', initialize)
|
||||
art.on('destroy', destroy)
|
||||
return { name: 'artplayerPluginChapter', update: ({ chapters }) => update(chapters) }
|
||||
}
|
||||
}
|
||||
|
||||
installStyle()
|
||||
@@ -0,0 +1,93 @@
|
||||
import type { Bar, Chapter } from './types'
|
||||
|
||||
interface Segment {
|
||||
start: number
|
||||
end: number
|
||||
title: string
|
||||
bars: Record<Bar, HTMLDivElement>
|
||||
}
|
||||
|
||||
export function createProgress(inner: HTMLElement) {
|
||||
const create = (parent: HTMLElement, className: string) => {
|
||||
const element = inner.ownerDocument.createElement('div')
|
||||
element.className = className
|
||||
parent.appendChild(element)
|
||||
return element
|
||||
}
|
||||
const control = create(inner, 'art-chapters')
|
||||
const title = create(inner, 'art-chapter-title')
|
||||
let segments: Segment[] = []
|
||||
|
||||
function clearTitle() {
|
||||
title.textContent = ''
|
||||
title.style.left = '0px'
|
||||
title.style.visibility = 'hidden'
|
||||
}
|
||||
|
||||
function clear() {
|
||||
segments = []
|
||||
control.textContent = ''
|
||||
clearTitle()
|
||||
}
|
||||
|
||||
function render(chapters: Chapter[], duration: number) {
|
||||
segments = chapters.map((chapter) => {
|
||||
const element = create(control, 'art-chapter')
|
||||
const body = create(element, 'art-chapter-inner')
|
||||
const length = chapter.end - chapter.start
|
||||
element.dataset.start = String(chapter.start)
|
||||
element.dataset.end = String(chapter.end)
|
||||
element.dataset.duration = String(length)
|
||||
element.dataset.title = chapter.title.trim()
|
||||
element.style.width = `${length / duration * 100}%`
|
||||
return {
|
||||
start: chapter.start,
|
||||
end: chapter.end,
|
||||
title: chapter.title.trim(),
|
||||
bars: {
|
||||
hover: create(body, 'art-progress-hover'),
|
||||
loaded: create(body, 'art-progress-loaded'),
|
||||
played: create(body, 'art-progress-played'),
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setBar(type: Bar, percentage: number, duration: number) {
|
||||
if (type !== 'hover' && type !== 'loaded' && type !== 'played')
|
||||
return
|
||||
if (!Number.isFinite(percentage))
|
||||
return
|
||||
const currentTime = duration * percentage
|
||||
let hovered: Segment | undefined
|
||||
for (const segment of segments) {
|
||||
const target = segment.bars[type]
|
||||
target.style.width = currentTime < segment.start
|
||||
? '0px'
|
||||
: currentTime > segment.end
|
||||
? '100%'
|
||||
: `${(currentTime - segment.start) / (segment.end - segment.start) * 100}%`
|
||||
if (currentTime >= segment.start && currentTime <= segment.end)
|
||||
hovered = segment
|
||||
}
|
||||
if (type !== 'hover')
|
||||
return
|
||||
|
||||
clearTitle()
|
||||
const width = control.clientWidth * percentage
|
||||
if (hovered?.title && width > 0) {
|
||||
title.textContent = hovered.title
|
||||
title.style.visibility = 'visible'
|
||||
title.style.left = `${Math.max(0, Math.min(width - title.clientWidth / 2, inner.clientWidth - title.clientWidth))}px`
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
clear()
|
||||
control.remove()
|
||||
title.remove()
|
||||
}
|
||||
|
||||
clearTitle()
|
||||
return { clear, render, setBar, destroy }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import style from './style.less?inline'
|
||||
|
||||
export function installStyle() {
|
||||
if (typeof document === 'undefined')
|
||||
return
|
||||
|
||||
const id = 'artplayer-plugin-chapter'
|
||||
const install = () => {
|
||||
let element = document.getElementById(id)
|
||||
if (!element) {
|
||||
element = document.createElement('style')
|
||||
element.id = id
|
||||
;(document.head || document.documentElement).appendChild(element)
|
||||
}
|
||||
element.textContent = style
|
||||
}
|
||||
if (document.getElementById(id) || document.readyState !== 'loading')
|
||||
install()
|
||||
else
|
||||
document.addEventListener('DOMContentLoaded', install, { once: true })
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type artplayerPluginChapter from '../types/artplayer-plugin-chapter'
|
||||
|
||||
export type Option = NonNullable<Parameters<typeof artplayerPluginChapter>[0]>
|
||||
export type Chapter = NonNullable<Option['chapters']>[number]
|
||||
export type Result = ReturnType<ReturnType<typeof artplayerPluginChapter>>
|
||||
export type Bar = 'hover' | 'loaded' | 'played'
|
||||
Reference in new issue
Block a user