This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/549920/1664856/Script%20Notifier.js
// ==UserScript==
// @name Script Notifier
// @namespace http://github.com/0H4S
// @version 1.1
// @author OHAS
// @description Sistema de notificações para UserScripts.
// @license Copyright © 2025 OHAS. All Rights Reserved.
// ==/UserScript==
/*
Copyright Notice & Terms of Use
Copyright © 2025 OHAS. All Rights Reserved.
This software is the exclusive property of OHAS and is licensed for personal, non-commercial use only.
You may:
- Install, use, and inspect the code for learning or personal purposes.
You may NOT (without prior written permission from the author):
- Copy, redistribute, or republish this software.
- Modify, sell, or use it commercially.
- Create derivative works.
For questions, permission requests, or alternative licensing, please contact via
- GitHub: https://github.com/0H4S
- Greasy Fork: https://greasyfork.org/users/1464180-ohas
This software is provided "as is", without warranty of any kind. The author is not liable for any damages arising from its use.
*/
class ScriptNotifier {
constructor({ notificationsUrl, scriptVersion, currentLang }) {
this.NOTIFICATIONS_URL = notificationsUrl;
this.SCRIPT_VERSION = scriptVersion;
this.currentLang = this._initializeLanguage(currentLang);
this.STAGGER_DELAY = 70;
this.DISMISSED_NOTIFICATIONS_KEY = 'DismissedNotifications';
this.NOTIFICATIONS_ENABLED_KEY = 'NotificationsEnabled';
this.hostElement = null;
this.shadowRoot = null;
this.activeNotifications = [];
this.uiStrings = this._getUIStrings();
this.icons = this._getIcons();
this.scriptPolicy = this._createPolicy();
if (!document.getElementById('script-notifier-host')) {
this._createHostAndInjectStyles();
}
this.shadowRoot = document.getElementById('script-notifier-host').shadowRoot;
}
_initializeLanguage(forcedLang) {
const supportedLanguages = ['pt-BR', 'en', 'es-419', 'zh-CN'];
let lang = forcedLang || navigator.language || 'en';
if (lang.startsWith('pt')) lang = 'pt-BR';
else if (lang.startsWith('es')) lang = 'es-419';
else if (lang.startsWith('zh')) lang = 'zh-CN';
else if (lang.startsWith('en')) lang = 'en';
if (!supportedLanguages.includes(lang)) {
return 'en';
}
return lang;
}
_getUIStrings() {
return {
showAllNotificationsCmd: {
'pt-BR': '🔔 Notificações',
'en': '🔔 Notifications',
'es-419': '🔔 Notificaciones',
'zh-CN': '🔔 通知' },
disableNotificationsCmd: {
'pt-BR': '❌ Desativar Notificações',
'en': '❌ Disable Notifications',
'es-419': '❌ Desactivar Notificaciones',
'zh-CN': '❌ 禁用通知' },
enableNotificationsCmd: {
'pt-BR': '✅ Ativar Notificações',
'en': '✅ Enable Notifications',
'es-419': '✅ Activar Notificaciones',
'zh-CN': '✅ 启用通知' },
closeButtonTitle: {
'pt-BR': 'Fechar',
'en': 'Close',
'es-419': 'Cerrar',
'zh-CN': '关闭' }
};
}
_getIcons() {
return {
success: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path></svg>`,
warning: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"></path></svg>`,
info: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"></path></svg>`
};
}
_createPolicy() {
return window.trustedTypes ? window.trustedTypes.createPolicy('script-notifier-policy', {
createHTML: (input) => input
}) : null;
}
async run() {
await this._registerUserCommands();
setTimeout(() => this.checkForNotifications(), 1500);
}
checkForNotifications(forceShow = false) {
if (!this.NOTIFICATIONS_URL || this.NOTIFICATIONS_URL.includes("SEU_USUARIO")) return;
GM_xmlhttpRequest({
method: 'GET',
url: `${this.NOTIFICATIONS_URL}?t=${new Date().getTime()}`,
onload: async (response) => {
if (response.status < 200 || response.status >= 300) return;
try {
const data = JSON.parse(response.responseText);
const notifications = data.notifications;
if (forceShow) {
this.activeNotifications.forEach(n => n.element.remove());
this.activeNotifications = [];
}
await this._cleanupDismissedNotifications(notifications);
const dismissed = await GM_getValue(this.DISMISSED_NOTIFICATIONS_KEY, []);
const notificationsToDisplay = notifications.filter(notification => {
if (this.activeNotifications.some(n => n.id === notification.id)) return false;
if (!forceShow && dismissed.includes(notification.id)) return false;
if (notification.expires && new Date(notification.expires) < new Date()) return false;
if (notification.targetVersion !== 'all' && notification.targetVersion !== this.SCRIPT_VERSION) return false;
if (notification.targetHostname && window.location.hostname !== notification.targetHostname) return false;
return true;
});
notificationsToDisplay.forEach((notification, index) => {
setTimeout(() => {
this.displayNotification(notification);
}, index * 200);
});
} catch (e) {
console.error('Script Notifier: Falha ao analisar as notificações.', e);
}
},
onerror: (error) => {
console.error('Script Notifier: Falha ao buscar as notificações.', error);
}
});
}
forceShowAllNotifications() {
this.checkForNotifications(true);
}
_createHostAndInjectStyles() {
this.hostElement = document.createElement('div');
this.hostElement.id = 'script-notifier-host';
document.body.appendChild(this.hostElement);
const shadow = this.hostElement.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = this._getNotifierStyles();
shadow.appendChild(style);
}
async displayNotification(notification) {
const notificationsEnabled = await GM_getValue(this.NOTIFICATIONS_ENABLED_KEY, true);
if (notification.priority !== 'high' && !notificationsEnabled) return;
const notificationId = `notification-${notification.id}`;
if (this.shadowRoot.getElementById(notificationId)) return;
const title = this._getTranslatedText(notification.title);
const message = this._getTranslatedText(notification.message);
if (!title && !message) return;
const container = document.createElement('div');
container.id = notificationId;
container.className = 'notification-container';
const notificationType = notification.type || 'info';
container.dataset.type = notificationType;
if (notification.customColor) {
container.style.borderLeftColor = notification.customColor;
container.style.setProperty('--type-color', notification.customColor);
}
let iconHTML = this.icons[notificationType] || this.icons['info'];
if (notification.customIconSvg) {
iconHTML = this._sanitizeAndStyleSvg(notification.customIconSvg);
}
const imageOrIconHTML = notification.imageUrl ?
`<img src="${notification.imageUrl}" class="notification-image" alt="Notification Image">` :
`<div class="notification-icon">${iconHTML}</div>`;
const closeIconSVG = `<svg viewBox="0 0 24 24" fill="currentColor" width="20" height="20"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></svg>`;
const notificationHTML = `
${imageOrIconHTML}
<div class="notification-content">
<h3 class="notification-title">${this._prepareMessageHTML(title)}</h3>
<div class="notification-message">${this._prepareMessageHTML(message)}</div>
</div>
<button class="dismiss-button" title="${this._getUIText('closeButtonTitle')}">${closeIconSVG}</button>
`;
this._setSafeInnerHTML(container, notificationHTML);
if (notification.buttons && notification.buttons.length > 0) {
const buttonsContainer = this._createButtons(notification.buttons, notification.id);
container.querySelector('.notification-content').appendChild(buttonsContainer);
}
this.shadowRoot.appendChild(container);
this.activeNotifications.push({ id: notification.id, element: container, isNew: true });
this._updateNotificationPositions();
container.querySelector('.dismiss-button').onclick = (e) => {
e.stopPropagation();
this._dismissNotification(notification.id);
};
}
_createButtons(buttonDataArray, notificationId) {
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'notification-buttons';
buttonDataArray.forEach((buttonData, index) => {
const button = document.createElement('button');
const buttonText = this._getTranslatedText(buttonData.text);
this._setSafeInnerHTML(button, this._prepareMessageHTML(buttonText));
button.className = 'notification-button';
if (buttonData.backgroundColor) {
button.style.backgroundColor = buttonData.backgroundColor;
button.classList.add('custom-bg');
} else if (index === 0) {
button.classList.add('primary');
}
if (buttonData.textColor) {
button.style.color = buttonData.textColor;
}
button.onclick = (e) => {
e.stopPropagation();
if (buttonData.action) {
switch (buttonData.action) {
case 'open_url':
window.location.href = buttonData.value;
break;
case 'open_url_new_tab':
window.open(buttonData.value, '_blank');
break;
}
}
this._dismissNotification(notificationId);
};
buttonsContainer.appendChild(button);
});
return buttonsContainer;
}
async _dismissNotification(notificationId) {
const notification = this.activeNotifications.find(n => n.id === notificationId);
if (!notification) return;
const dismissed = await GM_getValue(this.DISMISSED_NOTIFICATIONS_KEY, []);
if (!dismissed.includes(notificationId)) {
dismissed.push(notificationId);
await GM_setValue(this.DISMISSED_NOTIFICATIONS_KEY, dismissed);
}
notification.element.classList.remove('animate-in');
notification.element.classList.add('animate-out');
setTimeout(() => {
this.activeNotifications = this.activeNotifications.filter(n => n.id !== notificationId);
notification.element.remove();
this._updateNotificationPositions();
}, 600);
}
_updateNotificationPositions() {
const spacingValue = parseInt(getComputedStyle(this.shadowRoot.host).getPropertyValue('--sn-spacing')) || 20;
let currentTop = spacingValue;
this.activeNotifications.forEach((notif, index) => {
const { element } = notif;
element.style.top = `${currentTop}px`;
element.style.transitionDelay = `${index * this.STAGGER_DELAY}ms`;
if (notif.isNew) {
requestAnimationFrame(() => {
element.classList.add('animate-in');
});
delete notif.isNew;
}
currentTop += element.offsetHeight + (spacingValue / 2);
});
}
_getUIText(key) {
if (!this.uiStrings[key]) return '';
return this.uiStrings[key][this.currentLang] || this.uiStrings[key]['en'];
}
_setSafeInnerHTML(element, html) {
if (!element) return;
element.innerHTML = this.scriptPolicy ? this.scriptPolicy.createHTML(html) : html;
}
_getTranslatedText(translationObject) {
if (!translationObject) return '';
if (typeof translationObject === 'string') return translationObject;
return translationObject[this.currentLang] || translationObject[this.currentLang.split('-')[0]] || translationObject['en'] || '';
}
_prepareMessageHTML(text) {
return text || '';
}
_sanitizeAndStyleSvg(svgString) {
try {
const tempDiv = document.createElement('div');
this._setSafeInnerHTML(tempDiv, svgString);
const svgElement = tempDiv.querySelector('svg');
if (!svgElement) return '';
svgElement.setAttribute('fill', 'currentColor');
svgElement.removeAttribute('width');
svgElement.removeAttribute('height');
svgElement.removeAttribute('style');
svgElement.removeAttribute('class');
return svgElement.outerHTML;
} catch (e) {
return '';
}
}
async _cleanupDismissedNotifications(serverNotifications) {
const dismissed = await GM_getValue(this.DISMISSED_NOTIFICATIONS_KEY, []);
if (dismissed.length === 0) return;
const validServerIds = new Set(
serverNotifications
.filter(n => !n.expires || new Date(n.expires) >= new Date())
.map(n => n.id)
);
const cleanedDismissed = dismissed.filter(id => validServerIds.has(id));
if (cleanedDismissed.length < dismissed.length) {
await GM_setValue(this.DISMISSED_NOTIFICATIONS_KEY, cleanedDismissed);
}
}
async _registerUserCommands() {
GM_registerMenuCommand(this._getUIText('showAllNotificationsCmd'), () => this.forceShowAllNotifications());
const notificationsEnabled = await GM_getValue(this.NOTIFICATIONS_ENABLED_KEY, true);
const toggleCommandText = notificationsEnabled ?
this._getUIText('disableNotificationsCmd') :
this._getUIText('enableNotificationsCmd');
GM_registerMenuCommand(toggleCommandText, async () => {
const currentState = await GM_getValue(this.NOTIFICATIONS_ENABLED_KEY, true);
await GM_setValue(this.NOTIFICATIONS_ENABLED_KEY, !currentState);
window.location.reload();
});
}
_getNotifierStyles() {
return `
:host {
--sn-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--sn-color-background: #fff;
--sn-color-text-primary: #000;
--sn-color-text-secondary: #333;
--sn-color-border: #ddd;
--sn-color-link: currentColor;
--sn-color-link-underline: currentColor;
--sn-color-dismiss: #999;
--sn-color-dismiss-hover: #ff4d4d;
--sn-shadow-default: 0 8px 20px rgba(0,0,0,0.15);
--sn-card-background: rgba(0,0,0,0.05);
--sn-card-border: #ccc;
--sn-scrollbar-track: #f1f1f1;
--sn-scrollbar-thumb: #ccc;
--sn-scrollbar-thumb-hover: #aaa;
--sn-button-hover-bg: #555;
--sn-button-hover-text: #fff;
--sn-border-radius: 12px;
--sn-border-radius-small: 6px;
--sn-padding: 16px;
--sn-notification-width: 380px;
--sn-spacing: 20px;
--sn-icon-size: 24px;
--sn-image-size: 48px;
--sn-font-size-title: 16px;
--sn-font-size-body: 14px;
--sn-font-weight-title: 600;
--sn-message-max-height: 110px;
--sn-animation-duration-fast: 0.2s;
--sn-animation-duration-medium: 0.4s;
--sn-animation-duration-slow: 0.8s;
}
@media (prefers-color-scheme: dark) {
:host {
--sn-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--sn-color-background: #333;
--sn-color-text-primary: #fff;
--sn-color-text-secondary: #ddd;
--sn-color-border: #444;
--sn-color-link: currentColor;
--sn-color-link-underline: currentColor;
--sn-color-dismiss: #aaa;
--sn-color-dismiss-hover: #ff4d4d;
--sn-shadow-default: 0 8px 20px rgba(0,0,0,0.5);
--sn-card-background: rgba(0,0,0,0.1);
--sn-card-border: #555;
--sn-scrollbar-track: #444;
--sn-scrollbar-thumb: #666;
--sn-scrollbar-thumb-hover: #888;
--sn-button-hover-bg: #777;
--sn-button-hover-text: #fff;
--sn-border-radius: 12px;
--sn-border-radius-small: 6px;
--sn-padding: 16px;
--sn-notification-width: 380px;
--sn-spacing: 20px;
--sn-icon-size: 24px;
--sn-image-size: 48px;
--sn-font-size-title: 16px;
--sn-font-size-body: 14px;
--sn-font-weight-title: 600;
--sn-message-max-height: 110px;
--sn-animation-duration-fast: 0.2s;
--sn-animation-duration-medium: 0.4s;
--sn-animation-duration-slow: 0.8s;
}
}
.notification-container {
position: fixed;
top: 0;
right: var(--sn-spacing);
z-index: 2147483647;
width: var(--sn-notification-width);
font-family: var(--sn-font-family);
background-color: var(--sn-color-background);
color: var(--sn-color-text-secondary);
border-radius: var(--sn-border-radius);
box-shadow: var(--sn-shadow-default);
border: 1px solid var(--sn-color-border);
display: flex;
padding: var(--sn-padding);
box-sizing: border-box;
border-left: 5px solid transparent;
opacity: 0;
transform: translateX(120%);
will-change: transform, opacity, top;
}
.notification-container.animate-in {
opacity: 1;
transform: translateX(0);
transition: transform var(--sn-animation-duration-slow) cubic-bezier(0.22, 1.6, 0.5, 1),
opacity var(--sn-animation-duration-medium) ease-out,
top var(--sn-animation-duration-slow) cubic-bezier(0.22, 1.6, 0.5, 1);
}
.notification-container.animate-out {
opacity: 0;
transform: translateX(120%);
transition: transform var(--sn-animation-duration-medium) cubic-bezier(0.6, -0.28, 0.735, 0.045),
opacity var(--sn-animation-duration-medium) ease-out,
top var(--sn-animation-duration-medium) ease-out;
}
.notification-container[data-type="success"] { --type-color: #22c55e; }
.notification-container[data-type="warning"] { --type-color: #f97316; }
.notification-container[data-type="info"] { --type-color: #3b82f6; }
.notification-container[data-type] { border-left-color: var(--type-color); }
.notification-icon {
width: var(--sn-icon-size);
height: var(--sn-icon-size);
margin-right: 12px;
flex-shrink: 0;
color: var(--type-color);
}
.notification-image {
width: var(--sn-image-size);
height: var(--sn-image-size);
border-radius: var(--sn-border-radius-small);
object-fit: cover;
flex-shrink: 0;
margin-right: 15px;
}
.notification-content {
flex-grow: 1;
word-break: break-word;
}
.notification-title {
margin: 0 0 8px;
font-size: var(--sn-font-size-title);
font-weight: var(--sn-font-weight-title);
color: var(--sn-color-text-primary);
}
.notification-message {
font-size: var(--sn-font-size-body);
line-height: 1.5;
max-height: var(--sn-message-max-height);
overflow-y: auto;
padding-right: 8px;
}
.notification-message ul,
.notification-message ol {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
.notification-message blockquote {
margin: 0.5em 0;
padding: 0.5em 1em;
border-radius: var(--sn-border-radius-small);
background-color: var(--sn-card-background);
border-left: 4px solid var(--sn-card-border);
}
.notification-message a,
.notification-title a {
color: var(--sn-color-link);
text-decoration: none;
}
.notification-message a:hover,
.notification-title a:hover {
text-decoration: underline;
text-decoration-color: var(--sn-color-link-underline);
}
.dismiss-button {
background: none;
border: none;
color: var(--sn-color-dismiss);
cursor: pointer;
padding: 0;
margin-left: 10px;
align-self: flex-start;
transition: color var(--sn-animation-duration-fast) ease,
transform var(--sn-animation-duration-medium) cubic-bezier(0.25, 0.1, 0.25, 1.5);
width: var(--sn-icon-size);
height: var(--sn-icon-size);
display: inline-flex;
align-items: center;
justify-content: center;
}
.dismiss-button:hover {
color: var(--sn-color-dismiss-hover);
transform: rotate(90deg);
}
.dismiss-button:active {
transform: rotate(90deg) scale(0.9);
}
.notification-buttons {
margin-top: 12px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.notification-button {
background-color: var(--sn-color-border);
color: var(--sn-color-text-secondary);
border: none;
border-radius: var(--sn-border-radius-small);
padding: 6px 12px;
font-size: var(--sn-font-size-body);
font-weight: 500;
cursor: pointer;
transition: background-color var(--sn-animation-duration-fast) ease,
transform var(--sn-animation-duration-fast) ease,
filter var(--sn-animation-duration-fast) ease,
color var(--sn-animation-duration-fast) ease;
}
.notification-button:hover {
background-color: var(--sn-button-hover-bg);
color: var(--sn-button-hover-text);
transform: translateY(-2px);
}
.notification-button:active {
transform: translateY(-1px);
}
.notification-button.primary {
background-color: var(--sn-color-link);
color: #fff;
}
.notification-button.primary:hover {
background-color: var(--sn-color-link);
color: #fff;
filter: brightness(1.1);
}
.notification-button.custom-bg:hover {
filter: brightness(1.15);
}
.notification-message::-webkit-scrollbar { width: 6px; }
.notification-message::-webkit-scrollbar-track { background: var(--sn-scrollbar-track); border-radius: 3px; }
.notification-message::-webkit-scrollbar-thumb { background: var(--sn-scrollbar-thumb); border-radius: 3px; }
.notification-message::-webkit-scrollbar-thumb:hover { background: var(--sn-scrollbar-thumb-hover); }
`;
}
}