您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Sistema de notificações para UserScripts
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/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: http://greasyfork.icu/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(); }); } }