Last active
March 4, 2021 04:10
-
-
Save joshuatvernon/b76055f8e6b4ce772830b4735850a151 to your computer and use it in GitHub Desktop.
Add shortcuts to Google Meet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name GoogleMeetShortcuts | |
// @namespace joshuatvernon | |
// @version 1.1.3 | |
// @description Add shortcuts to Google Meet | |
// @author Joshua Vernon | |
// @match https://meet.google.com/* | |
// ==/UserScript== | |
// Instructions: | |
// 1. Install the Tampermonkey Google Chrome extension | |
// 2. Visit https://gist.github.com/joshuatvernon/b76055f8e6b4ce772830b4735850a151/raw/c006273e499e83d6a194a9b6e7827f0b98998d4a/GoogleMeetShortcuts.user.js | |
(function () { | |
'use strict'; | |
// Constants | |
const appName = 'GoogleMeetShortcuts'; | |
const debugApp = window.location.href.includes(`debug${appName}=true`); | |
const innerHtmls = { | |
raiseHand: 'Raise hand', | |
lowerHand: 'Lower hand', | |
meetingDetails: 'Meeting details', | |
copyJoiningInfo: 'Copy joining info', | |
presentNow: 'Present now', | |
fullScreen: 'Full screen' | |
}; | |
const ariaLabels = { | |
close: 'Close', | |
chatWithEveryone: 'Chat with everyone', | |
moreOptions: 'More options', | |
leaveCall: 'Leave call', | |
showEveryone: 'Show everyone' | |
}; | |
// Styles | |
const css = ` | |
.${appName}__modal { | |
z-index: 100; | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
min-width: 400px; | |
min-height: 400px; | |
background: white; | |
border-radius: 5px; | |
padding: 20px; | |
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; | |
text-align: -webkit-center; | |
} | |
.${appName}__title { | |
text-align: center; | |
margin: 0; | |
text-decoration: none; | |
color: black; | |
} | |
.${appName}__subTitle { | |
text-align: center; | |
color: grey; | |
} | |
.${appName}__table { | |
text-align: center; | |
border: 1px solid black; | |
border-spacing: 0; | |
border-collapse: collapse; | |
} | |
.${appName}__headerRow { | |
border: 1px solid black; | |
} | |
.${appName}__row { | |
border: 1px solid black; | |
} | |
.${appName}__header { | |
border: 1px solid black; | |
background: #B7B7B7; | |
padding: 10px; | |
} | |
.${appName}__cellName { | |
border: 1px solid black; | |
padding: 10px; | |
background: #F3F3F3; | |
} | |
.${appName}__cellKey { | |
border: 1px solid black; | |
padding: 10px; | |
} | |
`; | |
// Components | |
const head = document.head || document.getElementsByTagName('head')[0]; | |
const body = document.body || document.getElementsByTagName('body')[0]; | |
// Create style | |
const style = document.createElement('style'); | |
const styleText = document.createTextNode(css); | |
style.appendChild(styleText); | |
head.appendChild(style); | |
// Create modal | |
const modal = document.createElement('div'); | |
modal.className = `${appName}__modal`; | |
// Create title | |
const link = document.createElement('a'); | |
link.href = 'https://gist.github.com/joshuatvernon/b76055f8e6b4ce772830b4735850a151'; | |
link.target = '_blank'; | |
const title = document.createElement('h1'); | |
title.className = `${appName}__title`; | |
const titleText = document.createTextNode(appName); | |
title.appendChild(titleText); | |
link.appendChild(title); | |
modal.appendChild(link); | |
// Create subtitle | |
const subTitle = document.createElement('h2'); | |
subTitle.className = `${appName}__subTitle`; | |
const subTitleText = document.createTextNode('Shortcuts'); | |
subTitle.appendChild(subTitleText); | |
modal.appendChild(subTitle); | |
// Utilities | |
const debug = (message, ...args) => { | |
if (debugApp) { | |
console.debug(message, ...args); | |
} | |
}; | |
let Shortcut = function () { | |
let name; | |
let preventDefault = false; | |
let callback = () => {}; | |
let keyDownListener; | |
let keyUpListener; | |
let currentKeys = new Set(); | |
let shortcutKeys = new Set(); | |
return { | |
withName(shortcutName) { | |
name = shortcutName; | |
return this; | |
}, | |
withPreventDefault(shouldPreventDefault) { | |
preventDefault = shouldPreventDefault; | |
return this; | |
}, | |
withCallback(callbackFunction) { | |
callback = callbackFunction; | |
return this; | |
}, | |
withShiftKey() { | |
shortcutKeys.add('Shift'); | |
return this; | |
}, | |
withCtrlKey() { | |
shortcutKeys.add('Control'); | |
return this; | |
}, | |
withAltKey() { | |
shortcutKeys.add('Alt'); | |
return this; | |
}, | |
withMetaKey() { | |
shortcutKeys.add('Meta'); | |
return this; | |
}, | |
withKey(key) { | |
shortcutKeys.add(key); | |
return this; | |
}, | |
name() { | |
return name; | |
}, | |
preventDefault() { | |
return preventDefault; | |
}, | |
currentKeys() { | |
return currentKeys; | |
}, | |
shortcutKeys() { | |
return shortcutKeys; | |
}, | |
callback() { | |
return callback; | |
}, | |
toString() { | |
return `"${name}": "${Array.from(shortcutKeys).join('" + "')}"`; | |
}, | |
on() { | |
keyDownListener = (event) => { | |
event = event || window.event; | |
debug( | |
`name: ${JSON.stringify(name)}\ntype: "keydown"\nrepeat: ${JSON.stringify( | |
event.repeat | |
)}\ncurrentKeys: ${JSON.stringify([...currentKeys])}\nshortcutKeys: ${JSON.stringify([ | |
...shortcutKeys | |
])}\nkey: ${JSON.stringify(event.key)}` | |
); | |
if (!event.repeat && preventDefault) { | |
event.preventDefault(); | |
} | |
if (!event.repeat && !currentKeys.has(event.key)) { | |
debug(`Adding ${event.key} key to currentKeys...`); | |
currentKeys.add(event.key.toLowerCase()); | |
debug(`Added ${event.key} key to currentKeys...`, [...currentKeys]); | |
} | |
const difference = new Set([...shortcutKeys].filter((k) => !currentKeys.has(k.toLowerCase()))); | |
if (!event.repeat && difference.size === 0) { | |
callback(); | |
} | |
}; | |
keyUpListener = (event) => { | |
event = event || window.event; | |
debug( | |
`name: ${JSON.stringify(name)}\ntype: "keydown"\nrepeat: ${JSON.stringify( | |
event.repeat | |
)}\ncurrentKeys: ${JSON.stringify([...currentKeys])}\nshortcutKeys: ${JSON.stringify([ | |
...shortcutKeys | |
])}\nkey: ${JSON.stringify(event.key)}` | |
); | |
currentKeys.delete(event.key.toLowerCase()); | |
}; | |
document.addEventListener('keydown', keyDownListener); | |
document.addEventListener('keyup', keyUpListener); | |
return this; | |
}, | |
off() { | |
document.removeEventListener('keydown', keyDownListener); | |
document.removeEventListener('keyup', keyUpListener); | |
return this; | |
} | |
}; | |
}; | |
const delayCallback = (callback, seconds = 1) => { | |
setTimeout(() => { | |
debug('Calling callback...'); | |
callback(); | |
}, seconds * 1000); | |
}; | |
const clickElementByInnerHTML = (innerHTML, elementType = 'div') => { | |
const divs = Array.from(document.querySelectorAll(elementType)); | |
const element = divs.find((element) => element.textContent === innerHTML); | |
element.click(); | |
}; | |
const clickElementByAriaLabel = (ariaLabel) => { | |
const element = document.querySelector(`[aria-label="${ariaLabel}"]`); | |
element.click(); | |
}; | |
let isModalOpen = false; | |
const handleClick = (event) => { | |
const isClickInsideModal = modal.contains(event.target); | |
if (isModalOpen && !isClickInsideModal) { | |
hideModal(); | |
} | |
}; | |
const showModal = () => { | |
body.appendChild(modal); | |
document.addEventListener('click', handleClick); | |
isModalOpen = true; | |
}; | |
const hideModal = () => { | |
body.removeChild(modal); | |
document.removeEventListener('click', handleClick); | |
isModalOpen = false; | |
}; | |
const toggleModal = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} else { | |
showModal(); | |
} | |
}; | |
/** | |
* Copy joining info for meeting to the clipboard | |
*/ | |
const copyJoiningInfoToClipboard = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} | |
clickElementByInnerHTML(innerHtmls.meetingDetails); | |
delayCallback(() => clickElementByInnerHTML(innerHtmls.copyJoiningInfo)); | |
delayCallback(() => clickElementByInnerHTML(innerHtmls.meetingDetails)); | |
}; | |
/** | |
* Raise hand in Google meet by pressing the raise hand button | |
*/ | |
const raiseHand = () => { | |
debug('Raising hand...'); | |
clickElementByInnerHTML(innerHtmls.raiseHand); | |
}; | |
/** | |
* Lower hand in Google meet by pressing the lower hand button | |
*/ | |
const lowerHand = () => { | |
debug('Lowering hand...'); | |
clickElementByInnerHTML(innerHtmls.lowerHand); | |
}; | |
/** | |
* Present in Google meet by pressing the present now button | |
*/ | |
const present = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} | |
debug('Opening presenting options...'); | |
clickElementByInnerHTML(innerHtmls.presentNow); | |
}; | |
/** | |
* Open more options menu | |
*/ | |
const openMoreOptions = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} | |
debug('Opening more options menu...'); | |
clickElementByAriaLabel(ariaLabels.moreOptions); | |
}; | |
/** | |
* Make video full screen | |
*/ | |
const makeFullScreen = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} | |
openMoreOptions(); | |
debug('Making full screen...'); | |
delayCallback(() => clickElementByInnerHTML(innerHtmls.fullScreen)); | |
delayCallback(() => openMoreOptions()); | |
}; | |
/** | |
* Open the details window to chat | |
*/ | |
const openDetailsToChat = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} | |
debug('Opening details window to chat...'); | |
clickElementByAriaLabel(ariaLabels.chatWithEveryone); | |
}; | |
/** | |
* Open the details window to everyone | |
*/ | |
const openDetailsToEveryone = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} | |
debug('Opening details window to everyone...'); | |
clickElementByAriaLabel(ariaLabels.showEveryone); | |
}; | |
/** | |
* Close the details window | |
*/ | |
const closeDetails = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} | |
debug('Closing details window...'); | |
clickElementByAriaLabel(ariaLabels.close); | |
}; | |
/** | |
* Leave Google meet by pressing the leave call button | |
*/ | |
const leaveCall = () => { | |
if (isModalOpen) { | |
hideModal(); | |
} | |
debug('Leaving call...'); | |
clickElementByAriaLabel(ariaLabels.leaveCall); | |
}; | |
// Create shortcuts | |
// TODO: Add support for shortcuts having priority (so they can override each other) | |
const shortcuts = [ | |
new Shortcut() | |
.withName('Toggle shortcuts modal') | |
.withPreventDefault() | |
.withShiftKey() | |
.withCtrlKey() | |
.withKey('s') | |
.withCallback(toggleModal), | |
new Shortcut().withName('Leave call').withPreventDefault().withAltKey().withKey('Escape').withCallback(leaveCall), | |
new Shortcut().withName('Raise hand').withPreventDefault().withAltKey().withKey('ArrowUp').withCallback(raiseHand), | |
new Shortcut() | |
.withName('Lower hand') | |
.withPreventDefault() | |
.withAltKey() | |
.withKey('ArrowDown') | |
.withCallback(lowerHand), | |
new Shortcut() | |
.withName('More options') | |
.withPreventDefault() | |
.withCtrlKey() | |
.withKey('.') | |
.withCallback(openMoreOptions), | |
new Shortcut() | |
.withName('Open details to chat') | |
.withPreventDefault() | |
.withAltKey() | |
.withKey('ArrowLeft') | |
.withCallback(openDetailsToChat), | |
new Shortcut() | |
.withName('Open details to everyone') | |
.withPreventDefault() | |
.withCtrlKey() | |
.withShiftKey() | |
.withKey('ArrowLeft') | |
.withCallback(openDetailsToEveryone), | |
new Shortcut() | |
.withName('Close details') | |
.withPreventDefault() | |
.withAltKey() | |
.withKey('ArrowRight') | |
.withCallback(closeDetails), | |
new Shortcut() | |
.withName('Copy joining info to clipboard') | |
.withPreventDefault() | |
.withCtrlKey() | |
.withKey('m') | |
.withCallback(copyJoiningInfoToClipboard), | |
new Shortcut().withName('Present now').withPreventDefault().withCtrlKey().withKey('p').withCallback(present), | |
new Shortcut() | |
.withName('Make full screen') | |
.withPreventDefault() | |
.withCtrlKey() | |
.withKey('f') | |
.withCallback(makeFullScreen) | |
]; | |
// Create table | |
const table = document.createElement('table'); | |
table.className = `${appName}__table`; | |
modal.appendChild(table); | |
// Create header row | |
const headerRow = document.createElement('tr'); | |
headerRow.className = `${appName}__headerRow`; | |
table.appendChild(headerRow); | |
// Create name header | |
const header1 = document.createElement('th'); | |
header1.className = `${appName}__header`; | |
const header1Text = document.createTextNode('Name'); | |
header1.appendChild(header1Text); | |
headerRow.appendChild(header1); | |
// Create keys header | |
const header2 = document.createElement('th'); | |
header2.className = `${appName}__header`; | |
const header2Text = document.createTextNode('Keys'); | |
header2.appendChild(header2Text); | |
headerRow.appendChild(header2); | |
// Create rows (and cells) | |
shortcuts.forEach((shortcut) => { | |
// Create row | |
const row = document.createElement('tr'); | |
row.className = `${appName}__row`; | |
table.appendChild(row); | |
// Create name cell | |
const cell1 = document.createElement('td'); | |
cell1.className = `${appName}__cellName`; | |
const cell1Text = document.createTextNode(shortcut.name()); | |
cell1.appendChild(cell1Text); | |
row.appendChild(cell1); | |
// Create keys cell | |
const cell2 = document.createElement('td'); | |
cell2.className = `${appName}__cellKey`; | |
const cell2Text = document.createTextNode(`"${Array.from(shortcut.shortcutKeys()).join('" + "')}"`); | |
cell2.appendChild(cell2Text); | |
row.appendChild(cell2); | |
}); | |
// Turn on ALL shortcuts | |
shortcuts.forEach((shortcut) => shortcut.on()); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment