Site Filter (Protocol-Independent)

Manage allowed sites dynamically and reference this in other scripts.

目前为 2025-05-08 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/526770/1585264/Site%20Filter%20%28Protocol-Independent%29.js

  1. // ==UserScript==
  2. // @name Site Filter (Protocol-Independent)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.5
  5. // @description Manage allowed sites dynamically and reference this in other scripts.
  6. // @author blvdmd
  7. // @match *://*/*
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_download
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. const USE_EMOJI_FOR_STATUS = true; // Configurable flag to use emoji for true/false status
  19. const SHOW_STATUS_ONLY_IF_TRUE = true; // Configurable flag to show status only if any value is true
  20.  
  21. // ✅ Wait for `SCRIPT_STORAGE_KEY` to be set
  22. function waitForScriptStorageKey(maxWait = 1000) {
  23. return new Promise(resolve => {
  24. const startTime = Date.now();
  25. const interval = setInterval(() => {
  26. if (typeof window.SCRIPT_STORAGE_KEY !== 'undefined') {
  27. clearInterval(interval);
  28. resolve(window.SCRIPT_STORAGE_KEY);
  29. } else if (Date.now() - startTime > maxWait) {
  30. clearInterval(interval);
  31. console.error("🚨 SCRIPT_STORAGE_KEY is not set! Make sure your script sets it **before** @require.");
  32. resolve(null);
  33. }
  34. }, 50);
  35. });
  36. }
  37.  
  38. (async function initialize() {
  39. async function waitForDocumentReady() {
  40. if (document.readyState === "complete") return;
  41. return new Promise(resolve => {
  42. window.addEventListener("load", resolve, { once: true });
  43. });
  44. }
  45. // ✅ Wait for the script storage key
  46. const key = await waitForScriptStorageKey();
  47. if (!key) return;
  48.  
  49. // ✅ Ensure the document is fully loaded before setting `shouldRunOnThisSite`
  50. await waitForDocumentReady();
  51.  
  52. const STORAGE_KEY = `additionalSites_${key}`;
  53.  
  54. function getDefaultList() {
  55. return typeof window.GET_DEFAULT_LIST === "function" ? window.GET_DEFAULT_LIST() : [];
  56. }
  57.  
  58. function normalizeUrl(url) {
  59. if (typeof url !== 'string') {
  60. url = String(url);
  61. }
  62. return url.replace(/^https?:\/\//, '');
  63. }
  64.  
  65. let additionalSites = GM_getValue(STORAGE_KEY, []);
  66.  
  67. let mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  68. if (typeof item === 'string') {
  69. return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
  70. }
  71. return { ...item, pattern: normalizeUrl(item.pattern) };
  72. });
  73.  
  74. GM_registerMenuCommand("➕ Add Current Site to Include List", addCurrentSiteMenu);
  75. GM_registerMenuCommand("📜 View Included Sites", viewIncludedSites);
  76. GM_registerMenuCommand("🗑️ Delete Specific Entries", deleteEntries);
  77. GM_registerMenuCommand("✏️ Edit an Entry", editEntry);
  78. GM_registerMenuCommand("🚨 Clear All Entries", clearAllEntries);
  79. GM_registerMenuCommand("📤 Export Site List as JSON", exportAdditionalSites);
  80. GM_registerMenuCommand("📥 Import Site List from JSON", importAdditionalSites);
  81.  
  82. async function shouldRunOnThisSite() {
  83. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  84. return mergedSites.some(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  85. }
  86.  
  87. function wildcardToRegex(pattern) {
  88. return new RegExp("^" + pattern
  89. .replace(/[-[\]{}()+^$|#\s]/g, '\\$&')
  90. .replace(/\./g, '\\.')
  91. .replace(/\?/g, '\\?')
  92. .replace(/\*/g, '.*')
  93. + "$");
  94. }
  95.  
  96. function addCurrentSiteMenu() {
  97. const currentHost = window.top.location.hostname; // Use window.top to get the top-level hostname
  98. const currentPath = window.top.location.pathname; // Use window.top to get the top-level pathname
  99. const domainParts = currentHost.split('.');
  100. const baseDomain = domainParts.length > 2 ? domainParts.slice(-2).join('.') : domainParts.join('.');
  101. const secondLevelDomain = domainParts.length > 2 ? domainParts.slice(-2, -1)[0] : domainParts[0];
  102. const options = [
  103. { name: `Preferred Domain Match (*${secondLevelDomain}.*)`, pattern: `*${secondLevelDomain}.*` },
  104. { name: `Base Hostname (*.${baseDomain}*)`, pattern: `*.${baseDomain}*` },
  105. { name: `Base Domain (*.${secondLevelDomain}.*)`, pattern: `*.${secondLevelDomain}.*` },
  106. { name: `Host Contains (*${secondLevelDomain}*)`, pattern: `*${secondLevelDomain}*` },
  107. { name: `Exact Path (${currentHost}${currentPath})`, pattern: normalizeUrl(`${window.top.location.href}`) },
  108. { name: "Custom Wildcard Pattern", pattern: normalizeUrl(`${window.top.location.href}`) }
  109. ];
  110.  
  111. const userChoice = prompt(
  112. "Select an option to add the site:\n" +
  113. options.map((opt, index) => `${index + 1}. ${opt.name}`).join("\n") +
  114. "\nEnter a number or cancel."
  115. );
  116.  
  117. if (!userChoice) return;
  118. const selectedIndex = parseInt(userChoice, 10) - 1;
  119. if (selectedIndex >= 0 && selectedIndex < options.length) {
  120. let pattern = normalizeUrl(options[selectedIndex].pattern);
  121. if (options[selectedIndex].name === "Custom Wildcard Pattern") {
  122. pattern = normalizeUrl(prompt("Edit custom wildcard pattern:", pattern));
  123. if (!pattern.trim()) return alert("Invalid pattern. Operation canceled.");
  124. }
  125.  
  126. const preProcessingRequired = prompt("Is pre-processing required? (y/n)", "n").toLowerCase() === 'y';
  127. const postProcessingRequired = prompt("Is post-processing required? (y/n)", "n").toLowerCase() === 'y';
  128. const onDemandFloatingButtonRequired = prompt("Is on-demand floating button required? (y/n)", "n").toLowerCase() === 'y';
  129. const backgroundChangeObserverRequired = prompt("Is background change observer required? (y/n)", "n").toLowerCase() === 'y';
  130.  
  131. const entry = {
  132. pattern,
  133. preProcessingRequired,
  134. postProcessingRequired,
  135. onDemandFloatingButtonRequired,
  136. backgroundChangeObserverRequired
  137. };
  138. if (!additionalSites.some(item => item.pattern === pattern)) {
  139. additionalSites.push(entry);
  140. GM_setValue(STORAGE_KEY, additionalSites);
  141. mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  142. if (typeof item === 'string') {
  143. return {
  144. pattern: normalizeUrl(item),
  145. preProcessingRequired: false,
  146. postProcessingRequired: false,
  147. onDemandFloatingButtonRequired: false,
  148. backgroundChangeObserverRequired: false
  149. };
  150. }
  151. return {
  152. ...item,
  153. pattern: normalizeUrl(item.pattern),
  154. onDemandFloatingButtonRequired: item.onDemandFloatingButtonRequired || false,
  155. backgroundChangeObserverRequired: item.backgroundChangeObserverRequired || false
  156. };
  157. });
  158. alert(`✅ Added site with pattern: ${pattern}`);
  159. } else {
  160. alert(`⚠️ Pattern "${pattern}" is already in the list.`);
  161. }
  162. }
  163. }
  164.  
  165. function viewIncludedSites() {
  166. const siteList = additionalSites.map(item => {
  167. const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired, item.onDemandFloatingButtonRequired, item.backgroundChangeObserverRequired);
  168. return `${item.pattern}${status ? ` (${status})` : ''}`;
  169. }).join("\n");
  170. alert(`🔍 Included Sites:\n${siteList || "No sites added yet."}`);
  171. }
  172.  
  173. function deleteEntries() {
  174. if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to delete.");
  175. const userChoice = prompt("Select entries to delete (comma-separated numbers):\n" +
  176. additionalSites.map((item, index) => `${index + 1}. ${item.pattern}`).join("\n"));
  177. if (!userChoice) return;
  178. const indicesToRemove = userChoice.split(',').map(num => parseInt(num.trim(), 10) - 1);
  179. additionalSites = additionalSites.filter((_, index) => !indicesToRemove.includes(index));
  180. GM_setValue(STORAGE_KEY, additionalSites);
  181. mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  182. if (typeof item === 'string') {
  183. return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
  184. }
  185. return { ...item, pattern: normalizeUrl(item.pattern) };
  186. });
  187. alert("✅ Selected entries have been deleted.");
  188. }
  189.  
  190. function editEntry() {
  191. if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to edit.");
  192. const userChoice = prompt("Select an entry to edit:\n" +
  193. additionalSites.map((item, index) => {
  194. const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired, item.onDemandFloatingButtonRequired, item.backgroundChangeObserverRequired);
  195. return `${index + 1}. ${item.pattern}${status ? ` (${status})` : ''}`;
  196. }).join("\n"));
  197. if (!userChoice) return;
  198. const selectedIndex = parseInt(userChoice, 10) - 1;
  199. if (selectedIndex < 0 || selectedIndex >= additionalSites.length) return alert("❌ Invalid selection.");
  200. const entry = additionalSites[selectedIndex];
  201. const newPattern = normalizeUrl(prompt("Edit the pattern:", entry.pattern));
  202. if (!newPattern || !newPattern.trim()) return;
  203. const preProcessingRequired = prompt("Is pre-processing required? (y/n)", entry.preProcessingRequired ? "y" : "n").toLowerCase() === 'y';
  204. const postProcessingRequired = prompt("Is post-processing required? (y/n)", entry.postProcessingRequired ? "y" : "n").toLowerCase() === 'y';
  205. const onDemandFloatingButtonRequired = prompt("Is on-demand floating button required? (y/n)", entry.onDemandFloatingButtonRequired ? "y" : "n").toLowerCase() === 'y';
  206. const backgroundChangeObserverRequired = prompt("Is background change observer required? (y/n)", entry.backgroundChangeObserverRequired ? "y" : "n").toLowerCase() === 'y';
  207. entry.pattern = newPattern.trim();
  208. entry.preProcessingRequired = preProcessingRequired;
  209. entry.postProcessingRequired = postProcessingRequired;
  210. entry.onDemandFloatingButtonRequired = onDemandFloatingButtonRequired;
  211. entry.backgroundChangeObserverRequired = backgroundChangeObserverRequired;
  212. GM_setValue(STORAGE_KEY, additionalSites);
  213. mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  214. if (typeof item === 'string') {
  215. return {
  216. pattern: normalizeUrl(item),
  217. preProcessingRequired: false,
  218. postProcessingRequired: false,
  219. onDemandFloatingButtonRequired: false,
  220. backgroundChangeObserverRequired: false
  221. };
  222. }
  223. return {
  224. ...item,
  225. pattern: normalizeUrl(item.pattern),
  226. onDemandFloatingButtonRequired: item.onDemandFloatingButtonRequired || false,
  227. backgroundChangeObserverRequired: item.backgroundChangeObserverRequired || false
  228. };
  229. });
  230. alert("✅ Entry updated.");
  231. }
  232.  
  233. function clearAllEntries() {
  234. if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to clear.");
  235. if (confirm(`🚨 You have ${additionalSites.length} entries. Clear all?`)) {
  236. additionalSites = [];
  237. GM_setValue(STORAGE_KEY, additionalSites);
  238. mergedSites = [...getDefaultList()].map(item => {
  239. if (typeof item === 'string') {
  240. return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
  241. }
  242. return { ...item, pattern: normalizeUrl(item.pattern) };
  243. });
  244. alert("✅ All user-defined entries cleared.");
  245. }
  246. }
  247.  
  248. // function exportAdditionalSites() {
  249. // GM_download("data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(additionalSites, null, 2)), "additionalSites_backup.json");
  250. // alert("📤 Additional sites exported as JSON.");
  251. // }
  252.  
  253. function exportAdditionalSites() {
  254. const data = JSON.stringify(additionalSites, null, 2);
  255. const blob = new Blob([data], { type: 'application/json' });
  256. const url = URL.createObjectURL(blob);
  257. const a = document.createElement('a');
  258. a.href = url;
  259. a.download = 'additionalSites_backup.json';
  260. document.body.appendChild(a);
  261. a.click();
  262. document.body.removeChild(a);
  263. URL.revokeObjectURL(url);
  264. alert("📤 Additional sites exported as JSON.");
  265. }
  266.  
  267. // function importAdditionalSites() {
  268. // const input = document.createElement("input");
  269. // input.type = "file";
  270. // input.accept = ".json";
  271. // input.onchange = event => {
  272. // const reader = new FileReader();
  273. // reader.onload = e => {
  274. // additionalSites = JSON.parse(e.target.result);
  275. // GM_setValue(STORAGE_KEY, additionalSites);
  276. // mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  277. // if (typeof item === 'string') {
  278. // return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
  279. // }
  280. // return { ...item, pattern: normalizeUrl(item.pattern) };
  281. // });
  282. // alert("📥 Sites imported successfully.");
  283. // };
  284. // reader.readAsText(event.target.files[0]);
  285. // };
  286. // input.click();
  287. // }
  288.  
  289. function importAdditionalSites() {
  290. const input = document.createElement('input');
  291. input.type = 'file';
  292. input.accept = '.json';
  293. input.style.display = 'none';
  294. input.onchange = event => {
  295. const reader = new FileReader();
  296. reader.onload = e => {
  297. try {
  298. const importedData = JSON.parse(e.target.result);
  299. if (Array.isArray(importedData)) {
  300. additionalSites = importedData.map(item => {
  301. if (typeof item === 'string') {
  302. return normalizeUrl(item);
  303. } else if (typeof item === 'object' && item.pattern) {
  304. return { ...item, pattern: normalizeUrl(item.pattern) };
  305. }
  306. throw new Error('Invalid data format');
  307. });
  308. GM_setValue(STORAGE_KEY, additionalSites);
  309. mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  310. if (typeof item === 'string') {
  311. return normalizeUrl(item);
  312. }
  313. return { ...item, pattern: normalizeUrl(item.pattern) };
  314. });
  315. alert('📥 Sites imported successfully.');
  316. } else {
  317. throw new Error('Invalid data format');
  318. }
  319. } catch (error) {
  320. alert('❌ Failed to import sites: ' + error.message);
  321. }
  322. };
  323. reader.readAsText(event.target.files[0]);
  324. };
  325. document.body.appendChild(input);
  326. input.click();
  327. document.body.removeChild(input);
  328. }
  329.  
  330. function formatStatus(preProcessingRequired, postProcessingRequired, onDemandFloatingButtonRequired, backgroundChangeObserverRequired) {
  331. if (SHOW_STATUS_ONLY_IF_TRUE && !preProcessingRequired && !postProcessingRequired && !onDemandFloatingButtonRequired && !backgroundChangeObserverRequired) {
  332. return '';
  333. }
  334. const preStatus = USE_EMOJI_FOR_STATUS ? (preProcessingRequired ? '✅' : '✖️') : (preProcessingRequired ? 'true' : 'false');
  335. const postStatus = USE_EMOJI_FOR_STATUS ? (postProcessingRequired ? '✅' : '✖️') : (postProcessingRequired ? 'true' : 'false');
  336. const floatingButtonStatus = USE_EMOJI_FOR_STATUS ? (onDemandFloatingButtonRequired ? '✅' : '✖️') : (onDemandFloatingButtonRequired ? 'true' : 'false');
  337. const backgroundObserverStatus = USE_EMOJI_FOR_STATUS ? (backgroundChangeObserverRequired ? '✅' : '✖️') : (backgroundChangeObserverRequired ? 'true' : 'false');
  338. return `Pre: ${preStatus}, Post: ${postStatus}, Floating Button: ${floatingButtonStatus}, Background Observer: ${backgroundObserverStatus}`;
  339. }
  340.  
  341. window.shouldRunOnThisSite = shouldRunOnThisSite;
  342. window.isPreProcessingRequired = function() {
  343. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  344. const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  345. return entry ? entry.preProcessingRequired : false;
  346. };
  347. window.isPostProcessingRequired = function() {
  348. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  349. const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  350. return entry ? entry.postProcessingRequired : false;
  351. };
  352. // Expose isOnDemandFloatingButtonRequired
  353. window.isOnDemandFloatingButtonRequired = function() {
  354. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  355. const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  356. return entry ? entry.onDemandFloatingButtonRequired : false;
  357. };
  358.  
  359. // Expose isBackgroundChangeObserverRequired
  360. window.isBackgroundChangeObserverRequired = function() {
  361. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  362. const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  363. return entry ? entry.backgroundChangeObserverRequired : false;
  364. };
  365. })();
  366. })();
  367.  
  368. //To use this in another script use @require
  369.  
  370. // // @run-at document-end
  371. // // ==/UserScript==
  372.  
  373. // window.SCRIPT_STORAGE_KEY = "magnetLinkHashChecker"; // UNIQUE STORAGE KEY
  374.  
  375.  
  376. // window.GET_DEFAULT_LIST = function() {
  377. // return [
  378. // { pattern: "*1337x.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  379. // { pattern: "*yts.*", preProcessingRequired: true, postProcessingRequired: true, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  380. // { pattern: "*torrentgalaxy.*", preProcessingRequired: false, postProcessingRequired: true, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  381. // { pattern: "*bitsearch.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  382. // { pattern: "*thepiratebay.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  383. // { pattern: "*ext.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false }
  384. // ];
  385. // };
  386.  
  387.  
  388. // (async function () {
  389. // 'use strict';
  390.  
  391. // // ✅ Wait until `shouldRunOnThisSite` is available
  392. // while (typeof shouldRunOnThisSite === 'undefined') {
  393. // await new Promise(resolve => setTimeout(resolve, 50));
  394. // }
  395.  
  396. // if (!(await shouldRunOnThisSite())) return;
  397. // //alert("running");
  398.  
  399. // console.log("Pre-Customization enabled for this site: " + isPreProcessingRequired() );
  400. // console.log("Post-Customization enabled for this site: " + isPostProcessingRequired() );
  401.  
  402. // const OFFCLOUD_CACHE_API_URL = 'https://offcloud.com/api/cache';