// ==UserScript==
// @name GitHub Widescreen 🖥️
// @name:zh GitHub 宽银幕 🖥️
// @name:zh-CN GitHub 宽银幕 🖥️
// @name:zh-HK GitHub 寬銀幕 🖥️
// @name:zh-SG GitHub 宽银幕 🖥️
// @name:zh-TW GitHub 寬銀幕 🖥️
// @description Auto-hides obtrusive side panels on GitHub
// @description:zh 自动隐藏 GitHub 上引人注目的侧面板
// @description:zh-CN 自动隐藏 GitHub 上引人注目的侧面板
// @description:zh-HK 自動隱藏 GitHub 上引人注目的側面板
// @description:zh-SG 自动隐藏 GitHub 上引人注目的侧面板
// @description:zh-TW 自動隱藏 GitHub 上引人注目的側面板
// @author Adam Lui
// @namespace https://github.com/adamlui
// @version 2026.1.28
// @license MIT
// @icon data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2248%22%20height=%2248%22%20viewBox=%220%200%2048%2048%22%3E%3Cstyle%3E:root%7B--fill:%23000%7D@media%20(prefers-color-scheme:dark)%7B:root%7B--fill:%23fff%7D%7D%3C/style%3E%3Cpath%20fill=%22var(--fill)%22%20d=%22M24%201.9a21.6%2021.6%200%200%200-6.8%2042.2c1%20.2%201.8-.9%201.8-1.8v-2.9c-6%201.3-7.9-2.9-7.9-2.9a6.5%206.5%200%200%200-2.2-3.2c-2-1.4.1-1.3.1-1.3a4.3%204.3%200%200%201%203.3%202c1.7%202.9%205.5%202.6%206.7%202.1a5.4%205.4%200%200%201%20.5-2.9C12.7%2032%209%2028%209%2022.6a10.7%2010.7%200%200%201%202.9-7.6%206.2%206.2%200%200%201%20.3-6.4%208.9%208.9%200%200%201%206.4%202.9%2015.1%2015.1%200%200%201%205.4-.8%2017.1%2017.1%200%200%201%205.4.7%209%209%200%200%201%206.4-2.8%206.5%206.5%200%200%201%20.4%206.4%2010.7%2010.7%200%200%201%202.8%207.6c0%205.4-3.7%209.4-10.5%2010.6a5.4%205.4%200%200%201%20.5%202.9v6.2a1.8%201.8%200%200%200%201.9%201.8A21.7%2021.7%200%200%200%2024%201.9Z%22/%3E%3C/svg%3E
// @icon64 data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20width=%2264%22%20height=%2264%22%20viewBox=%220%200%2048%2048%22%3E%3Cstyle%3E:root%7B--fill:%23000%7D@media%20(prefers-color-scheme:dark)%7B:root%7B--fill:%23fff%7D%7D%3C/style%3E%3Cpath%20fill=%22var(--fill)%22%20d=%22M24%201.9a21.6%2021.6%200%200%200-6.8%2042.2c1%20.2%201.8-.9%201.8-1.8v-2.9c-6%201.3-7.9-2.9-7.9-2.9a6.5%206.5%200%200%200-2.2-3.2c-2-1.4.1-1.3.1-1.3a4.3%204.3%200%200%201%203.3%202c1.7%202.9%205.5%202.6%206.7%202.1a5.4%205.4%200%200%201%20.5-2.9C12.7%2032%209%2028%209%2022.6a10.7%2010.7%200%200%201%202.9-7.6%206.2%206.2%200%200%201%20.3-6.4%208.9%208.9%200%200%201%206.4%202.9%2015.1%2015.1%200%200%201%205.4-.8%2017.1%2017.1%200%200%201%205.4.7%209%209%200%200%201%206.4-2.8%206.5%206.5%200%200%201%20.4%206.4%2010.7%2010.7%200%200%201%202.8%207.6c0%205.4-3.7%209.4-10.5%2010.6a5.4%205.4%200%200%201%20.5%202.9v6.2a1.8%201.8%200%200%200%201.9%201.8A21.7%2021.7%200%200%200%2024%201.9Z%22/%3E%3C/svg%3E
// @match *://github.com/*
// @connect raw.githubusercontent.com
// @require https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@7595cd7/assets/js/lib/dom.js/dist/dom.min.js#sha256-xovdxRnmYD/eCgBiGCu5+Vd3+WWIvLUKVtU/MnRueeU=
// @resource rpgCSS https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@727feff/assets/styles/rising-particles/dist/gray.min.css#sha256-48sEWzNUGUOP04ur52G5VOfGZPSnZQfrF3szUr4VaRs=
// @resource rpwCSS https://cdn.jsdelivr.net/gh/adamlui/ai-web-extensions@727feff/assets/styles/rising-particles/dist/white.min.css#sha256-6xBXczm7yM1MZ/v0o1KVFfJGehHk47KJjq8oTktH4KE=
// @grant GM_getResourceText
// @grant GM_openInTab
// @grant GM_registerMenuCommand
// @grant GM.xmlHttpRequest
// @downloadURL https://raw.githubusercontent.com/adamlui/github-widescreen/main/greasemonkey/github-widescreen.user.js
// @updateURL https://raw.githubusercontent.com/adamlui/github-widescreen/main/greasemonkey/github-widescreen.user.js
// @homepageURL https://github.com/adamlui/github-widescreen
// @supportURL https://github.com/adamlui/github-widescreen/issues
// @contributionURL https://ko-fi.com/adamlui
// ==/UserScript==
(async () => {
'use strict'
localStorage.alertQueue = '[]' ; window.config = { bgAnimationsDisabled: false }
// Init ENV context
window.env = {
browser: {
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) },
ui: { scheme: isDarkMode() ? 'dark' : 'light' }
}
// Init APP data
const app = {
name: 'GitHub Widescreen', slug: 'github-widescreen',
urls: {
discuss: 'https://github.com/adamlui/github-widescreen/discuss',
gitHub: 'https://github.com/adamlui/github-widescreen',
support: 'https://github.com/adamlui/github-widescreen/issues',
update: 'https://raw.githubusercontent.com/adamlui/github-widescreen/main/greasemonkey/github-widescreen.user.js'
}
}
// Define FUNCTIONS
function ghAlert(title, msg, btns, checkbox, width) {
// [ title/msg = strings, btns = [named functions], checkbox = named function, width (px) = int ] = optional
// * Spaces are inserted into button labels by parsing function names in camel/kebab/snake case
// Init env context
const scheme = env.ui.scheme,
isMobile = env.browser.isMobile
// Define event handlers
const handlers = {
dismiss: {
click(event) {
if (event.target == event.currentTarget || event.target.closest('[class*=-close-btn]'))
dismissAlert()
},
key(event) {
if (!/^(?: |Space|Enter|Return|Esc)/.test(event.key) || ![32, 13, 27].includes(event.keyCode))
return
for (const alertId of alertQueue) { // look to handle only if triggering alert is active
const alert = document.getElementById(alertId)
if (!alert || alert.style.display == 'none') return
if (event.key.startsWith('Esc') || event.keyCode == 27) dismissAlert() // and do nothing
else { // Space/Enter pressed
const mainBtn = alert.querySelector('.modal-buttons').lastChild // look for main button
if (mainBtn) { mainBtn.click() ; event.preventDefault() } // click if found
}
}
}
},
drag: {
mousedown(event) { // find modal, update styles, attach listeners, init XY offsets
if (event.button != 0) return // prevent non-left-click drag
if (!/auto|default/.test(getComputedStyle(event.target).cursor))
return // prevent drag on interactive elems
env.draggingModal = event.currentTarget
event.preventDefault() // prevent sub-elems like icons being draggable
Object.assign(env.draggingModal.style, {
transition: '0.1s', willChange: 'transform', transform: 'scale(1.05)' })
document.body.style.cursor = 'grabbing' // update cursor
;[...env.draggingModal.children] // prevent hover FX if drag lags behind cursor
.forEach(child => child.style.pointerEvents = 'none')
;['mousemove', 'mouseup'].forEach(eventType => // add listeners
document.addEventListener(eventType, handlers.drag[eventType]))
const draggingModalRect = env.draggingModal.getBoundingClientRect()
handlers.drag.offsetX = event.clientX - draggingModalRect.left +21
handlers.drag.offsetY = event.clientY - draggingModalRect.top +12
},
mousemove(event) { // drag modal
if (!env.draggingModal) return
const newX = event.clientX - handlers.drag.offsetX,
newY = event.clientY - handlers.drag.offsetY
Object.assign(env.draggingModal.style, { left: `${newX}px`, top: `${newY}px` })
},
mouseup() { // restore styles/pointer events, remove listeners, reset env.draggingModal
Object.assign(env.draggingModal.style, { // restore styles
cursor: 'inherit', transition: 'inherit', willChange: 'auto', transform: 'scale(1)' })
document.body.style.cursor = '' // restore cursor
;[...env.draggingModal.children] // restore pointer events
.forEach(child => child.style.pointerEvents = '')
;['mousemove', 'mouseup'].forEach(eventType => // remove listeners
document.removeEventListener(eventType, handlers.drag[eventType]))
env.draggingModal = null
}
}
}
// Create modal parent/children elems
const modalContainer = document.createElement('div')
modalContainer.id = Math.floor(randomFloat() * 1000000) + Date.now()
modalContainer.classList.add('chatgpt-modal') // add class to main div
const modal = document.createElement('div'),
modalTitle = document.createElement('h2'),
modalMessage = document.createElement('p')
// Create/append/update modal style (if missing or outdated)
const thisUpdated = 1739338889852 // timestamp of last edit for this file's `modalStyle`
let modalStyle = document.querySelector('#chatgpt-modal-style') // try to select existing style
if (!modalStyle || parseInt(modalStyle.getAttribute('last-updated'), 10) < thisUpdated) { // if missing or outdated
if (!modalStyle) { // outright missing, create/id/attr/append it first
modalStyle = document.createElement('style') ; modalStyle.id = 'chatgpt-modal-style'
modalStyle.setAttribute('last-updated', thisUpdated.toString())
document.head.append(modalStyle)
}
modalStyle.textContent = ( // update prev/new style contents
`.chatgpt-modal { /* vars */
--transition: opacity 0.65s cubic-bezier(.165,.84,.44,1), /* for fade-in */
transform 0.55s cubic-bezier(.165,.84,.44,1) ; /* for move-in */
--bg-transition: background-color 0.25s ease ; /* for bg dim */
--btn-transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out ; /* for smooth zoom */
--btn-shadow: 2px 1px ${ scheme == 'dark' ? '54px #00cfff' : '30px #9cdaff' }}`
+ '.no-mobile-tap-outline { outline: none ; -webkit-tap-highlight-color: transparent }'
// Background styles
+ `.chatgpt-modal {
pointer-events: auto ; /* override any disabling from site modals (like guest login spam) */
position: fixed ; top: 0 ; left: 0 ; width: 100% ; height: 100% ; /* expand to full view-port */
display: flex ; justify-content: center ; align-items: center ; z-index: 9999 ; /* align */
transition: var(--bg-transition) ; /* for bg dim */
-webkit-transition: var(--bg-transition) ; -moz-transition: var(--bg-transition) ;
-o-transition: var(--bg-transition) ; -ms-transition: var(--bg-transition) }`
// Alert styles
+ `.chatgpt-modal > div {
position: absolute ; /* to be click-draggable */
opacity: 0 ; /* to fade-in */
font-family: -apple-system, system-ui, BlinkMacSystemFont, Segoe UI, Roboto,
Oxygen-Sans, Ubuntu, Cantarell, Helvetica Neue, sans-serif ;
padding: 20px ; margin: 12px 23px ; font-size: 20px ;
color: ${ scheme == 'dark' ? 'white' : 'black' };
background-image: linear-gradient(180deg, ${
scheme == 'dark' ? '#99a8a6 -200px, black 200px' : '#b6ebff -296px, white 171px' }) ;
border: 1px solid ${ scheme == 'dark' ? 'white' : '#b5b5b5' };
transform: translateX(-3px) translateY(7px) ; /* offset to move-in from */
max-width: 75vw ; word-wrap: break-word ; border-radius: 15px ;
--shadow: 0 30px 60px rgba(0,0,0,0.12) ; box-shadow: var(--shadow) ;
-webkit-box-shadow: var(--shadow) ; -moz-box-shadow: var(--shadow) ;
user-select: none ; -webkit-user-select: none ; -moz-user-select: none ;
-o-user-select: none ; -ms-user-select: none ;
transition: var(--transition) ; /* for fade-in + move-in */
-webkit-transition: var(--transition) ; -moz-transition: var(--transition) ;
-o-transition: var(--transition) ; -ms-transition: var(--transition) }
.chatgpt-modal h2 {
text-align: center ; font-weight: bold ; font-size: 44px ;
line-height: 46px ; margin: 0 0 14px 15px }
.chatgpt-modal p { text-align: center ; font-size: 16px ; line-height: 28px }
.chatgpt-modal a { color: ${ scheme == 'dark' ? '#00cfff' : '#1e9ebb' }}
.chatgpt-modal a:hover { text-decoration: underline }
.chatgpt-modal.animated > div {
z-index: 13456 ; opacity: 0.98 ; transform: translateX(0) translateY(0) }
@keyframes alert-zoom-fade-out {
0% { opacity: 1 } 50% { opacity: 0.25 ; transform: scale(1.05) }
100% { opacity: 0 ; transform: scale(1.35) }}`
// Button styles
+ `.modal-buttons {
display: flex ; justify-content: flex-end ; margin: 20px -5px -3px 0 ;
${ isMobile ? 'flex-direction: column-reverse' : '' }}
.chatgpt-modal button {
font-size: 14px ; text-transform: uppercase ; cursor: crosshair ;
margin-left: ${ isMobile ? 0 : 10 }px ; padding: ${ isMobile ? 15 : 8 }px 18px ;
${ isMobile ? 'margin-top: 5px ; margin-bottom: 3px ;' : '' }
border-radius: 0 ; border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' };
transition: var(--btn-transition) ;
-webkit-transition: var(--btn-transition) ; -moz-transition: var(--btn-transition) ;
-o-transition: var(--btn-transition) ; -ms-transition: var(--btn-transition) }
.chatgpt-modal button:hover {
transform: scale(1.055) ; color: black ;
background-color: #${ scheme == 'dark' ? '00cfff' : '9cdaff' };
box-shadow: var(--btn-shadow) ;
-webkit-box-shadow: var(--btn-shadow) ; -moz-box-shadow: var(--btn-shadow) }
.primary-modal-btn {
border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' };
background: ${ scheme == 'dark' ? 'white' : 'black' };
color: ${ scheme == 'dark' ? 'black' : 'white' }}
.modal-close-btn {
cursor: pointer ; width: 29px ; height: 29px ; border-radius: 17px ;
float: right ; position: relative ; right: -6px ; top: -5px }
.modal-close-btn svg { margin: 10px } /* center SVG for hover underlay */
.modal-close-btn:hover { background-color: #f2f2f2${ scheme == 'dark' ? '00' : '' }}`
// Checkbox styles
+ `.chatgpt-modal .checkbox-group { margin: 5px 0 -8px 5px }
.chatgpt-modal input[type=checkbox] {
cursor: pointer ; transform: scale(0.7) ; margin-right: 5px ;
border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' }}
.chatgpt-modal input[type=checkbox]:checked {
background-color: black ; position: inherit ;
border: 1px solid ${ scheme == 'dark' ? 'white' : 'black' }}
.chatgpt-modal input[type=checkbox]:focus {
outline: none ; box-shadow: none ; -webkit-box-shadow: none ; -moz-box-shadow: none }
.chatgpt-modal .checkbox-group label {
cursor: pointer ; font-size: 14px ; color: ${ scheme == 'dark' ? '#e1e1e1' : '#1e1e1e' }}`
)
}
// Insert text into elems
modalTitle.textContent = title || '' ; modalMessage.innerText = msg || '' ; renderHTML(modalMessage)
// Create/append buttons (if provided) to buttons div
const modalButtons = document.createElement('div')
modalButtons.classList.add('modal-buttons', 'no-mobile-tap-outline')
if (btns) { // are supplied
if (!Array.isArray(btns)) btns = [btns] // convert single button to array if necessary
btns.forEach((buttonFn) => { // create title-cased labels + attach listeners
const button = document.createElement('button')
button.textContent = buttonFn.name
.replace(/[_-]\w/g, match => match.slice(1).toUpperCase()) // convert snake/kebab to camel case
.replace(/([A-Z])/g, ' $1') // insert spaces
.replace(/^\w/, firstChar => firstChar.toUpperCase()) // capitalize first letter
button.onclick = () => { dismissAlert() ; buttonFn() }
modalButtons.insertBefore(button, modalButtons.firstChild)
})
}
// Create/append OK/dismiss button to buttons div
const dismissBtn = document.createElement('button')
dismissBtn.textContent = btns ? 'Dismiss' : 'OK'
modalButtons.insertBefore(dismissBtn, modalButtons.firstChild)
// Highlight primary button
modalButtons.lastChild.classList.add('primary-modal-btn')
// Create/append checkbox (if provided) to checkbox group div
const checkboxDiv = document.createElement('div')
if (checkbox) { // is supplied
checkboxDiv.classList.add('checkbox-group')
const checkboxFn = checkbox, // assign the named function to checkboxFn
checkboxInput = document.createElement('input')
checkboxInput.type = 'checkbox' ; checkboxInput.onchange = checkboxFn
// Create/show label
const checkboxLabel = document.createElement('label')
checkboxLabel.onclick = () => { checkboxInput.checked = !checkboxInput.checked ; checkboxFn() }
checkboxLabel.textContent = checkboxFn.name[0].toUpperCase() // capitalize first char
+ checkboxFn.name.slice(1) // format remaining chars
.replace(/([A-Z])/g, (match, letter) => ' ' + letter.toLowerCase()) // insert spaces, convert to lowercase
.replace(/\b(\w+)nt\b/gi, '$1n\'t') // insert apostrophe in 'nt' suffixes
.trim() // trim leading/trailing spaces
checkboxDiv.append(checkboxInput) ; checkboxDiv.append(checkboxLabel)
}
// Create close button
const closeBtn = document.createElement('div')
closeBtn.title = 'Close' ; closeBtn.classList.add('modal-close-btn', 'no-mobile-tap-outline')
const closeSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
closeSVG.setAttribute('height', '10px')
closeSVG.setAttribute('viewBox', '0 0 14 14')
closeSVG.setAttribute('fill', 'none')
const closeSVGpath = document.createElementNS('http://www.w3.org/2000/svg', 'path')
closeSVGpath.setAttribute('fill-rule', 'evenodd')
closeSVGpath.setAttribute('clip-rule', 'evenodd')
closeSVGpath.setAttribute('fill', isDarkMode() ? 'white' : 'black')
closeSVGpath.setAttribute('d', 'M13.7071 1.70711C14.0976 1.31658 14.0976 0.683417 13.7071 0.292893C13.3166 -0.0976312 12.6834 -0.0976312 12.2929 0.292893L7 5.58579L1.70711 0.292893C1.31658 -0.0976312 0.683417 -0.0976312 0.292893 0.292893C-0.0976312 0.683417 -0.0976312 1.31658 0.292893 1.70711L5.58579 7L0.292893 12.2929C-0.0976312 12.6834 -0.0976312 13.3166 0.292893 13.7071C0.683417 14.0976 1.31658 14.0976 1.70711 13.7071L7 8.41421L12.2929 13.7071C12.6834 14.0976 13.3166 14.0976 13.7071 13.7071C14.0976 13.3166 14.0976 12.6834 13.7071 12.2929L8.41421 7L13.7071 1.70711Z')
closeSVG.append(closeSVGpath) ; closeBtn.append(closeSVG)
// Assemble/append div
modal.append(closeBtn, modalTitle, modalMessage, checkboxDiv, modalButtons)
modal.style.width = `${ width || 458 }px`
modalContainer.append(modal) ; document.body.append(modalContainer)
// Enqueue alert
let alertQueue = JSON.parse(localStorage.alertQueue)
alertQueue.push(modalContainer.id)
localStorage.alertQueue = JSON.stringify(alertQueue)
// Show alert if none active
modalContainer.style.display = 'none'
if (alertQueue.length == 1) {
modalContainer.style.display = ''
setTimeout(() => { // dim bg
modal.parentNode.style.backgroundColor = `rgba(67,70,72,${ scheme == 'dark' ? 0.62 : 0.33 })`
modal.parentNode.classList.add('animated')
}, 100) // delay for transition fx
}
// Add listeners
[modalContainer, closeBtn, closeSVG, dismissBtn].forEach(elem => elem.onclick = handlers.dismiss.click)
document.addEventListener('keydown', handlers.dismiss.key)
modal.onmousedown = handlers.drag.mousedown // enable click-dragging
// Define alert dismisser
const dismissAlert = () => {
modalContainer.style.backgroundColor = 'transparent'
modal.style.animation = 'alert-zoom-fade-out 0.165s ease-out'
modal.onanimationend = () => {
// Remove alert
modalContainer.remove() // ...from DOM
alertQueue = JSON.parse(localStorage.alertQueue)
alertQueue.shift() // + memory
localStorage.alertQueue = JSON.stringify(alertQueue) // + storage
document.removeEventListener('keydown', handlers.dismiss.key) // prevent memory leaks
// Check for pending alerts in queue
if (alertQueue.length) {
const nextAlert = document.getElementById(alertQueue[0])
setTimeout(() => {
nextAlert.style.display = ''
setTimeout(() => nextAlert.classList.add('animated'), 100)
}, 500)
}
}
}
dom.addRisingParticles(modal)
return modalContainer.id // if assignment used
}
function hideSidePanels() {
hideBtns.push(...document.querySelectorAll(
// File Tree + Symbols Panel buttons in editor
'button[aria-expanded="true"][data-testid], '
// Hide File Tree button in diff views
+ 'button[id^="hide"]:not([hidden])'))
if (hideBtns.length) // click if needed
hideBtns.forEach(btn => { btn.click() })
}
function isDarkMode() {
return document.documentElement.dataset.colorMode == 'dark'
|| document.documentElement.dataset.darkreaderScheme == 'dark'
|| window.matchMedia?.('(prefers-color-scheme: dark)')?.matches
}
function randomFloat() {
// * Generates a random, cryptographically secure value between 0 (inclusive) & 1 (exclusive)
const crypto = window.crypto || window.msCrypto
return crypto?.getRandomValues(new Uint32Array(1))[0] / 0xFFFFFFFF || Math.random()
}
function renderHTML(node) {
const reTags = /<([a-z\d]+)\b([^>]*)>([\s\S]*?)<\/\1>/g,
reAttrs = /(\S+)=['"]?((?:.(?!['"]?\s+\S+=|[>']))+.)['"]?/g, // eslint-disable-line
nodeContent = node.childNodes
// Preserve consecutive spaces + line breaks
if (!renderHTML.preWrapSet) {
node.style.whiteSpace = 'pre-wrap' ; renderHTML.preWrapSet = true
setTimeout(() => renderHTML.preWrapSet = false, 100)
}
// Process child nodes
for (const childNode of nodeContent) {
// Process text node
if (childNode.nodeType == Node.TEXT_NODE) {
const text = childNode.nodeValue,
elems = [...text.matchAll(reTags)]
// Process 1st element to render
if (elems.length) {
const elem = elems[0],
[tagContent, tagName, tagAttrs, tagText] = elem.slice(0, 4),
tagNode = document.createElement(tagName) ; tagNode.textContent = tagText
// Extract/set attributes
const attrs = [...tagAttrs.matchAll(reAttrs)]
attrs.forEach(attr => {
const name = attr[1], value = attr[2].replace(/['"]/g, '')
tagNode.setAttribute(name, value)
})
const renderedNode = renderHTML(tagNode) // render child elems of newly created node
// Insert newly rendered node
const beforeTextNode = document.createTextNode(text.substring(0, elem.index)),
afterTextNode = document.createTextNode(text.substring(elem.index + tagContent.length))
// Replace text node with processed nodes
node.replaceChild(beforeTextNode, childNode)
node.insertBefore(renderedNode, beforeTextNode.nextSibling)
node.insertBefore(afterTextNode, renderedNode.nextSibling)
}
// Process element nodes recursively
} else if (childNode.nodeType == Node.ELEMENT_NODE) renderHTML(childNode)
}
return node // if assignment used
}
function safeWinOpen(url) { window.open(url, '_blank', 'noopener') } // to prevent backdoor vulnerabilities
function updateCheck() {
// Fetch latest meta
const currentVer = GM_info.script.version
GM.xmlHttpRequest({
method: 'GET', url: app.urls.update + '?t=' + Date.now(),
headers: { 'Cache-Control': 'no-cache' },
onload: response => { app.latestVer = /@version +(.*)/.exec(response.responseText)?.[1]
// Compare versions
if (app.latestVer) for (let i = 0 ; i < 4 ; i++) { // loop thru subver's
const currentSubVer = parseInt(currentVer.split('.')[i], 10) || 0,
latestSubVer = parseInt(app.latestVer.split('.')[i], 10) || 0
if (currentSubVer > latestSubVer) break // out of comparison since not outdated
else if (latestSubVer > currentSubVer) { // if outdated
// Alert to update
ghAlert('Update available! 🚀', // title
`A newer version of ${app.name} v${app.latestVer} is available! `
+ 'View changes',
function update() { // button
GM_openInTab(app.urls.update.replace('meta.js', 'user.js') + '?t=' + Date.now(),
{ active: true, insert: true } // focus, make adjacent
).onclose = () => location.reload() },
'', 383 // width
)
return
}}
ghAlert('Up to date!', `${app.name} (v${currentVer}) is up-to-date!`)
}})}
// Run MAIN routine
// Register ABOUT menu command
GM_registerMenuCommand(`💡 About ${app.name}`, async () => {
// Show alert
const pStyle = 'font-size: 1rem ; position: relative ; left: 3px ;',
pBrStyle = 'font-size: 1rem ; position: relative ; left: 9px ; bottom: 3px ;'
const aboutAlertID = ghAlert(
app.name, // title
`🏷️ Version: ${
GM_info.script.version}\n`
+ `📜 Source code:\n`
+ app.urls.gitHub + '',
[ // buttons
function checkForUpdates() { updateCheck() },
function getSupport() { safeWinOpen(app.urls.support) },
function discuss() { safeWinOpen(app.urls.discuss) }
], null, 501)
// Re-format buttons to include emojis + re-case + hide 'Dismiss'
for (const button of document.getElementById(aboutAlertID).querySelectorAll('button')) {
if (/updates/i.test(button.textContent))
button.textContent = '🚀 Check for Updates'
else if (/support/i.test(button.textContent))
button.textContent = '🧠 Get Support'
else if (/discuss/i.test(button.textContent))
button.textContent = '🗨️ Discuss'
else button.style.display = 'none' // hide Dismiss button
}
})
;['rpg', 'rpw'].forEach(cssType => // rising particles
document.head.append(dom.create.style(GM_getResourceText(`${cssType}CSS`))))
// Observe DOM for need to hide side panels
const hideBtns = [] ; let prevURL = null
const sidePanelObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type == 'childList' && mutation.addedNodes.length) {
if (location.href !== prevURL) { // if loaded/naved to new page
if (location.href.includes('edit')) { // if on editor, wait for side panel load
const editorSidePanelobserver = new MutationObserver(() => {
if (document.querySelector('[data-testid="editor-side-panel"]')) {
editorSidePanelobserver.disconnect() ; hideSidePanels()
}}) ; editorSidePanelobserver.observe(document.body, { childList: true, subtree: true })
} else hideSidePanels()
prevURL = location.href
}}})}) ; sidePanelObserver.observe(document.documentElement, { childList: true, subtree: true })
})()