// ==UserScript==
// @name 哔哩哔哩网页版展示 IP 属地
// @namespace http://zhangmaimai.com
// @version 1.6.2
// @author MaxChang3
// @description 我不喜欢 IP 属地,但是你手机都显示了,为什么电脑不显示呢?
// @license MIT
// @icon https://www.bilibili.com/favicon.ico
// @match https://www.bilibili.com/video/*
// @match https://www.bilibili.com/list/*
// @match https://www.bilibili.com/bangumi/play/*
// @match https://t.bilibili.com/*
// @match https://www.bilibili.com/opus/*
// @match https://space.bilibili.com/*
// @match https://www.bilibili.com/v/topic/detail/*
// @match https://www.bilibili.com/cheese/play/*
// @match https://www.bilibili.com/festival/*
// @match https://www.bilibili.com/blackroom/ban/*
// @match https://www.bilibili.com/read/*
// @match https://manga.bilibili.com/detail/*
// @require https://update.greasyfork.org/scripts/449444/1081400/Hook%20Vue3%20app.js
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
const getLocationString = (replyItem) => {
var _a;
const locationString = (_a = replyItem == null ? void 0 : replyItem.reply_control) == null ? void 0 : _a.location;
return locationString;
};
const injectBBComment = async (bbComment, { blackroom } = { blackroom: false }) => {
const { _createListCon: createListCon, _createSubReplyItem: createSubReplyItem } = bbComment.prototype;
const applyHandler = (target, thisArg, args) => {
const [item] = args;
const result = Reflect.apply(target, thisArg, args);
const replyTimeRegex = /<span class="reply-time">(.*?)<\/span>/;
if (blackroom) {
const blackroomRegex = /<span class="time">(.*?)<\/span>/;
return result.replace(blackroomRegex, `<span class="time">$1 ${getLocationString(item)}</span>`);
}
return result.replace(replyTimeRegex, `<span class="reply-time">$1</span><span class="reply-location">${getLocationString(item)}</span>`);
};
bbComment.prototype._createListCon = new Proxy(createListCon, { apply: applyHandler });
bbComment.prototype._createSubReplyItem = new Proxy(createSubReplyItem, { apply: applyHandler });
};
const hookBBComment = async ({ blackroom } = { blackroom: false }) => {
if (_unsafeWindow.bbComment) {
injectBBComment(_unsafeWindow.bbComment, { blackroom });
return;
}
let bbComment;
Object.defineProperty(_unsafeWindow, "bbComment", {
get: () => bbComment,
set: (value) => {
bbComment = value;
injectBBComment(value, { blackroom });
},
configurable: true
});
};
const isElementLoaded = async (selector, root = document) => {
const getElement = () => root.querySelector(selector);
return new Promise((resolve) => {
const element = getElement();
if (element)
return resolve(element);
const observer = new MutationObserver((_) => {
const element2 = getElement();
if (!element2)
return;
resolve(element2);
observer.disconnect();
});
observer.observe(root === document ? root.documentElement : root, {
childList: true,
subtree: true
});
});
};
const isConditionTrue = async (fn) => {
const timeStart = performance.now();
return new Promise((resolve) => {
const interval = setInterval(() => {
if (performance.now() - timeStart > 1e4) {
clearInterval(interval);
resolve(false);
}
if (!fn())
return;
clearInterval(interval);
resolve(true);
}, 100);
});
};
const getLocationFromReply = (replyItemEl) => {
let replyElement;
let locationString;
if (replyItemEl.className.startsWith("sub")) {
replyElement = replyItemEl;
locationString = getLocationString(replyElement == null ? void 0 : replyElement.__vue__.vnode.props.subReply);
} else {
replyElement = replyItemEl;
locationString = getLocationString(replyElement == null ? void 0 : replyElement.__vue__.vnode.props.reply);
}
return locationString;
};
const insertLocation = (replyItemEl) => {
const replyInfo = replyItemEl.className.startsWith("sub") ? replyItemEl.querySelector(".sub-reply-info") : replyItemEl.querySelector(".reply-info");
if (!replyInfo)
throw new Error("Can not detect reply info");
const locationString = getLocationFromReply(replyItemEl);
if (locationString && replyInfo.children.length !== 0 && !replyInfo.children[0].innerHTML.includes("IP属地")) {
replyInfo.children[0].innerHTML += ` ${locationString}`;
}
};
const isReplyItem = (el) => el instanceof HTMLDivElement && ["reply-item", "sub-reply-item"].includes(el.className);
const observeAndInjectComments = async (root) => {
const targetNode = await isElementLoaded(".reply-list", root);
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type !== "childList")
continue;
mutation.addedNodes.forEach((node) => {
if (!isReplyItem(node))
return;
insertLocation(node);
if (node.className.startsWith("sub"))
return;
const subReplyListEl = node.querySelector(".sub-reply-list");
if (!subReplyListEl)
return;
const subReplyList = Array.from(subReplyListEl.children);
subReplyList.pop();
subReplyList.map(insertLocation);
});
}
});
observer.observe(targetNode, { childList: true, subtree: true });
};
const serveNewComments = async (itemSelector, root = document) => {
const dynList = await isElementLoaded(itemSelector, root);
let lastObserved;
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type !== "childList" || !(mutation.target instanceof HTMLElement) || !mutation.target.classList.contains("bili-comment-container") || mutation.target === lastObserved)
continue;
observeAndInjectComments(mutation.target);
lastObserved = mutation.target;
}
});
observer.observe(dynList, { childList: true, subtree: true });
};
class Router {
constructor() {
__publicField(this, "routes", []);
}
serve(prefix, action, constrait = {}) {
if (Array.isArray(prefix)) {
prefix.forEach((p) => this.routes.push({ prefix: p, action, constrait }));
return;
}
this.routes.push({ prefix, action, constrait });
}
match(url) {
for (const { prefix, action, constrait } of this.routes) {
if (!url.startsWith(prefix))
continue;
if (constrait.endsWith && !url.endsWith(constrait.endsWith))
continue;
action();
break;
}
}
}
const createPatch = (ActionButtonsRender) => {
class PatchActionButtonsRender extends ActionButtonsRender {
update() {
super.update();
const pubDateEl = this.shadowRoot.querySelector("#pubdate");
if (!pubDateEl)
return;
let locationEl = this.shadowRoot.querySelector("#location");
const locationString = getLocationString(this.data);
if (!locationString) {
if (locationEl)
locationEl.remove();
return;
}
if (locationEl) {
locationEl.textContent = locationString;
return;
}
locationEl = document.createElement("div");
locationEl.id = "location";
locationEl.textContent = locationString;
pubDateEl.insertAdjacentElement("afterend", locationEl);
}
}
return PatchActionButtonsRender;
};
const hookLit = () => {
const { define: originalDefine } = _unsafeWindow.customElements;
const applyHandler = (target, thisArg, args) => {
const [name, constructor, ...rest] = args;
if (typeof constructor !== "function" || name !== "bili-comment-action-buttons-renderer")
return Reflect.apply(target, thisArg, args);
const PatchActionButtonsRender = createPatch(constructor);
return Reflect.apply(target, thisArg, [name, PatchActionButtonsRender, ...rest]);
};
_unsafeWindow.customElements.define = new Proxy(originalDefine, { apply: applyHandler });
};
const router = new Router();
router.serve([
/** 视频 */
"https://www.bilibili.com/video/",
/** 新列表 */
"https://www.bilibili.com/list/",
/** 新版单独动态页 */
"https://www.bilibili.com/opus/",
/** 课程页 */
"https://www.bilibili.com/cheese/play/"
], hookLit);
router.serve(
/** 拜年祭 */
"https://www.bilibili.com/festival/",
hookBBComment
);
router.serve(
/** 专栏 */
"https://www.bilibili.com/read/",
async () => {
var _a, _b;
observeAndInjectComments();
const articleDetail = await isElementLoaded(".article-detail");
await isConditionTrue(() => {
var _a2;
const readInfo = document.querySelector(".article-read-info");
return !!(readInfo && ((_a2 = readInfo.lastElementChild) == null ? void 0 : _a2.textContent) !== "--评论");
});
const publishText = articleDetail.querySelector(".publish-text");
if (!publishText || !((_b = (_a = articleDetail.__vue__) == null ? void 0 : _a.readViewInfo) == null ? void 0 : _b.location))
return;
publishText.innerHTML += ` IP属地:${articleDetail.__vue__.readViewInfo.location}`;
}
);
router.serve("https://www.bilibili.com/bangumi/play/", () => {
const isNewBangumi = !!document.querySelector("meta[name=next-head-count]");
if (isNewBangumi) {
observeAndInjectComments();
} else {
hookBBComment();
}
});
router.serve("https://www.bilibili.com/v/topic/detail/", () => serveNewComments(".list-view"));
router.serve("https://space.bilibili.com/", () => serveNewComments(".bili-dyn-list__items"), { endsWith: "dynamic" });
router.serve("https://space.bilibili.com/", async () => {
const dynamicTab = await isElementLoaded(".n-dynamic");
dynamicTab.addEventListener("click", () => {
serveNewComments(".bili-dyn-list__items");
}, { once: true });
});
router.serve("https://t.bilibili.com/", async () => {
const dynHome = await isElementLoaded(".bili-dyn-home--member");
const isNewDyn = (() => {
var _a;
const dynBtnText = (_a = dynHome.querySelector(".bili-dyn-sidebar__btn")) == null ? void 0 : _a.textContent;
return dynBtnText ? dynBtnText.includes("新版反馈") || dynBtnText.includes("回到旧版") : false;
})();
if (isNewDyn) {
hookLit();
} else {
hookBBComment();
}
}, { endsWith: "/" });
router.serve("https://t.bilibili.com/", async () => {
const dynItem = await isElementLoaded(".bili-dyn-item");
const isNewDyn = !dynItem.querySelector(".bili-dyn-item__footer");
if (isNewDyn) {
hookLit();
} else {
hookBBComment();
}
});
router.serve("https://www.bilibili.com/blackroom/ban/", () => hookBBComment({ blackroom: true }));
router.serve("https://manga.bilibili.com/detail/", observeAndInjectComments);
router.match(location.href);
})();