لا ينبغي أن لا يتم تثبيت هذا السكريت مباشرة. هو مكتبة لسكبتات لتشمل مع التوجيه الفوقية // @require https://update.greasyfork.org/scripts/549920/1662869/Script%20Notifier.js
// ==UserScript==
// @name Script Notifier
// @namespace http://github.com/0H4S
// @version 1.0
// @author OHAS
// @description Sistema de notificações para UserScripts
// @license Copyright © 2025 OHAS. All Rights Reserved.
// ==/UserScript==
/*
ScriptNotifier - Version: 1.0
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.uiStrings = {
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': '关闭'
}
};
this.currentLang = this._initializeLanguage(currentLang);
this.DISMISSED_NOTIFICATIONS_KEY = 'DismissedNotifications';
this.NOTIFICATIONS_ENABLED_KEY = 'NotificationsEnabled';
this.hostElement = null;
this.shadowRoot = null;
this.icons = {
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>`
};
this.activeNotifications = [];
this.baseSpacing = 20;
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;
}
_getUIText(key) {
if (!this.uiStrings[key]) return '';
return this.uiStrings[key][this.currentLang] || this.uiStrings[key]['en'];
}
async run() {
await this._registerUserCommands();
setTimeout(() => this.checkForNotifications(), 1500);
}
checkForNotifications() {
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) {
try {
const data = JSON.parse(response.responseText);
const notifications = data.notifications;
await this._cleanupDismissedNotifications(notifications);
const dismissed = await GM_getValue(this.DISMISSED_NOTIFICATIONS_KEY, []);
for (const notification of notifications) {
if (dismissed.includes(notification.id)) continue;
if (notification.expires && new Date(notification.expires) < new Date()) continue;
if (notification.targetVersion !== 'all' && notification.targetVersion !== this.SCRIPT_VERSION) continue;
if (notification.targetHostname && window.location.hostname !== notification.targetHostname) continue;
this.displayNotification(notification);
}
} catch (e) {}
}
},
onerror: () => {}
});
}
forceShowAllNotifications() {
if (!this.NOTIFICATIONS_URL || this.NOTIFICATIONS_URL.includes("SEU_USUARIO")) return;
GM_xmlhttpRequest({
method: 'GET', url: `${this.NOTIFICATIONS_URL}?t=${new Date().getTime()}`,
onload: (response) => {
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
const notifications = data.notifications;
for (const notification of notifications) {
if (notification.expires && new Date(notification.expires) < new Date()) continue;
if (notification.targetVersion !== 'all' && notification.targetVersion !== this.SCRIPT_VERSION) continue;
if (notification.targetHostname && window.location.hostname !== notification.targetHostname) continue;
this.displayNotification(notification);
}
} catch (e) {}
}
},
onerror: () => {}
});
}
_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 = `
:host {
--bg-color: #333;
--text-color: #ddd;
--title-color: #fff;
--link-color: #22c55e;
--shadow: 0 8px 20px rgba(0,0,0,0.5);
--border-color: #444;
--scrollbar-track-color: #444;
--scrollbar-thumb-color: #666;
--scrollbar-thumb-hover-color: #888;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
@media (prefers-color-scheme: light) {
:host {
--bg-color: #fff;
--text-color: #333;
--title-color: #000;
--shadow: 0 8px 20px rgba(0,0,0,0.15);
--border-color: #ddd;
--scrollbar-track-color: #f1f1f1;
--scrollbar-thumb-color: #ccc;
--scrollbar-thumb-hover-color: #aaa;
}
}
.notification-container {
position: fixed;
top: ${this.baseSpacing}px;
right: 20px;
z-index: 2147483647;
width: 380px;
background-color: var(--bg-color);
color: var(--text-color);
border-radius: 12px;
box-shadow: var(--shadow);
border: 1px solid var(--border-color);
font-family: var(--font-family);
transform: translateX(120%);
opacity: 0;
transition: transform 0.4s ease-out, opacity 0.4s ease-out;
overflow: hidden;
display: flex;
align-items: flex-start;
padding: 16px;
box-sizing: border-box;
border-left: 5px solid transparent;
}
.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: 24px;
height: 24px;
margin-right: 12px;
flex-shrink: 0;
color: var(--type-color);
}
.notification-image {
width: 48px;
height: 48px;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
margin-right: 15px;
}
.notification-content {
flex-grow: 1;
}
.notification-title {
margin: 0 0 8px;
font-size: 16px;
font-weight: 600;
color: var(--title-color);
}
.notification-message {
font-size: 14px;
line-height: 1.5;
max-height: 110px;
overflow-y: auto;
padding-right: 8px;
}
.dismiss-button {
background: none;
border: none;
color: #999;
font-size: 20px;
cursor: pointer;
padding: 0;
margin-left: 10px;
line-height: 1;
align-self: flex-start;
transition: color 0.2s ease, transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1.5);
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
transform-origin: center;
}
.dismiss-button:hover {
color: #ff4d4d;
transform: rotate(90deg);
}
.dismiss-button:active {
transform: rotate(90deg) scale(0.9);
}
.notification-buttons {
margin-top: 12px;
display: flex;
gap: 8px;
}
.notification-button {
background-color: var(--border-color);
color: var(--text-color);
border: none;
border-radius: 6px;
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.notification-button:hover {
background-color: #555;
color: #fff;
}
.notification-button.primary {
background-color: var(--link-color);
color: #fff;
}
.notification-button.primary:hover {
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(--scrollbar-track-color);
border-radius: 3px;
}
.notification-message::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-color);
border-radius: 3px;
}
.notification-message::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover-color);
}
.notification-message a {
color: var(--link-color);
text-decoration: none;
}
.notification-message a:hover {
text-decoration: underline;
}
`;
shadow.appendChild(style);
}
async displayNotification(notification) {
const notificationsEnabled = await GM_getValue(this.NOTIFICATIONS_ENABLED_KEY, true);
if (notification.priority !== 'high' && !notificationsEnabled) return;
const title = this._getTranslatedText(notification.title);
const message = this._getTranslatedText(notification.message);
if (!title || !message) return;
const notificationId = `notification-${notification.id}`;
if (this.shadowRoot.getElementById(notificationId)) return;
const container = document.createElement('div');
container.id = notificationId;
container.className = 'notification-container';
const notificationType = notification.type || 'info';
container.dataset.type = notificationType;
const iconSVG = this.icons[notificationType] || this.icons['info'];
const imageOrIconHTML = notification.imageUrl
? `<img src="${notification.imageUrl}" class="notification-image">`
: `<div class="notification-icon">${iconSVG}</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">${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 = document.createElement('div');
buttonsContainer.className = 'notification-buttons';
notification.buttons.forEach((buttonData, index) => {
const button = document.createElement('button');
button.textContent = this._getTranslatedText(buttonData.text);
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 = () => {
switch (buttonData.action) {
case 'open_url': window.location.href = buttonData.value; break;
case 'open_url_new_tab': window.open(buttonData.value, '_blank'); break;
}
container.querySelector('.dismiss-button').click();
};
buttonsContainer.appendChild(button);
});
container.querySelector('.notification-content').appendChild(buttonsContainer);
}
this.shadowRoot.appendChild(container);
this.activeNotifications.unshift({ id: notification.id, element: container });
this._updateNotificationPositions(true);
container.querySelector('.dismiss-button').onclick = async () => {
const dismissed = await GM_getValue(this.DISMISSED_NOTIFICATIONS_KEY, []);
if (!dismissed.includes(notification.id)) dismissed.push(notification.id);
await GM_setValue(this.DISMISSED_NOTIFICATIONS_KEY, dismissed);
container.style.transform = `translateX(120%)`;
container.style.opacity = '0';
setTimeout(() => {
this.activeNotifications = this.activeNotifications.filter(n => n.id !== notification.id);
container.remove();
this._updateNotificationPositions(false);
}, 400);
};
}
_updateNotificationPositions() {
let currentTop = this.baseSpacing;
this.activeNotifications.forEach(notif => {
const { element } = notif;
element.style.transform = `translateY(${currentTop - this.baseSpacing}px)`;
element.style.opacity = '1';
currentTop += element.offsetHeight + (this.baseSpacing / 2);
});
}
_createPolicy() {
return window.trustedTypes
? window.trustedTypes.createPolicy('script-notifier-policy', { createHTML: (input) => input }) : null;
}
_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 || '';
}
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();
});
}
}