配信者の名前をマウスホバーするだけで、自己紹介パネルとフォロワー数をプレビューします。配信を開く前に雰囲気を知ることができます。
// ==UserScript==
// @name Twitch Streamer Quick View
// @name:ja Twitch Streamer Quick View
// @namespace http://tampermonkey.net/
// @version 17.0
// @description Preview streamer's "About" panels and follower count by hovering over their name. Avoid clickbait and find the right vibe instantly.
// @description:ja 配信者の名前をマウスホバーするだけで、自己紹介パネルとフォロワー数をプレビューします。配信を開く前に雰囲気を知ることができます。
// @author anon00 & Gemini
// @match https://www.twitch.tv/*
// @grant GM_xmlhttpRequest
// @connect gql.twitch.tv
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const cache = new Map();
let hoverTimer;
const msg = {
loading: "Loading...",
noDesc: "No description available.",
error: "Error occurred!"
};
const style = document.createElement('style');
style.innerHTML = `
#tm-panel-popup {
position: fixed; z-index: 2147483647; background: #18181b; color: #efeff1;
border: 1px solid #464649; border-radius: 8px; padding: 12px;
width: 300px; max-height: 400px; overflow-y: auto;
box-shadow: 0 10px 30px rgba(0,0,0,0.9); font-size: 13px; line-height: 1.5;
display: none; pointer-events: none;
}
.tm-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #35353b; margin: 0 0 8px 0; padding: 0 0 6px 0; }
.tm-title { font-weight: bold; color: #bf94ff; font-size: 14px; margin: 0; padding: 0; line-height: 1; }
.tm-followers { color: #adadb8; font-size: 11px; font-weight: normal; margin: 0; padding: 0; line-height: 1; }
.tm-content { margin: 0; padding: 0; white-space: pre-wrap; word-break: break-word; }
`;
document.head.appendChild(style);
const popup = document.createElement('div');
popup.id = 'tm-panel-popup';
document.body.appendChild(popup);
document.addEventListener('mouseover', (e) => {
const link = e.target.closest('a');
if (!link || link.querySelector('img')) return;
const href = link.getAttribute('href');
if (!href || !href.startsWith('/') || href.includes('?') || href.split('/').filter(p=>p).length !== 1) return;
const username = href.split('/')[1];
const ignored = ['directory', 'videos', 'u', 'settings', 'p', 'search', 'friends', 'moderator', 'common', 'legal', 'about'];
if (username && !ignored.includes(username)) {
clearTimeout(hoverTimer);
hoverTimer = setTimeout(() => {
const rect = link.getBoundingClientRect();
showPopup(username, rect);
fetchUserData(username);
}, 600);
}
}, true);
document.addEventListener('mouseout', (e) => {
if (!e.target.closest('a')) {
clearTimeout(hoverTimer);
popup.style.display = 'none';
}
});
function showPopup(username, rect) {
const data = cache.get(username);
if (data) {
updateContent(username, data.description, data.followers);
} else {
popup.innerHTML = `<div class="tm-header"><span class="tm-title">@${username}</span></div><div class="tm-content">${msg.loading}</div>`;
}
popup.style.display = 'block';
updatePosition(rect);
}
function updatePosition(rect) {
const margin = 10;
const popupHeight = popup.offsetHeight;
const windowHeight = window.innerHeight;
// 横位置の設定(画面外にはみ出さないように調整)
let left = rect.left;
if (left + 320 > window.innerWidth) {
left = window.innerWidth - 320;
}
popup.style.left = `${left}px`;
// 判定:下側に十分なスペースがあるか?(リンクの下端 + ポップアップの高さ + 余裕)
const spaceBelow = windowHeight - rect.bottom;
if (spaceBelow > popupHeight + margin + 20) {
// 十分なスペースがあるなら「下」に表示
popup.style.top = `${rect.bottom + window.scrollY + margin}px`;
} else {
// スペースがないなら「上」に表示
popup.style.top = `${rect.top + window.scrollY - popupHeight - margin}px`;
}
}
function updateContent(username, desc, followers) {
const followerText = followers !== null ? `${followers.toLocaleString()} followers` : "";
const cleanDesc = desc ? desc.trim() : msg.noDesc;
popup.innerHTML = `<div class="tm-header"><span class="tm-title">@${username}</span><span class="tm-followers">${followerText}</span></div><div class="tm-content">${cleanDesc}</div>`;
// コンテンツ更新後に高さが変わるため、位置を再計算するにゃ
const activeLink = document.querySelector(`a[href="/${username}"]`);
if (activeLink && popup.style.display === 'block') {
updatePosition(activeLink.getBoundingClientRect());
}
}
function fetchUserData(username) {
if (cache.has(username)) return;
const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
const query = JSON.stringify({
query: `query($login: String!) { user(login: $login) { description followers { totalCount } } }`,
variables: { login: username.toLowerCase() }
});
GM_xmlhttpRequest({
method: "POST",
url: "https://gql.twitch.tv/gql",
headers: { "Client-ID": CLIENT_ID, "Content-Type": "application/json" },
data: query,
onload: function(response) {
try {
const res = JSON.parse(response.responseText);
const user = res.data.user;
cache.set(username, {
description: user?.description || "",
followers: user?.followers?.totalCount ?? null
});
if (popup.style.display === 'block') {
const data = cache.get(username);
updateContent(username, data.description, data.followers);
}
} catch (e) {}
}
});
}
})();