feature: combined-video player.
This commit is contained in:
@ -65,4 +65,219 @@
|
||||
.ui-media-video {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.ui-media-video-container {
|
||||
position: relative;
|
||||
background-color: #000;
|
||||
min-height: 200px;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
|
||||
--icon-size: 16px;
|
||||
|
||||
>.ui-video-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
>video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
>.ui-video-waiting {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, .3);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: opacity .2s;
|
||||
|
||||
>svg {
|
||||
fill: #eee;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
top: calc(50% - 15px);
|
||||
left: calc(50% - 15px);
|
||||
animation: spinner 1.2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.ui-video-control {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background: linear-gradient(transparent calc(100% - 100px), rgba(50, 50, 50, .7) calc(100% - 65px), #000);
|
||||
transition: opacity .5s;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ui-video-icon {
|
||||
position: absolute;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
cursor: pointer;
|
||||
|
||||
>svg {
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
display: block;
|
||||
fill: #eee;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-video-bar {
|
||||
position: absolute;
|
||||
height: 4px;
|
||||
padding: 4px 0;
|
||||
cursor: pointer;
|
||||
|
||||
>.ui-video-duration,
|
||||
>.ui-video-progress {
|
||||
position: absolute;
|
||||
border-radius: 2px;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
>.ui-video-duration {
|
||||
width: 100%;
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
>.ui-video-progress {
|
||||
width: 0;
|
||||
background-color: #fff;
|
||||
|
||||
&::after {
|
||||
content: '\a0';
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 6px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.play-icon {
|
||||
left: 20px;
|
||||
bottom: 41px;
|
||||
|
||||
>svg {
|
||||
position: absolute;
|
||||
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.pause {
|
||||
>svg:first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
>svg:last-child {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.ui-video-time-label {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
left: 48px;
|
||||
bottom: 40px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
>.fullscreen-icon {
|
||||
right: 20px;
|
||||
bottom: 41px;
|
||||
}
|
||||
|
||||
>.ui-video-volume-container {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: 50px;
|
||||
|
||||
>.volume-icon {
|
||||
right: 0;
|
||||
bottom: 1px;
|
||||
|
||||
>svg {
|
||||
position: absolute;
|
||||
|
||||
&:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.muted {
|
||||
>svg:first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
>svg:last-child {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.volume-bar {
|
||||
width: 60px;
|
||||
right: 30px;
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-fullscreen>.ui-video-volume-container {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
>.seek-bar {
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
|
||||
>.seek-progress::after {
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
}
|
||||
|
||||
&.active>.seek-bar>.seek-progress::after,
|
||||
>.seek-bar:hover>.seek-progress::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
5
lib/ui/date.d.ts
vendored
5
lib/ui/date.d.ts
vendored
@ -30,8 +30,9 @@ export function formatDate(date: Date | number | string, formatter?: string): st
|
||||
* 设置显示日期
|
||||
* @param element 要设置显示日期的元素
|
||||
* @param val 日期值,支持格式参见 {@linkcode formatDate}
|
||||
* @param formatter 日期格式化字符串(仅设置显示元素时调用)
|
||||
*/
|
||||
export function setDateValue(element: HTMLElement, val: Date | number | string): void;
|
||||
export function setDateValue(element: HTMLElement, val: Date | number | string, formatter?: string): void;
|
||||
|
||||
/**
|
||||
* 从日期选择框获取日期值
|
||||
@ -39,7 +40,7 @@ export function setDateValue(element: HTMLElement, val: Date | number | string):
|
||||
* @param formatter 自定义格式化函数,传入参数为 `Date` 类型
|
||||
* @returns 默认返回日期 `ticks` 的字符串
|
||||
*/
|
||||
export function getDateValue(element: HTMLInputElement, formatter?: (date: Date) => string): string;
|
||||
export function getDateValue(element: HTMLInputElement, formatter?: string | ((date: Date) => string)): string;
|
||||
|
||||
/** 日期选择框类 */
|
||||
export class DateSelector {
|
||||
|
@ -207,6 +207,25 @@ function getFormatter(date, utc) {
|
||||
*/
|
||||
export function formatDate(date, formatter) {
|
||||
formatter ??= 'm/d/Y';
|
||||
if (date === '') {
|
||||
return '';
|
||||
}
|
||||
if (isNaN(date)) {
|
||||
let e = /^(\d{4})-(\d{2})-(\d{2})/.exec(date);
|
||||
if (e == null) {
|
||||
e = /^(\d{4})\/(\d{2})\/(\d{2})/.exec(date);
|
||||
}
|
||||
if (e != null) {
|
||||
date = new Date(e[1], parseInt(e[2]) - 1, e[3]);
|
||||
} else {
|
||||
e = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(date);
|
||||
if (e != null) {
|
||||
date = new Date(e[3], parseInt(e[1]) - 1, e[2]);
|
||||
} else {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (date instanceof Date) {
|
||||
const f = getFormatter(date, false);
|
||||
return formatter.replace(/\\?(.?)/gi, (k, v) => f[k] ? f[k]() : v);
|
||||
@ -224,14 +243,17 @@ export function formatDate(date, formatter) {
|
||||
* 设置显示日期
|
||||
* @param {HTMLElement} element - 要设置显示日期的元素
|
||||
* @param {Date | number | string} val - 日期值,支持格式参见 {@linkcode formatDate}
|
||||
* @param {string} [formatter] - 日期格式化字符串(仅设置显示元素时调用)
|
||||
*/
|
||||
export function setDateValue(element, val) {
|
||||
export function setDateValue(element, val, formatter) {
|
||||
if (element.tagName === 'INPUT') {
|
||||
if (val === '') {
|
||||
element.value = '';
|
||||
} else if (isNaN(val)) {
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(val)) {
|
||||
element.value = String(val).substring(0, 10);
|
||||
} else if (/^\d{4}\/\d{2}\/\d{2}/.test(val)) {
|
||||
element.value = String(val).replace('/', '-').substring(0, 10);
|
||||
} else {
|
||||
const e = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(val);
|
||||
if (e != null) {
|
||||
@ -245,7 +267,7 @@ export function setDateValue(element, val) {
|
||||
}
|
||||
} else {
|
||||
if (val instanceof Date) {
|
||||
element.value = toDateValue(val);
|
||||
element.value = toDateValue(val, true);
|
||||
} else {
|
||||
const ticks = Number(val);
|
||||
if (!isNaN(ticks) && ticks > 0) {
|
||||
@ -256,7 +278,7 @@ export function setDateValue(element, val) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
element.innerText = formatDate(val);
|
||||
element.innerText = formatDate(val, formatter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,7 +292,7 @@ export function setDateValue(element, val) {
|
||||
/**
|
||||
* 从日期选择框获取日期值
|
||||
* @param {HTMLInputElement} element - 要获取的日期选择框
|
||||
* @param {DateFormatterCallback} [formatter] - 自定义格式化函数,传入参数为 `Date` 类型
|
||||
* @param {string | DateFormatterCallback} [formatter] - 自定义格式化字符串或函数,传入参数为 `Date` 类型
|
||||
* @returns {string | any} 默认返回日期 `ticks` 的字符串
|
||||
*/
|
||||
export function getDateValue(element, formatter) {
|
||||
@ -280,11 +302,15 @@ export function getDateValue(element, formatter) {
|
||||
if (year < 1900 || year > 9999) {
|
||||
return '';
|
||||
}
|
||||
if (typeof formatter === 'function') {
|
||||
if (formatter != null) {
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
||||
// 使外部 formatter 不需要再处理 `getUTCDate` 亦或是 `getDate`
|
||||
return formatter(new Date(`${year}-${month}-${day}T00:00:00`));
|
||||
const localDate = new Date(`${year}-${month}-${day}T00:00:00`);
|
||||
if (typeof formatter === 'function') {
|
||||
return formatter(localDate);
|
||||
}
|
||||
return formatDate(localDate, formatter);
|
||||
}
|
||||
return String(date.getTime() * 1e4 + 621355968e9);
|
||||
}
|
||||
|
@ -780,9 +780,11 @@ export class GridDateColumn extends GridColumn {
|
||||
* @ignore
|
||||
* @param {HTMLElement} element
|
||||
* @param {(string | number | Date)} val
|
||||
* @param {GridItemWrapper} _wrapper
|
||||
* @param {GridColumnDefinition} col
|
||||
*/
|
||||
static setValue(element, val) {
|
||||
setDateValue(element, val);
|
||||
static setValue(element, val, _wrapper, col) {
|
||||
setDateValue(element, val, col.dateDisplayFormatter);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -793,7 +795,10 @@ export class GridDateColumn extends GridColumn {
|
||||
*/
|
||||
static getValue(e, col) {
|
||||
if (e.target.tagName === 'INPUT') {
|
||||
return getDateValue(e.target, col.dateValueFormatter);
|
||||
return {
|
||||
value: getDateValue(e.target, col.dateValueFormatter),
|
||||
text: getDateValue(e.target, col.dateDisplayFormatter)
|
||||
};
|
||||
}
|
||||
return e.target.innerText;
|
||||
}
|
||||
|
@ -264,7 +264,8 @@ let r = lang;
|
||||
* @property {("fa-light" | "fa-regular" | "fa-solid")} [iconType=fa-light] - 列为图标类型时以该值设置图标样式
|
||||
* @property {string} [dateMin] - 列为日期类型时以该值作为最小可选日期值
|
||||
* @property {string} [dateMax] - 列为日期类型时以该值作为最大可选日期值
|
||||
* @property {DateFormatterCallback} [dateValueFormatter] - 列为日期类型时自定义日期格式化函数
|
||||
* @property {string} [dateDisplayFormatter] - 列为日期类型时日期显示的格式化字符串
|
||||
* @property {(string | DateFormatterCallback)} [dateValueFormatter] - 列为日期类型时自定义日期格式化字符串或函数
|
||||
* @property {(string | GridItemStringCallback)} [tooltip] - 额外设置单元格的 tooltip,支持直接使用字符串或者使用函数返回的字符串
|
||||
* @property {Function} [onAllChecked] - 列头复选框改变时触发事件
|
||||
* @property {Function} [onChanged] - 单元格变化时触发事件
|
||||
@ -2834,6 +2835,9 @@ export class Grid {
|
||||
if (type.editing) {
|
||||
val = type.getValue({ target: cell.children[0] }, col);
|
||||
this._onRowChanged(null, i, col, val, cell, oldValue);
|
||||
if (Object.prototype.hasOwnProperty.call(val, 'value')) {
|
||||
val = val.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stateChanged) {
|
||||
@ -4205,7 +4209,7 @@ export class Grid {
|
||||
});
|
||||
vals.__changed = true;
|
||||
if (typeof col.onChanged === 'function') {
|
||||
col.onChanged.call(this, item, value, oldValue, e);
|
||||
col.onChanged.call(this, item, v, oldValue, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
lib/ui/media.d.ts
vendored
8
lib/ui/media.d.ts
vendored
@ -23,4 +23,10 @@ export function createVideo(url: string): HTMLVideoElement
|
||||
* @param url 文件 url
|
||||
* @param icon 图标,默认为 `file-alt`
|
||||
*/
|
||||
export function createFile(url: string, icon?: string): HTMLDivElement
|
||||
export function createFile(url: string, icon?: string): HTMLDivElement
|
||||
|
||||
/**
|
||||
* 创建联动视频元素
|
||||
* @param urls 视频 url 数组
|
||||
*/
|
||||
export function createVideoList(urls: string[]): HTMLDivElement
|
293
lib/ui/media.js
293
lib/ui/media.js
@ -62,14 +62,24 @@ function playPcm(samples, ended) {
|
||||
}
|
||||
|
||||
function getTimeLabel(time) {
|
||||
time = Math.round(time);
|
||||
return String(Math.floor(time / 60)).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
|
||||
// time = Math.round(time);
|
||||
// return String(Math.floor(time / 60)).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
|
||||
if (isNaN(time) || time < 0) {
|
||||
return '0:00';
|
||||
}
|
||||
time = Math.floor(time);
|
||||
const m = Math.floor(time / 60);
|
||||
const h = Math.floor(m / 60);
|
||||
if (h > 0) {
|
||||
return h + ':' + String(m % 60).padStart(2, '0') + ':' + String(time % 60).padStart(2, '0');
|
||||
}
|
||||
return m + ':' + String(time % 60).padStart(2, '0');
|
||||
}
|
||||
|
||||
export function createAudio(mime, url) {
|
||||
if ((mime === 'audio/amr' || mime === '.amr') && typeof AMR !== 'undefined') {
|
||||
const timestamp = createElement('span', 'ui-media-timestamp');
|
||||
timestamp.textContent = '00:00 / 00:00';
|
||||
timestamp.textContent = '0:00 / 0:00';
|
||||
let context;
|
||||
let timer;
|
||||
return createElement('div', 'ui-media-audio',
|
||||
@ -80,7 +90,7 @@ export function createAudio(mime, url) {
|
||||
clearInterval(timer);
|
||||
context.close();
|
||||
context = null;
|
||||
timestamp.textContent = '00:00 / 00:00';
|
||||
timestamp.textContent = '0:00 / 0:00';
|
||||
button.className = 'play';
|
||||
button.replaceChildren(createIcon('fa-solid', 'play'));
|
||||
return;
|
||||
@ -92,7 +102,7 @@ export function createAudio(mime, url) {
|
||||
.then(r => playPcm(r, ctx => {
|
||||
context = null;
|
||||
clearInterval(timer);
|
||||
timestamp.textContent = '00:00 / ' + getTimeLabel(ctx.duration);
|
||||
timestamp.textContent = '0:00 / ' + getTimeLabel(ctx.duration);
|
||||
button.className = 'play';
|
||||
button.replaceChildren(createIcon('fa-solid', 'play'));
|
||||
}))
|
||||
@ -139,4 +149,277 @@ export function createFile(url, icon = 'file-alt') {
|
||||
a.innerText = 'Click here to view the file';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建联动视频元素
|
||||
* @param {string[]} urls - 视频 url 数组
|
||||
* @param {any} [options] - 播放参数
|
||||
* @param {boolean} options.[autoPlay] - 是否自动播放
|
||||
* @param {boolean} options.[autoFullScreen] - 是否自动全屏
|
||||
* @param {boolean} options.[autoLoop] - 是否循环播放
|
||||
* @param {Function} [callback] - 视频元素处理回调函数
|
||||
* @returns {HTMLDivElement} 返回联动视频元素
|
||||
*/
|
||||
export function createVideoList(urls, options, callback) {
|
||||
if (!Array.isArray(urls)) {
|
||||
urls = [urls];
|
||||
}
|
||||
const length = urls.length;
|
||||
const container = createElement('div', 'ui-media-video-container');
|
||||
let seekProgressBar;
|
||||
let playIcon;
|
||||
let timeLabel;
|
||||
let volumeProgressBar;
|
||||
let volumeIcon;
|
||||
|
||||
const videos = Array(length);
|
||||
const waiting = Array(length);
|
||||
let prepared = 0;
|
||||
let ended = 0;
|
||||
|
||||
let playing;
|
||||
let duration = -1;
|
||||
let durationLabel;
|
||||
let seekBarWidth;
|
||||
let mousing;
|
||||
const controller = createElement('div', 'ui-video-control active',
|
||||
createElement('div', seek => {
|
||||
seek.className = 'ui-video-bar seek-bar';
|
||||
seek.addEventListener('mousedown', e => {
|
||||
if (prepared < length || waiting.find(p => p) != null) {
|
||||
return;
|
||||
}
|
||||
const width = seekBarWidth = seek.offsetWidth;
|
||||
const currentTime = Math.min(Math.max(e.offsetX * duration / width, 0), duration);
|
||||
videos.forEach(video => typeof video.fastSeek === 'function' ?
|
||||
video.fastSeek(currentTime) :
|
||||
(video.currentTime = currentTime));
|
||||
mousing = true;
|
||||
e.stopPropagation();
|
||||
});
|
||||
seek.addEventListener('mousemove', e => {
|
||||
if (mousing) {
|
||||
if (!e.buttons) {
|
||||
mousing = false;
|
||||
} else {
|
||||
const v = Math.min(Math.max(e.offsetX / seekBarWidth, 0), 1);
|
||||
seekProgressBar.style.width = `${(v * 100).toFixed(2)}%`;
|
||||
}
|
||||
}
|
||||
});
|
||||
seek.addEventListener('mouseup', e => {
|
||||
if (mousing) {
|
||||
mousing = false;
|
||||
const currentTime = Math.min(Math.max(e.offsetX * duration / seekBarWidth, 0), duration);
|
||||
videos.forEach(video => typeof video.fastSeek === 'function' ?
|
||||
video.fastSeek(currentTime) :
|
||||
(video.currentTime = currentTime));
|
||||
}
|
||||
});
|
||||
},
|
||||
createElement('div', 'ui-video-duration seek-duration'),
|
||||
seekProgressBar = createElement('div', 'ui-video-progress seek-progress')
|
||||
),
|
||||
playIcon = createElement('div', 'ui-video-icon play-icon',
|
||||
createIcon('fa-solid', 'play'),
|
||||
createIcon('fa-solid', 'pause')
|
||||
),
|
||||
timeLabel = createElement('div', 'ui-video-time-label'),
|
||||
createElement('div', 'ui-video-volume-container',
|
||||
createElement('div', volume => {
|
||||
volume.className = 'ui-video-bar volume-bar';
|
||||
volume.addEventListener('mousedown', e => {
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
mousing = true;
|
||||
e.stopPropagation();
|
||||
});
|
||||
volume.addEventListener('mousemove', e => {
|
||||
if (mousing) {
|
||||
if (!e.buttons) {
|
||||
mousing = false;
|
||||
} else {
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
volumeProgressBar.style.width = `${(v * 100).toFixed(2)}%`;
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
}
|
||||
}
|
||||
});
|
||||
volume.addEventListener('mouseup', e => {
|
||||
if (mousing) {
|
||||
mousing = false;
|
||||
const v = Math.min(Math.max(e.offsetX / 60, 0), 1);
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
video.volume = v;
|
||||
}
|
||||
volumeIcon.classList[v === 0 ? 'add' : 'remove']('muted');
|
||||
}
|
||||
});
|
||||
},
|
||||
createElement('div', 'ui-video-duration video-duration'),
|
||||
volumeProgressBar = createElement('div', 'ui-video-progress video-progress')
|
||||
),
|
||||
volumeIcon = createElement('div', 'ui-video-icon volume-icon',
|
||||
createIcon('fa-solid', 'volume'),
|
||||
createIcon('fa-solid', 'volume-mute')
|
||||
)
|
||||
)
|
||||
);
|
||||
controller.addEventListener('mousedown', () => {
|
||||
if (prepared < length || waiting.find(p => p) != null) {
|
||||
// not prepared.
|
||||
return;
|
||||
}
|
||||
if (playing) {
|
||||
videos.forEach(video => video.pause());
|
||||
} else {
|
||||
ended = 0;
|
||||
videos.forEach(video => video.play().catch(() => { }));
|
||||
}
|
||||
});
|
||||
volumeIcon.addEventListener('mousedown', e => {
|
||||
if (volumeIcon.classList.contains('disabled')) {
|
||||
return;
|
||||
}
|
||||
const video = videos[0];
|
||||
if (video == null) {
|
||||
return;
|
||||
}
|
||||
if (volumeIcon.classList.contains('muted')) {
|
||||
video.volume = 1;
|
||||
volumeIcon.classList.remove('muted');
|
||||
} else {
|
||||
video.volume = 0;
|
||||
volumeIcon.classList.add('muted');
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
if (length === 1) {
|
||||
controller.append(createElement('div', icon => {
|
||||
icon.className = 'ui-video-icon fullscreen-icon';
|
||||
icon.addEventListener('mousedown', e => {
|
||||
if (prepared < length) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
const video = videos[0];
|
||||
if (video != null) {
|
||||
if (document.fullscreenElement == null) {
|
||||
video.requestFullscreen().catch(() => { });
|
||||
} else if (typeof document.exitFullscreen === 'function') {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
createIcon('fa-regular', 'expand')
|
||||
));
|
||||
} else {
|
||||
controller.classList.add('no-fullscreen');
|
||||
}
|
||||
|
||||
urls.forEach((url, i) => {
|
||||
const video = createElement('video');
|
||||
videos[i] = video;
|
||||
video.src = url;
|
||||
video.addEventListener('durationchange', () => {
|
||||
const d = video.duration;
|
||||
if (d > duration) {
|
||||
duration = d;
|
||||
durationLabel = getTimeLabel(d);
|
||||
timeLabel.innerText = '0:00 / ' + durationLabel;
|
||||
}
|
||||
});
|
||||
video.addEventListener('loadeddata', () => {
|
||||
if (i === 0) {
|
||||
volumeProgressBar.style.width = `${(video.volume * 100).toFixed(2)}%`;
|
||||
} else {
|
||||
video.volume = 0;
|
||||
}
|
||||
prepared += 1;
|
||||
if (options?.autoPlay && prepared >= length) {
|
||||
// auto play
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
if (options?.autoFullScreen && length === 1 && document.fullscreenElement == null) {
|
||||
video.requestFullscreen().catch(() => { });
|
||||
}
|
||||
}
|
||||
});
|
||||
video.addEventListener('canplaythrough', () => {
|
||||
const w = video.parentElement.querySelector('.ui-video-waiting');
|
||||
if (w != null) {
|
||||
w.style.opacity = 0;
|
||||
}
|
||||
if (waiting[i]) {
|
||||
waiting[i] = false;
|
||||
if (waiting.find(p => p) == null) {
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
}
|
||||
}
|
||||
});
|
||||
video.addEventListener('pause', () => {
|
||||
playing = false;
|
||||
controller.classList.add('active');
|
||||
playIcon.classList.remove('pause');
|
||||
});
|
||||
video.addEventListener('playing', () => {
|
||||
playing = true;
|
||||
controller.classList.remove('active');
|
||||
playIcon.classList.add('pause');
|
||||
});
|
||||
video.addEventListener('waiting', () => {
|
||||
waiting[i] = true;
|
||||
const w = video.parentElement.querySelector('.ui-video-waiting');
|
||||
if (w != null) {
|
||||
w.style.opacity = 1;
|
||||
}
|
||||
videos.forEach(v => v.pause());
|
||||
});
|
||||
video.addEventListener('ended', () => {
|
||||
ended += 1;
|
||||
if (ended >= length) {
|
||||
if (options?.autoLoop) {
|
||||
videos.forEach(v => v.play().catch(() => { }));
|
||||
} else {
|
||||
playing = false;
|
||||
controller.classList.add('active');
|
||||
seekProgressBar.style.width = '0';
|
||||
playIcon.classList.remove('pause');
|
||||
timeLabel.innerText = '0:00 / ' + durationLabel;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (i === 0) {
|
||||
video.addEventListener('timeupdate', () => {
|
||||
seekProgressBar.style.width = `${(video.currentTime * 100 / duration).toFixed(2)}%`;
|
||||
timeLabel.innerText = getTimeLabel(video.currentTime) + ' / ' + durationLabel;
|
||||
});
|
||||
video.addEventListener('volumechange', () => {
|
||||
volumeProgressBar.style.width = `${(video.volume * 100).toFixed(2)}%`;
|
||||
});
|
||||
}
|
||||
const wrapper = createElement('div', 'ui-video-wrapper',
|
||||
video,
|
||||
createElement('div', 'ui-video-waiting',
|
||||
createIcon('fa-regular', 'spinner-third')
|
||||
)
|
||||
);
|
||||
if (typeof callback === 'function') {
|
||||
callback(wrapper);
|
||||
}
|
||||
container.append(wrapper);
|
||||
});
|
||||
container.append(controller);
|
||||
|
||||
return container;
|
||||
}
|
Reference in New Issue
Block a user