Script Notifier

Sistema de notificações para UserScripts

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @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();
    });
  }
}