Greasy Fork is available in English.

PrivateView

隐匿浏览——浏览页面时,将关键信息进行隐匿,以保护个人信息安全。也许你在公共场所办公时,常常想不让其他人看见自己在B站上的用户昵称、头像、关注数、粉丝数、动态数,那就巧了,这个扩展脚本可以很好的解决该问题。目前支持bilibili、csdn、zhihu、linux.do、v2ex网站,后续计划实现让用户可自定义指定网站使用隐匿浏览的功能。

  1. // ==UserScript==
  2. // @name PrivateView
  3. // @version 1.3.2
  4. // @description 隐匿浏览——浏览页面时,将关键信息进行隐匿,以保护个人信息安全。也许你在公共场所办公时,常常想不让其他人看见自己在B站上的用户昵称、头像、关注数、粉丝数、动态数,那就巧了,这个扩展脚本可以很好的解决该问题。目前支持bilibili、csdn、zhihu、linux.do、v2ex网站,后续计划实现让用户可自定义指定网站使用隐匿浏览的功能。
  5. // @author DD1024z
  6. // @namespace https://github.com/10D24D/PrivateView/
  7. // @supportURL https://github.com/10D24D/PrivateView/
  8. // @match *://*/*
  9. // @icon https://raw.githubusercontent.com/10D24D/PrivateView/main/static/icon_max.png
  10. // @license Apache License 2.0
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_unregisterMenuCommand
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. if (window.top !== window.self) return; // 不在顶层页面时直接退出脚本
  21.  
  22. const APP_NAME = "PrivateView";
  23.  
  24. // 默认网站配置
  25. // BrowserTitle 浏览器标题
  26. // ProfileImg 用户头像的样式。多个样式使用, 逗号隔开
  27. // ProfileUserName 用户名称的元素
  28. // ArticleTitle 文章标题的元素
  29. // ProfileStatistics 用户统计数据的元素
  30. // CustomStatistics 自定义替换匹已匹配统计数据的元素
  31. const DEFAULT_SITE_CONFIG = {
  32. 'www.baidu.com': {
  33. "BrowserTitle": "百度",
  34. "ProfileImg": "#s-top-username span.s-top-img-wrapper img, a.username span[class$='top-img-wrapper'] img",
  35. "ProfileUserName": "#s-top-username span.user-name, a.username span[class$='top-username']",
  36. },
  37. 'chat.baidu.com': {
  38. "BrowserTitle": "百度AI助手",
  39. "ProfileImg": "div.user-info img.cos-avatar-img",
  40. "ProfileUserName": "div.user-info span.cos-line-clamp-1",
  41. },
  42. 'image.baidu.com': {
  43. "BrowserTitle": "百度图片",
  44. "ProfileImg": "#username_info span.s-top-img-wrapper img, div[class^='header-wrapper'] img.sc-avatar-img",
  45. "ProfileUserName": "#username_info span.s-top-username, div[class^='header-wrapper'] span[class^='user-name']",
  46. },
  47. 'tieba.baidu.com': {
  48. "BrowserTitle": "百度贴吧",
  49. "ProfileImg": "#user_info img.head_img",
  50. "ProfileUserName": "#j_u_username a.u_username_wrap span.u_username_title, #user_info div.user_name a",
  51. "ProfileStatistics": "div.user_score a.score_num",
  52. },
  53. 'wenku.baidu.com': {
  54. "BrowserTitle": "百度文库",
  55. "ProfileImg": "div.user-icon-content.login div.user-icon",
  56. },
  57. 'fanyi.baidu.com': {
  58. "BrowserTitle": "百度翻译",
  59. "ProfileImg": "img[src^='http://himg.bdimg.com/sys/portrait/item/']",
  60. "ProfileUserName": "#root > div > div > div:nth-child(4) > div:nth-child(2)"
  61. },
  62. 'baike.baidu.com': {
  63. "BrowserTitle": "百度百科",
  64. "ProfileImg": "#user_info img.head_img",
  65. "ProfileUserName": `
  66. div.user-bar.user-login > div:nth-child(2) a:first-of-type:not([href]):not([aria-label]):has(i),
  67. div.fixedWrapper a[href^='/usercenter']
  68. `
  69. },
  70. 'xueshu.baidu.com': {
  71. "BrowserTitle": "百度学术",
  72. "ProfileUserName": "#userName, #fixed_user a.username"
  73. },
  74. 'jingyan.baidu.com': {
  75. "BrowserTitle": "百度经验",
  76. "ProfileImg": "li.my-info div.user-avatar img, #wgt-user-info img.avatar-img",
  77. "ProfileUserName": "li.my-info span.user-name, #wgt-user-info p.u-name",
  78. "ProfileStatistics": "#activeDays, #wgt-user-info span.level",
  79. },
  80. 'zhidao.baidu.com': {
  81. "BrowserTitle": "百度知道",
  82. "ProfileImg": "#user-name span.avatar-container img, div.login-slogan img.avatar-image",
  83. "ProfileUserName": "#user-name span.user-name-span, div.login-slogan a.user-name-link",
  84. "ProfileStatistics": "div.answer-question-section span.item-num",
  85. "CustomStatistics": {
  86. "div.user-grade": "LV[0-9]+",
  87. "div.help-people-count": "/已经帮助了\\d+人/"
  88. }
  89. },
  90. 'baijiahao.baidu.com, mbd.baidu.com': {
  91. "BrowserTitle": "百家号",
  92. "ProfileImg": "img[data-testid='user-avatar'], div.xcp-publish div.x-avatar-img",
  93. "ProfileUserName": "a[href='http://i.baidu.com/']",
  94. },
  95. 'news.baidu.com': {
  96. "BrowserTitle": "百度新闻",
  97. "ProfileUserName": "#usrbar a.mn-lk[href='http://passport.baidu.com/']",
  98. },
  99. 'so.com': {
  100. "BrowserTitle": "360搜索",
  101. "ProfileUserName": "#hd_nav li.login span.uname, div.menu .show-list.user-group.u-logined a.title",
  102. },
  103. 'bing.com': {
  104. "BrowserTitle": "必应",
  105. "ProfileImg": "#id_p, #id_accountp",
  106. "ProfileUserName": "#id_n, #id_l, #id_currentAccount_primary, #id_currentAccount_secondary",
  107. "ProfileStatistics": "#id_rfb, span.points-container",
  108. },
  109. 'google.com': {
  110. "BrowserTitle": "Google",
  111. "ProfileImg": "#gb a.gb_A img.gb_O, div.XS2qof img",
  112. "ProfileUserName": "div.gb_Ac div.gb_g, div.gb_Ac div.gb_g + div, div.eYSAde, div.hCDve",
  113. },
  114. 'v2ex.com': {
  115. "BrowserTitle": "V2EX",
  116. "ProfileImg": "#Rightbar > div.box:nth-of-type(2) .cell a img.avatar",
  117. "ProfileUserName": "#Top .tools a[href^='/member/'], #Rightbar .cell span.bigger a",
  118. "ProfileStatistics": "#Rightbar .box a span.bigger, #money a",
  119. },
  120. 'linux.do': {
  121. "BrowserTitle": "LINUX DO",
  122. "ProfileImg": "#current-user img.avatar",
  123. "ArticleTitle": "div.title-wrapper",
  124. },
  125. 'zhihu.com': {
  126. "BrowserTitle": "知乎",
  127. "ProfileImg": ".Avatar.AppHeader-profileAvatar, div.Comments-container img.Avatar",
  128. "ArticleTitle": ".QuestionHeader-title",
  129. },
  130. 'csdn.net': {
  131. "BrowserTitle": "CSDN",
  132. "ProfileImg": ".csdn-profile-avatar img, .hasAvatar img",
  133. "ProfileUserName": ".csdn-profile-nickName",
  134. "ProfileStatistics": ".csdn-profile-fansCount, .csdn-profile-followCount, .csdn-profile-likeCount"
  135. },
  136. 'bilibili.com': {
  137. "BrowserTitle": "Bilibili",
  138. "ProfileImg": `
  139. li.header-avatar-wrap a.header-entry-avatar img,
  140. li.header-avatar-wrap a.header-entry-mini picture.v-img source,
  141. li.header-avatar-wrap a.header-entry-mini picture.v-img img,
  142. div.index-info div.home-head img,
  143. div.bili-dyn-my-info img.b-img__inner
  144. `,
  145. "ProfileUserName": `
  146. div.v-popover-content a.nickname-item, div.index-info span.home-top-msg-name, div.bili-dyn-my-info div.info__name
  147. `,
  148. "ProfileStatistics": `
  149. .counts-item .count-num, div.coins-item span.coin-item__num, div.home-top-bp span.curren-b-num,
  150. span.home-top-level-number i.now-num, span.home-top-level-number i.max-num,
  151. div.bili-dyn-my-info div.item-num
  152. `,
  153. "CustomStatistics": {
  154. "div.level-item__text": "当前成长\\d+,距离升级Lv\\.\\d+ 还需要\\d+",
  155. "span.home-top-level-head": "LV[0-9]",
  156. "i.home-level-tips": "LV[0-9]",
  157. }
  158. },
  159. 'jianshu.com': {
  160. "BrowserTitle": "简书",
  161. "ProfileImg": "div.user a[href^='/u/'].avatar img",
  162. "ArticleTitle": "h1[title^='简书']",
  163. },
  164. 'leetcode.cn': {
  165. "BrowserTitle": "力扣",
  166. "ProfileImg": "#navbar_user_avatar img, #web-user-menu a[href^='/u/'] img.object-cover",
  167. "ProfileUserName": "#web-user-menu div.pl-3 a[href^='/u/']",
  168. "ProfileStatistics": `
  169. a[href^='/problems/'] > svg + span, section div.text-center p span, #headlessui-popover-button-:r1: a span.text-brand-orange
  170. `,
  171. },
  172. 'juejin.cn': {
  173. "BrowserTitle": "稀土掘金",
  174. "ProfileImg": "ul.right-side-nav li.menu .avatar img, div.user-info div.avatar img",
  175. "ProfileUserName": "div.user-detail a.username",
  176. "ProfileStatistics": `
  177. ul.actions-count-list div.item-count, div.user-detail a.ore span
  178. `,
  179. "CustomStatistics": {
  180. "a.progress-bar div.jscore-level span": "/JY.[0-9]+/",
  181. "a.progress-bar div.progress span": "\\d+\\s*\\/\\s*\\d+"
  182. }
  183. },
  184. '52pojie.cn': {
  185. "BrowserTitle": "吾爱破解",
  186. "ProfileImg": "#um a[href^='home.php?mod=space&uid='] img",
  187. "ProfileUserName": "a[href^='home.php?mod=space&uid=']",
  188. "ProfileStatistics": "#extcreditmenu_menu li span",
  189. },
  190. 'itsk.com': {
  191. "BrowserTitle": "IT天空",
  192. "ProfileImg": "div.navbar-container a[href^='/space/'].avatar-box img",
  193. "ProfileUserName": "#el-popper-container-1024 span.user-name",
  194. },
  195. 'hifini.com': {
  196. "BrowserTitle": "HiFiNi",
  197. "ProfileImg": "li.nav-item.username a.nav-link img.avatar-1",
  198. "ProfileUserName": "li.nav-item.username a.nav-link",
  199. },
  200. 'oschina.net': {
  201. "BrowserTitle": "OSCHINA",
  202. "ProfileImg": "div.current-user-avatar img",
  203. "ProfileUserName": "#userSidebar h3.centered.header",
  204. "ProfileStatistics": "#userSidebar a.statistic div.value",
  205. },
  206. '51cto.com': {
  207. "BrowserTitle": "51CTO技术家园",
  208. "ProfileImg": "div.item-rt.loginbox img, div.mainindex_r.right a.position_r img",
  209. "ProfileUserName": "div.mainindex_r.right div.port_m_box.position_r a[href^='/space?uid=']",
  210. "ProfileStatistics": "div.mainindex_r.right div.datas.clearfix a span",
  211. },
  212. 'app.follow.is': {
  213. "BrowserTitle": "Follow",
  214. "ArticleTitle": "main div.items-end.text-theme-foreground",
  215. "ProfileImg": "img:not(main img)",
  216. "ProfileUserName": "[id^='radix-'] span.block.mx-auto",
  217. "ProfileStatistics": `
  218. div.text-theme-vibrancyFg button div, div.items-center span.tabular-nums span,
  219. div.items-center span.tabular-nums, div.items-center i.i-mgc-fire-cute-fi + span
  220. `,
  221. },
  222. 'gitee.com': {
  223. "BrowserTitle": "Gitee",
  224. "ProfileImg": "#git-nav-user img.avatar, header span.ant-avatar img, main div.top-header span.ant-avatar img, img#avatar-change",
  225. "ProfileUserName": "main div.top-header strong.self-center a span, div.user-info a.username",
  226. "ProfileStatistics": "main div.top-header li a span.float-right",
  227. },
  228. 'github.com': {
  229. "BrowserTitle": "GitHub",
  230. "ProfileImg": "div[aria-label='User navigation'] img, div.AppHeader-user img.avatar",
  231. "ProfileUserName": `
  232. div[aria-label='User navigation'] div.lh-condensed div.text-bold > div,
  233. div[aria-label='User navigation'] div.lh-condensed div.fgColor-muted > div
  234. `,
  235. },
  236. };
  237.  
  238. // 从油猴存储获取用户自定义的站点配置
  239. const storedConfig = GM_getValue(APP_NAME, null);
  240.  
  241. // 优先使用用户定义的配置
  242. const siteConfig = storedConfig || DEFAULT_SITE_CONFIG;
  243.  
  244. const IMG_SRC = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=="; // 隐匿图像资源后替换的内容。空白图片
  245. const IMG_ALT = ""; // 隐匿图像提示内容后替换的内容
  246. const USER_NAME = "User"; // 隐匿用户名称后显示的内容
  247. const USER_STATISTICS = "?"; // 隐匿用户统计数据后显示的内容
  248. let originalTitle = document.title; // 记录原始页面标题
  249.  
  250. const storageKey = `PrivateViewSettings`;
  251. const currentHostname = Object.keys(siteConfig).find(keys => keys.split(',').some(host => location.hostname.includes(host.trim())));
  252. const currentSite = siteConfig[currentHostname];
  253.  
  254. // 使用 localStorage 缓存开关状态
  255. let settings = JSON.parse(localStorage.getItem(storageKey)) || {
  256. hiddenModeEnabled: true,
  257. hideBrowserTitle: true,
  258. hideArticleTitle: true,
  259. hideProfileImg: true,
  260. hideProfileUserName: true,
  261. hideProfileStatistics: true,
  262. hideAllImg: false,
  263. };
  264.  
  265. if (!localStorage.getItem(storageKey) && currentSite) {
  266. saveSettings();
  267. location.reload();
  268. }
  269.  
  270. // 保存设置到 localStorage
  271. function saveSettings() {
  272. localStorage.setItem(storageKey, JSON.stringify(settings));
  273. }
  274.  
  275. // 修改文本内容
  276. function updateTextContent(selector, value) {
  277. const elements = document.querySelectorAll(selector);
  278. if (!elements.length) return; // 无匹配时直接返回
  279. elements.forEach(el => {
  280. // 如果是 input[type="text"],直接修改 value 属性
  281. if (el.tagName === 'INPUT' && el.type === 'text') {
  282. el.value = value;
  283. } else {
  284. // 遍历子节点,修改文本内容
  285. Array.from(el.childNodes).forEach(child => {
  286. if (child.nodeType === Node.TEXT_NODE && child.nodeValue.trim() !== "") {
  287. // 如果不匹配任何规则,直接修改为指定的值
  288. child.nodeValue = value;
  289. }
  290. });
  291. }
  292. // 额外:修改 aria-label 属性
  293. if (el.hasAttribute('aria-label')) {
  294. el.setAttribute('aria-label', value);
  295. }
  296. });
  297. }
  298.  
  299. // 修改图像属性值
  300. function updateImg(selector) {
  301. document.querySelectorAll(selector).forEach(el => {
  302. // 如果是 <img> 标签
  303. if (el.tagName === "IMG") {
  304. el.src = IMG_SRC; // 替换 src 属性
  305. el.srcset = IMG_SRC; // 替换 srcset 属性
  306. if (el.hasAttribute('data-src')) {
  307. el.setAttribute('data-src', IMG_SRC); // 替换 data-src 属性
  308. }
  309. }
  310.  
  311. // 如果是 <source> 标签(用于 <picture> 元素)
  312. if (el.tagName === "SOURCE") {
  313. el.srcset = IMG_SRC; // 替换 srcset 属性
  314. }
  315.  
  316. // 检查并修改 style 中的 background-image
  317. const backgroundImage = el.style.backgroundImage;
  318. if (backgroundImage && backgroundImage.includes("url")) {
  319. el.style.backgroundImage = `url(${IMG_SRC})`; // 替换背景图片
  320. }
  321.  
  322. // 遍历所有属性,替换其他与图片相关的自定义属性
  323. Array.from(el.attributes).forEach(attr => {
  324. if (attr.name.startsWith('data-') && attr.value.includes('url')) {
  325. el.setAttribute(attr.name, IMG_SRC); // 替换自定义属性的值
  326. }
  327. });
  328. });
  329.  
  330. // 递归处理 <picture> 元素内部的 <source> 和 <img> 标签
  331. document.querySelectorAll(`${selector} picture`).forEach(picture => {
  332. picture.querySelectorAll("source, img").forEach(sourceOrImg => {
  333. updateImgElement(sourceOrImg);
  334. });
  335. });
  336. }
  337.  
  338. // 单独处理 <source> 和 <img>
  339. function updateImgElement(el) {
  340. if (el.tagName === "IMG") {
  341. el.src = IMG_SRC;
  342. el.srcset = IMG_SRC;
  343. } else if (el.tagName === "SOURCE") {
  344. el.srcset = IMG_SRC;
  345. }
  346. }
  347.  
  348. // 修改元素可见性
  349. function updateVisibility(selector, visibility = "hidden") {
  350. document.querySelectorAll(selector).forEach(el => {
  351. el.style.visibility = visibility;
  352. });
  353. }
  354.  
  355. // 切换页面标题
  356. function toggleBrowserTitle() {
  357. if (settings.hideBrowserTitle) {
  358. const currentSite = siteConfig[currentHostname];
  359. if (currentSite && currentSite.BrowserTitle) {
  360. document.title = currentSite.BrowserTitle; // 设置为指定标题
  361. }
  362. } else {
  363. document.title = originalTitle; // 恢复原始标题
  364. }
  365. }
  366.  
  367. // 切换文章标题显示/隐藏
  368. function toggleArticleTitleVisibility() {
  369. const currentSite = siteConfig[currentHostname];
  370. if (!currentSite || !currentSite.ArticleTitle) return;
  371.  
  372. const visibility = settings.hideArticleTitle ? "hidden" : "visible";
  373.  
  374. document.querySelectorAll(currentSite.ArticleTitle).forEach(el => {
  375. el.style.visibility = visibility;
  376. el.style.opacity = settings.hideArticleTitle ? "0" : "1";
  377. el.style.pointerEvents = settings.hideArticleTitle ? "none" : "auto";
  378. });
  379. }
  380.  
  381. // 隐匿浏览的函数
  382. function hideElements() {
  383. if (!currentSite) return;
  384.  
  385. // 隐匿浏览器标题
  386. if (settings.hideBrowserTitle && currentSite.BrowserTitle) {
  387. updateTextContent("head title", currentSite.BrowserTitle);
  388. }
  389.  
  390. // 隐匿头像
  391. if (settings.hideProfileImg && currentSite.ProfileImg) {
  392. updateImg(currentSite.ProfileImg);
  393. }
  394.  
  395. // 隐匿用户名
  396. if (settings.hideProfileUserName && currentSite.ProfileUserName) {
  397. updateTextContent(currentSite.ProfileUserName, USER_NAME);
  398. }
  399.  
  400. // 针对 ProfileStatistics 处理
  401. if (settings.hideProfileStatistics && currentSite.ProfileStatistics) {
  402. updateTextContent(currentSite.ProfileStatistics, USER_STATISTICS);
  403. }
  404.  
  405. // 针对 CustomStatistics 进行精确处理
  406. if (settings.hideProfileStatistics && currentSite.CustomStatistics) {
  407. for (const [selector, regexString] of Object.entries(currentSite.CustomStatistics)) {
  408. try {
  409. // 去掉开头和结尾的 `/`,确保是合法正则
  410. let regexPattern = regexString.replace(/^\/|\/$/g, '');
  411. let regex = new RegExp(regexPattern);
  412.  
  413. document.querySelectorAll(selector).forEach(el => {
  414. if (!el.dataset.processed && regex.test(el.textContent)) {
  415. el.textContent = el.textContent.replace(/\d+/g, USER_STATISTICS); // 替换数字
  416. el.dataset.processed = "true";
  417. }
  418. });
  419. } catch (error) {
  420. console.error(`PrivateView: 解析正则失败 - ${regexString}`, error);
  421. }
  422. }
  423. }
  424.  
  425. // 隐匿文章标题
  426. if (settings.hideArticleTitle && currentSite.ArticleTitle) {
  427. updateVisibility(currentSite.ArticleTitle);
  428. }
  429.  
  430. // 屏蔽所有视图
  431. if (settings.hideAllImg) {
  432. // 清空视频源,防止加载
  433. document.querySelectorAll('video').forEach(video => {
  434. video.src = '';
  435. video.load(); // 清除现有加载的资源
  436. });
  437.  
  438. updateImg("img, source, svg, div, span, section, article, aside, header, footer, main, nav");
  439. }
  440. }
  441.  
  442. // 切换功能开关
  443. function toggleSetting(settingKey) {
  444. // 先保存当前的设置状态,用于可能的回滚
  445. const oldSettings = JSON.parse(JSON.stringify(settings));
  446.  
  447. settings[settingKey] = !settings[settingKey];
  448.  
  449. // 当 hiddenModeEnabled 被切换时,统一控制其他开关
  450. if (settingKey === "hiddenModeEnabled") {
  451. settings.hideBrowserTitle = settings.hideArticleTitle = settings.hideProfileImg = settings.hideProfileUserName = settings.hideProfileStatistics = settings.hiddenModeEnabled;
  452. } else {
  453. // 如果只切换单个设置,则根据各开关状态更新 hiddenModeEnabled
  454. settings.hiddenModeEnabled = (
  455. settings.hideBrowserTitle ||
  456. settings.hideArticleTitle ||
  457. settings.hideProfileImg ||
  458. settings.hideProfileUserName ||
  459. settings.hideProfileStatistics
  460. );
  461. }
  462.  
  463. saveSettings();
  464.  
  465. // 根据不同的设置项决定是否需要即时操作或刷新页面
  466. if (settingKey === "hideBrowserTitle") {
  467. toggleBrowserTitle();
  468. } else if (settingKey === "hideArticleTitle") {
  469. toggleArticleTitleVisibility();
  470. } else {
  471. // 对于头像、昵称、统计数据更改后,需要刷新页面生效
  472. // 弹出确认框,如果用户确认则刷新,否则回滚设置
  473. if (confirm("本次操作需要刷新页面才生效,是否继续?")) {
  474. location.reload();
  475. } else {
  476. // 用户取消了刷新,则回滚到修改前的状态
  477. settings = oldSettings;
  478. saveSettings();
  479. }
  480. }
  481.  
  482. updateMenuCommands();
  483. }
  484.  
  485. function getPrimaryDomain(hostname) {
  486. let parts = hostname.split('.');
  487. return parts.length > 2 ? parts.slice(-2).join('.') : hostname;
  488. }
  489.  
  490. function getCurrentSiteConfig() {
  491. const currentHost = location.hostname;
  492. const primaryDomain = getPrimaryDomain(currentHost);
  493. const storedConfig = GM_getValue(APP_NAME, {});
  494.  
  495. // 先尝试获取当前域名的配置,如果没有,则回退到主域名
  496. return storedConfig[currentHost] || storedConfig[primaryDomain] || {};
  497. }
  498.  
  499. // 添加全局 CSS 样式
  500. function addCustomStyles() {
  501. const styleId = `${APP_NAME}-styles`;
  502. if (document.getElementById(styleId)) return;
  503.  
  504. const style = document.createElement("style");
  505. style.id = styleId;
  506. style.textContent = `
  507. .${APP_NAME}-modal {
  508. position: fixed;
  509. top: 50%;
  510. left: 50%;
  511. transform: translate(-50%, -50%);
  512. z-index: 9999;
  513. background: white;
  514. padding: 20px;
  515. box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
  516. border-radius: 10px;
  517. width: 600px;
  518. max-height: 800px;
  519. font-family: Arial, sans-serif;
  520. text-rendering: optimizeLegibility;
  521. -webkit-font-smoothing: antialiased;
  522. -moz-osx-font-smoothing: grayscale;
  523. }
  524.  
  525. .${APP_NAME}-modal h3 {
  526. text-align: center;
  527. font-size: 18px;
  528. }
  529.  
  530. .${APP_NAME}-modal p {
  531. color: grey;
  532. text-align: center;
  533. font-size: 14px;
  534. }
  535.  
  536. .${APP_NAME}-modal label {
  537. display: block;
  538. margin: 10px 0 5px 0;
  539. font-size: 14px;
  540. font-weight: bold;
  541. text-align: left;
  542. }
  543.  
  544. .${APP_NAME}-modal input,
  545. .${APP_NAME}-modal textarea {
  546. width: 100%;
  547. padding: 6px;
  548. border: 1px solid #ccc;
  549. border-radius: 5px;
  550. font-size: 14px;
  551. }
  552.  
  553. .${APP_NAME}-modal button {
  554. padding: 10px 20px;
  555. border: none;
  556. cursor: pointer;
  557. margin: 10px 5px;
  558. border-radius: 5px;
  559. font-size: 14px;
  560. transition: all 0.2s ease-in-out;
  561. }
  562.  
  563. .${APP_NAME}-modal .${APP_NAME}-save-btn {
  564. background: rgb(40, 127, 167);
  565. color: white;
  566. }
  567.  
  568. .${APP_NAME}-modal .${APP_NAME}-cancel-btn {
  569. background: rgb(210, 216, 213);
  570. color: black;
  571. }
  572.  
  573. .${APP_NAME}-modal .${APP_NAME}-reset-btn {
  574. background: rgb(255, 99, 71);
  575. color: white;
  576. }
  577.  
  578. .${APP_NAME}-modal-buttons {
  579. text-align: center;
  580. }
  581.  
  582. .${APP_NAME}-scrollable-box {
  583. border: 1px solid #ccc;
  584. padding: 10px;
  585. max-height: 110px;
  586. overflow-y: auto;
  587. background: #ffffff;
  588. border-radius: 5px;
  589. min-height: 38px;
  590. }
  591.  
  592. .${APP_NAME}-rule-item {
  593. display: flex;
  594. align-items: center;
  595. padding: 8px;
  596. border: 1px solid #ddd;
  597. border-radius: 5px;
  598. background: #fff;
  599. margin-bottom: 5px;
  600. }
  601.  
  602. .${APP_NAME}-rule-key,
  603. .${APP_NAME}-rule-value {
  604. flex: 1;
  605. padding: 6px;
  606. border: 1px solid #ccc;
  607. border-radius: 5px;
  608. font-size: 14px;
  609. margin-right: 5px;
  610. }
  611.  
  612. .${APP_NAME}-removeRule {
  613. color: white;
  614. border: none;
  615. cursor: pointer;
  616. padding: 8px;
  617. border-radius: 5px;
  618. transition: background 0.2s ease-in-out;
  619. }
  620.  
  621. .${APP_NAME}-add-btn {
  622. display: block;
  623. width: 100%;
  624. background: #007bff;
  625. color: white;
  626. text-align: center;
  627. padding: 10px;
  628. margin-left: 0px !important;
  629. border-radius: 5px;
  630. font-size: 14px;
  631. cursor: pointer;
  632. transition: background 0.2s ease-in-out;
  633. }
  634.  
  635. `;
  636. document.head.appendChild(style);
  637. }
  638.  
  639. // 重新加载模态框数据
  640. function reloadModalData() {
  641. const currentConfig = getCurrentSiteConfig();
  642.  
  643. document.getElementById(`${APP_NAME}-siteName`).value = currentConfig.BrowserTitle || "";
  644. document.getElementById(`${APP_NAME}-profileImg`).value = currentConfig.ProfileImg || "";
  645. document.getElementById(`${APP_NAME}-profileUserName`).value = currentConfig.ProfileUserName || "";
  646. document.getElementById(`${APP_NAME}-articleTitle`).value = currentConfig.ArticleTitle || "";
  647. document.getElementById(`${APP_NAME}-profileStatistics`).value = currentConfig.ProfileStatistics || "";
  648.  
  649. // 重新填充自定义规则
  650. const container = document.getElementById(`${APP_NAME}-customStatsContainer`);
  651. container.innerHTML = ""; // 清空已有数据
  652. if (currentConfig.CustomStatistics) {
  653. Object.entries(currentConfig.CustomStatistics).forEach(([key, value]) => {
  654. addCustomRule(key, value);
  655. });
  656. }
  657. }
  658.  
  659. // 显示模态框
  660. function showModal(editMode = false) {
  661. let modal = document.getElementById(`${APP_NAME}-modal`);
  662. if (modal) {
  663. reloadModalData(); // **每次打开模态框时,重新加载数据**
  664. modal.style.display = "block";
  665. return;
  666. }
  667.  
  668. // 添加 CSS 修复字体模糊
  669. addCustomStyles();
  670.  
  671. const currentHost = location.hostname;
  672. const primaryDomain = getPrimaryDomain(currentHost);
  673. const hasDefaultConfig = editMode && (DEFAULT_SITE_CONFIG[currentHost] || DEFAULT_SITE_CONFIG[primaryDomain]);
  674.  
  675. modal = document.createElement("div");
  676. modal.id = `${APP_NAME}-modal`;
  677. modal.classList.add(`${APP_NAME}-modal`);
  678. modal.dataset.editMode = editMode ? "true" : "false";
  679.  
  680. modal.innerHTML = `
  681. <h3>🛠️ ${editMode ? '修改' : '添加'}网站配置</h3>
  682. <p>配置作用于 <b>${currentHost}</b></p>
  683.  
  684. <label>🔖 隐匿网页标题:</label>
  685. <input type="text" id="${APP_NAME}-siteName">
  686.  
  687. <label>🧢 隐匿个人头像的选择器:</label>
  688. <textarea id="${APP_NAME}-profileImg" rows="2"></textarea>
  689.  
  690. <label>👤 隐匿用户名的选择器:</label>
  691. <textarea id="${APP_NAME}-profileUserName" rows="2"></textarea>
  692.  
  693. <label>📰 隐匿文章标题的选择器:</label>
  694. <textarea id="${APP_NAME}-articleTitle" rows="2"></textarea>
  695.  
  696. <label>🏅 隐匿个人数据的选择器:</label>
  697. <textarea id="${APP_NAME}-profileStatistics" rows="2"></textarea>
  698.  
  699. <label>✏️ 隐匿自定义数据的选择器:</label>
  700. <div id="${APP_NAME}-customStatsContainer" class="${APP_NAME}-scrollable-box"></div>
  701. <button id="${APP_NAME}-addRule" class="${APP_NAME}-add-btn">➕ 添加规则</button>
  702.  
  703. <div class="${APP_NAME}-modal-buttons">
  704. ${hasDefaultConfig ? `<button id="${APP_NAME}-resetBtn" class="${APP_NAME}-reset-btn">🔄 恢复默认配置</button>` : ''}
  705. <button id="${APP_NAME}-saveBtn" class="${APP_NAME}-save-btn">💾 保存</button>
  706. <button id="${APP_NAME}-cancelBtn" class="${APP_NAME}-cancel-btn">❌ 取消</button>
  707. </div>
  708. `;
  709.  
  710. document.body.appendChild(modal);
  711.  
  712. // 每次打开模态框时,重新加载数据
  713. reloadModalData();
  714.  
  715. // 绑定事件
  716. document.getElementById(`${APP_NAME}-cancelBtn`).addEventListener("click", () => {
  717. modal.style.display = "none";
  718. });
  719.  
  720. document.getElementById(`${APP_NAME}-saveBtn`).addEventListener("click", () => {
  721. saveCurrentSiteConfig(modal.dataset.editMode === "true");
  722. });
  723.  
  724. if (hasDefaultConfig) {
  725. document.getElementById(`${APP_NAME}-resetBtn`).addEventListener("click", () => {
  726. resetSiteToDefaultConfig(currentHost);
  727. });
  728. }
  729.  
  730. document.getElementById(`${APP_NAME}-addRule`).addEventListener("click", () => {
  731. addCustomRule("", "");
  732. });
  733. }
  734.  
  735. // 动态添加一条自定义规则 (key-value 组)
  736. function addCustomRule(selector = "", regex = "") {
  737. const container = document.getElementById(`${APP_NAME}-customStatsContainer`);
  738.  
  739. const ruleDiv = document.createElement("div");
  740. ruleDiv.classList.add(`${APP_NAME}-rule-item`);
  741. ruleDiv.innerHTML = `
  742. <input type="text" class="${APP_NAME}-rule-key" placeholder="CSS 选择器" value="${selector}">
  743. <input type="text" class="${APP_NAME}-rule-value" placeholder="匹配规则 (正则)" value="${regex}">
  744. <span class="${APP_NAME}-removeRule">🗑️</span>
  745. `;
  746.  
  747. ruleDiv.querySelector(`.${APP_NAME}-removeRule`).addEventListener("click", () => {
  748. container.removeChild(ruleDiv);
  749. });
  750.  
  751. container.appendChild(ruleDiv);
  752. }
  753.  
  754. // 保存/修改网站配置(自动转换 key-value 组)
  755. function saveCurrentSiteConfig(editMode = false) {
  756. const currentHost = location.hostname;
  757. const primaryDomain = getPrimaryDomain(currentHost);
  758. const siteToSave = editMode && !GM_getValue(APP_NAME, {})[currentHost] ? primaryDomain : currentHost;
  759.  
  760. const siteName = document.getElementById(`${APP_NAME}-siteName`).value.trim();
  761. const profileImg = document.getElementById(`${APP_NAME}-profileImg`).value.trim();
  762. const profileUserName = document.getElementById(`${APP_NAME}-profileUserName`).value.trim();
  763. const articleTitle = document.getElementById(`${APP_NAME}-articleTitle`).value.trim();
  764. const profileStatistics = document.getElementById(`${APP_NAME}-profileStatistics`).value.trim();
  765.  
  766. if (!siteName) {
  767. alert("⚠️ 网站名称不能为空!");
  768. return;
  769. }
  770.  
  771. // 采集所有 key-value 规则
  772. let customStatsParsed = {};
  773. document.querySelectorAll(`.${APP_NAME}-rule-item`).forEach(ruleDiv => {
  774. const key = ruleDiv.querySelector(`.${APP_NAME}-rule-key`).value.trim();
  775. const value = ruleDiv.querySelector(`.${APP_NAME}-rule-value`).value.trim();
  776. if (key && value) {
  777. customStatsParsed[key] = value;
  778. }
  779. });
  780.  
  781. // 创建新配置对象
  782. let newSiteConfig = {
  783. "BrowserTitle": siteName,
  784. ...(profileImg ? { "ProfileImg": profileImg } : {}),
  785. ...(profileUserName ? { "ProfileUserName": profileUserName } : {}),
  786. ...(articleTitle ? { "ArticleTitle": articleTitle } : {}),
  787. ...(profileStatistics ? { "ProfileStatistics": profileStatistics } : {}),
  788. ...(Object.keys(customStatsParsed).length ? { "CustomStatistics": customStatsParsed } : {})
  789. };
  790.  
  791. let storedConfig = GM_getValue(APP_NAME, {});
  792. storedConfig[siteToSave] = newSiteConfig;
  793. GM_setValue(APP_NAME, storedConfig);
  794.  
  795. if (confirm(`✅ ${siteName} (${siteToSave}) 配置已${editMode ? "修改" : "添加"}!立即刷新页面即可生效。`)) {
  796. location.reload();
  797. }
  798.  
  799. document.getElementById(`${APP_NAME}-modal`).style.display = "none";
  800. }
  801.  
  802. // 移除当前网站配置
  803. function removeCurrentSiteConfig() {
  804. const host = location.hostname;
  805. if (!confirm(`⚠️ 确定要移除 ${host} 的配置吗?`)) return;
  806.  
  807. let storedConfig = GM_getValue(APP_NAME, {});
  808.  
  809. // 查找完全匹配当前域名的配置
  810. let matchedKey = Object.keys(storedConfig).find(key =>
  811. key.split(',').map(k => k.trim()).includes(host)
  812. );
  813.  
  814. // 获取一级域名(顶级域名 + 二级域名,例如 `mbd.baidu.com` → `baidu.com`)
  815. let domainParts = host.split('.');
  816. let primaryDomain = domainParts.slice(-2).join('.'); // 获取 `baidu.com`
  817.  
  818. // 查找一级域名的配置
  819. let matchedPrimaryKey = Object.keys(storedConfig).find(key =>
  820. key.split(',').map(k => k.trim()).includes(primaryDomain)
  821. );
  822.  
  823. if (!matchedKey && !matchedPrimaryKey) {
  824. alert(`⚠️ ${host} 及其上级域名 ${primaryDomain} 均未找到可删除的配置!`);
  825. return;
  826. }
  827.  
  828. if (matchedKey) {
  829. // 处理当前二级域名的情况
  830. let domains = matchedKey.split(',').map(k => k.trim());
  831. if (domains.length > 1) {
  832. // 只删除当前域名,保留其他域名
  833. let newKey = domains.filter(k => k !== host).join(', ');
  834. let oldConfig = storedConfig[matchedKey];
  835. delete storedConfig[matchedKey]; // 删除旧键
  836. storedConfig[newKey] = oldConfig; // 重新存储为新键
  837. } else {
  838. // 只有单个域名,直接删除
  839. delete storedConfig[matchedKey];
  840. }
  841.  
  842. GM_setValue(APP_NAME, storedConfig);
  843. if (confirm(`✅ ${host} 配置已移除!立即刷新页面即可生效。`)) {
  844. location.reload();
  845. }
  846. } else if (matchedPrimaryKey) {
  847. // 如果当前二级域名没有匹配,但一级域名有匹配,询问用户是否删除
  848. if (confirm(`⚠️ ${host} 没有找到匹配项,但 ${primaryDomain} 存在配置,是否删除 ${primaryDomain} 的配置?`)) {
  849. let domains = matchedPrimaryKey.split(',').map(k => k.trim());
  850. if (domains.length > 1) {
  851. // 只删除一级域名,保留其他域名
  852. let newKey = domains.filter(k => k !== primaryDomain).join(', ');
  853. let oldConfig = storedConfig[matchedPrimaryKey];
  854. delete storedConfig[matchedPrimaryKey];
  855. storedConfig[newKey] = oldConfig;
  856. } else {
  857. delete storedConfig[matchedPrimaryKey];
  858. }
  859.  
  860. GM_setValue(APP_NAME, storedConfig);
  861. if (confirm(`✅ ${primaryDomain} 配置已移除!立即刷新页面即可生效。`)) {
  862. location.reload();
  863. }
  864. }
  865. }
  866. }
  867.  
  868. // 恢复已有的默认网站配置
  869. function resetSiteToDefaultConfig(site) {
  870. if (!confirm(`⚠️ 确定要恢复 ${site} 的默认配置吗?自定义设置将会被删除!`)) return;
  871.  
  872. let storedConfig = GM_getValue(APP_NAME, {});
  873.  
  874. // 获取主域名
  875. let primaryDomain = getPrimaryDomain(site);
  876.  
  877. // 删除所有相关自定义配置(主域名 & 子域名)
  878. delete storedConfig[site];
  879. if (primaryDomain !== site) {
  880. delete storedConfig[primaryDomain];
  881. }
  882.  
  883. // 检查是否存在默认配置
  884. let defaultConfig = DEFAULT_SITE_CONFIG[primaryDomain] || DEFAULT_SITE_CONFIG[site];
  885.  
  886. if (defaultConfig) {
  887. // 如果存在默认配置,强制写入
  888. storedConfig[primaryDomain] = defaultConfig;
  889. GM_setValue(APP_NAME, storedConfig);
  890. if (confirm(`✅ ${site} 已恢复默认配置!立即刷新页面即可生效。`)) {
  891. location.reload();
  892. }
  893. } else {
  894. // 如果 `DEFAULT_SITE_CONFIG` 也没有值,那就是本身没有默认值
  895. alert(`⚠️ ${site} 的自定义配置已删除,但没有默认配置可恢复!`);
  896. }
  897. }
  898.  
  899. function exportConfig() {
  900. const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(GM_getValue(APP_NAME, {}), null, 4));
  901. const downloadAnchor = document.createElement('a');
  902. downloadAnchor.setAttribute("href", dataStr);
  903. downloadAnchor.setAttribute("download", "PrivateView_Config.json");
  904. document.body.appendChild(downloadAnchor);
  905. downloadAnchor.click();
  906. document.body.removeChild(downloadAnchor);
  907. }
  908.  
  909. function importConfig(event) {
  910. if (confirm(`⚠️ 导入配置,将会覆盖当前已有配置,是否继续?`)) {
  911. const file = event.target.files[0];
  912. if (!file) return;
  913.  
  914. const reader = new FileReader();
  915. reader.onload = function (e) {
  916. try {
  917. const importedConfig = JSON.parse(e.target.result);
  918. GM_setValue(APP_NAME, importedConfig);
  919. if (confirm(`✅ 配置已成功导入!立即刷新页面即可生效。`)) {
  920. location.reload();
  921. }
  922. } catch (error) {
  923. alert("配置文件格式错误,请检查后再试!");
  924. }
  925. };
  926. reader.readAsText(file);
  927. }
  928. }
  929.  
  930. function saveConfig() {
  931. const newConfig = document.getElementById("configTextarea").value;
  932. try {
  933. const parsedConfig = JSON.parse(newConfig);
  934. GM_setValue(APP_NAME, parsedConfig);
  935. if (confirm(`✅ 配置已保存!立即刷新页面即可生效。`)) {
  936. location.reload();
  937. }
  938. } catch (error) {
  939. alert("配置格式错误,请检查后再试!");
  940. }
  941. }
  942.  
  943. function showConfigEditor() {
  944. let modal = document.getElementById("configModal");
  945. if (modal) {
  946. modal.style.display = "block";
  947. return;
  948. }
  949.  
  950. modal = document.createElement("div");
  951. modal.id = "configModal";
  952. modal.style.position = "fixed";
  953. modal.style.top = "50%";
  954. modal.style.left = "50%";
  955. modal.style.transform = "translate(-50%, -50%)";
  956. modal.style.background = "white";
  957. modal.style.padding = "20px";
  958. modal.style.boxShadow = "0px 0px 10px rgba(0, 0, 0, 0.5)";
  959. modal.style.borderRadius = "10px";
  960. modal.style.width = "800px";
  961. modal.style.zIndex = "9999";
  962.  
  963. modal.innerHTML = `
  964. <h3>📜 网站配置</h3>
  965. <textarea id="configTextarea" rows="50" style="width:100%; font-family:monospace;">${JSON.stringify(GM_getValue(APP_NAME, {}), null, 4)}</textarea>
  966. <br><br>
  967. <input type="file" id="importFile" accept=".json" style="display:none;">
  968. <button id="importBtn">📥 导入配置</button>
  969. <button id="exportBtn">📤 导出配置</button>
  970. <button id="saveBtn">💾 保存配置</button>
  971. <button onclick="document.getElementById('configModal').style.display = 'none';">❌ 关闭</button>
  972. `;
  973. document.body.appendChild(modal);
  974.  
  975. document.getElementById("importFile").addEventListener("change", importConfig);
  976. document.getElementById("importBtn").addEventListener("click", () => document.getElementById("importFile").click());
  977. document.getElementById("exportBtn").addEventListener("click", exportConfig);
  978. document.getElementById("saveBtn").addEventListener("click", saveConfig);
  979. }
  980.  
  981. // 存储菜单项的引用
  982. let menuItems = {};
  983.  
  984. function updateMenuCommands() {
  985. // 先移除旧菜单
  986. Object.values(menuItems).forEach(GM_unregisterMenuCommand);
  987.  
  988. if (currentSite) {
  989.  
  990. menuItems.hiddenModeEnabled = GM_registerMenuCommand(
  991. settings.hiddenModeEnabled ? "🌐一键关闭隐匿浏览" : "🌐一键开启隐匿浏览",
  992. () => toggleSetting('hiddenModeEnabled')
  993. );
  994.  
  995. menuItems.hideBrowserTitle = GM_registerMenuCommand(
  996. settings.hideBrowserTitle ? "🔖隐匿网页标题✅" : "🔖隐匿网页标题❌",
  997. () => toggleSetting('hideBrowserTitle')
  998. );
  999.  
  1000. menuItems.hideArticleTitle = GM_registerMenuCommand(
  1001. settings.hideArticleTitle ? "📰隐匿文章标题✅" : "📰隐匿文章标题❌",
  1002. () => toggleSetting('hideArticleTitle')
  1003. );
  1004.  
  1005. menuItems.hideProfileImg = GM_registerMenuCommand(
  1006. settings.hideProfileImg ? "🧢隐匿个人头像✅" : "🧢隐匿个人头像❌",
  1007. () => toggleSetting('hideProfileImg')
  1008. );
  1009.  
  1010. menuItems.hideProfileUserName = GM_registerMenuCommand(
  1011. settings.hideProfileUserName ? "👤隐匿个人昵称✅" : "👤隐匿个人昵称❌",
  1012. () => toggleSetting('hideProfileUserName')
  1013. );
  1014.  
  1015. menuItems.hideProfileStatistics = GM_registerMenuCommand(
  1016. settings.hideProfileStatistics ? "🏅隐匿个人数据✅" : "🏅隐匿个人数据❌",
  1017. () => toggleSetting('hideProfileStatistics')
  1018. );
  1019.  
  1020. menuItems.hideAllImg = GM_registerMenuCommand(
  1021. settings.hideAllImg ? "🎞️屏蔽所有视图✅" : "🎞️屏蔽所有视图❌",
  1022. () => toggleSetting('hideAllImg')
  1023. );
  1024.  
  1025. menuItems.updateCurrentSite = GM_registerMenuCommand(
  1026. `✏️修改当前网站配置`,
  1027. () => showModal(true)
  1028. );
  1029.  
  1030. menuItems.removeCurrentSite = GM_registerMenuCommand(
  1031. `🗑️移除当前网站配置`,
  1032. () => removeCurrentSiteConfig()
  1033. );
  1034.  
  1035. menuItems.manageAllConfigs = GM_registerMenuCommand(
  1036. `⚙️管理所有网站配置`,
  1037. () => showConfigEditor()
  1038. );
  1039.  
  1040. menuItems.resetDefaultConfig = GM_registerMenuCommand(
  1041. `🏠关于PrivateView`,
  1042. () => window.open('https://greatest.deepsurf.us/zh-CN/scripts/520416-privateview')
  1043. );
  1044.  
  1045. } else {
  1046.  
  1047. GM_registerMenuCommand(
  1048. `⚠️当前网站未适配(${location.hostname})`,
  1049. () => {
  1050. window.open('https://greatest.deepsurf.us/zh-CN/scripts/520416-privateview/feedback', '_blank');
  1051. }
  1052. );
  1053. menuItems.addCurrentSite = GM_registerMenuCommand(
  1054. `➕添加网站配置`,
  1055. () => showModal(false)
  1056. );
  1057.  
  1058. menuItems.manageAllConfigs = GM_registerMenuCommand(
  1059. `⚙️管理所有网站配置`,
  1060. () => showConfigEditor()
  1061. );
  1062.  
  1063. return; // 不注册其他菜单项
  1064.  
  1065. }
  1066. }
  1067.  
  1068. // 注册菜单开关
  1069. updateMenuCommands();
  1070.  
  1071. // 页面变化时重新执行
  1072. const observer = new MutationObserver(() => {
  1073. if (settings.hiddenModeEnabled) {
  1074. hideElements();
  1075. }
  1076. });
  1077.  
  1078. // 检测页面变动
  1079. observer.observe(document.body, { childList: true, subtree: true });
  1080.  
  1081. // 初始化页面时内容
  1082. if (settings.hiddenModeEnabled) {
  1083. setTimeout(() => {
  1084. hideElements();
  1085. }, 100);
  1086. }
  1087. })();