mirror of
https://github.com/iv-org/invidious.git
synced 2026-10-25 14:29:16 -05:00
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
153 lines
6.0 KiB
JavaScript
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); });
|
|
}
|
|
};
|
|
});
|