Script Notifier

Sistema de notificações para UserScripts.

Tính đến 22-09-2025. Xem phiên bản mới nhất.

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyforks.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://greasyforks.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); }
        `;
    }
}
长期地址
遇到问题?请前往 GitHub 提 Issues。