Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save kennak0/9f454622f52880f04bf845701bcb102b to your computer and use it in GitHub Desktop.

Select an option

Save kennak0/9f454622f52880f04bf845701bcb102b to your computer and use it in GitHub Desktop.
Feedlyの記事一覧にはてなブックマーク数とHacker Newsスコアを表示(最適化版)
// ==UserScript==
// @name Feedly Hatena & Hacker News Score Display
// @namespace https://example.com/
// @version 2.1
// @description Feedlyの記事一覧にはてなブックマーク数とHacker Newsスコアを表示(最適化版)
// @author YourName
// @match https://feedly.com/*
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @connect b.hatena.ne.jp
// @connect hn.algolia.com
// ==/UserScript==
(function() {
'use strict';
// API endpoints
const HATENA_API_BASE = 'https://b.hatena.ne.jp/entry/jsonlite/?url=';
const HATENA_BOOKMARK_URL = 'https://b.hatena.ne.jp/entry/';
const HN_SEARCH_API = 'https://hn.algolia.com/api/v1/search?query=';
const processedUrls = new Map();
// グローバルスタイルを追加
const style = document.createElement('style');
style.textContent = `
.social-scores {
display: inline-flex !important;
gap: 4px !important;
margin-right: 8px !important;
vertical-align: middle !important;
position: relative !important;
z-index: 9999 !important;
opacity: 1 !important;
visibility: visible !important;
}
.score-badge {
display: inline-flex !important;
align-items: center !important;
padding: 3px 8px !important;
border-radius: 12px !important;
font-size: 12px !important;
line-height: 16px !important;
font-weight: bold !important;
font-family: Arial, sans-serif !important;
text-decoration: none !important;
cursor: pointer !important;
transition: transform 0.1s ease !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;
position: relative !important;
z-index: 9999 !important;
opacity: 1 !important;
visibility: visible !important;
min-height: 20px !important;
white-space: nowrap !important;
overflow: visible !important;
}
.score-badge:hover {
transform: scale(1.05) !important;
}
.hatena-score {
background-color: #00a0de !important;
color: white !important;
}
.hatena-score:hover {
background-color: #0080be !important;
}
.hn-score {
background-color: #ff6600 !important;
color: white !important;
}
.hn-score:hover {
background-color: #e55500 !important;
}
.score-icon {
margin-right: 3px !important;
font-size: 11px !important;
font-weight: bold !important;
}
/* Feedlyの特定のCSSを上書き */
.fx.fx > .social-scores,
.EntryMetadata .social-scores,
.EntryTitle .social-scores {
display: inline-flex !important;
opacity: 1 !important;
visibility: visible !important;
}
`;
document.head.appendChild(style);
// function debugLog(...args) {
// console.log('[FeedlyScores]', ...args);
// }
// XMLHttpRequest wrapper for compatibility
const xmlHttpRequest = (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : GM_xmlhttpRequest;
function getArticleLinks() {
const selectorCandidates = [
'a.EntryTitleLink',
'a.entryTitle',
'a.title',
'h2 a[href]',
'h3 a[href]',
'article a[href]',
'a[data-testid="entry-title"]',
'div.EntryContent a[href]'
];
// debugLog('Searching for article links...');
for (const selector of selectorCandidates) {
const elements = document.querySelectorAll(selector);
// debugLog(`Selector "${selector}" found ${elements.length} elements`);
const links = Array.from(elements)
.filter(a => a.href && a.href.startsWith('http'));
if (links.length > 0) {
// debugLog(`Found ${links.length} valid links with selector "${selector}"`);
// debugLog('First link example:', links[0].href);
return links;
}
}
// デバッグ用:すべてのリンクを探索
// const allLinks = document.querySelectorAll('a[href]');
// debugLog(`Total links on page: ${allLinks.length}`);
// const httpLinks = Array.from(allLinks).filter(a => a.href.startsWith('http'));
// debugLog(`HTTP links: ${httpLinks.length}`);
// if (httpLinks.length > 0) {
// debugLog('Sample links:', httpLinks.slice(0, 3).map(a => ({
// href: a.href,
// className: a.className,
// textContent: a.textContent.substring(0, 50)
// })));
// }
// debugLog('No article links found');
return [];
}
function fetchHatenaCount(url) {
return new Promise((resolve) => {
const apiUrl = HATENA_API_BASE + encodeURIComponent(url);
xmlHttpRequest({
method: 'GET',
url: apiUrl,
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
const result = {
count: data.count || 0,
url: HATENA_BOOKMARK_URL + encodeURIComponent(url)
};
resolve(result);
} catch (e) {
resolve({ count: 0, url: HATENA_BOOKMARK_URL + encodeURIComponent(url) });
}
} else {
resolve({ count: 0, url: HATENA_BOOKMARK_URL + encodeURIComponent(url) });
}
},
onerror: function() {
resolve({ count: 0, url: HATENA_BOOKMARK_URL + encodeURIComponent(url) });
}
});
});
}
function fetchHackerNewsScore(url) {
return new Promise((resolve) => {
// URLをクエリとして検索
const searchUrl = HN_SEARCH_API + encodeURIComponent(url);
xmlHttpRequest({
method: 'GET',
url: searchUrl,
onload: function(response) {
if (response.status === 200) {
try {
const data = JSON.parse(response.responseText);
// 最も関連性の高い結果を取得
if (data.hits && data.hits.length > 0) {
const hit = data.hits[0];
resolve({
score: hit.points || 0,
comments: hit.num_comments || 0,
url: `https://news.ycombinator.com/item?id=${hit.objectID}`
});
} else {
resolve({ score: 0, comments: 0, url: null });
}
} catch (e) {
resolve({ score: 0, comments: 0, url: null });
}
} else {
resolve({ score: 0, comments: 0, url: null });
}
},
onerror: function() {
resolve({ score: 0, comments: 0, url: null });
}
});
});
}
async function fetchAndDisplayScores(linkElement) {
const url = linkElement.href;
// 既に処理済みかチェック
if (processedUrls.has(url)) {
const cached = processedUrls.get(url);
if (cached.element === linkElement) {
// debugLog(`Already processed: ${url}`);
return;
}
}
// debugLog(`Fetching scores for: ${url}`);
try {
// 両方のAPIを並列で呼び出し
const [hatenaData, hnData] = await Promise.all([
fetchHatenaCount(url),
fetchHackerNewsScore(url)
]);
// debugLog(`Hatena result: count=${hatenaData.count}`);
// debugLog(`HN result: score=${hnData.score}, comments=${hnData.comments}`);
processedUrls.set(url, { element: linkElement, hatena: hatenaData, hn: hnData });
// 既存の表示を削除
const existingContainer = linkElement.parentNode?.querySelector('.social-scores');
if (existingContainer) {
existingContainer.remove();
// debugLog('Removed existing container');
}
// スコアコンテナを作成
const container = document.createElement('span');
container.className = 'social-scores';
// インラインスタイルも追加して確実に表示
container.style.cssText = `
display: inline-flex !important;
gap: 4px !important;
margin-right: 8px !important;
vertical-align: middle !important;
position: relative !important;
z-index: 9999 !important;
opacity: 1 !important;
visibility: visible !important;
`;
// debugLog('Created container element');
// はてなブックマーク数を表示
if (hatenaData.count > 0) {
const hatenaSpan = document.createElement('span');
hatenaSpan.style.cssText = `
display: inline-block !important;
padding: 3px 8px !important;
border-radius: 12px !important;
font-size: 12px !important;
font-weight: bold !important;
font-family: Arial, sans-serif !important;
background-color: #00a0de !important;
color: white !important;
cursor: pointer !important;
text-decoration: none !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;
`;
hatenaSpan.textContent = `B ${hatenaData.count}`;
hatenaSpan.title = `${hatenaData.count} はてなブックマーク`;
// クリックではてなブックマークページに移動
hatenaSpan.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
window.open(hatenaData.url, '_blank');
});
container.appendChild(hatenaSpan);
// debugLog(`Added Hatena badge: B ${hatenaData.count}`);
}
// Hacker Newsスコアを表示
if (hnData.score > 0 && hnData.url) {
const hnSpan = document.createElement('span');
hnSpan.style.cssText = `
display: inline-block !important;
padding: 3px 8px !important;
border-radius: 12px !important;
font-size: 12px !important;
font-weight: bold !important;
font-family: Arial, sans-serif !important;
background-color: #ff6600 !important;
color: white !important;
cursor: pointer !important;
text-decoration: none !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2) !important;
`;
hnSpan.textContent = `Y ${hnData.score}`;
hnSpan.title = hnData.comments > 0 ?
`${hnData.score} points, ${hnData.comments} comments` :
`${hnData.score} points`;
// クリックでHacker Newsページに移動
hnSpan.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
window.open(hnData.url, '_blank');
});
container.appendChild(hnSpan);
// debugLog(`Added HN badge: Y ${hnData.score}`);
}
// コンテナに要素がある場合のみ挿入
if (container.children.length > 0) {
// debugLog(`Inserting ${container.children.length} badge(s)`);
if (linkElement.parentNode) {
linkElement.parentNode.insertBefore(container, linkElement);
// debugLog('Successfully inserted badges');
} else {
// debugLog('ERROR: No parent node for link element');
}
} else {
// debugLog('No badges to display (both scores are 0)');
}
} catch (error) {
// debugLog('Error in fetchAndDisplayScores:', error);
}
}
function processLinks() {
// debugLog('processLinks called');
const links = getArticleLinks();
// debugLog(`Processing ${links.length} links`);
if (links.length === 0) {
// debugLog('No links found to process');
return;
}
links.forEach((link, index) => {
// debugLog(`Processing link ${index + 1}/${links.length}: ${link.href}`);
fetchAndDisplayScores(link);
});
}
// 初期処理とMutationObserverの設定
function init() {
// debugLog('Initializing...');
// 初回実行
processLinks();
// DOM変更を監視
let debounceTimer = null;
const observer = new MutationObserver(() => {
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
processLinks();
}, 500);
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// URL変更を監視
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
processedUrls.clear();
// debugLog('URL changed, cleared cache');
processLinks();
}
}).observe(document, { subtree: true, childList: true });
}
// ページ読み込み後に実行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// 念のためwindow.loadでも実行
window.addEventListener('load', () => {
setTimeout(init, 1000);
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment