您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Simpler keyboard shorcuts with visual indicators for Microsoft SharePoint videos
// ==UserScript== // @name Better Keyboard Shortcuts for SharePoint // @namespace http://tampermonkey.net/ // @version 1.3.0 // @description Simpler keyboard shorcuts with visual indicators for Microsoft SharePoint videos // @author kazcfz // @include https://*.sharepoint.com/* // @icon https://res-1.cdn.office.net/shellux/stream_24x.12dba766a9c30382b781c971070dc87c.svg // @grant none // @license MIT // ==/UserScript== /* eslint curly: "off" */ (function () { 'use strict'; // Wait until .oneplayer-root exists in the DOM, then run callback with it function waitForOnePlayerRoot(callback) { const root = document.querySelector('.oneplayer-root') || document.querySelector('.OnePlayer-container'); if (root) { callback(root); return; } const docObserver = new MutationObserver((mutations, obs) => { const el = document.querySelector('.oneplayer-root') || document.querySelector('.OnePlayer-container'); if (el) { obs.disconnect(); callback(el); } }); docObserver.observe(document.body || document.documentElement, { childList: true, subtree: true }); } waitForOnePlayerRoot(videoRoot => { if (getComputedStyle(videoRoot).position === 'static') videoRoot.style.position = 'relative'; // Overlay to indicate Skip const skipOverlay = document.createElement('div'); skipOverlay.style.position = 'absolute'; skipOverlay.style.top = '50%'; skipOverlay.style.transform = 'translateY(-50%)'; skipOverlay.style.padding = '15px 15px'; skipOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.9)'; skipOverlay.style.borderRadius = '50%'; skipOverlay.style.pointerEvents = 'none'; skipOverlay.style.opacity = '0'; skipOverlay.style.transition = 'opacity 0.1s ease'; skipOverlay.style.zIndex = '9999'; skipOverlay.style.userSelect = 'none'; skipOverlay.style.display = 'flex'; skipOverlay.style.flexDirection = 'column'; skipOverlay.style.alignItems = 'center'; skipOverlay.style.justifyContent = 'center'; skipOverlay.style.gap = '0.2em'; skipOverlay.style.minWidth = '75px'; skipOverlay.style.minHeight = '75px'; skipOverlay.style.textAlign = 'center'; videoRoot.appendChild(skipOverlay); // Overlay to indicate Volume const topMiddleOverlay = document.createElement('div'); topMiddleOverlay.style.position = 'absolute'; topMiddleOverlay.style.top = '10%'; topMiddleOverlay.style.left = '0'; topMiddleOverlay.style.right = '0'; topMiddleOverlay.style.margin = 'auto'; topMiddleOverlay.style.padding = '7px 11px'; topMiddleOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; topMiddleOverlay.style.borderRadius = '5px'; topMiddleOverlay.style.pointerEvents = 'none'; topMiddleOverlay.style.opacity = '0'; topMiddleOverlay.style.transition = 'opacity 0.015s ease'; topMiddleOverlay.style.zIndex = '9999'; topMiddleOverlay.style.userSelect = 'none'; topMiddleOverlay.style.display = 'flex'; topMiddleOverlay.style.flexDirection = 'column'; topMiddleOverlay.style.alignItems = 'center'; topMiddleOverlay.style.justifyContent = 'center'; topMiddleOverlay.style.gap = '0.2em'; topMiddleOverlay.style.width = '50px'; topMiddleOverlay.style.minHeight = '30px'; topMiddleOverlay.style.textAlign = 'center'; videoRoot.appendChild(topMiddleOverlay); // Overlay to indicate Play/Pause const centerOverlay = document.createElement('div'); centerOverlay.style.position = 'absolute'; centerOverlay.style.top = '50%'; centerOverlay.style.left = '50%'; centerOverlay.style.right = 'auto'; centerOverlay.style.margin = '0'; centerOverlay.style.transform = 'translate(-50%, -50%)'; centerOverlay.style.padding = '15px'; centerOverlay.style.fontVariantEmoji = 'text'; centerOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; centerOverlay.style.borderRadius = '50%'; centerOverlay.style.pointerEvents = 'none'; centerOverlay.style.opacity = '0'; centerOverlay.style.zIndex = '9999'; centerOverlay.style.userSelect = 'none'; centerOverlay.style.display = 'flex'; centerOverlay.style.flexDirection = 'column'; centerOverlay.style.alignItems = 'center'; centerOverlay.style.justifyContent = 'center'; centerOverlay.style.textAlign = 'center'; centerOverlay.style.color = 'rgba(255,255,255,0.85)'; videoRoot.appendChild(centerOverlay); const centerSymbol = document.createElement('span'); centerSymbol.style.opacity = '1'; centerOverlay.appendChild(centerSymbol); // Overlay text for current volume const topMiddleText = document.createElement('div'); topMiddleText.style.fontSize = '17px'; topMiddleText.style.fontWeight = '500'; topMiddleText.style.color = 'rgba(255,255,255,0.85)'; topMiddleText.style.userSelect = 'none'; topMiddleOverlay.appendChild(topMiddleText); // Overlay container for left/right triangles const trianglesContainer = document.createElement('div'); trianglesContainer.style.display = 'flex'; trianglesContainer.style.gap = '10px'; skipOverlay.appendChild(trianglesContainer); // Add triangles into its container const triangles = []; for (let i = 0; i < 3; i++) { const tri = document.createElement('span'); tri.textContent = '▶'; tri.style.fontSize = '13px'; tri.style.fontWeight = 'bold'; trianglesContainer.appendChild(tri); triangles.push(tri); } // Overlay text for seconds skipped const secondsText = document.createElement('div'); secondsText.style.fontSize = '14px'; secondsText.style.fontWeight = 'normal'; secondsText.style.color = 'rgba(255,255,255,0.85)'; secondsText.style.userSelect = 'none'; skipOverlay.appendChild(secondsText); let hideTimeoutSkipOverlay; let hideTimeoutTopMiddleOverlay; let hideTimeoutCenterOverlay; let animTimeouts = []; // Displays overlay triangle animation and seconds skipped const secondsToSkip = 5; function showAnimatedTriangles(side) { // Clear animation timers clearTimeout(hideTimeoutSkipOverlay); animTimeouts.forEach(t => clearTimeout(t)); animTimeouts = []; const isLeft = side === 'left'; const char = isLeft ? '◀' : '▶'; const order = isLeft ? [2, 1, 0] : [0, 1, 2]; triangles.forEach(t => { t.textContent = char; t.style.transition = 'none'; t.style.color = 'rgba(255,255,255,0.3)'; }); // Force style flush to apply the color immediately before re-enabling transitions void skipOverlay.offsetHeight; triangles.forEach(t => { t.style.transition = 'color 0.3s ease'; }); secondsText.textContent = `${secondsToSkip} seconds`; skipOverlay.style.opacity = '1'; skipOverlay.style.left = isLeft ? '10%' : 'auto'; skipOverlay.style.right = isLeft ? 'auto' : '10%'; skipOverlay.style.textAlign = 'center'; triangles[order[0]].style.color = 'rgba(255,255,255,0.75)'; const interval = 200; for (let step = 2; step <= 3; step++) { animTimeouts.push(setTimeout(() => { if (step === 2) { triangles[order[0]].style.color = 'rgba(255,255,255,0.5)'; triangles[order[1]].style.color = 'rgba(255,255,255,0.75)'; } else if (step === 3) { triangles[order[0]].style.color = 'rgba(255,255,255,0.3)'; triangles[order[1]].style.color = 'rgba(255,255,255,0.5)'; triangles[order[2]].style.color = 'rgba(255,255,255,0.75)'; } }, (step - 1) * interval)); } hideTimeoutSkipOverlay = setTimeout(() => { skipOverlay.style.opacity = '0'; }, 3.3 * interval); } // Checks that the video player is focused function isInOnePlayerRoot(element) { while (element) { if (element === videoRoot) return true; element = element.parentElement; } return false; } let video = null; function setupVideo(v) { if (!v || video === v) return; video = v; } // Initial attempt to find video setupVideo(videoRoot.querySelector('video')); // Observe videoRoot subtree for added/removed video const observer = new MutationObserver(mutations => { for (const mutation of mutations) for (const node of mutation.addedNodes) if (node.nodeType === 1) if (node.tagName === 'VIDEO') setupVideo(node); else { const v = node.querySelector && node.querySelector('video'); if (v) setupVideo(v); } }); observer.observe(videoRoot, { childList: true, subtree: true }); function triggerCenterSymbol(symbol) { clearTimeout(hideTimeoutCenterOverlay); centerOverlay.style.width = '75px'; centerOverlay.style.height = '75px'; centerOverlay.style.transition = 'opacity 0.1s ease'; centerOverlay.style.opacity = '1'; hideTimeoutCenterOverlay = setTimeout(() => { centerOverlay.style.transition = 'width 0.75s ease, height 0.75s ease, font-size 0.75s ease, opacity 0.75s ease'; centerOverlay.style.width = `${parseFloat(getComputedStyle(centerOverlay).width) + 25}px`; centerOverlay.style.height = `${parseFloat(getComputedStyle(centerOverlay).height) + 25}px`; centerOverlay.style.fontSize = `${parseFloat(getComputedStyle(centerOverlay).fontSize) + 22.5}px`; centerOverlay.style.opacity = '0'; }, 50); centerSymbol.textContent = symbol; if (symbol === '🔊' || symbol === '🔇') { centerSymbol.style.marginBottom = '9px'; centerSymbol.style.marginLeft = '5px'; centerOverlay.style.fontSize = '60px'; } else if (symbol === '🔉') { centerSymbol.style.marginBottom = '9px'; centerSymbol.style.marginLeft = '0px'; centerOverlay.style.fontSize = '60px'; } else if (symbol === '▶') { centerSymbol.style.marginBottom = '7px'; centerSymbol.style.marginLeft = '12px'; centerOverlay.style.fontSize = '50px'; } else if (symbol === '⏸') { centerSymbol.style.marginBottom = '14px'; centerSymbol.style.marginLeft = '4px'; centerOverlay.style.fontSize = '55px'; } } // Math time: MS Stream/SharePoint handles volume steps exponentially (cubically) rather than linearly // The sequence is (steps of 0.1)^3, counting down from 1 to 0: // volume (base 𝑛) = (1 − 0.1 × 𝑛)^3, where 𝑛 = 0, 1, 2, ..., 10. // Alternatively, could just keep it to linear volume steps let currentVolumeStepCount = 0; function triggerTopMiddleText(action) { if (action == 'volume') { const volumePercent = video.volume * 100; if (volumePercent >= 1 || video.volume == 0) topMiddleText.textContent = `${Math.round(volumePercent)}%`; else topMiddleText.textContent = `${volumePercent.toFixed(1)}%`; } else if (action == 'speed') { const speed = video.playbackRate; topMiddleText.textContent = (speed % 1 === 0) ? `${speed}x` : `${speed.toFixed(2)}x`; if (topMiddleText.textContent.endsWith('0x')) topMiddleText.textContent = `${speed.toFixed(1)}x`; } topMiddleOverlay.style.opacity = '1'; clearTimeout(hideTimeoutTopMiddleOverlay); hideTimeoutTopMiddleOverlay = setTimeout(() => { topMiddleOverlay.style.opacity = '0'; }, 500); } document.addEventListener('keydown', e => { if (!video) return; if (e.altKey || e.ctrlKey || e.metaKey) return; if (document.activeElement && !isInOnePlayerRoot(document.activeElement)) return; e.preventDefault(); // Home to jump to start if (e.code === 'Home') video.currentTime = 0; // End to jump to end else if (e.code === 'End') video.currentTime = video.duration; // Period to skip to next frame else if (e.code === 'Period' && !e.shiftKey) video.currentTime = Math.min(video.duration, video.currentTime + (1/30)); // Comma to skip to previous frame else if (e.code === 'Comma' && !e.shiftKey) video.currentTime = Math.max(0, video.currentTime - (1/30)); // > to speed up else if (e.key === '>') { document.querySelector('[aria-description*="Alt + X"]').click(); const items = [...document.querySelectorAll('[role="menuitemradio"]')]; const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true'); if (currentIndex > 0) items[currentIndex - 1].click(); else document.querySelector('[aria-description*="Alt + X"]').click(); triggerTopMiddleText('speed'); } // < to slow down else if (e.key === '<') { document.querySelector('[aria-description*="Alt + Z"]').click(); const items = [...document.querySelectorAll('[role="menuitemradio"]')]; const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true'); if (currentIndex < items.length - 1) items[currentIndex + 1].click(); else document.querySelector('[aria-description*="Alt + Z"]').click(); triggerTopMiddleText('speed'); } // 0–9 to skip to 0%–90% of video else if (/^Digit[0-9]$/.test(e.code)) video.currentTime = (video.duration * (parseInt(e.code.replace('Digit', ''), 10))) / 10; // → to skip forward else if (e.code === 'ArrowRight') { video.currentTime = Math.min(video.duration, video.currentTime + secondsToSkip); showAnimatedTriangles('right'); // ← to skip backward } else if (e.code === 'ArrowLeft') { video.currentTime = Math.max(0, video.currentTime - secondsToSkip); showAnimatedTriangles('left'); // ↑ to increase volume } else if (e.code === 'ArrowUp') { if (currentVolumeStepCount > 0) { currentVolumeStepCount--; video.volume = Math.pow(1 - 0.1 * currentVolumeStepCount, 3); //video.volume = Math.max(0, Math.min(1, Math.round((video.volume + 0.1) * 100) / 100)); } triggerTopMiddleText('volume'); triggerCenterSymbol('🔊'); // ↓ to decrease volume } else if (e.code === 'ArrowDown') { if (currentVolumeStepCount < 10) { currentVolumeStepCount++; video.volume = Math.pow(1 - 0.1 * currentVolumeStepCount, 3); //video.volume = Math.max(0, Math.min(1, Math.round((video.volume - 0.1) * 100) / 100)); } triggerTopMiddleText('volume'); if (video.volume == 0) triggerCenterSymbol('🔇'); else triggerCenterSymbol('🔉'); // / to go to search box } else if (e.key === '/') { const searchInput = document.querySelector('input[role="combobox"][type="search"][placeholder="Search"]'); searchInput.focus(); searchInput.select(); return; // Trigger SharePoint's keyboard shortcuts / advanced features } else { // Adapted from [Sharepoint Keyboard Shortcuts] by [CyrilSLi], MIT License // https://greatest.deepsurf.us/en/scripts/524190-sharepoint-keyboard-shortcuts const keys = { Space: document.querySelector('[aria-description*="Alt + K"]'), KeyK: document.querySelector('[aria-description*="Alt + K"]'), KeyJ: document.querySelector('[aria-description*="Alt + J"]'), KeyL: document.querySelector('[aria-description*="Alt + L"]'), KeyF: document.querySelector('[aria-description*="Alt + Enter"]'), KeyM: document.querySelector('[aria-description*="Alt + M"]'), KeyC: document.querySelector('[aria-description*="Alt + C"]'), KeyA: document.querySelector('[aria-description*="Alt + A"]'), } if (keys.hasOwnProperty(e.code)) keys[e.code].click(); if (e.code === 'Space' || e.code === 'KeyK') { if (video.paused) triggerCenterSymbol('⏸'); else triggerCenterSymbol('▶'); } else if (e.code === 'KeyC') { const items = [...document.querySelectorAll('[role="menuitemradio"]')]; const menuitem = document.querySelector('button[role="menuitem"][aria-label="Captions"]'); const currentIndex = items.findIndex(item => item.getAttribute('aria-checked') === 'true'); if (currentIndex >= items.length - 1) { items[0].click(); menuitem.setAttribute('aria-checked', 'false'); } else if (currentIndex < items.length - 1) { items[currentIndex + 1].click(); menuitem.setAttribute('aria-checked', 'true'); } } else if (e.code === 'KeyM') if (video.muted) triggerCenterSymbol('🔊'); else triggerCenterSymbol('🔇'); } document.querySelector(".fluent-critical-ui-container").focus(); }); }); })();