您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds 🌀 while generating and ✅ when done, plus desktop notifications.
// ==UserScript== // @name ChatGPT Desktop Notifier // @name:ja ChatGPT デスクトップ通知 & タイトルバッジ // @namespace https://github.com/usou/ // @version 1.0.0 // @description Adds 🌀 while generating and ✅ when done, plus desktop notifications. // @description:ja 生成中は🌀・完了で✅、完了時にデスクトップ通知を行います。 // @author usou // @license MIT // @match *://chat.openai.com/* // @match *://chatgpt.com/* // @grant GM_notification // @run-at document-end // @noframes // ==/UserScript== (() => { 'use strict'; /* ========= 定数 ========= */ const PREFIX = { GEN: '🌀', DONE: '✅' }; const STREAMING_SELECTORS = [ '.result-streaming', 'button[data-testid="stop-button"]', 'svg.animate-spin' ]; /* ========= ヘルパ ========= */ const stripBadges = title => title.replace(/^(?:🌀|✅)\s*/, ''); const clearPrefix = () => { document.title = stripBadges(document.title); }; const isGenerating = () => STREAMING_SELECTORS.some(sel => document.querySelector(sel)); const notifyDone = () => { GM_notification({ title: `「${stripBadges(document.title)}」${PREFIX.DONE}`, text: ' ', // 空文字では通知が来ない timeout: 5000, onclick: () => window.focus() }); }; /* ========= Enter 送信時にバッジ消去 ========= */ document.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey && e.target?.tagName === 'TEXTAREA') { clearPrefix(); } }, true); /* ========= 状態管理 ========= */ let generating = isGenerating(); let lastBadge = ''; // '' | PREFIX.GEN | PREFIX.DONE let updatingTitle = false; // 再入防止 const refreshBadge = () => { if (!lastBadge || updatingTitle) return; const clean = stripBadges(document.title); const desired = lastBadge + clean; if (document.title === desired) return; // 既に付与済み updatingTitle = true; document.title = desired; updatingTitle = false; }; /* ========= DOM 監視(生成開始 / 終了) ========= */ const domObserver = new MutationObserver(() => { const nowGenerating = isGenerating(); if (nowGenerating && !generating) { lastBadge = PREFIX.GEN; // 生成開始 refreshBadge(); } else if (!nowGenerating && generating) { lastBadge = PREFIX.DONE; // 生成終了 refreshBadge(); notifyDone(); } generating = nowGenerating; }); domObserver.observe(document.documentElement, { childList: true, subtree: true }); /* ========= タイトル監視(差し替え対策) ========= */ const watchTitle = titleNode => { if (!titleNode || titleNode.__badgeObserver) return; const obs = new MutationObserver(() => { if (!updatingTitle) refreshBadge(); }); obs.observe(titleNode, { childList: true, characterData: true, subtree: true }); titleNode.__badgeObserver = obs; refreshBadge(); // 監視開始時にも付与 }; watchTitle(document.querySelector('title')); const headObserver = new MutationObserver(records => { records.forEach(record => record.addedNodes.forEach(node => { if (node.nodeName === 'TITLE') watchTitle(node); })); }); headObserver.observe(document.head, { childList: true }); })();