// ==UserScript==
// @name NodeSeek <-> DeepFlood 联合访问
// @namespace http://tampermonkey.net/
// @license AGPL-3.0
// @version 2025-09-28
// @description Visit nodeseek.com and deepflood.com at the same time and push hot posts
// @author xykt
// @match https://nodeseek.com/
// @match https://www.nodeseek.com/
// @match https://nodeseek.com/page-*
// @match https://www.nodeseek.com/page-*
// @match https://deepflood.com/
// @match https://www.deepflood.com/
// @match https://deepflood.com/page-*
// @match https://www.deepflood.com/page-*
// @grant GM_xmlhttpRequest
// @connect nodeseek.com
// @connect www.nodeseek.com
// @connect deepflood.com
// @connect www.deepflood.com
// @icon https://www.nodeseek.com/static/image/favicon/android-chrome-192x192.png
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
const host = location.hostname.replace(/^www\./, '');
const A = host;
const B = A === 'nodeseek.com' ? 'deepflood.com' : 'nodeseek.com';
const scheme = location.protocol;
const baseA = scheme + '//' + A;
const baseB = 'https://' + B;
const pathMatch = location.pathname.match(/^\/(page-\d+)?\/?$/);
if (!pathMatch) return;
const currentPath = pathMatch[1] ? '/' + pathMatch[1] : '/';
document.documentElement.style.visibility = 'hidden';
function parseHTML(html) {
const parser = new DOMParser();
return parser.parseFromString(html, 'text/html');
}
function isRelative(url) {
if (!url || typeof url !== 'string') return false;
url = url.trim();
const lower = url.toLowerCase();
if (lower.startsWith('http://') || lower.startsWith('https://') || lower.startsWith('//') ||
lower.startsWith('mailto:') || lower.startsWith('tel:') || lower.startsWith('#') ||
lower.startsWith('data:') ) {
return false;
}
return true;
}
function absolutizeUrl(url, base) {
if (!url) return url;
if (!isRelative(url)) return url;
if (url.startsWith('/')) return base + url;
return base + '/' + url;
}
function convertRelativePaths(doc, base) {
const attrList = ['href','src','action','poster','data-src','data-href'];
attrList.forEach(attr => {
doc.querySelectorAll('['+attr+']').forEach(el => {
const val = el.getAttribute(attr);
if (isRelative(val)) el.setAttribute(attr, absolutizeUrl(val, base));
});
});
doc.querySelectorAll('[srcset]').forEach(el => {
const ss = el.getAttribute('srcset') || '';
const parts = ss.split(',').map(p => {
const seg = p.trim();
const spaceIdx = seg.indexOf(' ');
if (spaceIdx === -1) {
return isRelative(seg) ? absolutizeUrl(seg, base) : seg;
} else {
const u = seg.slice(0, spaceIdx);
const rest = seg.slice(spaceIdx+1);
return (isRelative(u) ? absolutizeUrl(u, base) : u) + ' ' + rest;
}
});
el.setAttribute('srcset', parts.join(', '));
});
doc.querySelectorAll('[style]').forEach(el => {
let s = el.getAttribute('style');
if (!s) return;
s = s.replace(/url\((['"]?)(?!https?:|\/\/|data:|#)([^'")]+)\1\)/g,
(m, q, p1) => 'url(' + absolutizeUrl(p1, base) + ')');
el.setAttribute('style', s);
});
doc.querySelectorAll('style').forEach(st => {
let txt = st.textContent;
if (!txt) return;
txt = txt.replace(/url\((['"]?)(?!https?:|\/\/|data:|#)([^'")]+)\1\)/g,
(m, q, p1) => 'url(' + absolutizeUrl(p1, base) + ')');
st.textContent = txt;
});
return doc;
}
function fetchBAndMerge() {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://' + B + currentPath,
responseType: 'text',
onload: function(res) {
if (res.status >= 200 && res.status < 400 && res.responseText) {
try {
const docB = parseHTML(res.responseText);
convertRelativePaths(docB, baseB);
function doMerge() {
try {
const headA = document.querySelector('div#nsk-head.nsk-container, div#nsk-head');
if (headA) {
const strong = headA.querySelector('strong.site-title');
let newInner = '';
if (A === 'nodeseek.com') {
newInner = `
<a href="https://nodeseek.com/"><img src="https://nodeseek.com/static/image/favicon/android-chrome-192x192.png" alt="logo" style="max-height: 36px;vertical-align: middle;"> <span class="title-text" style="vertical-align: middle;">NodeSeek</span><span class="beta-icon">beta</span></a>
<span class="title-text" style="vertical-align: middle;">+</span>
<a href="https://deepflood.com/"><span class="title-text" style="vertical-align: middle;"><span style="color:#0084ff">Deep</span><span style="color:#00a3a3">Flood</span></span><span class="beta-icon">beta</span></a>`;
} else {
newInner = `
<a href="https://deepflood.com/"><span class="title-text" style="vertical-align: middle;"><span style="color:#0084ff">Deep</span><span style="color:#00a3a3">Flood</span></span><span class="beta-icon">beta</span></a>
<span class="title-text" style="vertical-align: middle;">+</span>
<a href="https://nodeseek.com/"><img src="https://nodeseek.com/static/image/favicon/android-chrome-192x192.png" alt="logo" style="max-height: 36px;vertical-align: middle;"> <span class="title-text" style="vertical-align: middle;">NodeSeek</span><span class="beta-icon">beta</span></a>`;
}
if (strong) strong.innerHTML = newInner;
}
const containerA = document.querySelector('#nsk-left-panel-container');
const panelsB = Array.from(docB.querySelectorAll('div.nsk-panel.category-list'));
if (containerA && panelsB.length > 0) {
panelsB.forEach(bPanel => {
const cloned = document.importNode(bPanel, true);
containerA.appendChild(cloned);
});
}
(function handlePostList(docA, docB, site) {
const listA = Array.from(docA.querySelectorAll('ul.post-list > li.post-list-item:not([class*="topic-carousel"])'));
const listB = Array.from(docB.querySelectorAll('ul.post-list > li.post-list-item:not([class*="topic-carousel"])'));
const icons = {
nodeseek: `<span class="info-item info-site"><img src="https://www.nodeseek.com/static/image/favicon/android-chrome-192x192.png" width="12" height="12"></span> `,
deepflood: `<span class="info-item info-site"><img src="https://www.deepflood.com/static/image/favicon/android-chrome-192x192.png" width="12" height="12"></span>`
};
const siteShort = (typeof site === 'string' && site.indexOf('nodeseek') !== -1) ? 'nodeseek' : 'deepflood';
const siteOther = siteShort === 'nodeseek' ? 'deepflood' : 'nodeseek';
function markSite(posts, siteFrom) {
posts.forEach(li => {
const info = li.querySelector(".post-info");
if (info) {
if (!info.querySelector('.info-item.info-site')) {
info.insertAdjacentHTML("afterbegin", icons[siteFrom]);
}
}
});
}
markSite(listA, siteShort);
markSite(listB, siteOther);
let allPosts = [...listA, ...listB];
function parsePost(li) {
let timeTitle = "";
const timeEl = li.querySelector(".post-info time[title]");
if (timeEl) timeTitle = timeEl.getAttribute("title") || "";
let time = 0;
if (timeTitle) {
const parsed = Date.parse(timeTitle);
time = isNaN(parsed) ? (new Date(timeTitle)).getTime() || 0 : parsed;
}
let views = 0;
const viewsSpan = li.querySelector(".post-info .info-views span[title], .post-info .info-views span");
if (viewsSpan) {
const vt = viewsSpan.getAttribute("title") || viewsSpan.textContent || "";
const m = vt.match(/(\d[\d,]*)/);
if (m) views = parseInt(m[1].replace(/,/g, ''), 10) || 0;
}
let comments = 0;
const commentsSpan = li.querySelector(".post-info .info-comments-count span[title], .post-info .info-comments-count span");
if (commentsSpan) {
const ct = commentsSpan.getAttribute("title") || commentsSpan.textContent || "";
const m2 = ct.match(/(\d[\d,]*)/);
if (m2) comments = parseInt(m2[1].replace(/,/g, ''), 10) || 0;
}
const sticky = !!li.querySelector('use[href="#pin"], use[href="#pin"]');
return {
el: li,
time: time || 0,
weight: (views || 0) + (comments || 0) * 5,
sticky: !!sticky
};
}
let postsData = allPosts.map(parsePost);
let stickyPosts = postsData.filter(p => p.sticky).sort((a, b) => b.time - a.time);
let normalPosts = postsData.filter(p => !p.sticky);
normalPosts.sort((a, b) => b.weight - a.weight);
let hotPosts = normalPosts.slice(0, 5);
hotPosts.forEach(p => {
const info = p.el.querySelector(".post-info");
if (info) {
if (!info.querySelector('.info-item.info-hot')) {
info.insertAdjacentHTML("beforeend", `<span class="info-item info-hot"><svg class="iconpark-icon"><use href="#rocket" style="color: red;"></use></svg><a style="color: red;"> 热帖</a></span>`);
}
}
});
let otherPosts = normalPosts.slice(5).sort((a, b) => b.time - a.time);
let finalPosts = [...stickyPosts, ...hotPosts, ...otherPosts];
const postListA = docA.querySelector('ul.post-list:not([class*="topic-carousel"])');
if (postListA) {
postListA.innerHTML = "";
finalPosts.forEach(p => {
const nodeToInsert = (p.el.ownerDocument === document) ? p.el.cloneNode(true) : document.importNode(p.el, true);
postListA.appendChild(nodeToInsert);
});
}
})(document, docB, A);
const userCard = document.querySelector('div[data-v-244123cf].user-card, div.user-card[data-v-244123cf]');
if (userCard) {
let newUserHtml = '';
if (A === 'nodeseek.com') {
newUserHtml = `
<a href="/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">Nodeseek发帖</span></a>
<a href="https://deepflood.com/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">DeepFlood发帖</span></a>`;
} else {
newUserHtml = `
<a href="/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">DeepFlood发帖</span></a>
<a href="https://nodeseek.com/new-discussion" class="btn new-discussion"><svg class="iconpark-icon"><use href="#plus-cross-725o7jdo"></use></svg> <span style="vertical-align: middle;">Nodeseek发帖</span></a>`;
}
const nextDiv = userCard.nextElementSibling;
if (nextDiv && nextDiv.tagName.toLowerCase() === 'div') {
nextDiv.innerHTML = newUserHtml;
}
}
document.documentElement.style.visibility = '';
} catch (err) {
console.error('合并出错', err);
document.documentElement.style.visibility = '';
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', doMerge, {once:true});
} else {
doMerge();
}
} catch (e) {
console.error('解析失败', e);
document.documentElement.style.visibility = '';
}
} else {
document.documentElement.style.visibility = '';
}
},
onerror: function() {
document.documentElement.style.visibility = '';
}
});
}
fetchBAndMerge();
})();