Remove popups, overlays e elementos fixos de sites. Baseado na extensão PopUpOFF de RomanistHere. Modos: Agressivo, Moderado, Delicado.
// ==UserScript==
// @name PopUpOFF
// @name:en PopUpOFF
// @name:pt-BR PopUpOFF
// @namespace https://greasyfork.org/en/users/1301195-luciano-inf
// @version 1.0.1
// @description Removes popups, overlays, and fixed elements from websites. Based on PopUpOFF browser extension by RomanistHere. Modes: Aggressive, Moderate, Delicate.
// @description:pt-BR Remove popups, overlays e elementos fixos de sites. Baseado na extensão PopUpOFF de RomanistHere. Modos: Agressivo, Moderado, Delicado.
// @author Luciano.Oliveirals
// @license Apache-2.0
// @run-at document-end
// @grant GM_setValue
// @grant GM_getValue
// @match *://*/*
// @icon https://icons.iconarchive.com/icons/paomedia/small-n-flat/64/shield-warning-icon.png
// @supportURL https://greasyfork.org/scripts/587461-popupoff
// @homepageURL https://github.com/LucianoSkx/popupoff
// ==/UserScript==
;(function () {
'use strict'
if (window !== window.parent) return
const defaultSettings = {
mode: 'moderate',
restoreCont: false,
staticSubMode: 'relative',
}
const whitelist = [
'a.goodtime.io', 'about.google', 'app.hubspot.com', 'calendar.google.com',
'discord.com', 'docs.google.com', 'drive.google.com', 'mail.google.com',
'music.youtube.com', 'open.spotify.com', 'trello.com', 'twitter.com',
'vk.com', 'web.telegram.org', 'www.amazon.com', 'www.facebook.com',
'www.google.com', 'www.instagram.com', 'www.linkedin.com', 'www.netflix.com',
'www.reddit.com', 'www.spotify.com', 'www.youtube.com', 'zoom.us',
'store.steampowered.com', 'www.twitch.tv', 'www.apple.com', 'www.microsoft.com',
]
const forbWordsEasy = [
'cookie', 'privacy', 'adblock', 'ad block', 'blocker', 'ever miss',
't miss', 'our privacy', 'theguardian', 'bloqueador de anuncios',
'to continue us', 'mited acces', 'lusive acces', 'left this mon',
'be the fir', 'ble notif', 's the time', 'ur newslet', 'gister for fre',
'nload free', 'nload your free', 'gn to youtube', 'ble deal',
'started for fre', "it's free", 'free trial', 'tart fre',
'advertisement', '//consent.', 'ign in to youtub',
]
const forbWords = [...forbWordsEasy, 'policy', 'subscri', 'sale', 'updates',
'member', 'value', 'advertis', 'install',
]
const allowedWords = [
'sign in', 'language', 'basket', 'delivery', 'price', 'google meet',
'resume', 'apply', 'drive',
]
const hiddenClasses = [
'.hide', '.height_0', '.not_scroll', '.excerpt-cropped',
'.paragraph--faded', '.paragraph--reduced', '.paragraph--dynamic',
'.article-teaser-overflow', '.editor-description__wrapper--cropped',
]
let domObserver = null
let isCSSAppended = false
let infiniteLoopPreventCounter = 0
let myTimer = 0
let wasNotStopped = true
const getStyle = (elem, prop) => window.getComputedStyle(elem, null).getPropertyValue(prop)
const setPropImp = (elem, prop, val) => elem.style.setProperty(prop, val, 'important')
const getPureURL = () => location.hostname + (location.port ? ':' + location.port : '')
const isDecentElem = e => !['SCRIPT', 'HEAD', 'BODY', 'HTML', 'STYLE'].includes(e.nodeName)
const contentEasyCheck = el => forbWordsEasy.some(v => el.innerHTML.toLowerCase().includes(v))
const contentCheck = el => forbWords.some(v => el.innerHTML.toLowerCase().includes(v))
const contentUnlockCheck = el => !allowedWords.some(v => el.innerHTML.toLowerCase().includes(v))
const videoCheck = element => {
const nn = element.nodeName
if (nn === 'APP-DRAWER') return false
if (nn === 'VIDEO' || nn === 'IMG' || nn === 'FORM' || nn === 'BUTTON' || nn === 'INPUT' || nn === 'IFRAME') return false
if (element.shadowRoot) return videoCheck(element.shadowRoot)
if (element.contentDocument) return videoCheck(element.contentDocument)
for (let i = 0; i < element.childNodes.length; i++) {
if (element.childNodes[i].nodeType === 1 && !videoCheck(element.childNodes[i])) return false
}
return true
}
const positionCheck = (element, windowArea) => {
if (element.offsetHeight === 0 || element.offsetWidth === 0) {
return { shouldRemove: true, shouldMemo: !!contentEasyCheck(element) }
}
const layoutArea = element.offsetHeight * element.offsetWidth
const screenValue = Math.round((layoutArea / windowArea) * 100) / 100
const offsetBot = window.innerHeight - (element.offsetTop + element.offsetHeight)
if (screenValue >= 0.98) {
return { shouldRemove: contentEasyCheck(element) || videoCheck(element), shouldMemo: true }
}
if (element.offsetTop <= 100 && element.offsetHeight <= 250 && element.offsetWidth > 640) {
if (element.id === 'onesignal-slidedown-container') return { shouldRemove: true, shouldMemo: true }
return { shouldRemove: false, shouldMemo: true }
}
if (element.offsetLeft <= 0 && element.offsetWidth <= 360 && screenValue >= 0.1) {
return { shouldRemove: false, shouldMemo: true }
}
if (screenValue < 0.98 && screenValue >= 0.1) {
return { shouldRemove: contentEasyCheck(element), shouldMemo: true }
}
if (screenValue <= 0.02 && element.offsetTop > 100) {
return { shouldRemove: false, shouldMemo: true }
}
if (offsetBot <= 212) {
return { shouldRemove: contentEasyCheck(element), shouldMemo: true }
}
if (screenValue <= 0.1 && element.offsetTop > 100) {
return { shouldRemove: false, shouldMemo: true }
}
return { shouldRemove: false, shouldMemo: false }
}
const removeOverflow = () => {
const doc = document.documentElement
const body = document.body
const overDoc = getStyle(doc, 'overflow-y')
const overBody = getStyle(body, 'overflow-y')
const docPos = getStyle(doc, 'position')
const bodyPos = getStyle(body, 'position')
if (overDoc !== 'visible' && overDoc !== 'unset' && overDoc !== 'auto') setPropImp(doc, 'overflow-y', 'unset')
if (overBody !== 'visible' && overBody !== 'unset' && overBody !== 'auto') setPropImp(body, 'overflow-y', 'unset')
if (docPos === 'fixed' || docPos === 'absolute') { setPropImp(doc, 'min-height', '100vh'); setPropImp(doc, 'position', 'relative') }
if (bodyPos === 'fixed' || bodyPos === 'absolute') { setPropImp(body, 'position', 'relative'); setPropImp(body, 'min-height', '100vh') }
}
const findHidden = () => {
hiddenClasses.forEach(cls => {
document.querySelectorAll(cls).forEach(el => {
if (el.innerHTML.length > 5) {
el.classList.remove(...hiddenClasses.map(c => c.replace('.', '')))
}
})
})
try { document.querySelector('#sign-in-gate').remove() } catch (e) { }
}
const detectGrad = element => {
const bg = getStyle(element, 'background-image')
if (bg.includes('linear-gradient')) setPropImp(element, 'background-image', 'unset')
const before = getComputedStyle(element, '::before').getPropertyValue('background-image')
const after = getComputedStyle(element, '::after').getPropertyValue('background-image')
if (before.includes('linear-gradient') || after.includes('linear-gradient')) {
if (!isCSSAppended) {
document.head.insertAdjacentHTML('beforeend',
'<style>.PopUpOFF-no_grad::after,.PopUpOFF-no_grad::before{background-image:unset!important}</style>')
isCSSAppended = true
}
element.classList.add('PopUpOFF-no_grad')
}
}
const additionalChecks = (element, checkElem) => {
const f = getStyle(element, 'filter')
const wf = getStyle(element, '-webkit-filter')
if (f !== 'none' || wf !== 'none') { setPropImp(element, 'filter', 'none'); setPropImp(element, '-webkit-filter', 'none') }
const settings = getSettings()
if (settings.restoreCont) detectGrad(element)
if (element.shadowRoot) checkElemWithSibl(element.shadowRoot, checkElem)
}
const checkToConvertToStatic = elem => {
if (getStyle(elem, 'overflow') === 'hidden') {
const { width, height, top, left } = elem.getBoundingClientRect()
if (width > 0 && height > 0 && top === 0 && left === 0) {
setPropImp(elem, 'position', 'static')
elem.setAttribute('data-popupoff', 'st')
return true
}
}
return false
}
const checkElemWithSibl = (element, checkElem) => {
if (element instanceof HTMLElement) {
checkElem(element)
if (wasNotStopped) { const e = element.querySelectorAll('*'); e.forEach(checkElem) }
} else if (element instanceof ShadowRoot) { element.querySelectorAll('*').forEach(checkElem) }
}
const disconnectObserver = () => {
if (domObserver) { domObserver.disconnect(); domObserver = null }
}
const removeDomWatcher = (body, action) => {
disconnectObserver()
if (wasNotStopped) { setTimeout(() => { action(body.getElementsByTagName('*')) }, 2000) }
wasNotStopped = false
}
const prevLoop = () => {
if (infiniteLoopPreventCounter > 1200) return true
infiniteLoopPreventCounter++
if (myTimer === 0) { myTimer = setTimeout(() => { infiniteLoopPreventCounter = 0; clearTimeout(myTimer); myTimer = 0 }, 1000) }
return false
}
const watchMutations = (mutations, checkElem, body, action) => {
for (let i = 0; i < mutations.length; i++) {
if (prevLoop()) { removeDomWatcher(body, action); break }
const m = mutations[i]
if (m.attributeName === 'data-popupoff') continue
if (['SCRIPT', 'HEAD', 'STYLE'].includes(m.target.nodeName)) continue
checkElemWithSibl(m.target, checkElem)
m.addedNodes.forEach(el => {
if (!['#text', '#comment', 'SCRIPT', 'HEAD', 'STYLE'].includes(el.nodeName))
checkElemWithSibl(el, checkElem)
})
removeOverflow()
}
}
const aggressiveMode = () => {
const doc = document.documentElement
const body = document.body
const settings = getSettings()
const checkElem = element => {
if (!isDecentElem(element)) return
const pos = getStyle(element, 'position')
if (pos === 'fixed' || pos === 'sticky') {
if (checkToConvertToStatic(element)) return
if (element.getAttribute('data-popupoff') === 'notification') return
if (getStyle(element, 'display') !== 'none') element.setAttribute('data-popupoff', 'bl')
setPropImp(element, 'display', 'none')
}
additionalChecks(element, checkElem)
}
const action = elems => {
removeOverflow()
;[...elems].forEach(checkElem)
if (settings.restoreCont) findHidden()
domObserver = new MutationObserver(mutations => watchMutations(mutations, checkElem, body, action))
domObserver.observe(doc, { childList: true, subtree: true, attributes: true })
}
action(body.getElementsByTagName('*'))
}
const moderateMode = () => {
const doc = document.documentElement
const body = document.body
const memoize = new WeakMap()
const settings = getSettings()
const wArea = parseFloat(window.innerHeight * window.innerWidth)
const checkElem = element => {
if (!isDecentElem(element)) return
const pos = getStyle(element, 'position')
if (pos === 'fixed' || pos === 'sticky') {
if (checkToConvertToStatic(element)) return
if (element.getAttribute('data-popupoff') === 'notification') return
const memoized = memoize.has(element)
const { shouldRemove } = memoized
? { shouldRemove: memoize.get(element) }
: positionCheck(element, wArea)
if (shouldRemove) {
if (getStyle(element, 'display') !== 'none') element.setAttribute('data-popupoff', 'bl')
setPropImp(element, 'display', 'none')
}
if (!memoized) memoize.set(element, shouldRemove)
}
additionalChecks(element, checkElem)
}
const action = elems => {
removeOverflow()
;[...elems].forEach(checkElem)
if (settings.restoreCont) findHidden()
domObserver = new MutationObserver(mutations => watchMutations(mutations, checkElem, body, action))
domObserver.observe(doc, { childList: true, subtree: true, attributes: true })
}
action(body.getElementsByTagName('*'))
}
const delicateMode = () => {
const doc = document.documentElement
const body = document.body
const settings = getSettings()
const subMode = settings.staticSubMode || 'relative'
const checkElem = element => {
if (!isDecentElem(element)) return
const pos = getStyle(element, 'position')
if (pos === 'fixed' || pos === 'sticky') {
if (element.getAttribute('data-popupoff') === 'notification') return
if (getStyle(element, 'display') !== 'none') element.setAttribute('data-popupoff', 'st')
setPropImp(element, 'position', subMode)
}
}
const action = elems => {
removeOverflow()
;[...elems].forEach(checkElem)
if (settings.restoreCont) findHidden()
domObserver = new MutationObserver(mutations => watchMutations(mutations, checkElem, body, action))
domObserver.observe(doc, { childList: true, subtree: true, attributes: true })
}
action(body.getElementsByTagName('*'))
}
const restoreFixedElems = () => {
document.querySelectorAll('[data-popupoff]').forEach(el => {
const attr = el.getAttribute('data-popupoff')
if (attr === 'notification') return
if (attr === 'bl') el.style.display = null
else if (attr === 'st') el.style.setProperty('position', 'absolute')
el.removeAttribute('data-popupoff')
})
}
const getSettings = () => {
try { return JSON.parse(GM_getValue('settings', '{}')) || { ...defaultSettings } }
catch (e) { return { ...defaultSettings } }
}
const saveSettings = s => GM_setValue('settings', JSON.stringify(s))
const modes = {
aggressive: aggressiveMode,
moderate: moderateMode,
delicate: delicateMode,
whitelist: () => { },
}
const startMode = modeName => {
disconnectObserver()
restoreFixedElems()
if (modes[modeName]) modes[modeName]()
}
const modeNames = {
aggressive: { en: 'Aggressive', pt: 'Agressivo' },
moderate: { en: 'Moderate', pt: 'Moderado' },
delicate: { en: 'Delicate', pt: 'Delicado' },
whitelist: { en: 'OFF', pt: 'OFF' },
}
const modesList = ['moderate', 'aggressive', 'delicate', 'whitelist']
let widget = null
const updateWidget = mode => {
if (widget) widget.textContent = modeNames[mode]?.pt || mode
}
const createWidget = curMode => {
const existing = document.getElementById('popupoff-widget')
if (existing) existing.remove()
widget = document.createElement('div')
widget.id = 'popupoff-widget'
widget.setAttribute('data-popupoff', 'notification')
widget.textContent = modeNames[curMode]?.pt || curMode
widget.style.cssText =
'position:fixed;bottom:20px;right:20px;z-index:999999;' +
'background:#1a1a2e;color:#fff;padding:8px 16px;border-radius:20px;' +
'font:13px/1.4 sans-serif;cursor:pointer;box-shadow:0 2px 12px rgba(0,0,0,.35);' +
'user-select:none'
widget.addEventListener('click', cycleMode)
document.body.appendChild(widget)
}
const cycleMode = () => {
const settings = getSettings()
const idx = modesList.indexOf(settings.mode)
const nextMode = modesList[(idx + 1) % modesList.length]
settings.mode = nextMode
saveSettings(settings)
startMode(nextMode)
updateWidget(nextMode)
const n = document.createElement('div')
n.textContent = 'PopUpOFF: ' + (modeNames[nextMode]?.pt || nextMode)
n.style.cssText =
'position:fixed;top:20px;left:50%;transform:translateX(-50%);' +
'background:#1a1a2e;color:#fff;padding:10px 24px;border-radius:20px;' +
'z-index:999999;font:14px sans-serif;box-shadow:0 2px 12px rgba(0,0,0,.3);' +
'animation:popupoff-fade 2.5s forwards'
document.body.appendChild(n)
setTimeout(() => n.remove(), 2500)
}
const styleSheet = document.createElement('style')
styleSheet.textContent =
'@keyframes popupoff-fade{0%{opacity:1;transform:translateX(-50%) translateY(0)}' +
'70%{opacity:1}100%{opacity:0;transform:translateX(-50%) translateY(-20px)}}'
document.head.appendChild(styleSheet)
const init = () => {
const url = getPureURL()
if (whitelist.includes(url)) return
const settings = getSettings()
startMode(settings.mode)
createWidget(settings.mode)
}
document.addEventListener('keydown', e => {
if (e.altKey && e.key.toLowerCase() === 'x') {
e.preventDefault()
cycleMode()
}
})
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init)
else init()
})()