Dynamic Include Sites Script (Protocol-Independent)

Run on default & user-defined sites using wildcard patterns (ignores protocols), with full management features.

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greatest.deepsurf.us/scripts/526770/1596962/Dynamic%20Include%20Sites%20Script%20%28Protocol-Independent%29.js

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