invidious/assets/js/player-gestures.js
Nachiketh Reddy 09fc7878e2 feat(ui): modernize the browsing and watch experience
Refresh navigation, search results, login, channel cards, and the watch page while keeping native no-JavaScript fallbacks. Add scoped player gestures, theater mode, accessible action menus, theme controls, and focused regression coverage.

Model: OpenAI gpt-5.6-sol (high reasoning)
Harness: T3 Code Codex
2026-09-11 19:42:49 +08:00

153 lines
6.0 KiB
JavaScript

'use strict';
// Optional enhancements: native media controls remain available without this script.
(function (root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory;
else root.installPlayerGestures = factory;
})(typeof window === 'undefined' ? globalThis : window, function (player, environment) {
var env = environment || {};
var win = env.window || window;
var doc = env.document || document;
var later = env.setTimeout || win.setTimeout.bind(win);
var cancelTimer = env.clearTimeout || win.clearTimeout.bind(win);
var el = player.el();
var session = null;
var suppressClick = false;
var listeners = [];
function blocked(event) {
return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey ||
!player.controls() || (event.target && event.target.closest &&
event.target.closest('input, textarea, select, button, a, summary, [role="button"], [role="slider"], [role="textbox"], [contenteditable]:not([contenteditable="false"]), .vjs-control-bar, .vjs-menu'));
}
function consume(event) {
event.preventDefault();
event.stopImmediatePropagation();
}
function play() {
var result = player.play();
if (result && result.catch) result.catch(function () { finish(false); });
}
function toggle() {
if (player.paused()) play();
else player.pause();
}
function showHold(active) {
if (env.onHold) env.onHold(active);
else el.classList.toggle('vjs-speed-hold', active);
}
function begin(kind, event) {
if (session) return;
session = { kind: kind, pointerId: event.pointerId, x: event.clientX, y: event.clientY,
active: false, speed: player.playbackRate(), paused: player.paused() };
session.timer = later(function () {
if (!session) return;
session.active = true;
showHold(true);
player.playbackRate(2);
play();
}, 350);
}
function finish(tap) {
if (!session) return false;
var current = session;
cancelTimer(current.timer);
// Keep the hold marked active while ratechange handlers restore preferences.
if (current.active) {
player.playbackRate(current.speed);
if (current.paused) player.pause();
showHold(false);
}
session = null;
if (tap && !current.active && current.kind === 'keyboard') toggle();
return current.active;
}
function listen(target, type, handler) {
target.addEventListener(type, handler, true);
listeners.push([target, type, handler]);
}
listen(win, 'keydown', function (event) {
var playButton = event.target && event.target.closest &&
event.target.closest('.vjs-play-control, .vjs-big-play-button');
var playerControl = event.key !== ' ' && event.target && event.target.closest &&
event.target.closest('.vjs-control-bar') &&
!event.target.closest('.vjs-menu, input, select, [role="slider"]');
if (!el.contains(doc.activeElement) || (!playButton && !playerControl && blocked(event)) ||
event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
if (event.key === ' ') {
consume(event);
if (!event.repeat) begin('keyboard', event);
} else if (event.key === 'k') {
consume(event);
if (!event.repeat && !session) toggle();
} else if (event.key === 'j' || event.key === 'l') {
consume(event);
var time = Math.max(0, player.currentTime() + (event.key === 'j' ? -10 : 10));
var duration = player.duration();
player.currentTime(Number.isFinite(duration) ? Math.min(duration, time) : time);
}
});
listen(win, 'keyup', function (event) {
if (event.key === ' ' && session && session.kind === 'keyboard') {
consume(event);
finish(el.contains(doc.activeElement));
}
});
listen(el, 'pointerdown', function (event) {
suppressClick = false;
if (event.button !== 0 || event.pointerType !== 'mouse' || blocked(event)) return;
el.focus({ preventScroll: true });
begin('pointer', event);
});
listen(win, 'pointermove', function (event) {
if (!session || session.kind !== 'pointer' || session.active || event.pointerId !== session.pointerId) return;
if (Math.abs(event.clientX - session.x) + Math.abs(event.clientY - session.y) > 12) finish(false);
});
listen(win, 'pointerup', function (event) {
if (!session || session.kind !== 'pointer' || event.pointerId !== session.pointerId) return;
if (finish(false)) {
// Browsers may deliver click in a later task. A new pointerdown clears
// this flag, so releasing outside cannot consume the next deliberate click.
suppressClick = true;
}
});
// Video.js toggles on mouseup; the subsequent browser click must also be
// consumed so other click handlers cannot turn a completed hold into a tap.
listen(el, 'mouseup', function (event) {
if (suppressClick && !blocked(event)) consume(event);
});
listen(el, 'click', function (event) {
if (suppressClick && event.detail !== 0 && !blocked(event)) {
consume(event);
suppressClick = false;
}
});
listen(win, 'pointercancel', function () { finish(false); });
listen(win, 'blur', function () { finish(false); });
listen(doc, 'visibilitychange', function () { if (doc.hidden) finish(false); });
listen(doc, 'focusin', function (event) { if (!el.contains(event.target) || blocked(event)) finish(false); });
return {
isHolding: function () { return !!(session && session.active); },
dispose: function () {
finish(false);
listeners.forEach(function (entry) { entry[0].removeEventListener(entry[1], entry[2], true); });
}
};
});