Better Keyboard Shortcuts for SharePoint

Simpler keyboard shorcuts with visual indicators for Microsoft SharePoint videos

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

  1. // ==UserScript==
  2. // @name Better Keyboard Shortcuts for SharePoint
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3.0
  5. // @description Simpler keyboard shorcuts with visual indicators for Microsoft SharePoint videos
  6. // @author kazcfz
  7. // @include https://*.sharepoint.com/*
  8. // @icon https://res-1.cdn.office.net/shellux/stream_24x.12dba766a9c30382b781c971070dc87c.svg
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. /* eslint curly: "off" */
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. // Wait until .oneplayer-root exists in the DOM, then run callback with it
  19. function waitForOnePlayerRoot(callback) {
  20. const root = document.querySelector('.oneplayer-root') || document.querySelector('.OnePlayer-container');
  21. if (root) {
  22. callback(root);
  23. return;
  24. }
  25. const docObserver = new MutationObserver((mutations, obs) => {
  26. const el = document.querySelector('.oneplayer-root') || document.querySelector('.OnePlayer-container');
  27. if (el) {
  28. obs.disconnect();
  29. callback(el);
  30. }
  31. });
  32. docObserver.observe(document.body || document.documentElement, { childList: true, subtree: true });
  33. }
  34.  
  35. waitForOnePlayerRoot(videoRoot => {
  36. if (getComputedStyle(videoRoot).position === 'static')
  37. videoRoot.style.position = 'relative';
  38.  
  39. // Overlay to indicate Skip
  40. const skipOverlay = document.createElement('div');
  41. skipOverlay.style.position = 'absolute';
  42. skipOverlay.style.top = '50%';
  43. skipOverlay.style.transform = 'translateY(-50%)';
  44. skipOverlay.style.padding = '15px 15px';
  45. skipOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.9)';
  46. skipOverlay.style.borderRadius = '50%';
  47. skipOverlay.style.pointerEvents = 'none';
  48. skipOverlay.style.opacity = '0';
  49. skipOverlay.style.transition = 'opacity 0.1s ease';
  50. skipOverlay.style.zIndex = '9999';
  51. skipOverlay.style.userSelect = 'none';
  52. skipOverlay.style.display = 'flex';
  53. skipOverlay.style.flexDirection = 'column';
  54. skipOverlay.style.alignItems = 'center';
  55. skipOverlay.style.justifyContent = 'center';
  56. skipOverlay.style.gap = '0.2em';
  57. skipOverlay.style.minWidth = '75px';
  58. skipOverlay.style.minHeight = '75px';
  59. skipOverlay.style.textAlign = 'center';
  60. videoRoot.appendChild(skipOverlay);
  61.  
  62. // Overlay to indicate Volume
  63. const topMiddleOverlay = document.createElement('div');
  64. topMiddleOverlay.style.position = 'absolute';
  65. topMiddleOverlay.style.top = '10%';
  66. topMiddleOverlay.style.left = '0';
  67. topMiddleOverlay.style.right = '0';
  68. topMiddleOverlay.style.margin = 'auto';
  69. topMiddleOverlay.style.padding = '7px 11px';
  70. topMiddleOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
  71. topMiddleOverlay.style.borderRadius = '5px';
  72. topMiddleOverlay.style.pointerEvents = 'none';
  73. topMiddleOverlay.style.opacity = '0';
  74. topMiddleOverlay.style.transition = 'opacity 0.015s ease';
  75. topMiddleOverlay.style.zIndex = '9999';
  76. topMiddleOverlay.style.userSelect = 'none';
  77. topMiddleOverlay.style.display = 'flex';
  78. topMiddleOverlay.style.flexDirection = 'column';
  79. topMiddleOverlay.style.alignItems = 'center';
  80. topMiddleOverlay.style.justifyContent = 'center';
  81. topMiddleOverlay.style.gap = '0.2em';
  82. topMiddleOverlay.style.width = '50px';
  83. topMiddleOverlay.style.minHeight = '30px';
  84. topMiddleOverlay.style.textAlign = 'center';
  85. videoRoot.appendChild(topMiddleOverlay);
  86.  
  87. // Overlay to indicate Play/Pause
  88. const centerOverlay = document.createElement('div');
  89. centerOverlay.style.position = 'absolute';
  90. centerOverlay.style.top = '50%';
  91. centerOverlay.style.left = '50%';
  92. centerOverlay.style.right = 'auto';
  93. centerOverlay.style.margin = '0';
  94. centerOverlay.style.transform = 'translate(-50%, -50%)';
  95. centerOverlay.style.padding = '15px';
  96. centerOverlay.style.fontVariantEmoji = 'text';
  97. centerOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
  98. centerOverlay.style.borderRadius = '50%';
  99. centerOverlay.style.pointerEvents = 'none';
  100. centerOverlay.style.opacity = '0';
  101. centerOverlay.style.zIndex = '9999';
  102. centerOverlay.style.userSelect = 'none';
  103. centerOverlay.style.display = 'flex';
  104. centerOverlay.style.flexDirection = 'column';
  105. centerOverlay.style.alignItems = 'center';
  106. centerOverlay.style.justifyContent = 'center';
  107. centerOverlay.style.textAlign = 'center';
  108. centerOverlay.style.color = 'rgba(255,255,255,0.85)';
  109. videoRoot.appendChild(centerOverlay);
  110.  
  111. const centerSymbol = document.createElement('span');
  112. centerSymbol.style.opacity = '1';
  113. centerOverlay.appendChild(centerSymbol);
  114.  
  115. // Overlay text for current volume
  116. const topMiddleText = document.createElement('div');
  117. topMiddleText.style.fontSize = '17px';
  118. topMiddleText.style.fontWeight = '500';
  119. topMiddleText.style.color = 'rgba(255,255,255,0.85)';
  120. topMiddleText.style.userSelect = 'none';
  121. topMiddleOverlay.appendChild(topMiddleText);
  122.  
  123. // Overlay container for left/right triangles
  124. const trianglesContainer = document.createElement('div');
  125. trianglesContainer.style.display = 'flex';
  126. trianglesContainer.style.gap = '10px';
  127. skipOverlay.appendChild(trianglesContainer);
  128.  
  129. // Add triangles into its container
  130. const triangles = [];
  131. for (let i = 0; i < 3; i++) {
  132. const tri = document.createElement('span');
  133. tri.textContent = '▶';
  134. tri.style.fontSize = '13px';
  135. tri.style.fontWeight = 'bold';
  136. trianglesContainer.appendChild(tri);
  137. triangles.push(tri);
  138. }
  139.  
  140. // Overlay text for seconds skipped
  141. const secondsText = document.createElement('div');
  142. secondsText.style.fontSize = '14px';
  143. secondsText.style.fontWeight = 'normal';
  144. secondsText.style.color = 'rgba(255,255,255,0.85)';
  145. secondsText.style.userSelect = 'none';
  146. skipOverlay.appendChild(secondsText);
  147.  
  148. let hideTimeoutSkipOverlay;
  149. let hideTimeoutTopMiddleOverlay;
  150. let hideTimeoutCenterOverlay;
  151. let animTimeouts = [];
  152.  
  153. // Displays overlay triangle animation and seconds skipped
  154. const secondsToSkip = 5;
  155. function showAnimatedTriangles(side) {
  156. // Clear animation timers
  157. clearTimeout(hideTimeoutSkipOverlay);
  158. animTimeouts.forEach(t => clearTimeout(t));
  159. animTimeouts = [];
  160.  
  161. const isLeft = side === 'left';
  162. const char = isLeft ? '◀' : '▶';
  163. const order = isLeft ? [2, 1, 0] : [0, 1, 2];
  164.  
  165. triangles.forEach(t => {
  166. t.textContent = char;
  167. t.style.transition = 'none';
  168. t.style.color = 'rgba(255,255,255,0.3)';
  169. });
  170.  
  171. // Force style flush to apply the color immediately before re-enabling transitions
  172. void skipOverlay.offsetHeight;
  173. triangles.forEach(t => { t.style.transition = 'color 0.3s ease'; });
  174.  
  175. secondsText.textContent = `${secondsToSkip} seconds`;
  176.  
  177. skipOverlay.style.opacity = '1';
  178. skipOverlay.style.left = isLeft ? '10%' : 'auto';
  179. skipOverlay.style.right = isLeft ? 'auto' : '10%';
  180. skipOverlay.style.textAlign = 'center';
  181.  
  182. triangles[order[0]].style.color = 'rgba(255,255,255,0.75)';
  183.  
  184. const interval = 200;
  185.  
  186. for (let step = 2; step <= 3; step++) {
  187. animTimeouts.push(setTimeout(() => {
  188. if (step === 2) {
  189. triangles[order[0]].style.color = 'rgba(255,255,255,0.5)';
  190. triangles[order[1]].style.color = 'rgba(255,255,255,0.75)';
  191. }
  192. else if (step === 3) {
  193. triangles[order[0]].style.color = 'rgba(255,255,255,0.3)';
  194. triangles[order[1]].style.color = 'rgba(255,255,255,0.5)';
  195. triangles[order[2]].style.color = 'rgba(255,255,255,0.75)';
  196. }
  197. }, (step - 1) * interval));
  198. }
  199.  
  200. hideTimeoutSkipOverlay = setTimeout(() => { skipOverlay.style.opacity = '0'; }, 3.3 * interval);
  201. }
  202.  
  203. // Checks that the video player is focused
  204. function isInOnePlayerRoot(element) {
  205. while (element) {
  206. if (element === videoRoot)
  207. return true;
  208. element = element.parentElement;
  209. }
  210. return false;
  211. }
  212.  
  213. let video = null;
  214.  
  215. function setupVideo(v) {
  216. if (!v || video === v)
  217. return;
  218. video = v;
  219. }
  220.  
  221. // Initial attempt to find video
  222. setupVideo(videoRoot.querySelector('video'));
  223.  
  224. // Observe videoRoot subtree for added/removed video
  225. const observer = new MutationObserver(mutations => {
  226. for (const mutation of mutations)
  227. for (const node of mutation.addedNodes)
  228. if (node.nodeType === 1)
  229. if (node.tagName === 'VIDEO')
  230. setupVideo(node);
  231. else {
  232. const v = node.querySelector && node.querySelector('video');
  233. if (v)
  234. setupVideo(v);
  235. }
  236. });
  237.  
  238. observer.observe(videoRoot, { childList: true, subtree: true });
  239.  
  240.  
  241. function triggerCenterSymbol(symbol) {
  242. clearTimeout(hideTimeoutCenterOverlay);
  243.  
  244. centerOverlay.style.width = '75px';
  245. centerOverlay.style.height = '75px';
  246. centerOverlay.style.transition = 'opacity 0.1s ease';
  247. centerOverlay.style.opacity = '1';
  248.  
  249. hideTimeoutCenterOverlay = setTimeout(() => {
  250. centerOverlay.style.transition = 'width 0.75s ease, height 0.75s ease, font-size 0.75s ease, opacity 0.75s ease';
  251. centerOverlay.style.width = `${parseFloat(getComputedStyle(centerOverlay).width) + 25}px`;
  252. centerOverlay.style.height = `${parseFloat(getComputedStyle(centerOverlay).height) + 25}px`;
  253. centerOverlay.style.fontSize = `${parseFloat(getComputedStyle(centerOverlay).fontSize) + 22.5}px`;
  254. centerOverlay.style.opacity = '0';
  255. }, 50);
  256.  
  257. centerSymbol.textContent = symbol;
  258. if (symbol === '🔊' || symbol === '🔇') {
  259. centerSymbol.style.marginBottom = '9px';
  260. centerSymbol.style.marginLeft = '5px';
  261. centerOverlay.style.fontSize = '60px';
  262. } else if (symbol === '🔉') {
  263. centerSymbol.style.marginBottom = '9px';
  264. centerSymbol.style.marginLeft = '0px';
  265. centerOverlay.style.fontSize = '60px';
  266. } else if (symbol === '▶') {
  267. centerSymbol.style.marginBottom = '7px';
  268. centerSymbol.style.marginLeft = '12px';
  269. centerOverlay.style.fontSize = '50px';
  270. } else if (symbol === '⏸') {
  271. centerSymbol.style.marginBottom = '14px';
  272. centerSymbol.style.marginLeft = '4px';
  273. centerOverlay.style.fontSize = '55px';
  274. }
  275. }
  276.  
  277.  
  278. // Math time: MS Stream/SharePoint handles volume steps exponentially (cubically) rather than linearly
  279. // The sequence is (steps of 0.1)^3, counting down from 1 to 0:
  280. // volume (base 𝑛) = (1 − 0.1 × 𝑛)^3, where 𝑛 = 0, 1, 2, ..., 10.
  281. // Alternatively, could just keep it to linear volume steps
  282. let currentVolumeStepCount = 0;
  283. function triggerTopMiddleText(action) {
  284. if (action == 'volume') {
  285. const volumePercent = video.volume * 100;
  286. if (volumePercent >= 1 || video.volume == 0)
  287. topMiddleText.textContent = `${Math.round(volumePercent)}%`;
  288. else
  289. topMiddleText.textContent = `${volumePercent.toFixed(1)}%`;
  290.  
  291. } else if (action == 'speed') {
  292. const speed = video.playbackRate;
  293. topMiddleText.textContent = (speed % 1 === 0) ? `${speed}x` : `${speed.toFixed(2)}x`;
  294. if (topMiddleText.textContent.endsWith('0x'))
  295. topMiddleText.textContent = `${speed.toFixed(1)}x`;
  296. }
  297.  
  298. topMiddleOverlay.style.opacity = '1';
  299. clearTimeout(hideTimeoutTopMiddleOverlay);
  300. hideTimeoutTopMiddleOverlay = setTimeout(() => { topMiddleOverlay.style.opacity = '0'; }, 500);
  301. }
  302.  
  303. document.addEventListener('keydown', e => {
  304. if (!video)
  305. return;
  306. if (e.altKey || e.ctrlKey || e.metaKey)
  307. return;
  308. if (document.activeElement && !isInOnePlayerRoot(document.activeElement))
  309. return;
  310.  
  311. e.preventDefault();
  312.  
  313. // Home to jump to start
  314. if (e.code === 'Home')
  315. video.currentTime = 0;
  316.  
  317. // End to jump to end
  318. else if (e.code === 'End')
  319. video.currentTime = video.duration;
  320.  
  321. // Period to skip to next frame
  322. else if (e.code === 'Period' && !e.shiftKey)
  323. video.currentTime = Math.min(video.duration, video.currentTime + (1/30));
  324.  
  325. // Comma to skip to previous frame
  326. else if (e.code === 'Comma' && !e.shiftKey)
  327. video.currentTime = Math.max(0, video.currentTime - (1/30));
  328.  
  329. // > to speed up
  330. else if (e.key === '>') {
  331. document.querySelector('[aria-description*="Alt + X"]').click();
  332. const items = [...document.querySelectorAll('[role="menuitemradio"]')];
  333. const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true');
  334.  
  335. if (currentIndex > 0)
  336. items[currentIndex - 1].click();
  337. else
  338. document.querySelector('[aria-description*="Alt + X"]').click();
  339. triggerTopMiddleText('speed');
  340. }
  341.  
  342. // < to slow down
  343. else if (e.key === '<') {
  344. document.querySelector('[aria-description*="Alt + Z"]').click();
  345. const items = [...document.querySelectorAll('[role="menuitemradio"]')];
  346. const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true');
  347.  
  348. if (currentIndex < items.length - 1)
  349. items[currentIndex + 1].click();
  350. else
  351. document.querySelector('[aria-description*="Alt + Z"]').click();
  352. triggerTopMiddleText('speed');
  353. }
  354.  
  355. // 0–9 to skip to 0%–90% of video
  356. else if (/^Digit[0-9]$/.test(e.code))
  357. video.currentTime = (video.duration * (parseInt(e.code.replace('Digit', ''), 10))) / 10;
  358.  
  359. // → to skip forward
  360. else if (e.code === 'ArrowRight') {
  361. video.currentTime = Math.min(video.duration, video.currentTime + secondsToSkip);
  362. showAnimatedTriangles('right');
  363.  
  364. // ← to skip backward
  365. } else if (e.code === 'ArrowLeft') {
  366. video.currentTime = Math.max(0, video.currentTime - secondsToSkip);
  367. showAnimatedTriangles('left');
  368.  
  369. // ↑ to increase volume
  370. } else if (e.code === 'ArrowUp') {
  371. if (currentVolumeStepCount > 0) {
  372. currentVolumeStepCount--;
  373. video.volume = Math.pow(1 - 0.1 * currentVolumeStepCount, 3);
  374. //video.volume = Math.max(0, Math.min(1, Math.round((video.volume + 0.1) * 100) / 100));
  375. }
  376. triggerTopMiddleText('volume');
  377. triggerCenterSymbol('🔊');
  378.  
  379. // ↓ to decrease volume
  380. } else if (e.code === 'ArrowDown') {
  381. if (currentVolumeStepCount < 10) {
  382. currentVolumeStepCount++;
  383. video.volume = Math.pow(1 - 0.1 * currentVolumeStepCount, 3);
  384. //video.volume = Math.max(0, Math.min(1, Math.round((video.volume - 0.1) * 100) / 100));
  385. }
  386. triggerTopMiddleText('volume');
  387.  
  388. if (video.volume == 0)
  389. triggerCenterSymbol('🔇');
  390. else
  391. triggerCenterSymbol('🔉');
  392.  
  393. // / to go to search box
  394. } else if (e.key === '/') {
  395. const searchInput = document.querySelector('input[role="combobox"][type="search"][placeholder="Search"]');
  396. searchInput.focus();
  397. searchInput.select();
  398. return;
  399.  
  400. // Trigger SharePoint's keyboard shortcuts / advanced features
  401. } else {
  402. // Adapted from [Sharepoint Keyboard Shortcuts] by [CyrilSLi], MIT License
  403. // https://greatest.deepsurf.us/en/scripts/524190-sharepoint-keyboard-shortcuts
  404. const keys = {
  405. Space: document.querySelector('[aria-description*="Alt + K"]'),
  406. KeyK: document.querySelector('[aria-description*="Alt + K"]'),
  407. KeyJ: document.querySelector('[aria-description*="Alt + J"]'),
  408. KeyL: document.querySelector('[aria-description*="Alt + L"]'),
  409. KeyF: document.querySelector('[aria-description*="Alt + Enter"]'),
  410. KeyM: document.querySelector('[aria-description*="Alt + M"]'),
  411. KeyC: document.querySelector('[aria-description*="Alt + C"]'),
  412. KeyA: document.querySelector('[aria-description*="Alt + A"]'),
  413. }
  414.  
  415. if (keys.hasOwnProperty(e.code))
  416. keys[e.code].click();
  417.  
  418.  
  419. if (e.code === 'Space' || e.code === 'KeyK') {
  420. if (video.paused)
  421. triggerCenterSymbol('⏸');
  422. else
  423. triggerCenterSymbol('▶');
  424.  
  425. } else if (e.code === 'KeyC') {
  426. const items = [...document.querySelectorAll('[role="menuitemradio"]')];
  427. const menuitem = document.querySelector('button[role="menuitem"][aria-label="Captions"]');
  428. const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true');
  429.  
  430. if (currentIndex >= items.length - 1) {
  431. items[0].click();
  432. menuitem.setAttribute('aria-checked', 'false');
  433.  
  434. } else if (currentIndex < items.length - 1) {
  435. items[currentIndex + 1].click();
  436. menuitem.setAttribute('aria-checked', 'true');
  437. }
  438.  
  439. } else if (e.code === 'KeyM')
  440. if (video.muted)
  441. triggerCenterSymbol('🔊');
  442. else
  443. triggerCenterSymbol('🔇');
  444. }
  445.  
  446. document.querySelector(".fluent-critical-ui-container").focus();
  447. });
  448.  
  449. });
  450. })();