Script Notifier

Sistema de notificações para UserScripts

Ovu skriptu ne treba izravno instalirati. To je biblioteka za druge skripte koje se uključuju u meta direktivu // @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); }
        `;
    }
}