add keyboard shortcuts to video player (#342)

* add keyboard shortcuts to video player

* fix modal on the inline player
This commit is contained in:
Kevin Gibbons 2022-10-24 06:11:00 -07:00 committed by GitHub
parent 51f7210195
commit ff82690d3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 160 additions and 10 deletions

View File

@ -2,7 +2,9 @@
{% block content %}
{% load static %}
{% load humanize %}
<div class="video-main"></div>
<div class="video-main">
<div class="video-modal"><span class="video-modal-text"></span></div>
</div>
<div class="notifications" id="notifications"></div>
<div class="sponsorblock" id="sponsorblock">
{% if video.sponsorblock.is_enabled %}
@ -67,7 +69,7 @@
<p class="thumb-icon"><img class="dislike" src="{% static 'img/icon-thumb.svg' %}" alt="thumbs-down">: {{ video.stats.dislike_count|intcomma }}</p>
{% endif %}
{% if video.stats.average_rating %}
<p class="rating-stars">Rating:
<p class="rating-stars">Rating:
{% for star in video.stats.average_rating %}
<img src="/static/img/icon-star-{{ star }}.svg" alt="{{ star }}">
{% endfor %}
@ -90,7 +92,7 @@
<a href="{% url 'playlist_id' playlist_item.playlist_meta.playlist_id %}">
<h3>Playlist [{{ playlist_item.playlist_meta.current_idx|add:"1" }}]: {{ playlist_item.playlist_meta.playlist_name }}</h3>
</a>
<div class="playlist-nav">
<div class="playlist-nav">
<div class="playlist-nav-item">
{% if playlist_item.playlist_previous %}
<a href="{% url 'video' playlist_item.playlist_previous.youtube_id %}">

View File

@ -388,6 +388,7 @@ button:hover {
display: grid;
align-content: space-evenly;
height: 100vh;
position: relative; /* needed for modal */
}
.notifications {
@ -744,6 +745,22 @@ video:-webkit-full-screen {
/* video page */
.video-main {
margin: 1rem 0;
position: relative; /* needed for modal */
}
.video-modal {
position: absolute;
z-index: 1;
top: 20%;
width: 100%;
text-align: center;
}
.video-modal-text {
background: rgba(0,0,0,.5);
color: #eeeeee;
font-size: 1.3em;
display: none;
}
.video-main video {

View File

@ -1,4 +1,3 @@
function sortChange(sortValue) {
var payload = JSON.stringify({'sort_order': sortValue});
sendPost(payload);
@ -411,7 +410,7 @@ function createPlayer(button) {
} else {
var watchStatusIndicator = createWatchStatusIndicator(videoId, "unwatched");
}
var playerStats = `<div class="thumb-icon player-stats"><img src="/static/img/icon-eye.svg" alt="views icon"><span>${videoViews}</span>`;
if (videoData.data.stats.like_count) {
@ -426,6 +425,7 @@ function createPlayer(button) {
const markup = `
<div class="video-player" data-id="${videoId}">
<div class="video-modal"><span class="video-modal-text"></span></div>
${videoTag}
<div class="notifications" id="notifications"></div>
${sponsorBlockElements}
@ -444,13 +444,14 @@ function createPlayer(button) {
`;
const divPlayer = document.getElementById("player");
divPlayer.innerHTML = markup;
recordTextTrackChanges();
}
// Add video tag to video page when passed a video id, function loaded on page load `video.html (115-117)`
function insertVideoTag(videoData, videoProgress) {
var videoTag = createVideoTag(videoData, videoProgress);
var videoMain = document.getElementsByClassName("video-main");
videoMain[0].innerHTML = videoTag;
var videoMain = document.querySelector(".video-main");
videoMain.innerHTML += videoTag;
}
// Generates a video tag with subtitles when passed videoData and videoProgress.
@ -793,8 +794,8 @@ function setProgressBar(videoId, currentTime, duration) {
}
// progressBar = document.getElementById("progress-" + videoId);
}
// multi search form
@ -1046,7 +1047,7 @@ function createFulltext(fullText) {
</a>
<div class="video-desc list">
<p>${subtitle_start} - ${subtitle_end}</p>
<p>${subtitleLine}</p>
<p>${subtitleLine}</p>
<div>
<a href="/channel/${channelId}/"><h3>${channelName}</h3></a>
<a class="video-more" href="/video/${videoId}/?t=${subtitle_start}"><h2>${videoTitle}</h2></a>
@ -1173,3 +1174,133 @@ function animate(elementId, animationClass) {
toAnimate.classList.remove(animationClass);
}
}
// keep track of changes to the subtitles list made with the native UI
// needed so that when toggling subtitles with the shortcut we go to the last selected one, not the first one
addEventListener('DOMContentLoaded', recordTextTrackChanges);
let lastSeenTextTrack = 0;
function recordTextTrackChanges() {
let player = getVideoPlayer();
if (player == null) {
return;
}
player.textTracks.addEventListener('change', () => {
let active = [...player.textTracks].findIndex(x => x.mode === 'showing');
if (active !== -1) {
lastSeenTextTrack = active;
}
});
}
// keyboard shortcuts for the video player
document.addEventListener('keydown', doShortcut);
let modalHideTimeout = -1;
function showModal(html, duration) {
let player = getVideoPlayer();
let modal = document.querySelector('.video-modal-text');
modal.innerHTML = html;
modal.style.display = 'initial';
clearTimeout(modalHideTimeout);
modalHideTimeout = setTimeout(() => { modal.style.display = 'none'; }, duration);
}
let videoSpeeds = [.25, .5, .75, 1, 1.25, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3];
function doShortcut(e) {
if (!(e.target instanceof HTMLElement)) {
return;
}
let target = e.target;
let targetName = target.nodeName.toLowerCase();
if (targetName === 'textarea' || targetName === 'input' || targetName === 'select' || target.isContentEditable) {
return;
}
if (e.altKey || e.ctrlKey || e.metaKey) {
return;
}
let player = getVideoPlayer();
if (player == null) {
// not on the video page
return;
}
switch (e.key) {
case 'c': {
// toggle captions
let tracks = [...player.textTracks];
if (tracks.length === 0) {
break;
}
let active = tracks.find(x => x.mode === 'showing');
if (active != null) {
active.mode = 'disabled';
} else {
tracks[lastSeenTextTrack].mode = 'showing';
}
break;
}
case 'm': {
player.muted = !player.muted;
break;
}
case 'ArrowLeft': {
if (targetName === 'video') {
// hitting arrows while the video is focused will use the built-in skip
break;
}
showModal('- 5 seconds', 500);
player.currentTime -= 5;
break;
}
case 'ArrowRight': {
if (targetName === 'video') {
// hitting space while the video is focused will use the built-in skip
break;
}
showModal('+ 5 seconds', 500);
player.currentTime += 5;
break;
}
case '<':
case '>': {
// change speed
let currentSpeedIdx = videoSpeeds.findIndex(s => s >= player.playbackRate);
if (currentSpeedIdx === -1) {
// handle the case where the user manually set the speed above our max speed
currentSpeedIdx = videoSpeeds.length - 1;
}
let newSpeedIdx = e.key === '<' ? Math.max(0, currentSpeedIdx - 1) : Math.min(videoSpeeds.length - 1, currentSpeedIdx + 1);
let newSpeed = videoSpeeds[newSpeedIdx];
player.playbackRate = newSpeed;
showModal(newSpeed + 'x', 500);
break;
}
case ' ': {
if (targetName === 'video') {
// hitting space while the video is focused will toggle it anyway
break;
}
e.preventDefault();
if (player.paused) {
player.play();
} else {
player.pause();
}
break;
}
case '?': {
showModal(`
<table style="margin: auto; background: rgba(0,0,0,.5)"><tbody>
<tr><td>Show help</td><td>?</td>
<tr><td>Toggle mute</td><td>m</td>
<tr><td>Toggle subtitles (if available)</td><td>c</td>
<tr><td>Increase speed</td><td>&gt;</td>
<tr><td>Decrease speed</td><td>&lt;</td>
<tr><td>Back 5 seconds</td><td></td>
<tr><td>Forward 5 seconds</td><td></td>
`, 3000);
break;
}
}
}